目录
FactoryServerInMemoryDataService
WeatherForecastControllerService
这是构建Blazor数据库应用程序系列中的第二篇文章。它描述了如何将数据和业务逻辑层构建为通用库代码,从而使部署特定于应用程序的数据服务变得简单。它是对早期版本的完全重写。
该系列文章如下:
- 项目结构和框架。
- 服务——构建CRUD数据层。
- 查看组件——UI中的CRUD编辑和查看操作。
- UI 组件——构建HTML/CSS控件。
- 查看组件——UI中的CRUD列表操作。
存储库和数据库
存储库已移至CEC.Database存储库。您可以将其用作开发您自己的应用程序的模板。以前的存储库已过时,将被删除。
存储库中的/SQL中有一个用于构建数据库的SQL脚本。该应用程序可以使用真正的SQL数据库或内存中的SQLite数据库。
您可以在同一站点上看到在此处运行的项目的Server和WASM版本。
目标
在深入研究细节之前,让我们先看看我们的目标:构建库代码,这样声明一个标准的UI控制器服务就像这样简单:
public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast>
{
public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { }
}
并声明一个如下所示的数据库DbContext:
public class LocalWeatherDbContext : DbContext
{
public LocalWeatherDbContext(DbContextOptions<LocalWeatherDbContext> options)
: base(options)
{}
// A DbSet per database entity
public DbSet<WeatherForecast> WeatherForecast { get; set; }
}
我们添加新数据库实体的过程是:
- 将必要的表添加到数据库中。
- 定义一个数据类。
- 在DbContext中定义DbSet。
- 定义一个public class nnnnnnControllerService服务并将其注册到服务容器。
某些实体会出现复杂情况,但这不会使该方法无效——库中80%以上的代码。
服务
Blazor建立在DI [依赖注入]和IOC [控制反转]原则之上。如果您不熟悉这些概念,请在深入研究Blazor之前进行一些了解。从长远来看,它会为您节省时间!
Blazor Singleton和Transient服务相对简单。您可以在Microsoft 文档中阅读有关它们的更多信息。Scoped稍微复杂一些。
- 作用域服务对象存在于客户端应用程序会话的生命周期内——请注意客户端而不是服务器。任何应用程序重置,例如F5或离开应用程序的导航,都会重置所有范围服务。浏览器中的重复选项卡会创建一个新应用程序和一组新的范围服务。
- 范围服务可以进一步限定为代码中的单个对象。OwningComponentBase组件类都有功能来限制范围的服务的生命到组件的寿命。
Services是Blazor IOC [控制反转]容器。服务实例声明如下:
- 在Blazor服务器Startup.cs的ConfigureServices中
- 在Blazor WASM Program.cs中。
该解决方案使用服务集合扩展方法,例如AddApplicationServices,将所有特定于应用程序的服务集中在一个屋檐下。
// Blazor.Database.Web/startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
// the local application Services defined in ServiceCollectionExtensions.cs
// services.AddApplicationServices(this.Configuration);
services.AddInMemoryApplicationServices(this.Configuration);
}
扩展在静态类中被声明为静态扩展方法。这两种方法如下所示。
//Blazor.Database.Web/Extensions/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// Local DB Setup
var dbContext = configuration.GetValue<string>("Configuration:DBContext");
services.AddDbContextFactory<LocalWeatherDbContext>(options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton);
services.AddSingleton<IFactoryDataService, LocalDatabaseDataService>();
services.AddScoped<WeatherForecastControllerService>();
return services;
}
public static IServiceCollection AddInMemoryApplicationServices(this IServiceCollection services, IConfiguration configuration)
{
// In Memory DB Setup
var memdbContext = "Data Source=:memory:";
services.AddDbContextFactory<InMemoryWeatherDbContext>(options => options.UseSqlite(memdbContext), ServiceLifetime.Singleton);
services.AddSingleton<IFactoryDataService, TestDatabaseDataService>();
services.AddScoped<WeatherForecastControllerService>();
return services;
}
}
在WASM项目的program.cs中:
// 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 Application
builder.Services.AddWASMApplicationServices();
.....
}
// ServiceCollectionExtensions.cs
public static IServiceCollection AddWASMApplicationServices(this IServiceCollection services)
{
services.AddScoped<IFactoryDataService, FactoryWASMDataService>();
services.AddScoped<WeatherForecastControllerService>();
return services;
}
要点:
- 每个项目/库都有一个IServiceCollection扩展方法来封装项目所需的特定服务。
- 只有数据层服务不同。由Blazor服务器和WASM API服务器使用的服务器版本与数据库和实体框架接口。它的范围是单例。
- 一切都是异步的,使用DbContextFactory和在使用时管理DbContext实例。WASM客户端版本使用HttpClient(这是一个作用域服务)来调用API,因此是作用域的。
- 实现IFactoryDataService的IFactoryDataService通过泛型处理所有数据请求。TRecord定义检索和返回哪个数据集。工厂服务样板所有核心数据服务代码。
- 既有真正的SQL数据库,也有内存中的SQLite DbContext。
泛型
工厂库代码严重依赖泛型。定义了两个通用实体:
- TRecord表示模型记录类。它必须是一个类,实现IDbRecord并定义一个空的new()。 TRecord用于方法级别。
- TDbContext是数据库上下文。它必须从DbContext类继承。
类声明如下所示:
//Blazor.SPA/Services/FactoryDataService.cs
public abstract class FactoryDataService<TContext>: IFactoryDataService<TContext>
where TContext : DbContext
......
// example method template
public virtual Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new TRecord());
数据访问
在深入研究细节之前,让我们先看看我们需要实现的主要CRUDL方法:
- GetRecordList——获取数据集中的记录列表。这可以被分页和排序。
- GetRecord——通过ID获取单个记录
- CreateRecord——创建新记录
- UpdateRecord——根据ID更新记录
- DeleteRecord——根据ID删除记录
在我们阅读本文时,请记住这些。
DbTaskResult
数据层CUD操作返回一个DbTaskResult对象。大多数属性是不言而喻的。它旨在由UI使用以构建CSS框架实体,例如警报和Toast。NewID从创建操作返回新ID 。
public class DbTaskResult
{
public string Message { get; set; } = "New Object Message";
public MessageType Type { get; set; } = MessageType.None;
public bool IsOK { get; set; } = true;
public int NewID { get; set; } = 0;
}
数据类
数据类实现IDbRecord。
- ID是标准的数据库标识字段。通常一个int。
- GUID 是此记录副本的唯一标识符。
- DisplayName为记录提供通用名称。我们可以在标题和其他UI组件中使用它。
public interface IDbRecord<TRecord>
where TRecord : class, IDbRecord<TRecord>, new()
{
public int ID { get; }
public Guid GUID { get; }
public string DisplayName { get; }
}
WeatherForecast
这是WeatherForecast数据实体的数据类。
要点:
- 用于属性标签的实体框架属性。
- IDbRecord的实现。
- IValidation的实现。我们将在第三篇文章中介绍自定义验证。
public class WeatherForecast : IValidation, IDbRecord<WeatherForecast>
{
[Key] public int ID { get; set; } = -1;
public DateTime Date { get; set; } = DateTime.Now;
public int TemperatureC { get; set; } = 0;
[NotMapped] public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string Summary { get; set; } = string.Empty;
[NotMapped] public Guid GUID { get; init; } = Guid.NewGuid();
[NotMapped] public string DisplayName => $"Weather Forecast for {this.Date.ToShortDateString()} ";
public bool Validate(ValidationMessageStore validationMessageStore, string fieldname, object model = null)
{
model = model ?? this;
bool trip = false;
this.Summary.Validation("Summary", model, validationMessageStore)
.LongerThan(2, "Your description needs to be a little longer! 3 letters minimum")
.Validate(ref trip, fieldname);
this.Date.Validation("Date", model, validationMessageStore)
.NotDefault("You must select a date")
.LessThan(DateTime.Now.AddMonths(1), true, "Date can only be up to 1 month ahead")
.Validate(ref trip, fieldname);
this.TemperatureC.Validation("TemperatureC", model, validationMessageStore)
.LessThan(70, "The temperature must be less than 70C")
.GreaterThan(-60, "The temperature must be greater than -60C")
.Validate(ref trip, fieldname);
return !trip;
}
实体框架层
该应用程序实现了两个实体框架DBContext类。
WeatherForecastDBContext
该DbContext有DbSet每个记录类型。每个DbSet都链接到OnModelCreating()中的一个视图。WeatherForecast应用程序具有一种记录类型。
LocalWeatherDbContext
该类非常基础,为每个数据类创建一个DbSet。DBSet必须与数据类同名。
public class LocalWeatherDbContext : DbContext
{
private readonly Guid _id;
public LocalWeatherDbContext(DbContextOptions<LocalWeatherDbContext> options)
: base(options)
=> _id = Guid.NewGuid();
public DbSet<WeatherForecast> WeatherForecast { get; set; }
}
InMemoryWeatherDbContext
内存版本稍微复杂一些,它需要即时构建和填充数据库。
public class InMemoryWeatherDbContext : DbContext
{
private readonly Guid _id;
public InMemoryWeatherDbContext(DbContextOptions<InMemoryWeatherDbContext> options)
: base(options)
{
this._id = Guid.NewGuid();
this.BuildInMemoryDatabase();
}
public DbSet<WeatherForecast> WeatherForecast { get; set; }
private void BuildInMemoryDatabase()
{
var conn = this.Database.GetDbConnection();
conn.Open();
var cmd = conn.CreateCommand();
cmd.CommandText = "CREATE TABLE [WeatherForecast]([ID] INTEGER PRIMARY KEY AUTOINCREMENT, [Date] [smalldatetime] NOT NULL, [TemperatureC] [int] NOT NULL, [Summary] [varchar](255) NULL)";
cmd.ExecuteNonQuery();
foreach (var forecast in this.NewForecasts)
{
cmd.CommandText = $"INSERT INTO WeatherForecast([Date], [TemperatureC], [Summary]) VALUES('{forecast.Date.ToLongDateString()}', {forecast.TemperatureC}, '{forecast.Summary}')";
cmd.ExecuteNonQuery();
}
}
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private List<WeatherForecast> NewForecasts
{
get
{
{
var rng = new Random();
return Enumerable.Range(1, 10).Select(index => new WeatherForecast
{
//ID = index,
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
}).ToList();
}
}
}
DbContextExtensions
我们使用泛型,因此我们需要一种方法来获取声明为TRecord的数据类的DbSet。这是作为扩展方法实现的DbContext。为此,每个DbSet名称都应与数据类具有相同的名称。如果名称不同,则dbSetName提供备份。
该方法使用反射来为 TRecord查找DbSet。
public static DbSet<TRecord> GetDbSet<TRecord>(this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord<TRecord>, new()
{
var recname = new TRecord().GetType().Name;
// Get the property info object for the DbSet
var pinfo = context.GetType().GetProperty(dbSetName ?? recname);
DbSet<TRecord> dbSet = null;
// Get the property DbSet
try
{
dbSet = (DbSet<TRecord>)pinfo.GetValue(context);
}
catch
{
throw new InvalidOperationException($"{recname} does not have a matching DBset ");
}
Debug.Assert(dbSet != null);
return dbSet;
}
IFactoryDataService
IFactoryDataService定义了DataServices必须实现的基本CRUDL方法。数据服务使用接口在服务容器中定义并通过接口使用。注意每种方法及其约束的TRecord。有两种GetRecordListAsync方法。一个获取整个数据集,另一个使用PaginstorData对象对数据集进行分页和排序。更多关于Paginator的内容在第五篇文章中。
public interface IFactoryDataService
{
public Task<List<TRecord>> GetRecordListAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new();
public Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) where TRecord : class, IDbRecord<TRecord>, new();
public Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new();
public Task<int> GetRecordListCountAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new();
public Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new();
public Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new();
public Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new();
}
FactoryDataService
FactoryDataService是IFactoryDataService的抽象实现。它提供默认记录、列表或未实现的 DBTaskResult消息。
public abstract class FactoryDataService: IFactoryDataService
{
public Guid ServiceID { get; } = Guid.NewGuid();
public IConfiguration AppConfiguration { get; set; }
public FactoryDataService(IConfiguration configuration) => this.AppConfiguration = configuration;
public virtual Task<List<TRecord>> GetRecordListAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new List<TRecord>());
public virtual Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new List<TRecord>());
public virtual Task<TRecord> GetRecordAsync<TRecord>(int id) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new TRecord());
public virtual Task<int> GetRecordListCountAsync<TRecord>() where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(0);
public virtual Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" });
public virtual Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" });
public virtual Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record) where TRecord : class, IDbRecord<TRecord>, new()
=> Task.FromResult(new DbTaskResult() { IsOK = false, Type = MessageType.NotImplemented, Message = "Method not implemented" });
}
FactoryServerDataService
这是具体的服务器端实现。每个数据库操作都是使用单独的DbContext实例实现的。GetDBSet用于为TRecord获取正确DBSet的注意事项。
public class FactoryServerDataService<TDbContext> : FactoryDataService where TDbContext : DbContext
{
protected virtual IDbContextFactory<TDbContext> DBContext { get; set; } = null;
public FactoryServerDataService(IConfiguration configuration, IDbContextFactory<TDbContext> dbContext) : base(configuration)
=> this.DBContext = dbContext;
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>()
=> await this.DBContext
.CreateDbContext()
.GetDbSet<TRecord>()
.ToListAsync() ?? new List<TRecord>();
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData)
{
var startpage = paginatorData.Page <= 1
? 0
: (paginatorData.Page - 1) * paginatorData.PageSize;
var context = this.DBContext.CreateDbContext();
var dbset = this.DBContext
.CreateDbContext()
.GetDbSet<TRecord>();
var x = typeof(TRecord).GetProperty(paginatorData.SortColumn);
var isSortable = typeof(TRecord).GetProperty(paginatorData.SortColumn) != null;
if (isSortable)
{
var list = await dbset
.OrderBy(paginatorData.SortDescending ? $"{paginatorData.SortColumn} descending" : paginatorData.SortColumn)
.Skip(startpage)
.Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>();
return list;
}
else
{
var list = await dbset
.Skip(startpage)
.Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>();
return list;
}
}
public override async Task<TRecord> GetRecordAsync<TRecord>(int id)
=> await this.DBContext.
CreateDbContext().
GetDbSet<TRecord>().
FirstOrDefaultAsync(item => ((IDbRecord<TRecord>)item).ID == id) ?? default;
public override async Task<int> GetRecordListCountAsync<TRecord>()
=> await this.DBContext.CreateDbContext().GetDbSet<TRecord>().CountAsync();
public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record)
{
var context = this.DBContext.CreateDbContext();
context.Entry(record).State = EntityState.Modified;
return await this.UpdateContext(context);
}
public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record)
{
var context = this.DBContext.CreateDbContext();
context.GetDbSet<TRecord>().Add(record);
return await this.UpdateContext(context);
}
public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record)
{
var context = this.DBContext.CreateDbContext();
context.Entry(record).State = EntityState.Deleted;
return await this.UpdateContext(context);
}
protected async Task<DbTaskResult> UpdateContext(DbContext context)
=> await context.SaveChangesAsync() > 0 ? DbTaskResult.OK() : DbTaskResult.NotOK();
}
该FactoryWASMDataService看起来有点不同。它实现了接口,但用于HttpClient获取/发布到服务器上的API。
服务映射如下所示:
UI控制器服务 => WASMDataService => API控制器 => ServerDataService => DBContext
public class FactoryWASMDataService : FactoryDataService, IFactoryDataService
{
protected HttpClient HttpClient { get; set; }
public FactoryWASMDataService(IConfiguration configuration, HttpClient httpClient) : base(configuration)
=> this.HttpClient = httpClient;
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>()
=> await this.HttpClient.GetFromJsonAsync<List<TRecord>>($"{GetRecordName<TRecord>()}/list");
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData)
{
var response = await this.HttpClient.PostAsJsonAsync($"{GetRecordName<TRecord>()}/listpaged", paginatorData);
return await response.Content.ReadFromJsonAsync<List<TRecord>>();
}
public override async Task<TRecord> GetRecordAsync<TRecord>(int id)
{
var response = await this.HttpClient.PostAsJsonAsync($"{GetRecordName<TRecord>()}/read", id);
var result = await response.Content.ReadFromJsonAsync<TRecord>();
return result;
}
public override async Task<int> GetRecordListCountAsync<TRecord>()
=> await this.HttpClient.GetFromJsonAsync<int>($"{GetRecordName<TRecord>()}/count");
public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record)
{
var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"{GetRecordName<TRecord>()}/update", record);
var result = await response.Content.ReadFromJsonAsync<DbTaskResult>();
return result;
}
public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record)
{
var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"{GetRecordName<TRecord>()}/create", record);
var result = await response.Content.ReadFromJsonAsync<DbTaskResult>();
return result;
}
public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record)
{
var response = await this.HttpClient.PostAsJsonAsync<TRecord>($"{GetRecordName<TRecord>()}/update", record);
var result = await response.Content.ReadFromJsonAsync<DbTaskResult>();
return result;
}
protected string GetRecordName<TRecord>() where TRecord : class, IDbRecord<TRecord>, new()
=> new TRecord().GetType().Name;
}
API控制器
控制器在Web项目中实现,每个DataClass一个。
WeatherForecast控制器如下所示。它基本上通过IFactoryService接口将请求传递到FactoryServerDataService。
[ApiController]
public class WeatherForecastController : ControllerBase
{
protected IFactoryDataService DataService { get; set; }
private readonly ILogger<WeatherForecastController> logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IFactoryDataService dataService)
{
this.DataService = dataService;
this.logger = logger;
}
[MVC.Route("weatherforecast/list")]
[HttpGet]
public async Task<List<WeatherForecast>> GetList() => await DataService.GetRecordListAsync<WeatherForecast>();
[MVC.Route("weatherforecast/listpaged")]
[HttpGet]
public async Task<List<WeatherForecast>> Read([FromBody] PaginatorData data) => await DataService.GetRecordListAsync<WeatherForecast>( paginator: data);
[MVC.Route("weatherforecast/count")]
[HttpGet]
public async Task<int> Count() => await DataService.GetRecordListCountAsync<WeatherForecast>();
[MVC.Route("weatherforecast/get")]
[HttpGet]
public async Task<WeatherForecast> GetRec(int id) => await DataService.GetRecordAsync<WeatherForecast>(id);
[MVC.Route("weatherforecast/read")]
[HttpPost]
public async Task<WeatherForecast> Read([FromBody]int id) => await DataService.GetRecordAsync<WeatherForecast>(id);
[MVC.Route("weatherforecast/update")]
[HttpPost]
public async Task<DbTaskResult> Update([FromBody]WeatherForecast record) => await DataService.UpdateRecordAsync<WeatherForecast>(record);
[MVC.Route("weatherforecast/create")]
[HttpPost]
public async Task<DbTaskResult> Create([FromBody]WeatherForecast record) => await DataService.CreateRecordAsync<WeatherForecast>(record);
[MVC.Route("weatherforecast/delete")]
[HttpPost]
public async Task<DbTaskResult> Delete([FromBody] WeatherForecast record) => await DataService.DeleteRecordAsync<WeatherForecast>(record);
}
FactoryServerInMemoryDataService
为了测试和演示,还有另一个使用SQLite in-memory DbContext的服务器数据服务。
该代码类似于FactoryServerDataService,但对所有事务使用单个DbContext。
public class FactoryServerInMemoryDataService<TDbContext> : FactoryDataService, IFactoryDataService where TDbContext : DbContext
{
protected virtual IDbContextFactory<TDbContext> DBContext { get; set; } = null;
private DbContext _dbContext;
public FactoryServerInMemoryDataService(IConfiguration configuration, IDbContextFactory<TDbContext> dbContext) : base(configuration)
{
this.DBContext = dbContext;
_dbContext = this.DBContext.CreateDbContext();
}
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>()
{
var dbset = _dbContext.GetDbSet<TRecord>();
return await dbset.ToListAsync() ?? new List<TRecord>();
}
public override async Task<List<TRecord>> GetRecordListAsync<TRecord>(PaginatorData paginatorData)
{
var startpage = paginatorData.Page <= 1
? 0
: (paginatorData.Page - 1) * paginatorData.PageSize;
var dbset = _dbContext.GetDbSet<TRecord>();
var isSortable = typeof(TRecord).GetProperty(paginatorData.SortColumn) != null;
if (isSortable)
{
var list = await dbset
.OrderBy(paginatorData.SortDescending ? $"{paginatorData.SortColumn} descending" : paginatorData.SortColumn)
.Skip(startpage)
.Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>();
return list;
}
else
{
var list = await dbset
.Skip(startpage)
.Take(paginatorData.PageSize).ToListAsync() ?? new List<TRecord>();
return list;
}
}
public override async Task<TRecord> GetRecordAsync<TRecord>(int id)
{
var dbset = _dbContext.GetDbSet<TRecord>();
return await dbset.FirstOrDefaultAsync(item => ((IDbRecord<TRecord>)item).ID == id) ?? default;
}
public override async Task<int> GetRecordListCountAsync<TRecord>()
{
var dbset = _dbContext.GetDbSet<TRecord>();
return await dbset.CountAsync();
}
public override async Task<DbTaskResult> UpdateRecordAsync<TRecord>(TRecord record)
{
_dbContext.Entry(record).State = EntityState.Modified;
var x = await _dbContext.SaveChangesAsync();
return new DbTaskResult() { IsOK = true, Type = MessageType.Success };
}
public override async Task<DbTaskResult> CreateRecordAsync<TRecord>(TRecord record)
{
var dbset = _dbContext.GetDbSet<TRecord>();
dbset.Add(record);
var x = await _dbContext.SaveChangesAsync();
return new DbTaskResult() { IsOK = true, Type = MessageType.Success, NewID = record.ID };
}
public override async Task<DbTaskResult> DeleteRecordAsync<TRecord>(TRecord record)
{
_dbContext.Entry(record).State = EntityState.Deleted;
var x = await _dbContext.SaveChangesAsync();
return new DbTaskResult() { IsOK = true, Type = MessageType.Success };
}
}
控制器服务
控制器服务是数据服务和UI之间的接口。他们实现了管理他们负责的数据类所需的逻辑。虽然大部分代码都驻留在FactoryControllerService中,但不可避免地会有一些特定于数据类的代码。
IFactoryControllerService
IFactoryControllerService 定义基本表单代码使用的公共接口。
注意:
- 泛型的TRecord.
- 保存当前记录和记录列表的属性。
- 用于简化状态管理的布尔逻辑属性。
- 记录和列表更改的事件。
- 重置方法以重置服务/记录/列表。
- 更新/使用当前记录/列表的CRUDL方法。
public interface IFactoryControllerService<TRecord> where TRecord : class, IDbRecord<TRecord>, new()
{
public Guid Id { get; }
public TRecord Record { get; }
public List<TRecord> Records { get; }
public int RecordCount => this.Records?.Count ?? 0;
public int RecordId { get; }
public Guid RecordGUID { get; }
public DbTaskResult DbResult { get; }
public Paginator Paginator { get; }
public bool IsRecord => this.Record != null && this.RecordId > -1;
public bool HasRecords => this.Records != null && this.Records.Count > 0;
public bool IsNewRecord => this.IsRecord && this.RecordId == -1;
public event EventHandler RecordHasChanged;
public event EventHandler ListHasChanged;
public Task Reset();
public Task ResetRecordAsync();
public Task ResetListAsync();
public Task GetRecordsAsync() => Task.CompletedTask;
public Task<bool> SaveRecordAsync();
public Task<bool> GetRecordAsync(int id);
public Task<bool> NewRecordAsync();
public Task<bool> DeleteRecordAsync();
}
FactoryControllerService
FactoryControllerService是IFactoryControllerService的抽象实现。它包含所有样板代码。大部分代码是不言自明的。
public abstract class FactoryControllerService<TRecord> : IDisposable, IFactoryControllerService<TRecord> where TRecord : class, IDbRecord<TRecord>, new()
{
// unique ID for this instance
public Guid Id { get; } = Guid.NewGuid();
// Record Property. Triggers Event when changed.
public TRecord Record
{
get => _record;
private set
{
this._record = value;
this.RecordHasChanged?.Invoke(value, EventArgs.Empty);
}
}
private TRecord _record = null;
// Recordset Property. Triggers Event when changed.
public List<TRecord> Records
{
get => _records;
private set
{
this._records = value;
this.ListHasChanged?.Invoke(value, EventArgs.Empty);
}
}
private List<TRecord> _records = null;
public int RecordId => this.Record?.ID ?? 0;
public Guid RecordGUID => this.Record?.GUID ?? Guid.Empty;
public DbTaskResult DbResult { get; set; } = new DbTaskResult();
/// Property for the Paging object that controls paging and interfaces with the UI Paging Control
public Paginator Paginator { get; private set; }
public bool IsRecord => this.Record != null && this.RecordId > -1;
public bool HasRecords => this.Records != null && this.Records.Count > 0;
public bool IsNewRecord => this.IsRecord && this.RecordId == -1;
/// Data Service for data access
protected IFactoryDataService DataService { get; set; }
public event EventHandler RecordHasChanged;
public event EventHandler ListHasChanged;
public FactoryControllerService(IFactoryDataService factoryDataService)
{
this.DataService = factoryDataService;
this.Paginator = new Paginator(10, 5);
this.Paginator.PageChanged += this.OnPageChanged;
}
/// Method to reset the service
public Task Reset()
{
this.Record = null;
this.Records = null;
return Task.CompletedTask;
}
/// Method to reset the record list
public Task ResetListAsync()
{
this.Records = null;
return Task.CompletedTask;
}
/// Method to reset the Record
public Task ResetRecordAsync()
{
this.Record = null;
return Task.CompletedTask;
}
/// Method to get a recordset
public async Task GetRecordsAsync()
{
this.Records = await DataService.GetRecordListAsync<TRecord>(this.Paginator.GetData);
this.Paginator.RecordCount = await GetRecordListCountAsync();
this.ListHasChanged?.Invoke(null, EventArgs.Empty);
}
/// Method to get a record
/// if id < 1 will create a new record
public async Task<bool> GetRecordAsync(int id)
{
if (id > 0)
this.Record = await DataService.GetRecordAsync<TRecord>(id);
else
this.Record = new TRecord();
return this.IsRecord;
}
/// Method to get the current record count
public async Task<int> GetRecordListCountAsync()
=> await DataService.GetRecordListCountAsync<TRecord>();
public async Task<bool> SaveRecordAsync()
{
if (this.RecordId == -1)
this.DbResult = await DataService.CreateRecordAsync<TRecord>(this.Record);
else
this.DbResult = await DataService.UpdateRecordAsync(this.Record);
await this.GetRecordsAsync();
return this.DbResult.IsOK;
}
public async Task<bool> DeleteRecordAsync()
{
this.DbResult = await DataService.DeleteRecordAsync<TRecord>(this.Record);
return this.DbResult.IsOK;
}
public Task<bool> NewRecordAsync()
{
this.Record = default(TRecord);
return Task.FromResult(false);
}
protected async void OnPageChanged(object sender, EventArgs e)
=> await this.GetRecordsAsync();
protected void NotifyRecordChanged(object sender, EventArgs e)
=> this.RecordHasChanged?.Invoke(sender, e);
protected void NotifyListChanged(object sender, EventArgs e)
=> this.ListHasChanged?.Invoke(sender, e);
public virtual void Dispose() {}
}
WeatherForecastControllerService
样板化的回报来自于以下WeatheForcastControllerService声明:
public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast>
{
public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { }
}
总结
本文展示了如何使用一组实现CRUDL操作样板代码的抽象类来构建数据服务。我特意将代码中的错误检查保持在最低限度,以使其更具可读性。您可以根据需要尽可能少地或尽可能多地实现。
需要注意的一些关键点:
- 尽可能使用Aysnc代码。数据访问功能都是异步的。
- 泛型使大部分样板成为可能。它们会造成复杂性,但值得付出努力。
- 接口对于依赖注入和UI样板化至关重要。
如果您在未来阅读本文,请查看存储库中的自述文件以获取文章集的最新版本。
https://www.codeproject.com/Articles/5279596/Building-a-Database-Application-in-Blazor-Part-2-S