在 Blazor 中构建数据库应用程序 - 第二部分 - 服务 - 构建 CRUD 数据层






4.73/5 (7投票s)
如何在 Blazor 数据库应用程序中构建 CRUD 数据层
这是关于构建 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 服务会稍微复杂一些。
- Scoped 服务对象存在于客户端应用程序会话的生命周期内 - 请注意是客户端而不是服务器。任何应用程序重置,例如按 F5 或导航离开应用程序,都会重置所有 Scoped 服务。浏览器中的重复标签页会创建一个新应用程序,以及一组新的 Scoped 服务。
- Scoped 服务可以在代码中进一步限定到单个对象。OwningComponentBase组件类具有将 Scoped 服务的生命周期限制在组件生命周期内的功能。
Services 是 Blazor IOC [控制反转] 容器。服务实例被声明为
- 在 Blazor Server 的 Startup.cs的ConfigureServices中。
- 在 Blazor WASM 的 Program.cs中。
该解决方案使用 Service Collection 扩展方法,例如 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;
}
Points
- 每个项目/库都有一个 IServiceCollection扩展方法来封装项目所需的特定服务。
- 只有数据层服务不同。Server 版本(由 Blazor Server 和 WASM API Server 使用)与数据库和 Entity Framework 进行交互。它被作用域为 Singleton。
- 一切都是异步的,使用 DbContextFactory并管理使用的DbContext实例。WASM 客户端版本使用HttpClient(这是一个 Scoped 服务)来调用 API,因此它是 Scoped 的。
- 实现 IFactoryDataService的FactoryDataService通过泛型处理所有数据请求。TRecord定义了要检索和返回的数据集。工厂服务为所有核心数据服务代码提供了样板。
- 同时有真实的 SQL 数据库和内存中的 SQLite DbContext。
Generics
工厂库代码严重依赖泛型。定义了两个泛型实体。
- 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 框架实体,如 Alerts 和 Toasts。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 数据实体的类。
Points
- 用于属性标签的 Entity Framework 属性。
- 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;
        }
Entity Framework 层
该应用程序实现了两个 Entity Framework 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 的信息将在第 5 篇文章中介绍。
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 实例来实现。注意用于获取 TRecord 的正确 DBSet 的 GetDBSet。
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 Controller Service => WASMDataService => API Controller => 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 Controllers
控制器在 Web 项目中实现,每个数据类一个。
下面显示了 WeatherForecast Controller。它基本上通过 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 内存 DbContext 的 Server Data Service。
代码与 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 };
    }
}
Controller Services
Controller Services 是 Data Service 和 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 操作样板代码的抽象类来构建数据服务。我故意将错误检查放在代码的最低限度,以使其更易于阅读。您可以根据需要实现。
一些需要注意的关键点
- 尽可能使用异步代码。数据访问函数都是异步的。
- 泛型使得许多样板化成为可能。它们会增加复杂性,但值得付出努力。
- 接口对于依赖注入和 UI 样板化至关重要。
如果您在未来很久之后阅读本文,请查看存储库中的自述文件以获取本文集的最新版本。
历史
* 2020 年 9 月 15 日:初始版本。
* 2020 年 10 月 2 日:小的格式更新和拼写错误修复。
* 2020年11月17日:Blazor.CEC 库重大更改。ViewManager 更改为 Router,以及新的 Component 基类实现。
* 2021 年 3 月 28 日:服务、项目结构和数据编辑的重大更新。


