目录
介绍
储存库和数据库
服务
泛型
实体框架层
WeatherForecastDBContext
数据服务层
IDbRecord
IDataService
BaseDataService
BaseServerDataService
BaseWASMDataService
项目具体实现
业务逻辑/控制器服务层
WeatherForecastControllerService
WeatherForecastController
总结
介绍本文是有关构建Blazor项目的系列文章中的第二篇:它描述了将数据和业务逻辑层抽象到库中的样板代码中的技术和方法。
- 项目结构与框架
- 服务——构建CRUD数据层
- View组件——UI中的CRUD编辑和查看操作
- UI组件——构建HTML / CSS控件
- View组件-UI中的CRUD列表操作
- 逐步详细介绍如何向应用程序添加气象站和气象站数据
CEC.Blazor GitHub存储库
存储库中有一个SQL脚本在/SQL中,用于构建数据库。
您可以在此处查看运行的项目的服务器版本。
你可以看到该项目的WASM版本运行在这里。
服务Blazor建立在DI [依赖注入]和IOC [控制反转]的基础上。
Blazor Singleton和Transient服务相对简单。您可以在Microsoft文档中阅读有关它们的更多信息。范围要稍微复杂一些。
- 在客户端应用程序会话的生命周期内存在作用域服务对象——注意客户端而不是服务器。任何应用程序重置(例如F5或离开应用程序的导航)都会重置所有作用域服务。浏览器中重复的选项卡将创建一个新的应用程序和一组新的范围服务。
- 作用域服务可以作用域到代码中的对象。这在UI组件中是最常见的。OwningComponentBase组件类都有功能来限制作用域服务的生命周期到组件的声明周期。在另一篇文章中将对此进行更详细的介绍。
服务是Blazor IOC [Inversion of Control]容器。
在服务器模式下,服务在startup.cs中配置:
// CEC.Blazor.Server/startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
// the Services for the CEC.Blazor .
services.AddCECBlazor();
// the local application Services defined in ServiceCollectionExtensions.cs
services.AddApplicationServices(Configurtion);
}
// CEC.Blazor.Server/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddApplicationServices
(this IServiceCollection services, IConfiguration configuration)
{
// Singleton service for the Server Side version of WeatherForecast Data Service
// services.AddSingleton();
services.AddSingleton();
// Scoped service for the WeatherForecast Controller Service
services.AddScoped();
// Transient service for the Fluent Validator for the WeatherForecast record
services.AddTransient();
// Factory that builds the specific DBContext
var dbContext = configuration.GetValue("Configuration:DBContext");
services.AddDbContextFactory
(options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton);
return services;
}
和WASM模式下的program.cs:
// CEC.Blazor.WASM.Client/program.cs
public static async Task Main(string[] args)
{
.....
// Added here as we don't have access to builder in AddApplicationServices
builder.Services.AddScoped(sp => new HttpClient
{ BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
// the Services for the CEC.Blazor Library
builder.Services.AddCECBlazor();
// the local application Services defined in ServiceCollectionExtensions.cs
builder.Services.AddApplicationServices();
.....
}
// CEC.Blazor.WASM.Client/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
// Scoped service for the WASM Client version of WeatherForecast Data Service
services.AddScoped();
// Scoped service for the WeatherForecast Controller Service
services.AddScoped();
services.AddTransient();
// Transient service for the Fluent Validator for the WeatherForecast record
return services;
}
要点:
- 每个项目/库都有一种IServiceCollection扩展方法,用于封装项目所需的特定服务。
- 仅数据层服务不同。Blazor服务器和WASM API服务器都使用的Server版本与数据库和Entity Framework连接。它的作用域为Singleton——当我们运行异步时,每个查询都会创建和关闭DbContext。客户端版本使用HttpClient(是作用域服务)对API进行调用,因此本身就是作用域的。还有一个虚拟数据服务来模拟数据库。
- 使用代码工厂来构建特定的DBContext,并提供必要的抽象级别,以将基本数据服务代码复制到基础库中。
样板库代码在很大程度上依赖于泛型。使用的两个通用实体是:
- TRecord——这代表模型记录类。它必须实现IDbRecord,new()并且是一个类。
- TContext——这是数据库上下文,必须从DbContext类继承。
类声明如下所示:
// CEC.Blazor/Services/BaseDataClass.cs
public abstract class BaseDataService:
IDataService
where TRecord : class, IDbRecord, new()
where TContext : DbContext
{......}
实体框架层
该解决方案结合了实体框架[EF]和常规数据库访问。以前是老派(应用程序离数据表很远),我通过存储过程实现了CUD [CRUD不带读取],并通过视图实现了R [读取访问]和列表。数据层具有两层——EF数据库上下文和数据服务。
实体框架数据库使用的数据库帐户具有访问权限,只能在视图上选择并在存储过程上执行。
该演示应用程序可以在有或没有完整数据库连接的情况下运行——有一个“虚拟数据库”服务器数据服务。
所有EF代码都在共享项目CEC.Weather特定的库中实现。
WeatherForecastDBContextDbContext有每个记录类型的DbSet。每个DbSet都链接到OnModelCreating()中的视图。WeatherForecast应用程序具有一种记录类型。
该类如下所示:
// CEC.Weather/Data/WeatherForecastDbContext.cs
public class WeatherForecastDbContext : DbContext
{
public WeatherForecastDbContext
(DbContextOptions options) : base(options) { }
public DbSet WeatherForecasts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity(eb =>
{
eb.HasNoKey();
eb.ToView("vw_WeatherForecast");
});
}
}
数据服务层
IDbRecord 定义所有数据库记录的公共接口。
// CEC.Blazor/Data/Interfaces/IDbRecord.cs
public interface IDbRecord
{
public int ID { get; }
public string DisplayName { get; }
public T ShadowCopy();
}
IDbRecord 确保:
- 选择下拉列表的ID/Value对
- 显示记录时在任何控件的标题区域中使用的默认名称
- 编辑期间需要时的记录的深层副本
IDataService接口中定义了核心数据服务功能。
// CEC.Blazor/Services/Interfaces/IDataService.cs
public interface IDataService
where TRecord : class, IDbRecord, new()
where TContext : DbContext
{
/// Used by the WASM client, otherwise set to null
public HttpClient HttpClient { get; set; }
/// Access to the DBContext using the IDbContextFactory interface
public IDbContextFactory DBContext { get; set; }
/// Access to the application configuration in Server
public IConfiguration AppConfiguration { get; set; }
/// Record Configuration object that contains routing and
/// naming information about the specific record type
public RecordConfigurationData RecordConfiguration { get; set; }
/// Method to get the full Record List
public Task GetRecordListAsync() =>
Task.FromResult(new List());
/// Method to get a filtered Record List using a IFilterLit object
public Task GetFilteredRecordListAsync
(IFilterList filterList) => Task.FromResult(new List());
/// Method to get a single Record
public Task GetRecordAsync(int id) => Task.FromResult(new TRecord());
/// Method to get the current record count
public Task GetRecordListCountAsync() => Task.FromResult(0);
/// Method to update a record
public Task UpdateRecordAsync(TRecord record) =>
Task.FromResult(new DbTaskResult() { IsOK = false,
Type = MessageType.NotImplemented, Message = "Method not implemented" });
/// Method to create and add a record
public Task CreateRecordAsync(TRecord record) =>
Task.FromResult(new DbTaskResult() { IsOK = false,
Type = MessageType.NotImplemented, Message = "Method not implemented" });
/// Method to delete a record
public Task DeleteRecordAsync(TRecord record) =>
Task.FromResult(new DbTaskResult() { IsOK = false,
Type = MessageType.NotImplemented, Message = "Method not implemented" });
/// Method to build the a list of SqlParameters for a CUD Stored Procedure.
/// Uses custom attribute data.
public List GetSQLParameters(TRecord item, bool withid = false) =>
new List();
}
BaseDataService
BaseDataService 实现接口:
// CEC.Blazor/Services/Interfaces
public abstract class BaseDataService:
IDataService where TRecord : IDbRecord, new()
{
// The HttpClient used by the WASM dataservice implementation -
// set to null by default - set in the WASM implementation
public HttpClient HttpClient { get; set; } = null;
// The DBContext access through the IDbContextFactory interface -
// set to null by default - set in the Server implementation
public virtual IDbContextFactory DBContext { get; set; } = null;
// Access to the Application Configuration
public IConfiguration AppConfiguration { get; set; }
// Record Configuration - set in each specific model implementation
public virtual RecordConfigurationData RecordConfiguration { get; set; } =
new RecordConfigurationData();
// Base new
public BaseDataService(IConfiguration configuration) =>
this.AppConfiguration = configuration;
}
BaseServerDataService
请参阅项目代码以获取完整的类——这相当长。
该服务实现样板代码:
- 实现IDataService接口CRUD方法。
- 用于构建“创建、更新和删除存储过程”的异步方法。
- 使用EF DbSet获取列表和单个记录的异步方法。
该代码依赖于
- 使用命名约定,
- 模型类名称DbRecordName——例如DbWeatherForecast,
- DbContext DbSet属性命名RecordName——例如WeatherForecast,
- 使用自定义属性
- DbAccess——定义存储过程名称的类级别属性,或
- SPParameter——特定于属性的特性,用于标记存储过程中使用的所有属性。
下面显示了带有自定义DbWeatherForecast属性的模型类的一小段。
[DbAccess(CreateSP = "sp_Create_WeatherForecast",
UpdateSP ="sp_Update_WeatherForecast", DeleteSP ="sp_Delete_WeatherForecast") ]
public class DbWeatherForecast :IDbRecord
{
[SPParameter(IsID = true, DataType = SqlDbType.Int)]
public int WeatherForecastID { get; set; } = -1;
[SPParameter(DataType = SqlDbType.SmallDateTime)]
public DateTime Date { get; set; } = DateTime.Now.Date;
......
}
EF上的数据操作被实现为DBContext的扩展方法。
存储过程通过调用ExecStoredProcAsync()来运行。该方法如下所示。它使用EF DBContext获取常规的ADO数据库命令对象,然后使用Model类中的自定义属性构建的参数集执行存储过程。
// CEC.Blazor/Extensions/DBContextExtensions.cs
public static async Task ExecStoredProcAsync
(this DbContext context, string storedProcName, List parameters)
{
var result = false;
var cmd = context.Database.GetDbConnection().CreateCommand();
cmd.CommandText = storedProcName;
cmd.CommandType = CommandType.StoredProcedure;
parameters.ForEach(item => cmd.Parameters.Add(item));
using (cmd)
{
if (cmd.Connection.State == ConnectionState.Closed) cmd.Connection.Open();
try
{
await cmd.ExecuteNonQueryAsync();
}
catch {}
finally
{
cmd.Connection.Close();
result = true;
}
}
return result;
}
例如使用Create
// CEC.Blazor/Services/DBServerDataService.cs
public async Task CreateRecordAsync(TRecord record) =>
await this.RunStoredProcedure(record, SPType.Create);
请参阅注释以获取信息:
// CEC.Blazor/Services/DBServerDataService.cs
protected async Task RunStoredProcedure(TRecord record, SPType spType)
{
// Builds a default error DbTaskResult
var ret = new DbTaskResult()
{
Message = $"Error saving {this.RecordConfiguration.RecordDescription}",
IsOK = false,
Type = MessageType.Error
};
// Gets the correct Stored Procedure name.
var spname = spType switch
{
SPType.Create => this.RecordInfo.CreateSP,
SPType.Update => this.RecordInfo.UpdateSP,
SPType.Delete => this.RecordInfo.DeleteSP,
_ => string.Empty
};
// Gets the Parameters List
var parms = this.GetSQLParameters(record, spType);
// Executes the Stored Procedure with the parameters.
// Builds a new Success DbTaskResult. In this case (Create) it retrieves the new ID.
if (await this.DBContext.CreateDbContext().ExecStoredProcAsync(spname, parms))
{
var idparam = parms.FirstOrDefault
(item => item.Direction == ParameterDirection.Output &&
item.SqlDbType == SqlDbType.Int && item.ParameterName.Contains("ID"));
ret = new DbTaskResult()
{
Message = $"{this.RecordConfiguration.RecordDescription} saved",
IsOK = true,
Type = MessageType.Success
};
if (idparam != null) ret.NewID = Convert.ToInt32(idparam.Value);
}
return ret;
}
您可以在GitHub代码文件中深入研究GetSqlParameters。
Read与List方法得到通过反射的DbSet名称,并使用EF方法和IDbRecord接口来获取数据。
// CEC.Blazor/Extensions/DBContextExtensions
public async static Task GetRecordListAsync
(this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord
{
var par = context.GetType().GetProperty(dbSetName ?? IDbRecord.RecordName);
var set = par.GetValue(context);
var sets = (DbSet)set;
return await sets.ToListAsync();
}
public async static Task GetRecordListCountAsync
(this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord
{
var par = context.GetType().GetProperty(dbSetName ?? IDbRecord.RecordName);
var set = par.GetValue(context);
var sets = (DbSet)set;
return await sets.CountAsync();
}
public async static Task GetRecordAsync
(this DbContext context, int id, string dbSetName = null) where TRecord : class,
IDbRecord
{
var par = context.GetType().GetProperty(dbSetName ?? IDbRecord.RecordName);
var set = par.GetValue(context);
var sets = (DbSet)set;
return await sets.FirstOrDefaultAsync(item => ((IDbRecord)item).ID == id);
}
BaseWASMDataService
有关完整的类,请参见项目代码。
该类的客户端版本相对简单,使用HttpClient可以对服务器进行API调用。同样,我们依靠命名约定来使样板工作。
例如使用Create,
// CEC.Blazor/Services/DBWASMDataService.cs
public async Task CreateRecordAsync(TRecord record)
{
var response = await this.HttpClient.PostAsJsonAsync
($"{RecordConfiguration.RecordName}/create", record);
var result = await response.Content.ReadFromJsonAsync();
return result;
}
我们将很快讨论服务器端控制器。
为了抽象目的,我们定义了一个通用的数据服务接口。这没有实现任何新功能,仅指定了泛型。
// CEC.Weather/Services/Interfaces/IWeatherForecastDataService.cs
public interface IWeatherForecastDataService :
IDataService
{
// Only code here is to build dummy data set
}
WASM服务继承BaseWASMDataService并实现IWeatherForecastDataService。它定义了泛型并配置RecordConfiguration。
// CEC.Weather/Services/WeatherForecastWASMDataService.cs
public class WeatherForecastWASMDataService :
BaseWASMDataService,
IWeatherForecastDataService
{
public WeatherForecastWASMDataService
(IConfiguration configuration, HttpClient httpClient) : base(configuration, httpClient)
{
this.RecordConfiguration = new RecordConfigurationData()
{ RecordName = "WeatherForecast", RecordDescription = "Weather Forecast",
RecordListName = "WeatherForecasts", RecordListDecription = "Weather Forecasts" };
}
}
Server服务继承自BaseServerDataService并实现IWeatherForecastDataService。它定义了泛型并配置RecordConfiguration。
// CEC.Weather/Services/WeatherForecastServerDataService.cs
public class WeatherForecastServerDataService :
BaseServerDataService,
IWeatherForecastDataService
{
public WeatherForecastServerDataService(IConfiguration configuration,
IDbContextFactory dbcontext) : base(configuration, dbcontext)
{
this.RecordConfiguration = new RecordConfigurationData()
{ RecordName = "WeatherForecast", RecordDescription = "Weather Forecast",
RecordListName = "WeatherForecasts", RecordListDecription = "Weather Forecasts" };
}
}
业务逻辑/控制器服务层
控制器通常配置为作用域服务。
控制器层接口和基类是泛型的,位于CEC.Blazor库中。两个接口,IControllerService和IControllerPagingService定义所需的功能。两者都在BaseControllerService类中实现。
IControllerService,IControllerPagingService和BaseControllerService的代码太长,无法在此处显示。当我们研究UI层与控制器层的接口时,我们将介绍大多数功能。
实现的主要功能是:
- 用于保存当前记录和记录集及其状态的属性
- 属性和方法(在IControllerPagingService中定义)用于大型数据集上的UI分页操作
- 数据集排序的属性和方法
- 跟踪记录的编辑状态(Dirty/Clean)的属性和方法
- 通过IDataService接口实现CRUD操作的方法
- 记录和记录集更改触发的事件。UI用来控制页面刷新
- 在路由到使用相同作用域的控制器实例的新页面期间重置控制器的方法
上述功能所需的所有代码都在基类中进行了重复处理。实现基于特定记录的控制器是一项简单的任务,只需最少的编码。
WeatherForecastControllerService类
- 实现获取所需的DI服务的类构造函数,设置基类并为db数据集分页和排序设置默认的sort列。
- 获取用户界面中为Outlook Enum选择框的Dictionary对象。
请注意,使用的数据服务是IWeatherForecastDataService,其是在服务中配置的。对于WASM,这是WeatherForecastWASMDataService;对于服务器或API EASM服务器,这是WeatherForecastServerDataService。
// CEC.Weather/Controllers/ControllerServices/WeatherForecastControllerService.cs
public class WeatherForecastControllerService : BaseControllerService, IControllerService
{
/// List of Outlooks for Select Controls
public SortedDictionary OutlookOptionList =>
Utils.GetEnumList();
public WeatherForecastControllerService(NavigationManager navmanager,
IConfiguration appconfiguration,
IWeatherForecastDataService weatherForecastDataService) :
base(appconfiguration, navmanager)
{
this.Service = weatherForecastDataService;
this.DefaultSortColumn = "WeatherForecastID";
}
}
WeatherForecastController
虽然它不是一项服务,但WeatherForecastController是要覆盖的数据层的最后一部分。它使用IWeatherForecastDataService访问其数据业务,使得相同的调用的ControllerService进入DataService访问,并返回所请求的数据集。我还没有找到一种抽象的方法,因此我们需要为每个记录实现一个。
// CEC.Blazor.WASM.Server/Controllers/WeatherForecastController.cs
[ApiController]
public class WeatherForecastController : ControllerBase
{
protected IWeatherForecastDataService DataService { get; set; }
private readonly ILogger logger;
public WeatherForecastController(ILogger logger,
IWeatherForecastDataService weatherForecastDataService)
{
this.DataService = weatherForecastDataService;
this.logger = logger;
}
[MVC.Route("weatherforecast/list")]
[HttpGet]
public async Task GetList() =>
await DataService.GetRecordListAsync();
[MVC.Route("weatherforecast/count")]
[HttpGet]
public async Task Count() => await DataService.GetRecordListCountAsync();
[MVC.Route("weatherforecast/get")]
[HttpGet]
public async Task
GetRec(int id) => await DataService.GetRecordAsync(id);
[MVC.Route("weatherforecast/read")]
[HttpPost]
public async Task
Read([FromBody]int id) => await DataService.GetRecordAsync(id);
[MVC.Route("weatherforecast/update")]
[HttpPost]
public async Task Update([FromBody]DbWeatherForecast record) =>
await DataService.UpdateRecordAsync(record);
[MVC.Route("weatherforecast/create")]
[HttpPost]
public async Task Create([FromBody]DbWeatherForecast record) =>
await DataService.CreateRecordAsync(record);
[MVC.Route("weatherforecast/delete")]
[HttpPost]
public async Task Delete([FromBody] DbWeatherForecast record) =>
await DataService.DeleteRecordAsync(record);
}
总结
本文演示了如何将数据和控制器层代码抽象到一个库中。
需要注意的一些关键点:
- 尽可能使用Aysnc代码。数据访问功能都是异步的。
- 泛型使很多重复的事情成为可能。它们造成了复杂性,但是值得付出努力。
- 接口对于依赖关系注入和UI样板至关重要。
下一节将介绍展示层/UI框架。