Click here to Skip to main content
13,549,128 members
Click here to Skip to main content
Add your own
alternative version

Stats

12.8K views
850 downloads
33 bookmarked
Posted 10 Jan 2018
Licenced CPOL

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

, 17 Jan 2018
Rate this:
Please Sign up or sign in to vote.
Showing a full-structured data service sample application migrated from ASP.NET Web API 2.0 to ASP.NET Core 2.0 MVC and also describing the various issues and resolutions.

Introduction

I have recently completed the work on moving the ASP.NET Web API sample application, SM.Store.WebApi, to the ASP.NET Core 2.0 MVC data service application, SM.Store.CoreApi. My objectives are to take the full coding and structural advantages of the Core (except using it in other platforms) but still keep the same functionality and request/response signatures and workflow. Thus, any client application will not be affected by the migration of the data service application. This article is not the step-by-step tutorials, for which audiences can reference other resources if needed, but rather shares the completed sample application source code together with below topics and issue resolutions:

Set up and Run Sample Applications

The existing SM.Store.WebApi can run with the Visual Studio 2015 or 2017. Please see my previous post for how to set up and run the application. You can use this sample application as the pre-migration source for comparisons.

To run the new SM.Store.CoreApi that is migrated from the existing SM.Store.WebApi, you need Visual Studio 2017 version 15.3 or above and DotNet Core 2.0 SDK installed on your machine. 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 the Visual Studio, you can select one of 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.

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:

The TestCasesForDataServices.txt file contains many cases of requesting data items. Feel free to use the cases for your test calls to both new SM.Store.CoreApi and existing 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 or rename it to something else 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.

"StoreDbConnection": "Server=(localdb)\\<your-instance-name>; Database=StoreCF7;Trusted_Connection=True;MultipleActiveResultSets=true;"

Library Projects

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

When migrating to the Core, those projects must be converted to the .NET Core class library projects targeting to either .NET Core 2.0 or .NET Standard 2.0. For more compatibility and flexibility, I created new .NET Standard 2.0 projects in the ASP.NET Core solution and moved all folders and files from existing projects to the new projects. The completed new solution is like this.

Some migration details are explained below.

  • The existing 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 existing SM.Store.Api.Entities and SM.Store.Api.Models were merged into the new 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 2.0 project, SM.Store.Api.Web. There is no need to separate those controller classes to another project targeting to the .NET Core 2.0.

  • When copying existing .NET Framework class files to the .NET Standard 2.0 projects, the most references to the .NET Framework 4x assemblies should already been included since the .NET Standard 2.0 is the contracts of most .NET Framework 4x implementations. In case any .NET Framework item is not found, then the package needs to manually be downloaded from the NuGet, such as the System.Configuration.ConfigurationManager if you need to use it in any project.

  • The .NET Standard 2.0 project template doesn’t automatically include any component from the .NET Core 2.0. Thus, if any reference from the .NET Core 2.0 is needed, the component should also manually be added into the project from the NuGet. As an example, the Microsoft.ASpNetCore.Mvc package is added into the SM.Store.Api.Common for being used by the custom model binder.

  • Renaming a .NET Core project doesn’t seem easy as doing so for a .NET Framework 4x project in the solution. In most cases, the project references will not automatically be restored with the Visual Studio 2017 (I use the version 15.4.4). You need to manually remove the existing references and reset the new references when renaming the projects. Any library reference is not affected by renaming a project.

Independency Injections

Since the existing SM.Store.WebApi application uses the Unity tool for the DI logic and the new SM.Store.CoreApi has the ConfigurationServices routine in the Startup class ready for settings including DI, switching 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 existing and new applications.

The type registration and mapping code in the Unity.config file of the existing 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 existing SM.Store.WebApi, only the IStoreDataUnitOfWork type registrtion 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 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 existing SM.Store.WebApi calls the DI factory method to instantiate the object instance:

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

In the new SM.Store.CoreApi, the 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 to 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, below are the steps.

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);

Access 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 prevail 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 an existing .NET Framework application having many static classes to the .NET Core application, however, this sort of changes plus related impacts could be very large.

The new SM.Store.CoreApi provides an 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 MVC 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 MVC 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");

Enable CORS for Localhost

There is no issue for the existing SM.Store.WebApi when running on the localhost website and an AJAX call request is sent from a browser with another localhost website but a different port number. The IIS or IIS Express process takes care of the “local cross-domain” operations. The new SM.Store.CoreApi, however, does not run under the IIS or IIS Express process (details on the later section). After the migration is completed and the new data services are up with the localhost, only AJAX calls from IE 11 client and tools such as Postman work fine. The AJAX calls from other browsers all render the error status code 415 “Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource”.

Note that if you need to reproduce the issue, you need to run another Javascript AJAX call application, such as the Angular application in my previous post.

Thus, we need to explicitly enable the CORS policy for the ASP.NET Core MVC applications. The policy needs to be added into the service collections by calling the middleware. Multiple policies with different policy names can be added for the application. Here is the code in the Startup.ConfigureServices() method for the SM.Store.CoreApi:

services.AddCors(options => 
{ 
    options.AddPolicy("CorsPolicy", 
        builder => builder 
            .AllowAnyOrigin() 
            .AllowAnyMethod() 
            .AllowAnyHeader() 
            .AllowCredentials()); 
});

The “CorsPolicy” can then be enabled either globally or at the controller/action level. The SM.Store.CoreApi enables it globally in the Startup.Configure method:

app.UseCors("CorsPolicy");

You can also only enable the “CorsPolicy” for a particular controller class or action method in the class by adding such an attribute:

[EnableCors("CorsPolicy")]

For a business application in the production, the CORS policy is usually managed at the infrastructure level, such as web server or network, based on the company’s policies and security requirements. As a common rule, the network and security teams in any enterprise company won’t allow these policies set within the application code. On the other hand, this kind of settings should not be concerned by developers when writing the code and running the application in the debugging mode. I guess that the CORS policies need to be set in the code here due mainly to the default web server Kestrel used by the ASP.NET Core MVC, which limits many high-level settings. The best way for now may be to make the enabling/disabling CORS policies configurable or set it in effect only for the development environment.

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 new project, SM.Store.Api.Common, and resolved all references, errors still occurred. The IModelBinder interface type now 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 now composed by a set of request features via the Kestrel web server, which breaks the compatibility to previous versions.

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 existing SM.Store.WebApi:

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 new model binder works the same as the old version. Although the "getproductlist" method uses the updated FieldValueModelBinder, more test cases are provided in the included file, TestCasesForModelBinder.txt. 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.

Entity Framework Core related changes

The new SM.Store.CoreApi with the Entity Framework Core still uses the code-first workflow. The coding structures should be almost the same when porting from the EF6 to EF Core for the application. Only limited number of changes needed to be concerned regarding the changes in the Entity Framework versions.

1. Primary Key identity insert issue

If using the existing models for the new 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. 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("2016-08-26")}, 
    new ProductStatusType { StatusCode = 2, Description = "Out of Stock", AuditTime = Convert.ToDateTime("2016-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; } 
    - - - 
}

2. Data Context and Connection String

The existing SM.Store.WebApi (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 new 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 17 or the latest) and then create the database with the script before running the sample application the first time or after deleting the existing database.

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

3. 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 existing SM.Store.WebApi with the EF6 to the new SM.Store.CoreApi with the EF Core. Some updates are made due simply to outdated coding structures which should have already been corrected in the existing application with the EF6:

  • Removing UnitOfWork class. The existing 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 to achieve the ACID results.

    In the new 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(); 
    }

    Moveover, 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 existing GenericRepository class 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 new GenericRepository class, 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 workflow run fine except for directly using the GenericRepository instance to obtain the simple lookup data sets. The existing 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 new GenericRepository instance 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 replaced the text “GenericRepository” with the “StoreLookupRepository”. It’s now working as the same as before the migration.

  • Updating to Async methods if available. The group of Async methods has already been provided in the EF6. The exising SM.Store.WebApi doesn’t use any. My plan of the migration includes the work on updating the methods to perform asynchronous operations whenever possible in the new 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 non-Queryable or non-Enumerable processes.
    • Making related changes in the BLL caller code accordingly.

    The application uses LinQ in many methods to obtain data lists with selection of needed columns/fields and custom models. Async approaches used with both the EF6 and EF Core, such as ToListAsync(), do not work for the Queryable and Enumerable processes and results. Thus, all Queryable-related methods in the new DAL and BLL projects have still been kept the non-async originals.

Using IIS Express and local IIS

One of the prominent changes from the ASP.NET Web API to the ASP.NET Core MVC is the application output and host types even I’m only concerned on the applications running in the Windows systems. The migrated SM.Store.CoreApi is now the out-process console application that runs on the built-in Kestrel web server. 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 Exprsss and Kestrel web server. The ASP.NET Core Module is automatically installed with the Visual Studio 2017 installation on the development machine.

When starting the sample application within the Visual Studio 2017, the existing SM.Store.WebApi 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 SM.Store.WebApi path>\.vs\config\applicationhost.config"

The same command line execution of the IIS Express doesn’t work for the new SM.Store.CoreApi application since it runs in a process separate from the IIS Express worker process. When the application starts with the Visual Studio 2017, the ASP.NET Core Module manages the links between the application, Kestrel web server, and IIS Express processes. 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 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 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 StoreCoreApiPool, 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 StoreCoreApiPool 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 even if the application pool account, IIS AppPool\StoreCoreApiPool, is mapped as the SQL Server login and user.

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, I recommend creating a specific SQL Server user for the login and role mapping. You can easily do this by running the script in the SSMS:

--Create login and user.
USE master
GO
CREATE LOGIN WebUser WITH PASSWORD = 'password123',
DEFAULT_DATABASE = [StoreCF7],
CHECK_POLICY = OFF,
CHECK_EXPIRATION = OFF;
GO

USE StoreCF7
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 

The above script is included in the StoreCF7.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=StoreCF7;User Id=WebUser;Password=password123;MultipleActiveResultSets=true;" 
}

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

Differences are apparent between the ASP.NET Core and ASP.NET Web API data service applications in respect to project types, settings, built-in tools, workflow, running processes, hosting schemes, etc. It needs more efforts and practices to migrating the existing to new version of the application. The samples, code, and discussions in this article can help developers catch up essences of the migration tasks and also speed up the coding work on the new ASP.NET Core applications.

History

  • 1/10/2018: Initial post.
  • 1/17/2018: Added test cases for custom model binder and updated source code. 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Shenwei Liu
United States United States
Shenwei is a software developer and architect, and has been working on business applications using Microsoft and Oracle technologies since 1996. He obtained Microsoft Certified Systems Engineer (MCSE) in 1998 and Microsoft Certified Solution Developer (MCSD) in 1999. He has experience in ASP.NET, C#, Visual Basic, Windows and Web Services, Silverlight, WPF, JavaScript/AJAX, HTML, SQL Server, and Oracle.

You may also be interested in...

Pro
Pro

Comments and Discussions

 
QuestionWhy DAL is being referenced directly from API layer Pin
Selwe2-Apr-18 7:18
memberSelwe2-Apr-18 7:18 
QuestionDifference Between IProductBS and IProductRepository Pin
scvoman30-Jan-18 1:48
memberscvoman30-Jan-18 1:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03-2016 | 2.8.180515.1 | Last Updated 17 Jan 2018
Article Copyright 2018 by Shenwei Liu
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid