ASP.NET Core:从ASP.NET Web API迁移的多层数据服务应用程序

目录

介绍

设置和运行示例应用程序

类库项目

依赖注入

访问应用程序设置

实体框架核心相关更改

主键标识插入问题

数据上下文和连接字符串

自定义存储库(Repositories)

LINQ表达式翻新(仅适用于EF Core 3.0)

执行存储过程

定制模型绑定器

使用IIS Express和本地IIS

摘要


介绍

RESTful数据服务API多年来一直是主流数据层应用程序类型。随着技术和框架的发展,旧的代码和结构将不可避免地被更新的代码和结构取代。对于将样本数据服务应用程序迁移到下一代ASP.NET或更高版本的工作,我的目标是利用更新的技术和框架的优势,但仍保持相同的功能以及请求/响应签名和工作流。因此,任何客户端应用程序都不会受到从ASP.NET Web APIASP.NET CoreASP.NET Core版本之间的数据服务迁移更改的影响。本文不是分步教学,读者可以根据需要查看目录中具体章节。

设置和运行示例应用程序

要使用首选的.NET Core版本运行SM.Store.CoreApi解决方案,您需要在计算机上分别安装Visual Studio 20192017.NET Core版本:

  • ASP.NET Core 3.0Visual Studio 2019(建议使用16.3.x或更高版本;包括.NET Core 3.0 SDK
  • ASP.NET Core 2.2Visual Studio 20192017 15.9(或更高版本)和.NET Core 2.2 SDK
  • ASP.NET Core 2.1Visual Studio 20192017 15.7(或更高版本)和.NET Core 2.1 SDK
  • ASP.NET Core 2.0Visual Studio 20192017 15.3(或更高版本)和.NET Core 2.0 SDK如果已安装的.NET Core版本是2.1.302(或更高版本),则可以在命令提示符窗口中使用命令dotnet --list-sdks查看所有已安装的.NET Core SDK库版本的列表。此命令仅在安装2.1.302或更高版本后可用。

我还建议下载并安装免费版本的Postman作为您的服务客户端工具。使用Visual Studio 打开并构建解决方案SM.Store.CoreApi后,可以从菜单栏上的IIS Express按钮下拉列表中选择一种可用的浏览器,然后单击该按钮以启动应用程序。

由于应用程序使用内存数据库和当前配置,因此最初不需要建立数据库。内置的起始页面将显示从服务方法调用获得的JSON格式的响应数据,这是在开发计算机上使用IIS Express启动服务应用程序的简单方法。

现在,您可以使Visual Studio会话保持打开状态,并使用Postman调用服务方法。结果(数据还是错误)将显示在响应部分:

可下载的源AspNetCore2.0-2.2_DataServicesAspNetCore3.0_DataServices包括TestCasesForDataServices_*.txt文件,该文件可用于所有类型的示例应用程序项目。该文件包含许多请求数据项的情况。可以将这些案例用于对新版SM.Store.CoreApi和旧版SM.Store.WebApi应用程序的测试调用。

如果您想使用SQL Server数据库或LocalDB,则可以打开appsettings.json文件并执行以下步骤。

  • 删除UseInMemoryDatabase行或在AppConfig部分将其值设置为false
  • 使用设置更新该ConnectionStrings部分下的StoreDbConnection值。例如,如果使用SQL Server LocalDB,则可以启用连接字符串并将<your-instance-name>替换为LocalDB实例名称。您甚至可以将StoreCF8更改为您自己的数据库名称。
"StoreDbConnection": "Server=(localdb)\\<your-instance-name>; 
 Database=StoreCF8;Trusted_Connection=True;MultipleActiveResultSets=true;"
  • 通过按F5启动Visual Studio解决方案时,将自动创建数据库并显示startup.html页面。

如果需要设置和运行旧版ASP.NET 5 Web API示例应用程序,则可以在下载WebApi_DataServices之后执行以下操作。

  •  使用Visual Studio 20172019 (也适用于2015版本)打开解决方案SM.Store.WebApi
  • 重新构建解决方案,该解决方案会自动从NuGet下载所有已配置的库。
  • 使用本地计算机上的旧版本设置SQL Server 20162017 LocalDB或其他SQL Server实例。请调整web.config文件中的connectionString 以指向您的数据库实例。
  • 确保SM.Store.Api.Web是启动项目,然后按F5键。这将启动IIS ExpressWeb API主机站点,在数据库实例中自动创建数据库,并使用所有示例数据记录填充表。
  • Web API项目中的测试页将被呈现,表明Web API数据提供者已准备好接收客户端调用。

如果您再次使用不同的ASP.NET版本运行示例应用程序,建议您删除具有相同名称的现有数据库,以便初始化一个新的数据库。或者,您可以在现有数据库上执行迁移任务。否则,可能会发生某些列映射错误。

类库项目

旧版的SM.Store.WebApi是具有多层.NET Framework类库结构的ASP.NET Web API 2应用程序。

迁移到ASP.NET Core时,必须将这些项目转换为以.NET Core.NET Standard框架为目标的.NET Core类库项目。可以使用项目类型.NET Standard来提高兼容性和灵活性。但是,即使项目类型不同,文件夹结构和文件也相同。对于非常原始的向.NET Core 2.0示例应用程序的迁移,将.NET Standard 2.x NetStandard.Library用作类库项目类型。对于具有更高版本.NET Core的示例应用程序,类库项目类型将切换为Microsoft.NETCore.AppVisual Studio解决方案中的完整项目如下所示:

下面说明了将示例应用程序从旧版ASP.NET Web API迁移到ASP.NET Core的一些详细信息:

  • 旧版的SM.Store.Api.DALSM.Store.Api.BLLSM.Store.Api.Common项目已迁移到具有相同名称的相应项目。
  • 旧版的SM.Store.Api.EntitiesSM.Store.Api.Models已合并到ASP.NET Core SM.Store.Api.Contracts项目中。所有接口也都移入了该项目,该项目可以被任何其他项目引用,但是在解决方案中没有任何其他项目的引用。
  • Web API控制器类已从SM.Store.Api项目移至主.NET Core项目SM.Store.Api.Web无需将那些控制器类与针对.NET Core的另一个项目分开。
  • 将旧版的.NET Framework类文件复制到.NET Framework 4x项目时,应已包含对程序及的大多数引用,因为.NET Standard 2.x是大多数.NET Framework实现的协定。如果未找到任何.NET Framework项,则需要从NuGet中手动下载该程序包,例如System.Configuration.ConfigurationManager,如果需要在任何项目中使用它。
  • .NET Standard,甚至.NET Core App、项目模板,可能不会自动包含任何需要的组件。因此,缺少的组件也应该从NuGet中手动添加到项目中。作为使用.NET Core 2x的示例应用程序的示例,该Microsoft.ASpNetCore.Mvc包已添加到SM.Store.Api.Common中,该项目供自定义模型绑定程序使用。.NET Core 3.0 Microsoft.NetCore.App已包含Microsoft.ASpNetCore.Mvc,因此无需在项目中显式引用它。

ASP.NET Core 2.x3.0Visual Studio解决方案中的库项目的结构和文件没有实质性变化。对于示例应用程序,框架引用类型Microsoft.NetCore.AppNetStandard.Library甚至可以交换库项目的不同ASP.NET Core版本。

依赖注入

由于旧版的SM.Store.WebApi应用程序使用Unity工具进行依赖注入(DI)逻辑,而新的SM.Store.CoreApiStartup类中具有ConfigurationServices例程,可以进行包括DI在内的设置,因此将Unity迁移到Core内置DI服务非常简单。不再需要低级DI Factory类的自定义代码和实例解析方法。Unity容器注册可以由Core服务配置代替。为了进行比较,我在旧应用程序和新应用程序中的SM.Store.Api.DALSM.Store.Api.BLL对象的设置代码行下面列出。

在旧版的SM.Store.WebApi Unity.config文件中的类型注册和映射代码:

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

SM.Store.CoreApiStartup.ConfigureServices()方法中的DI实例和类型注册:

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

请注意,在旧版SM.Store.WebApi中,只有IStoreDataUnitOfWork类型注册才具有singleton生存期管理器。所有其他类型都使用默认值,即注册的瞬态(Transient)生存期。该StoreDataUnitOfWork对象已过时,并且不被新SM.Store.CoreApi(在后面的部分中讨论)对象使用。现在,所有数据操作对象在迁移后都设置为Scoped生存期,这将对象实例持久保存在同一请求上下文中。

对象实例向构造函数的注入或调用者对对象实例的使用也没有变化,例如存储库和业务服务类。对于控制器类,旧版SM.Store.WebApi将调用DI工厂方法以实例化对象实例:

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

在新版SM.Store.CoreApi中,通过将对象实例注入到控制器的构造函数中来执行类似的功能:

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

许多第三方工具提供了我们也可以直接从ASP.NET Core访问的static方法。但是对于那些需要一个或多个抽象层的对象,通过DI访问抽象层实例是理想的方法。示例应用程序中使用的AutoMapper工具就是一个示例。为了使AutoMapperASP.NET Core DI容器一起正常工作,下面给出了以下步骤:

1、通过Nuget下载AutoMapper软件包。

2、创建IAutoMapConverter接口:

public interface IAutoMapConverter<TSourceObj, TDestinationObj> 
    where TSourceObj : class 
    where TDestinationObj : class 
{ 
    TDestinationObj ConvertObject(TSourceObj srcObj); 
    List<TDestinationObj> ConvertObjectCollection(IEnumerable<TSourceObj> srcObj); 
}

3、将代码添加到AutoMapConverter类中。

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、将此实例注册行添加到Startup.ConfigureServices()方法中:

services.AddScoped(typeof(IAutoMapConverter<,>), typeof(AutoMapConverter<,>));

5、将AutoMapConverter实例注入调用者类的构造函数中:

private IAutoMapConverter<Entities.Contact, Models.Contact> mapEntityToModel; 
public ContactsController
    (IAutoMapConverter<Entities.Contact, Models.Contact> convertEntityToModel) 
{ 
    this.mapEntityToModel = convertEntityToModel; 
}

6、在启动的对象实例中调用一个方法(有关详细信息,请参见ContactController.cs):

var convtList = mapEntityToModel.ConvertObjectCollection(rtnList);

内置依赖注入的模式和实践在.NET Core 2.x3.0版中基本相同。较新的.NET Core版本无需更改代码。

访问应用程序设置

.NET Core应用程序使用更通用的配置API。但是对于ASP.NET Core应用程序,从AppSetting.json文件设置和获取项目是主要选项,这与在ASP.NET Web API应用程序中使用web.config XML文件完全不同。如果新版SM.Store.CoreApi中需要任何配置值,则有两种方法可以在Configuration对象创建后访问该值。

1、如果可以直接访问IConfigurationIConfigurationRoot类型的Configuration对象,请指定Configuration数组项,例如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、将强类型的定制POCO类对象链接到Option服务:

POCO类:

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

Startup.ConfigurationServices()中的代码:

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

然后通过将Option服务实例注入到调用者类的构造函数中来访问配置项。

 

private IOptions<AppConfig> config { get; set; } 
public ProductsController(IOptions<AppConfig> appConfig) 
{    
    config = appConfig; 
}

//Get config value. 
var testConfig = config.TestConfig1;

如果调用者来自没有构造函数的static类,该怎么办?解决方案之一是将static类更改为常规类。但是,对于将具有许多static类的旧版.NET Framework应用程序迁移到.NET Core应用程序,这些变化以及相关影响可能会非常大。

新版本SM.Store.CoreApi提供了一个实用程序类文件StaticConfigs.cs,以从AppSettings.json文件获取任何项目值。所述逻辑用于配置key名称传递给static方法,GetConfig(),其中,与Startup类中相同的ConfigurationBuilder用于解析JSON数据并返回键的值。

//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; 
}

可以在应用程序中的任何位置调用此方法。调用此方法的一个例子是从SM.Store.Api.DAL项目中的静态类StoreDataInitializer的代码。

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

如果您愿意,ASP.NET Core应用程序仍然可以将web.config文件用于appSettings XML部分中的任何旧项。但是,该ConfigurationManager.AppSettings集合不适用于作为控制台应用程序的ASP.NET Core中的web.config。要解决此问题,StaticConfigs.cs还包含方法GetAppSetting(),用于从Core项目根目录中的web.config文件获取AppSetting值:

//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;            
}

通过传递键名来获取配置值也是单行调用。

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

实体框架核心相关更改

实体框架的核心的SM.Store.CoreApi依旧采用代码优先工作流。从应用程序的EF6移植到EF Core 2.x3.0时,编码结构应基本相同。但是,对于EF版本和行为的更改,需要注意一些问题。

主键标识插入问题

如果将现有模型用于EF Core项目,则由于默认情况下数据库端的IDENTITY INSERT将设置为ON,因此无法插入手动设定种子的主键值。EF6自动处理该问题,如果指定了任何键列并提供了值,则会关闭标识插入,否则将使用标识插入。

ProductStatusType模型为例。以下代码适用于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; } 
}

数据种子数组包括StatusCode列和值:

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")}, 
    - - - 
};

使用EF CoreDatabaseGeneratedOption.None需要将需求显式添加到主键属性中,以避免因显式提供键和值而导致失败。上面的模型应该这样更新:

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

数据上下文和连接字符串

带有EF6 的旧版SM.Store.WebApi将连接字符串传递给数据上下文类,如下所示:

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

EF CoreDbContextOption对象传递给数据上下文类。这将是类上的一个小变化。

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

将数据上下文添加到Startup.ConfigurationServices()中的DI容器时,需要指定DbContextOption项目。借助此EF Core功能,我们可以使用不同的数据提供程序和与数据库操作相关的设置。在此示例应用程序中,可以使用配置设置来启用内存数据库或SQL Server数据库。

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

SM.Store.CoreApiSQL Server连接字符串的值在appsettings.json文件的标准ConnectionStrings部分中进行配置数据库文件将保存到LocalDBWindows登录用户文件夹中,并保存到任何常规版本(包括SQL Server Express)的SQL Server定义的数据文件夹中。不能像EF6一样在连接字符串中设置LocalDB数据库文件位置的选项。如果需要为LocalDB指定其他文件位置,则可以使用SQL Server Management StudioSSMS,免费版本17.x或最新版本)打开LocalDB实例,然后在第一次运行示例应用程序之前或在删除现有数据库之后使用脚本创建数据库。

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

自定义存储库(Repositories)

尽管Microsoft声称DbContext实例结合了存储库和工作单元模式,但是自定义存储库仍然是多层应用程序的DALBLL之间的良好抽象层。从带有EF6 的旧版SM.Store.WebApi迁移到带有EF Core 2.x3.0 SM.Store.CoreApi时,SM.Store.Api.DAL项目中的所有存储库文件都应该没有重大更改。仅仅由于过时的编码结构而进行了一些更新,这些结构应该已经在具有EF6的旧应用程序中进行了纠正:

  • 删除UnitOfWork。旧版SM.Store.WebApi使用以下UnitOfWork类包装任何存储库类:
private StoreDataContext context; 
public class StoreDataUnitOfWork : IStoreDataUnitOfWork 
{ 
    public StoreDataUnitOfWork(string connectionString) 
    { 
        - - - 
        this.context = new StoreDataContext(connectionString); 
    }     
    - - - 
    public void Commit() 
    {            
        this.Context.SaveChanges();         
    } 
    - - - 
}

然后将该UnitOfWork实例注入到任何单独的存储库和基本GenericRepository库中:

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

尽管仍然需要自定义存储库并迁移自定义存储库,但是工作单元实践对于应用程序(甚至使用EF6)似乎是多余的。数据上下文类本身充当工作单元,在该工作单元中,SaveChanges()方法立即更新当前上下文中的挂起的更改。此外,对于具有多个数据上下文类的应用程序,我们可以使用在上下文的事务相关的方法的Database对象,如UseTransactionBeginTransactionCommitTransaction,实现了ACID结果。

SM.Store.CoreApi中,IUnitOfWork接口和UnitOfWork类不再存在。该StoreDataContext实例直接注入到存储库类中:

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

GenericRepository类中,该CommitAllChanges()方法被新Commit()方法替换:

过时的UnitOfWork类中的方法:

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

GenericRepository类中的方法:

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

此外,GenericRepository类中的任何基于InsertUpdateDelete方法都具有可选的第二个参数,如果您不想直到最后才调用Commit()方法,则可立即调用SaveChanges()方法。例如,这是Insert方法:

public virtual object Insert(TEntity entity, bool saveChanges = false) 
{ 
    var rtn = this.DbSet.Add(entity); 
    if (saveChanges) 
    { 
        Context.SaveChanges(); 
    } 
    return rtn; 
}
  • 使GenericRepository成为真正的泛型。旧版Web API DAL中的GenericRepository类仅适用于单个数据上下文,因为它接收通过StoreDataUnitOfWork传递的派生StoreDataContext实例。
public class GenericRepository<TEntity> : 
               IGenericRepository<TEntity> where TEntity : class 
{ 
    public IStoreDataUnitOfWork UnitOfWork { get; set; } 
    public GenericRepository(IStoreDataUnitOfWork unitOfWork) 
    { 
        this.UnitOfWork = unitOfWork; 
    } 
    - - - 
}

因此,如果存在多个数据上下文对象,则需要创建具有不同名称的其他泛型存储库类。.NET Core DALGenericRepository类中,现在将基本DbContext注入到其构造函数中,允许其他数据上下文对象的任何继承存储库使用它。

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

进行了这样的更改后,除了直接使用GenericRepository实例来获取简单查找数据集之外,所有关联的工作流程都运行良好。SM.Store.Api.Bll/LookupBS.cs文件中的旧版Web API代码如下所示:

//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; 
}

.NET Core DALGenericRepository实例不能由BLL项目中的类直接使用,因为它需要实例化的数据上下文StoreDbContext,而不是基本DbContext,才能注入到GenericRepository中。为了在LookupBS.cs保持几乎相同的代码行,我们需要带有空成员的新IStoreLookupRepository.cs,和带有仅用于其构造函数代码的StoreLookupRepository.cs

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) 
    {            
    }        
}

然后在LookupBS.cs,只需将文本GenericRepository替换为StoreLookupRepository。现在,它的工作原理与从ASP.NET Web API迁移到ASP.NET Core之前的工作相同。

.NET CoreEF Core版本从2x迁移到应用程序的3.0时,GenericRepository的代码实现保持不变。

  • 更新到异步方法(如果可用)EF6中已经提供了Async方法组。SM.Store.WebApi不使用任何Async方法组。我的迁移计划包括更新方法以在SM.Store.CoreApi应用程序中尽可能执行异步操作的工作。观众可以查看项目文件以了解详细的更改,但是此处列出了更改的概述。
    • 通过GenericRepository中的Async操作添加另一组方法。
    • 将现有方法更改为所有可能过程的Async操作。
    • 相应地在BLLAPI控制器代码中进行相关更改。

有一个例外。该Async方法不支持带有输出参数的任何方法。因此,新的DALBLLAPI控制器中具有任何输出参数的所有方法仍保持为非异步原始格式。

LINQ表达式翻新(仅适用于EF Core 3.0

EF Core 3.0带来了许多重大变化,其中最突出的变化是删除了客户端上的LINQ评估。尽管这可以提高数据访问性能并减少SQL注入的机会,但是将具有EF的应用程序从以前的版本迁移到Core 3.0可能需要进行大量的代码更改和测试,尤其是对于使用大量LINQ-to-SQL查询的任何大型企业数据应用程序而言。

在示例应用程序中,用于获取经排序和分页的数据列表的主要LINQ查询使用GroupJoinSelectManylambda表达式配合使用,在EF Core 2.x而非3.0上可以正常使用。以下IQueryable代码无法转换为正确的SQL查询,因此会显示错误消息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
    });

LINQ表达式更改为对SQL OUTTER JOIN场景使用 from...join...select语法时,该代码适用于EF Core 3.0 

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

此更改还会影响代码的其他部分。在SM.Store.Api.Common/GenericSorterPager.csSM.Store.Api.Commo/GenericMultiSorterPager.csGetSortedPagedList()方法中的退出代码调用用于返回结果集中独特数据项的IQureryable.Distinct()方法。

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

使用 from...join...select语法的更改要求将默认值添加OrderByLINQ查询。否则,如果有任何调用不包含排序请求,它将呈现错误 ORDER BY items must appear in the select list if SELECT DISTINCT is specified需要将以下代码段添加到传递给GetSortedPagedList()方法调用的IQueryable源中,以按主键将select列表排序为默认设置。

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

执行存储过程

尽管相同的SqlParameter项用作方法参数,但不同的EF版本支持使用EF数据上下文执行存储过程的不同方法。示例应用程序的代码段演示了详细信息。还要注意,用于存储过程执行的方法实际上是执行任何原始SQL脚本。

  • EF6:现有SM.Store.WebApi直接使用该Database.SqlQuery<T>()方法来执行GetPagedProductList存储过程。
var result = this.Database.SqlQuery<Models.ProductCM>("dbo.GetPagedProductList " +
    "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
	_filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 2.0 EF Core不支持该Database.SqlQuery方法。或者,我们可以将FromSql方法用于Dbset<T>。要实现此方法,需要执行以下步骤。

          1、创建具有该Dbset<T>类型的属性。动态模型是存储过程的返回类型。

//Needed for calling stored procedures with .NET Core 2.0 EF.
public DbSet<Models.ProductCM> ProductCM_List { get; set; }

          2、使用属性NotMappedKey修改POCO模型。特别是对于Key,如果未设置,则会导致运行时错误实体类型ProductCM'需要定义主键

[NotMapped]
public partial class ProductCM
{
    [Key]
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    - - -
}

       3、然后,使用Dbset属性的FromSql方法执行存储过程。

var result = this.ProductCM_List.FromSql("dbo.GetPagedProductList " +
    "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
    _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 2.12.2:Core 2.1和2.2引入了DbContext.Query方法,可以调用相同FromSql方法来执行存储过程。为了获得最佳代码实践,还将在OnModelCreating方法中定义具有返回类型的Query<T>()
protected override void OnModelCreating(ModelBuilder builder)
{
    - - -
    //For GetProductListSp.
    builder.Query<Models.ProductCM>();
}

然后使用Query<T>()GetProductListSp()中调用存储过程的方法,如下:

var result = this.Query<Models.ProductCM>().FromSql("dbo.GetPagedProductList " +
    "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
    _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
  • Core 3.0:将Query<T>()删除,然后Dbset<T>重新启用。该FromSqlRaw()FromSqlInterpolated()替换FromSql()用于执行存储过程。
var result = this.ProductCMs.FromSqlRaw("dbo.GetPagedProductList " +
     "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT",
     _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();

ProductCMs声明为无PK关键的Dbset<T>属性。

public DbSet<Models.ProductCM> ProductCMs { get; set; }

- - -

protected override void OnModelCreating(ModelBuilder builder)
{
    - - -    
    builder.Entity<Models.ProductCM>().HasNoKey();
}

尽管可以正常工作,但对于使用EF Core 3.0执行存储过程的方法,我仍然有两个负面评论。

  1. 无法避免为该DbSet<T>属性创建物理表。使用Ignore()[NotMapped]属性或builder.Entity<T>.ToTable(null)全部都不相关。我必须从启动或迁移的数据库中手动删除表(在这种情况下是ProductCM)。
  2. FromSqlInterpolated()方法的语法不错,但是它不支持执行具有任何输出参数的任何存储过程。不过,将FromSqlRaw()SqlParameter对象一起使用即可。

定制模型绑定器

之前,我分享了有关自定义模型绑定程序的工作,该绑定程序用于将查询字符串中的复杂层次结构对象传递给ASP.NET Web API方法。当我将文件FieldValueModelBinder.cs复制到ASP.NET Core库项目SM.Store.Api.Common并解决所有引用时,仍然发生错误。IModelBinder接口类型来源于Microsoft.AspNetCore.Mvc.ModelBinding命名空间,而它是先前是System.Web.Http.ModelBinding的成员。这是一项重大更改,因为HttpContext由一组新的请求功能组成,这破坏了与ASP.NET Web API版本的兼容性。

幸运的是,我可以将对象、属性和方法重新映射到新的可用对象。此外,唯一实现的方法BindModel(),将会切换为异步类型,BindModelAsync(),返回类型为Task

下面是SM.Store.WebApi中的BindModel()方法,它使用的是.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; 
}

在新版SM.Store.CoreApi中的BindModeAsync()方法似乎比ASP.NET Web API版本更简洁:

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

... Request.Query.ToList()… Request.Form.ToList()返回的KeyValuePairkvps)类型现在是List<KeyValuePair<string, StringValues>>而不是List<KeyValuePair<string, string>>。因此,任何相关的参考和代码行都需要相应地更改,主要是针对对象声明和赋值:

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

进行这些更改之后,ASP.NET Core模型绑定程序的所有功能都与旧版本相同。可下载的AspNetCore2.0-2.2_DataServicesAspNetCore3.0_DataServices中包含的文件TestCasesForDataServices_*.txt 中提供了更多测试用例。您可以将带有查询字符串的任何URL输入到Postman的请求输入区域,然后单击发送按钮。响应部分将显示基于查询字符串的复杂对象结构和值。

上面的测试用例显示了一个多列排序场景, Core 2.23.0示例应用程序中的自定义FieldValueModelBinder支持该场景。TestCasesForDataServices_*.txt 文件中提供了更多测试用例,例如以下URL 。您可能需要更改URL中的端口号,以指向您设置的正确网站。

http://localhost:5112/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

FieldValueModelBinder的结构和代码在.NET Core 2x3.0版之间兼容。这些版本之间没有重大变化。

使用IIS Express和本地IIS

ASP.NET Web APIASP.NET Core的显着变化之一是应用程序输出和托管类型,尽管我只关心Windows系统中运行的应用程序。在.NET Core2.2版本之前迁移的SM.Store.CoreApi的是默认情况下在内置Kestrel Web服务器上运行的仅限进程控制台应用程序。我们仍然可以将IIS Express用作开发环境的包装器,尤其是在Visual Studio中。我们还可以将IIS用作反向代理,以中继所有环境的请求和响应。在幕后,一个名为ASP.NET Core Module的结构在管理所有进程以及协调来自IIS/IIS ExpressKestrel Web服务器的功能方面发挥作用。ASP.NET Core ModuleVisual Studio 2017/2019安装一起自动安装在开发计算机上。

使用IIS时的进程内托管是.NET Core 2.2的一个选项,而.NET Core 3.0是默认设置。如果您在ASP.NET Core 2.23.0中下载并打开示例应用程序,并使用下面提到的步骤来设置本地IIS网站,则您已经可以使用IIS进程内宿主来运行该网站。但是,如果仅安装了DotNet Core 2.2 SDK,则仍然需要安装ASP.NET Core 2.2运行时和托管捆绑包,该捆绑包是用IIS托管ASP.NET Core 2.2网站所必需的。具有运行时和托管包ASP.NET Core 3.0也需要这样做

Visual Studio 2017/2019中启动旧版SM.Store.WebApi时,示例应用程序将在IIS Express进程下运行网站。您还可以通过使用命令行或批处理文件执行IIS Express来轻松启动Web API

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

IIS Express的相同命令行执行不适用于该SM.Store.CoreApi应用程序,因为它在与IIS Express工作进程分开的控制台应用程序进程中运行。如果您希望SM.Store.CoreApiIIS Express 保持运行状态,以便在开发环境中向多个客户端提供数据服务,只需执行以下步骤:

  • SM.Store.CoreApi使用Visual Studio 2017/2019实例打开。
  • Ctrl + F5。起始页面将显示在选定的浏览器中。
  • 关闭浏览器并最小化Visual Studio实例。
  • IIS Express现在在Windows的后台运行,用于接收来自客户端调用的任何HTTP请求。

如果在开发计算机上需要更稳定和持久的数据服务,则可以使用类似于传统ASP.NET网站或Web API应用程序的方法将SM.Store.CoreApi发布到本地IIS。这些是主要的设置步骤:

  1. Visual Studio 2017/2019和更高的解决方案中打开SM.Store.CoreApi,从生成菜单中选择发布SM.Store.CoreApi选择文件夹作为发布目标,指定您的文件夹个人资料中的文件夹路径,然后单击发布
  2. 打开IIS管理器(inetmgr.exe),选择应用程序池,然后选择添加应用程序池…”,输入名称StoreCoreApiPool,然后从.NET CLR版本下拉列表中选择No Managed Code
  3. 右键单击网站/默认网站,然后选择添加应用程序。输入StoreCore 别名,从应用程序池下拉列表中选择StoreCoreApiPool,然后输入(或浏览到)保存已发布的应用程序文件的文件夹路径。
  4. 右键单击默认网站,选择管理网站,然后选择重新启动。由于SM.Store.CoreApi应用程序使用内存数据库作为初始设置,因此您现在可以使用带有URL http://localhost/storecore/api/<method-name>的任何客户端工具访问数据服务方法。

请注意,应用程序池名称不再是应用程序正在运行的进程标识,因此它不能作为授权帐户传递,以从应用程序访问其他资源。例如,如果您尝试使用integrated security=TrueTrusted_Connection=True” 从具有本地IISSM.Store.CoreApi访问本地SQL ServerSQL Server Express实例中的数据,则即使应用程序池帐户IIS AppPool\StoreCoreApiPool映射为SQL Server登录名和用户。

如果您需要使用本地IISSQL Server数据库而不是内存数据库来运行SM.Store.CoreApi应用程序,建议您为登录和角色映射创建特定的SQL Server用户,并建议授予执行权限。您可以通过在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

上面的脚本包含在来自下载源的StoreCF8.sql文件中。您实际上可以在此文件中运行整个脚本,以使用登录用户创建SQL Server数据库,然后在SQL Server实例的SM.Store.CoreApi发布文件夹下的appsettings.json文件中启用或更新连接字符串:

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

您还需要在appsettings.json文件中为内存数据库删除此行(或将值true替换为false):

"UseInMemoryDatabase": "true"

具有本地IISSQL Server数据库的SM.Store.CoreApi应用程序现在应该可以在本地计算机上运行。

摘要

将数据服务应用程序迁移到更高版本的工具或框架时,需要努力重写代码,并解决与项目类型、设置、内置工具、工作流、运行流程、托管方案等有关的或多或少的问题。示例代码以及本文中的讨论可以帮助开发人员掌握ASP.NET应用程序不同版本上的迁移任务的实质,还可以加快ASP.NET Core应用程序较新版本上的代码工作。

发布了71 篇原创文章 · 获赞 152 · 访问量 52万+

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/105025047