ASP.NET Core:从 ASP.NET Web API 迁移的多层数据服务应用程序
一个完整的结构化数据服务示例应用程序,从 ASP.NET Web API 2.0 迁移到 ASP.NET Core 的 2.1、3.1 和 5.0 版本之间。
- 下载 AspNetCore5.0_DataService - 71.9 KB
- 下载 AspNetCore3.1_DataService - 66.6 KB
- 下载 AspNetCore2.1_DataService - 63.6 KB
- 下载 WebApi_DataServices - 309.1 KB
引言
RESTful 数据服务 API 多年来一直是主流的数据层应用程序类型。随着技术和框架的不断演进,旧的代码和结构不可避免地会被更新的取代。在将示例数据服务应用程序迁移到下一个 ASP.NET 代或更高版本的过程中,我的目标是利用新技术和框架的优势,同时保持相同的功能、请求/响应签名和工作流程。因此,任何客户端应用程序都不会受到数据服务从 ASP.NET Web API 到 ASP.NET Core 或 ASP.NET Core 各版本之间迁移变化的影响。本文并非一步步的教程,受众如有需要可参考其他资源,而是分享完整的示例应用程序源代码,并探讨以下主题和问题解决方案:
设置和运行示例应用程序
要运行 SM.Store.CoreApi
解决方案的您偏好的 .NET Core 版本,您需要在您的计算机上安装相应的 Visual Studio 2019 或 2017 以及 .NET Core 版本。
- ASP.NET Core 5.0:需要 Visual Studio 2019 (16.8.x 或更高版本;包含 .NET 5.0 SDK,该 SDK 也向下兼容 .NET Core 2.1 和 3.1 SDK)。
- ASP.NET Core 3.1:需要 Visual Studio 2019 (16.4.x 或更高版本;包含 .NET Core 3.1 SDK)。
- ASP.NET Core 2.1:需要 Visual Studio 2019 或 2017 15.7 (或更高版本) 和 .NET Core 2.1 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 调用服务方法。结果(数据或错误)将显示在响应部分。
可下载的源代码 AspNetCore5.0_DataService 包含 TestCasesForDataServices.txt 文件,可用于所有类型和版本的示例应用程序项目。该文件包含许多请求数据项的用例。您可以随意使用这些用例来测试更新的 SM.Store.CoreApi
和旧的 SM.Store.WebApi
应用程序。
如果您想使用 SQL Server 数据库或 LocalDB,可以打开 appsettings.json 文件并执行以下步骤:
-
在
AppConfig
部分下,删除UseInMemoryDatabase
行或将其值设置为false
。 -
在
ConnectionStrings
部分下,更新StoreDbConnection
的值以匹配您的设置。例如,如果您使用 SQL ServerLocalDB
,可以启用连接字符串并用您的LocalDB
实例名称替换 <your-instance-name>。您甚至可以将StoreCF8
更改为您自己的数据库名称。"StoreDbConnection": "Server=(localdb)\\<your-instance-name>; Database=StoreCF8;Trusted_Connection=True;MultipleActiveResultSets=true;"
-
当按 **F5** 启动 Visual Studio 解决方案时,数据库将自动创建,并显示 startup.html 页面。
如果您需要设置和运行旧版 ASP.NET Web API 示例应用程序,下载 WebApi_DataServices 后可以执行以下操作:
-
使用 Visual Studio 2017 或 2019 (也可与 2015 版本兼容) 打开
SM.Store.WebApi
解决方案。 -
重新生成解决方案,这将自动从 NuGet 下载所有配置的库。
-
在本地机器上设置 SQL Server 2017 或 2019 LocalDB,或其他的 SQL Server 实例,即使是较旧的版本。请在 web.config 文件中调整
connectionString
指向您的数据库实例。 -
确保
SM.Store.Api.Web
是启动项目,然后按 **F5**。这将启动 IIS Express 和 Web API 托管站点,自动在您的数据库实例中创建数据库,并用所有示例数据记录填充表。 -
Web API 项目中的一个测试页面将被渲染,表明 Web API 数据提供者已准备好接收客户端调用。
建议您删除现有同名数据库,如果您再次运行具有不同 ASP.NET 版本的示例应用程序,以便初始化一个新数据库。或者,您可以对现有数据库执行 迁移任务。否则,可能会出现某些列映射错误。
库项目
旧版 SM.Store.WebApi
是一个具有多层 .NET Framework 类库结构的 ASP.NET Web API 2 应用程序。
迁移到 ASP.NET Core 时,这些项目必须转换为 .NET Core 类库项目,目标是 .NET Core 或 .NET Standard 框架。项目类型 .NET Standard 可用于提高兼容性和灵活性。然而,即使项目类型不同,文件夹结构和文件也是相同的。对于最初迁移到 .NET Core 的示例应用程序,.NET Standard 2.x,NetStandard.Library
,被用作类库项目类型。对于较新版本的 .NET Core 的示例应用程序,类库项目类型切换为 Microsoft.NETCore.App
。Visual Studio 解决方案中的完整项目如下所示:
下面解释了将示例应用程序从旧版 ASP.NET Web API 迁移到 ASP.NET Core 的一些细节:
-
旧版的
SM.Store.Api.DAL
、SM.Store.Api.BLL
和SM.Store.Api.Common
项目被迁移到同名的对应项目中。 -
旧版的
SM.Store.Api.Entities
和SM.Store.Api.Models
被合并到 ASP.NET Core 的SM.Store.Api.Contracts
项目中。所有接口也被移入该项目,该项目可被任何其他项目引用,但自身不引用解决方案中的任何其他项目。 -
Web API 控制器类已从
SM.Store.Api
项目移动到主 .NET Core 项目SM.Store.Api.Web
。无需将这些控制器类分拆到另一个以 .NET Core 为目标的项目中。 -
某些需要的库或组件可能不会自动包含在项目模板中。因此,应手动从 NuGet 将缺失的项添加到项目中。例如,对于带有 .NET Core 2x 的示例应用程序,
Microsoft.ASpNetCore.Mvc
包被添加到SM.Store.Api.Common
中,供自定义模型绑定器使用。对于 .NET Core 3.x 及更高版本,Microsoft.NetCore.App
已包含Microsoft.ASpNetCore.Mvc
,因此无需在项目中显式引用它。
ASP.NET Core 版本之间的 Visual Studio 解决方案中库项目的结构和文件的实质性变化不大。对于示例应用程序,框架引用类型 Microsoft.NetCore.App
和 NetStandard.Library
(如果使用) 甚至可以互换用于不同 ASP.NET Core 版本的库项目。
依赖注入
由于旧版的 SM.Store.WebApi
应用程序使用 Unity 工具进行依赖注入 (DI) 逻辑,而新的 SM.Store.CoreApi
在 Startup
类中拥有 ConfigurationServices
例程,可用于设置包括 DI 在内的配置,因此将 Unity 迁移到 Core 内置 DI 服务非常直接。低级 DI 工厂类和实例解析方法的自定义代码不再需要。Unity 容器注册可以被 Core 服务配置替换。作为比较,我列出了旧版和新应用程序中 SM.Store.Api.DAL
和 SM.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.CoreApi
的 Startup.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 工具就是一个例子。为了让 AutoMapper 与 ASP.NET Core DI 容器良好配合,步骤如下:
-
通过 Nuget 下载 AutoMapper 包。
-
创建
IAutoMapConverter
接口。public interface IAutoMapConverter<TSourceObj, TDestinationObj> where TSourceObj : class where TDestinationObj : class { TDestinationObj ConvertObject(TSourceObj srcObj); List<TDestinationObj> ConvertObjectCollection(IEnumerable<TSourceObj> srcObj); }
-
将代码添加到
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(); } }
-
将此实例注册行添加到
Startup.ConfigureServices()
方法中。services.AddScoped(typeof(IAutoMapConverter<,>), typeof(AutoMapConverter<,>));
-
将
AutoMapConverter
实例注入到调用类的构造函数中。private IAutoMapConverter<Entities.Contact, Models.Contact> mapEntityToModel; public ContactsController (IAutoMapConverter<Entities.Contact, Models.Contact> convertEntityToModel) { this.mapEntityToModel = convertEntityToModel; }
-
调用已实例化对象实例中的方法(有关详细信息,请参阅 ContactController.cs)。
var convtList = mapEntityToModel.ConvertObjectCollection(rtnList);
从 .NET Core 2.x 到 5.0,内置依赖注入的模式和实践基本相同。对于更新的 .NET Core 版本,无需进行代码更改。
访问应用程序设置
.NET Core 应用程序使用更通用的配置 API。但对于 ASP.NET Core 应用程序,从 AppSetting.json 文件设置和获取项是普遍的选择,这与在 ASP.NET Web API 应用程序中使用 web.config XML 文件有很大不同。如果新的 SM.Store.CoreApi
中需要任何配置值,在 Configuration
对象构建后,有两种方法可以访问该值。
-
在
Configuration
对象具有IConfiguration
或IConfigurationRoot
类型可直接访问的地方,指定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"))); }
-
将强类型自定义 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
服务实例注入到调用类的构造函数来访问config
项。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 文件获取任何项的值。其逻辑是将配置键名传递给 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");
Entity Framework Core 相关更改
使用 Entity Framework Core 的 SM.Store.CoreApi
仍使用代码优先工作流程。从 EF6 移植到 EF Core 2.x 到 5.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 Core 时,需要显式将 DatabaseGeneratedOption.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 Core 将 DbContextOption
对象传递给数据上下文类。这将是类中的一个小更改。
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.CoreApi
的 SQL Server 连接字符串值在 appsettings.json 文件的标准 ConnectionStrings
部分进行配置。数据库文件保存在 LocalDB 的 Windows 登录用户文件夹中,对于包括 SQL Server Express 在内的任何常规版本,则保存在 SQL Server 定义的数据文件夹中。与 EF6 不同,连接字符串中无法设置 LocalDB 数据库文件的位置选项。如果您需要为 LocalDB 指定不同的文件位置,可以使用 SQL Server Management Studio (SSMS,免费版 18.x 或最新版) 打开 LocalDB 实例,然后执行以下任一选项:
-
对于现有数据库,右键单击 **Object Explorer** 中的数据库实例 > **Properties** > **Database Settings**。在 **Database default locations** 部分下更改数据库文件的路径。
-
对于创建新数据库,在首次运行示例应用程序或删除现有数据库后,执行脚本。
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
自定义存储库
尽管 Microsoft 声称 DbContext
实例结合了 Repository 和 Unit of Work 模式,但自定义存储库仍然是多层应用程序的 DAL 和 BLL 之间的良好抽象层。从使用 EF6 的旧版 SM.Store.WebApi
迁移到使用 EF Core 2.x 到 5.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
对象中的事务相关方法,如UseTransaction
、BeginTransaction
和CommitTransaction
,来实现 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
类中的任何基类Insert
、Update
或Delete
方法都具有可选的第二个参数,用于在您不想等到最后才调用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
DAL
的GenericRepository
类中,基类DbContext
现在被注入到其构造函数中,允许它被任何派生自其他数据上下文对象的存储库使用。public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class { private DbContext Context { get; set; } public GenericRepository(DbContext context) { Context = context; } - - - }
通过这种更改,所有相关的流程都运行正常,除了直接使用
GenericRepository
实例来获取简单的查找数据集。旧版 Web API 中 SM.Store.Api.Bll/LookupBS.cs 文件中的代码如下所示://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
DAL
的GenericRepository
实例不能被 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 Core 和 EF Core 版本从 2x 到 5.0 的应用程序时,
GenericRepository
的代码实现保持不变。 -
更新为异步方法(如果可用)。EF6 中已经提供了一组
Async
方法。SM.Store.WebApi
由于早期实现而未使用任何异步方法。我的迁移计划包括在SM.Store.CoreApi
应用程序中尽可能将方法更新为执行异步操作。受众可以查看项目文件以了解详细更改,但更改的概述在此列出:- 在
GenericRepository
中添加另一组具有Async
操作的方法。 - 将现有方法更改为对所有可能过程的
Async
操作。 - 相应地在 BLL 和 API 控制器代码中进行相关更改。
有一个例外。
Async
方法不支持任何带有输出参数的方法。因此,新 DAL、BLL 和 API 控制器中带有任何输出参数的所有方法仍然保留为非异步的。 - 在
LINQ 表达式重构(EF Core 3.x 及更高版本)
EF Core 3.x 相对于 EF Core 2x 存在许多重大更改,其中最突出的是移除了客户端上的 LINQ 评估。虽然这提高了数据访问性能并降低了 SQL 注入的风险,但将应用程序从旧版本 EF 迁移到 Core 3.x 及更高版本可能需要大量的代码更改和测试,特别是对于使用大量 LINQ-to-SQL 查询的大型企业数据应用程序。
在示例应用程序中,用于获取排序和分页数据列表的主要 LINQ 查询使用 GroupJoin
和 SelectMany
与 lambda 表达式,这在 EF Core 2.x 中运行良好,但在 3.x 及更高版本中则不行。下面的 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 表达式更改为使用“from...join...select
”语法并配合 SQL OUTTER JOIN
场景时,代码在 EF Core 3.x 及更高版本中运行正常。
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.cs 和 SM.Store.Api.Common/GenericMultiSorterPager.cs 中,GetSortedPagedList()
方法中的现有代码调用 IQureryable.Distinct()
方法来返回结果集中的唯一数据项。
public static IList<T> GetSortedPagedList<T>(IQueryable<T> source, PaginationRequest paging)
{
- - -
source = source.Distinct();
- - -
}
更改为使用“from...join...select
”语法需要为 LINQ 查询添加默认的 OrderBy
。否则,如果任何调用不包含排序请求,它将显示错误“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"};
}
执行存储过程
不同的 EF 版本支持不同的方法来执行具有 EF 数据上下文的存储过程,尽管使用相同的 SqlParameter
项作为方法参数。示例应用程序的代码片段演示了详细信息。另请注意,用于执行存储过程的方法实际上是执行任何原始 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:[注意:此版本未获得 Microsoft 的支持,因此我在当前更新中移除了 ASP.NET Core 2.0 的源代码。Core 2.0 中执行存储过程的方法仍在此处保留以供历史参考]
EF Core 不支持
Database.SqlQuery
方法。作为替代,我们可以为Dbset<T>
使用FromSql
方法。要实现此方法,需要以下步骤:-
创建具有
Dbset<T>
类型的属性。动态模型是存储过程返回类型。//Needed for calling stored procedures with .NET Core 2.0 EF. public DbSet<Models.ProductCM> ProductCM_List { get; set; }
-
修改 POCO 模型,添加
NotMapped
和Key
属性。特别是对于Key
,如果不设置,将出现运行时错误“实体类型 'ProductCM
' 需要定义主键”。[NotMapped] public partial class ProductCM { [Key] public int ProductId { get; set; } public string ProductName { get; set; } - - - }
-
然后使用
Dbset
属性的FromSql
方法执行存储过程。var result = this.ProductCM_List.FromSql("dbo.GetPagedProductList " + "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT", _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
-
-
Core 2.1:[注意:同样适用于未获得 Microsoft 支持的 Core 2.2。]
Core 2.1 引入了
DbContext.Query
方法,可以使用相同的FromSql
方法来执行存储过程。为了最佳的代码实践,应在OnModelCreating
方法中定义返回类型的Query<T>
。protected override void OnModelCreating(ModelBuilder builder) { - - - //For GetProductListSp. builder.Query<Models.ProductCM>(); }
然后,
GetProductListSp()
中调用存储过程的方法如下使用Query<T>
:var result = this.Query<Models.ProductCM>().FromSql("dbo.GetPagedProductList " + "@FilterString, @SortString, @PageNumber, @PageSize, @TotalCount OUT", _filterString, _sortString, _pageNumber, _pageSize, _totalCount).ToList();
-
Core 3.x 及更高版本:
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
被声明为Dbset<T>
属性,没有 PK 键。public DbSet<Models.ProductCM> ProductCMs { get; set; } - - - protected override void OnModelCreating(ModelBuilder builder) { - - - builder.Entity<Models.ProductCM>().HasNoKey(); }
尽管有效,但我仍然对 EF Core 3.x 及更高版本的存储过程执行方法有两个负面评价。
-
无法避免为
DbSet<T>
属性创建物理表。使用Ignore()
、[NotMapped]
属性或builder.Entity<T>.ToTable(null)
均无效。我必须手动从数据库中删除该表(本例中为ProductCM
),该表在数据库初始化或迁移时创建。 -
FromSqlInterpolated()
方法的语法很不错,但它不支持执行带有任何输出参数的存储过程。使用带有SqlParameter
对象的FromSqlRaw()
则可以正常工作。
-
自定义模型绑定器
我之前分享了我关于 自定义模型绑定器 的工作,用于将复杂的层次结构对象通过查询字符串传递到 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
中使用 .NET Framework 4x 的 BindModel()
方法。
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()
返回的 KeyValuePair
(kvps
) 类型现在是 List<KeyValuePair<string, StringValues>>
,而不是 List<KeyValuePair<string, string>>
。因此,任何相关的引用和代码行都需要相应更改,主要针对对象声明和赋值。
List<KeyValuePair<string, StringValues>> kvpsWork;
- - -
kvpsWork = new List<KeyValuePair<string, StringValues>>(kvps);
进行这些更改后,ASP.NET Core 模型绑定器的所有功能与旧版本相同。在可下载的 AspNetCore5.0_DataService (AspNetCore5.0_DataService) 中包含的 TestCasesForDataServices.txt 文件中提供了更多测试用例。您可以将任何带有查询字符串的 URL 输入到 Postman 的请求输入区域,然后点击 **Send** 按钮。基于查询字符串的复杂对象结构和值将在响应部分显示。
上述测试用例显示了 ASP.NET Core 示例应用程序中自定义 FieldValueModelBinder
支持的多列排序场景。TestCasesForDataServices.txt 文件中提供了更多类似下面 URL 的测试用例。您可能需要更改 URL 中的端口号以指向您设置的正确网站。
https://: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
FieldValueModelBinder
的结构和代码在 .NET Core 2x 到 5.0 版本之间兼容。这些版本之间没有重大更改。
使用 IIS Express 和本地 IIS
从 ASP.NET Web API 到 ASP.NET Core 的一个显著变化是应用程序的输出和托管类型,尽管我只关注 Windows 系统上运行的应用程序。ASP.NET Core 2.1 版本(实际上是 2.2 之前的任何 .NET Core 版本)中迁移的 SM.Store.CoreApi
是一个仅进程外 (out-of-process) 的控制台应用程序,默认情况下运行在内置的 Kestrel Web 服务器上。我们仍然可以在开发环境中将 IIS Express 用作包装器,尤其是在 Visual Studio 中。我们还可以将 IIS 作为反向代理,为所有环境转发请求和响应。在后台,一个名为 ASP.NET Core Module 的结构在管理所有进程和协调 IIS/IIS Express 与 Kestrel Web 服务器的功能方面发挥着作用。ASP.NET Core Module 会随 Visual Studio 2017/2019 在开发机器上的安装而自动安装。
使用 IIS 时的进程内托管在 ASP.NET Core 3.x 及更高版本中可用(在 ASP.NET Core 2.2 中作为选项)。如果您下载并打开 ASP.NET Core 3.1 或 5.0 的示例应用程序,并按照下面提到的步骤设置本地 IIS 网站,您就可以使用 IIS 进程内托管来运行该站点。但是,您需要为 3.1 或 5.0 版本安装 ASP.NET Core Hosting Bundle。
在 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"
对于 SM.Store.CoreApi
应用程序,相同的命令行执行 IIS Express 不起作用,因为它运行在一个独立的控制台应用程序进程中,与 IIS Express 工作进程分离。如果您想在开发环境中保持 SM.Store.CoreApi
和 IIS Express 运行以向多个客户端提供数据服务,请按照以下步骤操作:
- 使用 Visual Studio 2017/2019 实例打开
SM.Store.CoreApi
。 - 按 **Ctrl + F5**。起始页面将在选定的浏览器中显示。
- 关闭浏览器并最小化 Visual Studio 实例。
- IIS Express 现在正在 Windows 后台运行,以接收来自客户端调用的任何 HTTP 请求。
如果需要在开发机器上获得更稳定和持久的数据服务,可以将 SM.Store.CoreApi
部署到本地 IIS,其方法类似于传统 ASP.NET 网站或 Web API 应用程序。这些是主要的设置步骤:
-
在 Visual Studio 2017/2019 中打开
SM.Store.CoreApi
,高亮显示解决方案,从 **Build** 菜单中选择 **Publish SM.Store.CoreApi**,选择 **Folder** 作为发布目标,指定您的文件夹路径用于您的 Folder Profile,然后点击 **Publish**。 -
打开 IIS 管理器 (inetmgr.exe),选择 **Application Pools**,然后选择 **Add Application Pool…**,输入名称
StoreCorePool
,并在 **.NET CLR Version** 下拉菜单中选择 **No Managed Code**。 -
右键单击 **Sites/Default Web Site**,然后选择 **Add Application**。将
StoreCore
作为 **Alias** 输入,从 **Application pool** 下拉菜单中选择StoreCorePool
,然后输入(或浏览到)包含已发布应用程序文件的文件夹路径。 -
右键单击 **Default Web Site**,选择 **Manage Website**,然后选择 **Restart**。由于
SM.Store.CoreApi
应用程序使用内存数据库作为初始设置,您现在可以使用任何客户端工具通过 URLhttps:///storecore/api/<method-name>
访问数据服务方法。
请注意,应用程序池名称不再是应用程序运行进程的标识,因此不能将其作为授权帐户用于访问应用程序的其他资源。例如,如果您尝试使用“integrated security=True
”或“Trusted_Connection=True
”从本地 IIS 上的 SM.Store.CoreApi
访问本地 SQL Server 或 SQL Server Express 实例中的数据,您将收到 SQL Server 访问权限错误。
如果您需要使用本地 IIS 和 SQL Server 数据库运行 SM.Store.CoreApi
应用程序,而不是使用仅用于简单测试用例且不支持存储过程的内存数据库,则有两种选择:
-
将应用程序池帐户
IIS AppPool\StoreCorePool
映射到 SQL Server 登录名和用户。然后,您可以在数据服务应用程序中使用现有的数据库连接字符串,并启用集成安全。- 在 SSMS 工具中打开 **Object Explorer**,然后展开 **Security**。
- 右键单击 **Logins** > **New Login…**
- 将
IIS AppPool\StoreCorePool
作为 **Login name** 输入。不要点击 **Search...** 按钮。 - 从 **Default database** 下拉列表框中选择 “StoreCF8”(或您的数据库名称)。
- 在左上角的 **User Mapping** 面板中,选择 “StoreCF8”(或您的数据库名称)。
- 在 **Database role membership for:** 面板中,选择 “db datareader” 和 “db datawriter”。
- 返回 **Object Explorer**。
- 右键单击 **StoreCF8** 数据库 > **Properties** > **Permissions**。
- 在 **Users or roles** 面板中,高亮显示
IIS APPPOOL\StoreCorePool
。 - 在 **Explicit Permissions** 列表中,勾选 **Execute** 的 **Grant** 复选框。
-
创建特定的 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"
现在,带有本地 IIS 和 SQL Server 数据库的 SM.Store.CoreApi
应用程序应该可以在您的本地计算机上正常工作了。
摘要
将数据服务应用程序迁移到更高的工具或框架版本需要重写代码和解决各种问题,涉及项目类型、设置、内置工具、工作流程、运行进程、托管方案等。本文中的示例、代码和讨论可以帮助开发人员掌握不同版本 ASP.NET 应用程序迁移任务的精髓,并加速新版本 ASP.NET Core 应用程序的代码工作。
历史
-
2018年1月10日
-
初次发布
-
-
2018年1月17日
-
为自定义模型绑定器添加了测试用例并更新了源代码。
-
-
2018年8月2日
-
ASP.NET Core 2.1 版本示例应用程序可用。
-
-
2019年3月1日
- 使用 ASP.NET Core 2.2 升级了示例应用程序的源代码。
- 更新了某些部分的文本。
-
2019年6月7日
- 添加了“执行存储过程”子节。
- 删除了“为 Localhost 启用 CORS”(使用后期版本的 VS 2017 和 VS 2019,不同 localhost 端口之间不存在跨域问题)部分。
- 编辑了多个部分的文本。
- 更新了源代码文件。所有类型的示例应用程序项目现在都支持分页数据列表的多列排序。
-
2019年11月27日
- 使用 ASP.NET Core 3.0 更新了示例应用程序。
- 为 ASP.NET Core 3.0 添加了文章的章节和段落。
- 编辑了所有部分的现有文本。
-
2020年11月29日
- 使用 ASP.NET Core 5.0 和 3.1 更新了示例应用程序。
- 更新了可下载的源代码 zip 列表,包含目前由 Microsoft 支持的 .NET 版本(.NET 5、.NET Core 3.1、.NET Core 2.1 和 .NET Framework 4x)。
- 编辑了与 ASP.NET Core 5.0 和 3.1 相关的部分的现有文本。