Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Core: A Multi-Layer Data Service Application Migrated from ASP.NET Web API

0.00/5 (No votes)
29 Nov 2020 3  
A full-structured data service sample application migrated from ASP.NET Web API 2.0 to and between ASP.NET Core version 2.1, 3.1 and 5.0
Providing the code details and explanations for major data-related processes and workflows in the RESTful data service sample application evolved from the legacy ASP.NET Web API to different versions of ASP.NET Core technologies. The code issues and resolutions are particularly described to help developers work on their own projects. The source code files for the .NET versions currently supported by the Microsoft (.NET 5, .NET Core 3.1, .NET Core 2.1, and .NET Framework 4x) are available for downloading.

Introduction

The RESTful data service API has been a mainstream data layer application type for many years. With the technology and framework evolutions, the older code and structures would inevitably be replaced with the newer ones. For the work on migrating the sample data service application to the next ASP.NET generations or higher versions, my objectives are to take the advantages of the newer technology and framework but still keep the same functionality and request/response signatures and workflow. Thus, any client application will not be affected by changes in data service migrations from either ASP.NET Web API to the ASP.NET Core or between versions of the ASP.NET Core. This article is not a step-by-step tutorial, for which audiences can reference other resources if needed, but rather share the completed sample application source code together with below topics and issue resolutions:

Setting Up and Running Sample Applications

To run the SM.Store.CoreApi solution with the .NET Core version you prefer, you need the respective Visual Studio 2019 or 2017 and .NET Core versions installed on your machine:

  • ASP.NET Core 5.0: Visual Studio 2019 (16.8.x or above is needed; .NET 5.0 SDK included which also back supports the .NET Core 2.1 and 3.1 SDKs.)
  • ASP.NET Core 3.1: Visual Studio 2019 (16.4.x or above is needed; .NET Core 3.1 SDK included)
  • ASP.NET Core 2.1: Visual Studio 2019 or 2017 15.7 (or above) and .NET Core 2.1 SDK

If the .NET Core version you have installed is version 2.1.302 (or above), you can use the command "dotnet --list-sdks" on the Command Prompt window to see the list of all installed .NET Core SDK library versions. This command is only available after installing version 2.1.302 or above.

I also recommend downloading and installing the free version of the Postman as your service client tool. After opening and building the SM.Store.CoreApi solution with Visual Studio, you can select one of the available browsers from the IIS Express button dropdown on the menu bar, and then click that button to start the application.

No database needs to be set up initially since the application uses the in-memory database with the current configurations. The built-in starting page will show the response data in the JSON format obtained from a service method call, which is just a simple way to start the service application with the IIS Express on the development machine.

Image 1

You can now keep the Visual Studio session open and call a service method using the Postman. The results, either the data or error, will be displayed in the response section:

Image 2

The downloadable sources AspNetCore5.0_DataService include the TestCasesForDataServices.txt file that can be used for all types and versions of the sample application projects. The file contains many cases of requesting data items. Feel free to use the cases for your test calls to both newer SM.Store.CoreApi and legacy SM.Store.WebApi applications.

If you would like to use SQL Server database or LocalDB, you can open the appsettings.json file and perform the following steps:

  • Remove the UseInMemoryDatabase line or set its value to false under the section AppConfig.

  • Update the StoreDbConnection value under the ConnectionStrings section with your settings. For example, if you use the SQL Server LocalDB, you can enable the connection string and replace the <your-instance-name> with your LocalDB instance name. You can even change the StoreCF8 to your own database name.

    "StoreDbConnection": "Server=(localdb)\\<your-instance-name>; 
     Database=StoreCF8;Trusted_Connection=True;MultipleActiveResultSets=true;"
  • When starting the Visual Studio solution by pressing F5, the database will automatically be created and the startup.html page will be shown.

If you need to set up and run the legacy ASP.NET Web API sample application, you can do the following after downloading the WebApi_DataServices.

  • Open the SM.Store.WebApi solution with the Visual Studio 2017 or 2019 (also working with version 2015).

  • Rebuild the solution, which automatically downloads all configured libraries from the NuGet.

  • Set up the SQL Server 2017 or 2019 LocalDB, or other SQL Server instance even with older versions on your local machine. Please adjust the connectionString in the web.config file to point to your database instance.

  • Make sure the SM.Store.Api.Web is the startup project and than press F5. This will start the IIS Express and the Web API host site, automatically create the database in your database instance, and populate tables with all sample data records.

  • A test page in the Web API project will be rendered, indicating that the Web API data provider is ready to receive client calls.

It's recommended that you delete the existing database with the same name if you run the sample application again with a different ASP.NET version so that a fresh database will be initialized. Or you can do the migration task on the existing database. Otherwise, some column mapping errors may occur.

Library Projects

The legacy SM.Store.WebApi is an ASP.NET Web API 2 application with multi-layer .NET Framework class library structures.

Image 3

When migrating to the ASP.NET Core, those projects must be converted to the .NET Core class library projects targeting to either .NET Core or .NET Standard framework. The project type .NET Standard could be used for more compatibility and flexibility. However, the folder structures and files are the same even if the project types are different. For the very original migrations to the .NET Core sample applications, the .NET Standard 2.x, NetStandard.Library, was used as the class library project type. For the sample applications with later versions of .NET Core, the class library project type is switched to the Microsoft.NETCore.App. The complete projects in the Visual Studio solution look like this:

Image 4

Some details of migrating sample application from legacy ASP.NET Web API to ASP.NET Core are explained below:

  • The legacy SM.Store.Api.DAL, SM.Store.Api.BLL, and SM.Store.Api.Common projects were migrated to their corresponding projects with the same names.

  • The legacy SM.Store.Api.Entities and SM.Store.Api.Models were merged into the ASP.NET Core SM.Store.Api.Contracts project. All interfaces were also moved into this project which can be referenced by any other project but doesn’t have a reference to any other project in the solution.

  • The Web API Controller classes were moved from the SM.Store.Api project to the main .NET Core project, SM.Store.Api.Web. There is no need to separate those controller classes to another project targeting to the .NET Core.

  • Some needed libraries or components may not be automatically included in the project template. Thus, the missing items should also manually be added into the project from the NuGet. As an example for the sample applications with .NET Core 2x, the Microsoft.ASpNetCore.Mvc package is added into the SM.Store.Api.Common for being used by the custom model binder. For the .NET Core 3.x and above, the Microsoft.NetCore.App already contains the Microsoft.ASpNetCore.Mvc so that there is no need to explicitly reference it in the project.

There is no substantial change regarding the structures and files of the library projects in the Visual Studio solution between the ASP.NET Core versions. For the sample application, the framework reference types Microsoft.NetCore.App and NetStandard.Library, if used, can even be exchangeable for the different ASP.NET Core versions of the library projects.

Dependency Injections

Since the legacy SM.Store.WebApi application uses the Unity tool for the dependency injection(DI) logic and the new SM.Store.CoreApi has the ConfigurationServices routine in the Startup class ready for settings including DI, migrating the Unity to the Core built-in DI service is pretty straightforward. The custom code of the low-level DI Factory class and instance resolving method are no more required. The Unity container registrations can be replaced by the Core service configurations. For a comparison, I list below the setup code lines for the SM.Store.Api.DAL and SM.Store.Api.BLL objects in both legacy and new applications.

The type registration and mapping code in the Unity.config file of the legacy SM.Store.WebApi:

<container> 
    <register type="SM.Store.Api.DAL.IStoreDataUnitOfWork" 

     mapTo="SM.Store.Api.DAL.StoreDataUnitOfWork"> 
      <lifetime type="singleton" /> 
    </register> 
    <register type="SM.Store.Api.DAL.IGenericRepository[Category]" 

    mapTo="SM.Store.Api.DAL.GenericRepository[Category]"/> 
    <register type="SM.Store.Api.DAL.IGenericRepository[ProductStatusType]" 

    mapTo="SM.Store.Api.DAL.GenericRepository[ProductStatusType]"/> 
    <register type="SM.Store.Api.DAL.IProductRepository" 

    mapTo="SM.Store.Api.DAL.ProductRepository"/> 
    <register type="SM.Store.Api.DAL.IContactRepository" 

    mapTo="SM.Store.Api.DAL.ContactRepository"/>    
    <register type="SM.Store.Api.BLL.IProductBS" 

    mapTo="SM.Store.Api.BLL.ProductBS"/> 
    <register type="SM.Store.Api.BLL.IContactBS" 

    mapTo="SM.Store.Api.BLL.ContactBS"/> 
    <register type="SM.Store.Api.BLL.ILookupBS" 

    mapTo="SM.Store.Api.BLL.LookupBS"/> 
</container>

The DI instance and type registrations in the Startup.ConfigureServices() method of the new SM.Store.CoreApi:

services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>)); 
services.AddScoped(typeof(IStoreLookupRepository<>), typeof(StoreLookupRepository<>));
services.AddScoped<IProductRepository, ProductRepository>();
services.AddScoped<IContactRepository, ContactRepository>();
services.AddScoped<ILookupBS, LookupBS>();
services.AddScoped<IProductBS, ProductBS>();
services.AddScoped<IContactBS, ContactBS>();

Note that in the legacy SM.Store.WebApi, only the IStoreDataUnitOfWork type registration has the “singleton” lifetime manager. All other types use the default value which is the Transient lifetime for the registration. The StoreDataUnitOfWork object is outdated and not used by the new SM.Store.CoreApi (discussed in later section). All data-operative objects are now set as Scoped lifetime after the migration, which persists the object instances in the same request context.

There is also no change in the object instance injections to constructors or uses of the object instances from the callers, such as repository and business service classes. For the controller classes, the legacy SM.Store.WebApi calls the DI factory method to instantiate the object instance:

IProductBS bs = DIFactoryDesigntime.GetInstance<IProductBS>();

In the new SM.Store.CoreApi, similar functionality is performed by injecting an object instance to the controller’s constructor:

private IProductBS bs;       
public ProductsController(IProductBS productBS) 
{ 
    bs = productBS;            
}

Many third-party tools provide the static methods which we can also directly access from the ASP.NET Core. But for those that need one or more abstract layers, accessing the abstract layer instance through the DI is the ideal approach. The AutoMapper tool used in the sample application is an example. To make the AutoMapper work well with the ASP.NET Core DI container, the steps are given below:

  1. Download the AutoMapper package through the Nuget.

  2. Create the IAutoMapConverter interface:

    public interface IAutoMapConverter<TSourceObj, TDestinationObj> 
        where TSourceObj : class 
        where TDestinationObj : class 
    { 
        TDestinationObj ConvertObject(TSourceObj srcObj); 
        List<TDestinationObj> ConvertObjectCollection(IEnumerable<TSourceObj> srcObj); 
    }
  3. Add the code into the AutoMapConverter class:

    public class AutoMapConverter<TSourceObj, 
           TDestinationObj> : IAutoMapConverter<TSourceObj, TDestinationObj> 
         where TSourceObj : class 
         where TDestinationObj : class 
    { 
        private AutoMapper.IMapper mapper; 
        public AutoMapConverter() 
        { 
            var config = new AutoMapper.MapperConfiguration(cfg => 
            { 
                cfg.CreateMap<TSourceObj, TDestinationObj>(); 
            }); 
            mapper = config.CreateMapper(); 
        }
    
        public TDestinationObj ConvertObject(TSourceObj srcObj) 
        { 
             return mapper.Map<TSourceObj, TDestinationObj>(srcObj); 
        }
    
        public List<TDestinationObj> 
               ConvertObjectCollection(IEnumerable<TSourceObj> srcObjList) 
        { 
            if (srcObjList == null) return null; 
            var destList = srcObjList.Select(item => this.ConvertObject(item)); 
            return destList.ToList(); 
        } 
    }
  4. Add this instance registration line into the Startup.ConfigureServices() method:

    services.AddScoped(typeof(IAutoMapConverter<,>), typeof(AutoMapConverter<,>));
  5. Inject the AutoMapConverter instance into the caller class constructor:

    private IAutoMapConverter<Entities.Contact, Models.Contact> mapEntityToModel; 
    public ContactsController
        (IAutoMapConverter<Entities.Contact, Models.Contact> convertEntityToModel) 
    { 
        this.mapEntityToModel = convertEntityToModel; 
    }
  6. Call a method in the initiated object instance (see ContactController.cs for details):

    var convtList = mapEntityToModel.ConvertObjectCollection(rtnList);

The patterns and practices of the built-in dependent injections are basically the same across the .NET Core version 2.x through 5.0. No code change is needed for the newer .NET Core versions.

Accessing Application Settings

The .NET Core application uses more versatile configuration API. But for the ASP.NET Core application, setting and getting items from the AppSetting.json file is the prevailing option which is quite different from using the web.config XML file in the ASP.NET Web API application. If any configuration value is needed in the new SM.Store.CoreApi, there are two approaches to access the value after the Configuration object has been built.

  1. Where the Configuration object with the IConfiguration or IConfigurationRoot type can be directly accessible, specify the Configuration array item, such as the code in the Startup.cs:

    //Set database. 
    if (Configuration["AppConfig:UseInMemoryDatabase"] == "true") 
    { 
        services.AddDbContext<StoreDataContext>
                 (opt => opt.UseInMemoryDatabase("StoreDbMemory")); 
    } 
    else 
    { 
        services.AddDbContext<StoreDataContext>(c => 
            c.UseSqlServer(Configuration.GetConnectionString("StoreDbConnection"))); 
    }
  2. Link the strong typed custom POCO class object to the Option service:

    POCO class:

    public class AppConfig 
    { 
        public string TestConfig1 { get; set; } 
        public bool UseInMemoryDatabase { get; set; } 
    }

    Code in the Startup.ConfigurationServices():

    //Add Support for strongly typed Configuration and map to class 
    services.AddOptions(); 
    services.Configure<AppConfig>(Configuration.GetSection("AppConfig"));

    Then access the config item via injecting the Option service instance to the constructor of caller classes.

    private IOptions<AppConfig> config { get; set; } 
    public ProductsController(IOptions<AppConfig> appConfig) 
    {    
        config = appConfig; 
    }
    
    //Get config value. 
    var testConfig = config.TestConfig1;

What if the caller is from a static class in which no constructor is available? One of the resolutions is to change the static classes to regular ones. For migrating a legacy .NET Framework application having many static classes to the .NET Core application, however, these sort of changes plus related impacts could be very large.

The new SM.Store.CoreApi provides a utility class file, StaticConfigs.cs, to get any item value from the AppSettings.json file. The logic is to pass a configuration key name to the static method, GetConfig(), in which the same ConfigurationBuilder as in the Startup class is used to parse the JSON data and return the key’s value.

//Read key and get value from AppConfig section of AppSettings.json. 
public static string GetConfig(string keyName) 
{ 
    var rtnValue = string.Empty; 
    var builder = new ConfigurationBuilder() 
        .SetBasePath(Directory.GetCurrentDirectory()) 
        .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) 
        .AddEnvironmentVariables();
    
    IConfigurationRoot configuration = builder.Build(); 
    var value = configuration["AppConfig:" + keyName]; 
    if (!string.IsNullOrEmpty(value)) 
    { 
        rtnValue = value; 
    } 
    return rtnValue; 
}

This method can be called anywhere in the application. An example of calling this method is the code from the static class StoreDataInitializer in the SM.Store.Api.DAL project.

public static class StoreDataInitializer 
{ 
    public static void Initialize(StoreDataContext context) 
    { 
        if (StaticConfigs.GetConfig("UseInMemoryDatabase") != "true") 
        { 
            context.Database.EnsureCreated(); 
        } 
        - - - 
    } 
    - - - 
}

If you would like, an ASP.NET Core application can still use the web.config file for any legacy item from the appSettings XML section. However, the ConfigurationManager.AppSettings collection doesn’t work on the web.config in the ASP.NET Core which is a console application. To resolve this issue, the StaticConfigs.cs also contains the method, GetAppSetting(), for obtaining the AppSetting values from the web.config file in the Core project root:

//Read key and get value from AppSettings section of web.config. 
public static string GetAppSetting(string keyName) 
{ 
    var rtnString = string.Empty; 
    var configPath = Path.Combine(Directory.GetCurrentDirectory(), "Web.config");            
    XmlDocument x = new XmlDocument(); 
    x.Load(configPath); 
    XmlNodeList nodeList = x.SelectNodes("//appSettings/add"); 
    foreach (XmlNode node in nodeList) 
    { 
        if (node.Attributes["key"].Value == keyName) 
        { 
            rtnString = node.Attributes["value"].Value; 
            break; 
        } 
    } 
    return rtnString;            
} 

Getting the configuration value is also a one-line call by passing the key name.

var testValue = StaticConfigs.GetAppSetting("TestWebConfig");

Entity Framework Core Related Changes

The SM.Store.CoreApi with the Entity Framework Core still uses the code-first workflow. The coding structures should be mostly the same when porting from the EF6 to EF Core 2.x through 5.0 for the application. Some issues and concerns, however, need to be taken care of regarding the changes of EF versions and behaviors.

Primary Key Identity Insert Issue

If using the existing models for the EF Core projects, the manually seeded primary key values cannot be inserted due to the IDENTITY INSERT is set to ON in the database side by default. The EF6 automatically handles the issue, which turns off the identity insert if any key column is specified and value provided, or use the identity insert otherwise.

Take the ProductStatusType model for example. The below code works with the EF6:

public class ProductStatusType 
{ 
    [Key]    
    public int StatusCode { get; set; } 
    public string Description { get; set; } 
    public System.DateTime? AuditTime { get; set; }

    public virtual ICollection<Product> Products { get; set; } 
} 

The data seeding array includes the StatusCode column and values:

var statusTypes = new ProductStatusType[] 
{ 
    new ProductStatusType { StatusCode = 1, Description = "Available", 
                            AuditTime = Convert.ToDateTime("2017-08-26")}, 
    new ProductStatusType { StatusCode = 2, Description = "Out of Stock", 
                            AuditTime = Convert.ToDateTime("2017-09-26")}, 
    - - - 
};

With the EF Core, the DatabaseGeneratedOption.None needs to be explicitly added into the primary key attribute to avoid the failure resulting from explicitly providing the key and value. The above model should be updated like this:

public class ProductStatusType 
{ 
    [Key] 
    [DatabaseGenerated(DatabaseGeneratedOption.None)] 
    public int StatusCode { get; set; } 
    public string Description { get; set; } 
    - - - 
}

Data Context and Connection String

The legacy SM.Store.WebApi with the EF6 passes the connection string to the data context class like this:

public class StoreDataContext : DbContext 
{    
    public StoreDataContext(string connectionString) 
        : base(connectionString) 
    {                
    } 
    - - - 
}

The EF Core passes the DbContextOption object to the data context class. This would be a minor change in the class.

public class StoreDataContext : DbContext 
{    
public StoreDataContext(DbContextOptions<StoreDataContext> options) 
    : base(options) 
    { 
    } 
    - - - 
}

The DbContextOption items need to be specified when adding the data context into the DI container in the Startup.ConfigurationServices(). With this EF Core feature, we can use different data providers and database operation-related settings. In this sample application, either the in-memory or the SQL Server database can be enabled with the configuration settings.

//Set database. 
if (Configuration["AppConfig:UseInMemoryDatabase"] == "true") 
{ 
    services.AddDbContext<StoreDataContext>(opt => opt.UseInMemoryDatabase("StoreDbMemory")); 
} 
else 
{ 
    services.AddDbContext<StoreDataContext>(c => 
        c.UseSqlServer(Configuration.GetConnectionString("StoreDbConnection"))); 
}

The value of the SQL Server connection string for the SM.Store.CoreApi is configured in the standard ConnectionStrings section of the appsettings.json file. The database files are saved to the Windows login user folder for the LocalDB and the SQL Server defined data folder for any regular edition including the SQL Server Express. No option of LocalDB database file location can be set in the connection string as the EF6 does. If you need to specify a different file location for the LocalDB, you can open the LocalDB instance with the SQL Server Management Studio (SSMS, free version 18.x or the latest) and so either below options:

  • For existing database, right click the database instance in the Object Explorer > Properties > Database Settings. Change the paths of the database files under the Database default locations section.

  • For creating the new database, execute the script before running the sample application for the first time or after deleting the existing database.

    USE MASTER 
    GO 
    CREATE DATABASE [StoreCF8] 
    ON (NAME = 'StoreCF8.mdf', FILENAME = <your path>\StoreCF8.mdf') 
    LOG ON (NAME = 'StoreCF8_log.ldf', FILENAME = <your path>\StoreCF8_log.ldf'); 
    GO

Custom Repositories

Although Microsoft claims that the DbContext instance combines both Repository and Unit of Work patterns, custom repositories are still a good abstraction layer between the DAL and BLL of a multi-layer application. All repository files in the SM.Store.Api.DAL project should have no major changes when migrating from the legacy SM.Store.WebApi with the EF6 to the SM.Store.CoreApi with the EF Core 2.x through 5.0. Some updates are made due simply to outdated coding structures which should have already been corrected in the legacy application with the EF6:

  • Removing UnitOfWork class. The legacy SM.Store.WebApi wraps any repository class with the UnitOfWork class like this:

    private StoreDataContext context; 
    public class StoreDataUnitOfWork : IStoreDataUnitOfWork 
    { 
        public StoreDataUnitOfWork(string connectionString) 
        { 
            - - - 
            this.context = new StoreDataContext(connectionString); 
        }     
        - - - 
        public void Commit() 
        {            
            this.Context.SaveChanges();         
        } 
        - - - 
    }

    The UnitOfWork instance is then injected into any individual repository and the base GenericRepository:

    public class ProductRepository : GenericRepository<Entities.Product>, IProductRepository 
    { 
        public ProductRepository(IStoreDataUnitOfWork unitOfWork) 
            : base(unitOfWork) 
        { 
        } 
        - - - 
    }

    Although custom repositories are still needed and migrated, the unit-of-work practice seems redundant for applications (even with EF6). The data context class itself acts as the unit-of-work in which the SaveChanges() method updates pending changes in the current context all at once. In addition, for an application with multiple data context classes, we can use transaction related methods in the context’s Database object, such as UseTransaction, BeginTransaction, and CommitTransaction, to achieve the ACID results.

    In the SM.Store.CoreApi, the IUnitOfWork interface and UnitOfWork class no more exist. The StoreDataContext instance is directly injected into repository classes:

    public class ProductRepository : GenericRepository<Entities.Product>, IProductRepository 
    { 
        private StoreDataContext storeDBContext; 
        public ProductRepository(StoreDataContext context) 
            : base(context) 
        { 
            storeDBContext = context; 
        } 
         - - - 
    }

    In the GenericRepository class, the CommitAllChanges() method is replaced with the new Commit() method:

    Method in the obsolete UnitOfWork class:

    public virtual void CommitAllChanges() 
    { 
        this.UnitOfWork.Commit(); 
    }

    Method in the new GenericRepository class:

    public virtual void Commit() 
    { 
        Context.SaveChanges(); 
    }

    Moreover, any base Insert, Update, or Delete method in the GenericRepository class has the optional second argument to call the SaveChanges() method immediately if you would not like to call the Commit() method till last. Here is the Insert method, for example:

    public virtual object Insert(TEntity entity, bool saveChanges = false) 
    { 
        var rtn = this.DbSet.Add(entity); 
        if (saveChanges) 
        { 
            Context.SaveChanges(); 
        } 
        return rtn; 
    }
  • Making GenericRepository real generic. The GenericRepository class from the legacy Web API DAL only works on single data context since it receives the derived StoreDataContext instance passed via the StoreDataUnitOfWork.

    public class GenericRepository<TEntity> : 
                   IGenericRepository<TEntity> where TEntity : class 
    { 
        public IStoreDataUnitOfWork UnitOfWork { get; set; } 
        public GenericRepository(IStoreDataUnitOfWork unitOfWork) 
        { 
            this.UnitOfWork = unitOfWork; 
        } 
        - - - 
    }

    Hence, other generic repository classes with different names need to be created if there are multiple data context objects. In the GenericRepository class from the .NET Core DAL, the base DbContext is now injected into its constructor, allowing it to be used by any inheriting repository of other data context objects.

    public class GenericRepository<TEntity> : 
                    IGenericRepository<TEntity> where TEntity : class 
    {            
        private DbContext Context { get; set; } 
        public GenericRepository(DbContext context) 
        { 
            Context = context; 
        } 
        - - - 
    } 

    With such a change, all of the associated workflows run fine except for directly using the GenericRepository instance to obtain the simple lookup data sets. The legacy Web API code in the SM.Store.Api.Bll/LookupBS.cs file looks like this:

    //Instantiate directly from the IGenericRepository 
    private IGenericRepository<Entities.Category> _categoryRepository; 
    private IGenericRepository<Entities.ProductStatusType> _productStatusTypeRepository; 
            
    public LookupBS(IGenericRepository<Entities.Category> cateoryRepository, 
        IGenericRepository<Entities.ProductStatusType> productStatusTypeRepository) 
    { 
        this._categoryRepository = cateoryRepository; 
        this._productStatusTypeRepository = productStatusTypeRepository; 
    }

    The GenericRepository instance from the .NET Core DAL cannot be directly used by the classes in the BLL project since it needs an instantiated data context, StoreDbContext, not the base DbContext, to be injected into the GenericRepository. To keep almost the same code lines in the LookupBS.cs, we need the new IStoreLookupRepository.cs with empty member and StoreLookupRepository.cs with the code only for its constructor:

    public interface IStoreLookupRepository<TEntity> : IGenericRepository<TEntity>  
                        where TEntity : class 
    {        
    }
    
    public class StoreLookupRepository<TEntity> : GenericRepository<TEntity>, 
                   IStoreLookupRepository<TEntity> where TEntity : class 
    { 
        //Just need to pass db context to GenericRepository. 
        public StoreLookupRepository(StoreDataContext context) 
            : base(context) 
        {            
        }        
    }

    Then in the LookupBS.cs, simply replace the text “GenericRepository” with the “StoreLookupRepository”. It’s now working the same as before the migration from ASP.NET Web API to the ASP.NET Core.

    The code implementation for the GenericRepository is kept the same when migrating the .NET Core and EF Core version from 2x through 5.0 for the application.

  • Updating to Async methods if available. The group of Async methods has already been provided in the EF6. The SM.Store.WebApi doesn’t use any due to early implementations. My plan of the migration includes the work on updating the methods to perform asynchronous operations whenever possible in the SM.Store.CoreApi application. Audiences can look into the project files for detailed changes but the outlines of the changes are listed here.

    • Adding another set of methods with the Async operations in the GenericRepository.
    • Changing the existing methods to Async operations for all possible processes.
    • Making related changes in the BLL and API controller code accordingly.

    There is an exception. The Async approach doesn't support any method with an output argument. Thus, all methods with any output argument in the new DAL, BLL, and API controllers have still been kept the non-async originals.

LINQ Expression Refurbishment (EF Core 3.x and above)

The EF Core 3.x poses many breaking changes over the EF Core 2x, in which the most prominent one is to remove the LINQ evaluation on the client. Although this increases the data access performance and reduces the chance of SQL injection, migrating an application with the EF from previous version to Core 3.x and above may need tremendous code changes and tests, especially for any large enterprise data application using a large number of LINQ-to-SQL queries.

In the sample application, the major LINQ query for getting the sorted and paginated data list uses the GroupJoin and SelectMany with lambda expressions works fine with the EF Core 2.x but not 3.x and above. The below IQueryable code cannot be converted to the correct SQL query and therefore renders the error message "NavigationExpandingExpressionVisitor failed".

var query = storeDBContext.Products
    .GroupJoin(storeDBContext.Categories,
    p => p.CategoryId, c => c.CategoryId,
    (p, c) => new { p, c })
    .GroupJoin(storeDBContext.ProductStatusTypes,
    p1 => p1.p.StatusCode, s => s.StatusCode,
    (p1, s) => new { p1, s })
    .SelectMany(p2 => p2.s.DefaultIfEmpty(), (p2, s2) => new { p2 = p2.p1, s2 = s2 })
    .Select(f => new Models.ProductCM
    {
        ProductId = f.p2.p.ProductId,
        ProductName = f.p2.p.ProductName,
        CategoryId = f.p2.p.CategoryId,
        CategoryName = f.p2.p.Category.CategoryName,
        UnitPrice = f.p2.p.UnitPrice,
        StatusCode = f.p2.p.StatusCode,
        StatusDescription = f.s2.Description,
        AvailableSince = f.p2.p.AvailableSince
    });

The code is working for the EF Core 3.x and above when the LINQ expression is changed to using the "from...join...select" syntax with the SQL OUTTER JOIN scenario.

var query =
    from pr in storeDBContext.Products
    join ca in storeDBContext.Categories
        on pr.CategoryId equals ca.CategoryId
    join ps in storeDBContext.ProductStatusTypes
        on pr.StatusCode equals ps.StatusCode into tempJoin
    from t2 in tempJoin.DefaultIfEmpty()
    select new Models.ProductCM
    {
        ProductId = pr.ProductId,
        ProductName = pr.ProductName,
        CategoryId = pr.CategoryId,
        CategoryName = ca.CategoryName,
        UnitPrice = pr.UnitPrice,
        StatusCode = pr.StatusCode,
        StatusDescription = t2.Description,
        AvailableSince = pr.AvailableSince
    };

This change also impacts the other parts of the code. In the SM.Store.Api.Common/GenericSorterPager.cs and SM.Store.Api.Common/GenericMultiSorterPager.cs, the exiting code in the GetSortedPagedList() method calls the IQureryable.Distinct() method for returning the distinctive data items in the resultset.

public static IList<T> GetSortedPagedList<T>(IQueryable<T> source, PaginationRequest paging)
{
    - - -          
    source = source.Distinct();
	- - -
}

The change to using the "from...join...select" syntax requires for adding the default OrderBy to the LINQ query. Otherwise, it will render the error "ORDER BY items must appear in the select list if SELECT DISTINCT is specified" if any call contains no sorting request. The below code piece needs to be added into the IQueryable source that passes to the GetSortedPagedList() method call to sort the select list by the primary key as default setting.

if (paging != null && (paging.Sort == null || string.IsNullOrEmpty(paging.Sort.SortBy)))
{
    paging.Sort = new Models.Sort() {SortBy = "ProductId"};
}

Executing Stored Procedures

Different EF versions support different methods to execute stored procedures with the EF data context although the same SqlParameter items are used as the method arguments. The code pieces of the sample applications demonstrate the details. Also note that the method used for stored procedure execution is actually to execute any raw SQL script.

  • EF6: The existing SM.Store.WebApi directly uses the Database.SqlQuery<T>() method to execute the GetPagedProductList stored procedure.

    var result = this.Database.SqlQuery<Models.ProductCM>("dbo.GetPagedProductList " +
        "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
    	_filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 2.0: [Note: This version has not been supported by Microsoft so that I remove the source code in the ASP.NET Core 2.0 with the current update. The approach of executing a stored procedure in the Core 2.0 is still kept here for history references]

    The Database.SqlQuery method is not supported by the EF Core. As an alternative, we can use the FromSql method for the Dbset<T>. To implement this approach, the below steps are needed.

    1. Create a property with the Dbset<T> type. The dynamic model is the stored procedure return type.

      //Needed for calling stored procedures with .NET Core 2.0 EF.
      public DbSet<Models.ProductCM> ProductCM_List { get; set; }
    2. Modify the POCO model with attributes NotMapped and Key. Especially for the Key, if not set, it would render the runtime error "The entity type 'ProductCM' requires a primary key to be defined".

      [NotMapped]
      public partial class ProductCM
      {
          [Key]
          public int ProductId { get; set; }
          public string ProductName { get; set; }
          - - -
      }
    3. Then use the FromSql method of the Dbset property to execute the stored procedure.

      var result = this.ProductCM_List.FromSql("dbo.GetPagedProductList " +
          "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
          _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 2.1: [Note: Also for Core 2.2 which has not been supported by Microsoft.]

    The Core 2.1 introduced the DbContext.Query method of which the same FromSql method can be called to execute stored procedures. For the best code practice, the Query<T>() with the return type would also be defined in the OnModelCreating method.

    protected override void OnModelCreating(ModelBuilder builder)
    {
        - - -
        //For GetProductListSp.
        builder.Query<Models.ProductCM>();
    }

    Then the calling stored procedure method in the GetProductListSp() uses the Query<T>() like this:

    var result = this.Query<Models.ProductCM>().FromSql("dbo.GetPagedProductList " +
        "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
        _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 3.x and above: The Query<T>() is removed and the Dbset<T> is back to the play. The FromSqlRaw() or FromSqlInterpolated() replaces the FromSql() for executing the stored procedures.

    var result = this.ProductCMs.FromSqlRaw("dbo.GetPagedProductList " +
         "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
         _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();

    The ProductCMs is declared as the Dbset<T> property without the PK key.

    public DbSet<Models.ProductCM> ProductCMs { get; set; }
    
    - - -
    
    protected override void OnModelCreating(ModelBuilder builder)
    {
        - - -    
        builder.Entity<Models.ProductCM>().HasNoKey();
    }

    Although it's working, I still have two negative comments on the approach of stored procedure executions with the EF Core 3.x and above.

    1. There is no way to avoid creating the physical table for the DbSet<T> property. Using the Ignore(), [NotMapped] attribute, or builder.Entity<T>.ToTable(null) are all not relevant. I have to manually delete the table (ProductCM in this case) from the database initiated or migrated.

    2. The syntax of the FromSqlInterpolated() method is nice but it doesn't support executing any stored procedure that has any output parameter. Using the FromSqlRaw() with the SqlParameter objects works though.

Custom Model Binder

I previously shared my work on a custom model binder for passing complex hierarchical object in a query string to ASP.NET Web API methods. When I copied the file, FieldValueModelBinder.cs, to the ASP.NET Core library project, SM.Store.Api.Common, and resolved all references, errors still occurred. The IModelBinder interface type comes from the Microsoft.AspNetCore.Mvc.ModelBinding namespace whereas it previously was a member of the System.Web.Http.ModelBinding. It's a major change since the HttpContext is composed by a set of new request features, which breaks the compatibility to the ASP.NET Web API version.

Fortunately, I could re-map the objects, properties, and methods to the new available ones. In addition, the only implemented method, BindModel(), would be switched to the asynchronous type, BindModelAsync(), with return type as the Task.

Here is the BindModel() method in the SM.Store.WebApi with the .NET Framework 4x.

public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) 
{ 
    //Check and get source data from uri 
    if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query)) 
    {                
        kvps = actionContext.Request.GetQueryNameValuePairs().ToList(); 
    } 
    //Check and get source data from body 
    else if (actionContext.Request.Content.IsFormData()) 
    {                
        var bodyString = actionContext.Request.Content.ReadAsStringAsync().Result; 
        try 
        { 
            kvps = ConvertToKvps(bodyString); 
        } 
        catch (Exception ex) 
        { 
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message); 
            return false; 
        } 
    } 
    else 
    { 
        bindingContext.ModelState.AddModelError(bindingContext.ModelName, "No input data"); 
        return false; 
    }            
    //Initiate primary object 
    var obj = Activator.CreateInstance(bindingContext.ModelType); 
    try 
    {                
        //First call for processing primary object 
        SetPropertyValues(obj); 
    } 
    catch (Exception ex) 
    { 
        bindingContext.ModelState.AddModelError( 
            bindingContext.ModelName, ex.Message); 
        return false; 
    } 
    //Assign completed object tree to Model 
    bindingContext.Model = obj; 
    return true; 
}

The BindModeAsync() method in the new SM.Store.CoreApi seems more concise than the ASP.NET Web API version:

public Task BindModelAsync(ModelBindingContext bindingContext) 
{    
    //Check and get source data from query string. 
    if (bindingContext.HttpContext.Request.QueryString != null) 
    { 
        kvps = bindingContext.ActionContext.HttpContext.Request.Query.ToList();     
    } 
    //Check and get source data from request body (form). 
    else if (bindingContext.HttpContext.Request.Form != null) 
    { 
        try 
        { 
            kvps = bindingContext.ActionContext.HttpContext.Request.Form.ToList(); 
        } 
        catch (Exception ex) 
        { 
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
        } 
    } 
    else 
    { 
        bindingContext.ModelState.AddModelError(bindingContext.ModelName, "No input data");
    }            
            
    //Initiate primary object 
    var obj = Activator.CreateInstance(bindingContext.ModelType); 
    try 
    {                
        //First call for processing primary object 
        SetPropertyValues(obj);

        //Assign completed object tree to Model and return it. 
        bindingContext.Result = ModelBindingResult.Success(obj); 
    } 
    catch (Exception ex) 
    { 
        bindingContext.ModelState.AddModelError( 
            bindingContext.ModelName, ex.Message);               
    } 
    return Task.CompletedTask; 
}

The KeyValuePair (kvps) type returned from the …Request.Query.ToList() and …Request.Form.ToList() is now the List<KeyValuePair<string, StringValues>> instead of List<KeyValuePair<string, string>>. Thus, any related reference and code line need to be changed accordingly, mostly for the object declarations and assignments:

List<KeyValuePair<string, StringValues>> kvpsWork; 
- - - 
kvpsWork = new List<KeyValuePair<string, StringValues>>(kvps);

After making those changes, everything of the ASP.NET Core model binder works the same as the legacy version. More test cases are provided in the file, TestCasesForDataServices.txt included in the downloadable AspNetCore5.0_DataService. You can enter any URL with the query string into the request input area of the Postman, and then click Send button. The complex object structures and values based on the query string will be shown in the response section.

Image 5

The above test case shows a multiple-column sorting scenario that is supported by the custom FieldValueModelBinder in the ASP.NET Core sample applications. More test cases like the below URL are provided in the TestCasesForDataServices.txt file. You may need to change the port number in the URLs to point to the correct website you set.

http://localhost:7200/api/getproductlist?ProductSearchFilter[0]
ProductSearchField=CategoryID&ProductSearchFilter[0]ProductSearchText=2&PaginationRequest[0]
PageIndex=0&PaginationRequest[0]PageSize=10&PaginationRequest[0]SortList[0]SortItem[0]
SortBy=StatusDescription&PaginationRequest[0]SortList[0]SortItem[0]
SortDirection=desc&PaginationRequest[0]SortList[1]SortItem[1]
SortBy=ProductName&PaginationRequest[0]SortList[1]SortItem[1]SortDirection=asc

The structures and code of the FieldValueModelBinder are compatible across the .NET Core versions 2x through 5.0. There is no breaking change between these versions.

Using IIS Express and Local IIS

One of the prominent changes from the ASP.NET Web API to the ASP.NET Core is the application output and host types even though I’m only concerned about the applications running in the Windows systems. The migrated SM.Store.CoreApi in ASP.NET Core version 2.1 (actually any .NET Core version before 2.2) is the out-process-only console application that runs on the built-in Kestrel web server by default. We can still use the IIS Express as a wrapper for the development environment especially with the Visual Studio. We can also use the IIS as a reverse proxy to relay the requests and responses for all environments. Behind the scene, a structure called ASP.NET Core Module plays rolls in managing all processes and coordinating functionalities from the IIS/IIS Express and Kestrel web server. The ASP.NET Core Module is automatically installed with the Visual Studio 2017/2019 installation on the development machine.

The in-process hosting when using the IIS is available in the ASP.NET Core 3.x and above (as an option in the ASP.NET Core 2.2). If you download and open the sample application in the ASP.NET Core 3.1 or 5.0, and use the steps mentioned below to setup the local IIS website, you can already run the site with the IIS in-process hosting. However, you need to install ASP.NET Core Hosting Bundle for version 3.1 or 5.0.

When starting the legacy SM.Store.WebApi within the Visual Studio 2017/2019, the sample application runs the website under the IIS Express process. You can also easily start the Web API by executing the IIS Express with command lines or a batch file.

"C:\Program Files\IIS Express\iisexpress.exe" /site:SM.Store.Api.Web
      /config:"<your <code>SM.Store.WebApi</code> path>\.vs\config\applicationhost.config"

The same command line execution of the IIS Express doesn’t work for the SM.Store.CoreApi application since it runs in a console application process separate from the IIS Express worker process. If you would like to keep the SM.Store.CoreApi and IIS Express running for providing data services to multiple clients in the development environment, simply follow these steps:

  • Open the SM.Store.CoreApi with the Visual Studio 2017/2019 instance.
  • Press Ctrl + F5. The starting page will be shown in the selected browser.
  • Close the browser and minimize the Visual Studio instance.
  • The IIS Express is now running in the Windows’ background for receiving any HTTP request from client calls.

If more stable and persistent data services are needed on the development machine, you can publish the SM.Store.CoreApi to the local IIS using the approaches similar to those for the traditional ASP.NET website or Web API applications. These are major setup steps:

  1. Open the SM.Store.CoreApi in the Visual Studio 2017/2019 and highlight the solution, select Publish SM.Store.CoreApi from the Build menu, select Folder as the publishing target, specify your folder path for your Folder Profile, and then click Publish.

  2. Open the IIS manager (inetmgr.exe), select Application Pools and then Add Application Pool…, enter the name StoreCorePool, and select the No Managed Code from the .NET CLR Version dropdown.

  3. Right click Sites/Default Web Site, and select Add Application. Enter the StoreCore as Alias, select the StoreCorePool from the Application pool dropdown, and then enter (or browse to) your folder path that holds the published application files.

  4. Right click the Default Web Site, select Manage Website and then Restart. Since the SM.Store.CoreApi application uses the in-memory database as the initial setting, you can now access the data service methods using any client tool with the URL http://localhost/storecore/api/<method-name>.

Note that the application pool name is no more the application running process identity so that it cannot be passed as an authorization account to access other resources from the application. For example, if you try to access the data in your local SQL Server or SQL Server Express instance from the SM.Store.CoreApi with the local IIS using the “integrated security=True” or “Trusted_Connection=True”, you will get the SQL Server access permission error

If you need to run the SM.Store.CoreApi application with the local IIS and the SQL Server database instead of using the in-memory database that is only for simple test cases and doesn't support stored procedures, there are two options:

  1. Mapping the application pool account, IIS AppPool\StoreCorePool, to the SQL Server login and user. You can then use the existing database connection string in the data service application with the integreted security.

    • Open the Object Explorer in the SSMS tool, and then expand the Security.
    • Right click Logins > New Login...
    • Enter IIS AppPool\StoreCorePool as Login name. Do not click Search... button.
    • Select the "StoreCF8" (or your database name) from the Default database dropdown list.
    • Open User Mapping on the upper left panel, and select the "StoreCF8" (or your database name).
    • Select the "db datareader" and "db datawriter" in the Database role membership for: panel.
    • Back to the Object Explorer.
    • Right-click the StoreCF8 database > Properties > Permissions
    • Highlight the IIS APPPOOL\StoreCorePool on the Users or roles panel. 
    • Check the Grant box for the Execute from the Explicit Permissions list.
  2. Creating a specific SQL Server user for the login and role mapping, and also granting the execute permission. You can run the script with the SSMS:

    --Create login and user.
    USE master
    GO
    CREATE LOGIN WebUser WITH PASSWORD = 'password123',
    DEFAULT_DATABASE = [StoreCF8],
    CHECK_POLICY = OFF,
    CHECK_EXPIRATION = OFF;
    GO
    
    USE StoreCF8
    GO
    IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'WebUser')
    BEGIN
        CREATE USER [WebUser] FOR LOGIN [WebUser]
        EXEC sp_addrolemember N'db_datareader', N'WebUser'
    	EXEC sp_addrolemember N'db_datawriter', N'WebUser'
    	EXEC sp_addrolemember N'db_ddladmin', N'WebUser'	
    END;
    GO 
    
    GRANT EXECUTE TO WebUser;
    GO

    The above script is included in the StoreCF8.sql file from the downloaded source. You can actually run the entire script in this file to create the SQL Server database with the login user and then enable or update the connection string in the appsettings.json file under the published folder of SM.Store.CoreApi for the SQL Server instance:

    "ConnectionStrings": { 
       "StoreDbConnection": "Server=<your SQL Server instance>;Database=StoreCF8;
               User Id=WebUser;Password=password123;MultipleActiveResultSets=true;" 
    }

With either of the above options, you also need to remove this line for the in-memory database in the appsettings.json file (or replace the value "true" with "false"):

"UseInMemoryDatabase": "true"

The SM.Store.CoreApi application with local IIS and SQL Server database should now be working on your local machine.

Summary

Migrating a data service application to higher tool or framework versions needs efforts on rewriting the code and solving more or less problems in respect to project types, settings, built-in tools, workflow, running processes, hosting schemes, etc. The samples, code, and discussions in this article can help developers catch up essences of the migration tasks on different versions of the ASP.NET applications and also speed up the code work on newer versions of the ASP.NET Core applications.

History

  • 10th January, 2018

    • Initial post

  • 17th January, 2018

    • Added test cases for custom model binder and updated source code

  • 2nd August, 2018

    • ASP.NET Core 2.1 version of the sample application is available

  • 1st March, 2019

    • Upgraded source code of the sample application with the ASP.NET Core 2.2
    • Updated text in some sections
  • 7th June, 2019

    • Added the sub-section of Executing Stored Procedures
    • Removed the section of Enable CORS for Localhost (no cross-domain issue for different localhost ports with later versions of VS 2017 and VS 2019)
    • Edited text in several sections
    • Updated the source code files. All types of the sample application projects now support the multiple-column sorting for paginated data list.
  • 27th November, 2019

    • Updated the sample application with the ASP.NET Core 3.0
    • Added sections and paragraphs into the article for the ASP.NET Core 3.0
    • Edited existing text in all sections
  • 29th November, 2020

    • Updated the sample application with the ASP.NET Core 5.0 and 3.1
    • Updated the downloadable source code zip list to those with .NET versions currently supported by Microsoft (.NET 5, .NET Core 3.1, .NET Core 2.1, and .NET Framework 4x)
    • Edited existing text in sections related to the changes with the ASP.NET Core 5.0 and 3.1

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here