65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (7投票s)

2020年9月15日

CPOL

8分钟阅读

viewsIcon

30496

downloadIcon

180

如何在 Blazor 数据库应用程序中构建 CRUD 数据层

这是关于构建 Blazor 数据库应用程序系列的第二篇文章。它描述了如何将数据层和业务逻辑层样板化为通用的库代码,从而简化应用程序特定数据服务的部署。这是早期版本的完全重写。

系列文章包括

  1. 项目结构和框架。
  2. 服务 - 构建 CRUD 数据层。
  3. 视图组件 - UI 中的 CRUD 编辑和查看操作。
  4. UI 组件 - 构建 HTML/CSS 控件。
  5. 视图组件 - 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; }

    }

我们为新数据库实体添加过程是

  1. 将必要的表添加到数据库。
  2. 定义一个数据类。
  3. DbContext 中定义一个 DbSet
  4. 定义一个 public class nnnnnnControllerService 服务并将其注册到服务容器。

某些实体会遇到并发症,但这并不排除该方法 - 80% 以上的代码都在库中。

服务

Blazor 构建于 DI [依赖注入] 和 IOC [控制反转] 原则之上。如果您不熟悉这些概念,请在深入 Blazor 之前进行一些背景阅读。长远来看,这将为您节省时间!

Blazor 的 Singleton 和 Transient 服务相对直接。您可以在Microsoft 文档中了解更多关于它们的信息。Scoped 服务会稍微复杂一些。

  1. Scoped 服务对象存在于客户端应用程序会话的生命周期内 - 请注意是客户端而不是服务器。任何应用程序重置,例如按 F5 或导航离开应用程序,都会重置所有 Scoped 服务。浏览器中的重复标签页会创建一个新应用程序,以及一组新的 Scoped 服务。
  2. Scoped 服务可以在代码中进一步限定到单个对象。OwningComponentBase 组件类具有将 Scoped 服务的生命周期限制在组件生命周期内的功能。

Services 是 Blazor IOC [控制反转] 容器。服务实例被声明为

  1. 在 Blazor Server 的 Startup.csConfigureServices 中。
  2. 在 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

  1. 每个项目/库都有一个 IServiceCollection 扩展方法来封装项目所需的特定服务。
  2. 只有数据层服务不同。Server 版本(由 Blazor Server 和 WASM API Server 使用)与数据库和 Entity Framework 进行交互。它被作用域为 Singleton。
  3. 一切都是异步的,使用 DbContextFactory 并管理使用的 DbContext 实例。WASM 客户端版本使用 HttpClient(这是一个 Scoped 服务)来调用 API,因此它是 Scoped 的。
  4. 实现 IFactoryDataServiceFactoryDataService 通过泛型处理所有数据请求。TRecord 定义了要检索和返回的数据集。工厂服务为所有核心数据服务代码提供了样板。
  5. 同时有真实的 SQL 数据库和内存中的 SQLite DbContext

Generics

工厂库代码严重依赖泛型。定义了两个泛型实体。

  1. TRecord 代表一个模型记录类。它必须是一个类,实现 IDbRecord 并定义一个空的 new()TRecord 在方法级别使用。
  2. 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 方法。

  1. GetRecordList - 获取数据集中记录的列表。这可以进行分页和排序。
  2. GetRecord - 通过 ID 获取单个记录。
  3. CreateRecord - 创建新记录。
  4. UpdateRecord - 基于 ID 更新记录。
  5. 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

  1. ID 是标准的数据库标识字段。通常是一个 int
  2. GUID 是此记录副本的唯一标识符。
  3. 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

  1. 用于属性标签的 Entity Framework 属性。
  2. IDbRecord 的实现。
  3. 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。每个 DbSetOnModelCreating() 中链接到一个视图。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 在名称不同时提供后备。

该方法使用反射来查找 TRecordDbSet

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

FactoryDataServiceIFactoryDataService 的抽象实现。它提供了默认记录、列表或“未实现”的 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 定义了基本表单代码使用的通用接口。

注意

  1. 通用 TRecord
  2. 持有当前记录和记录列表的属性。
  3. 布尔逻辑属性,用于简化状态管理。
  4. 用于记录和列表更改的事件。
  5. 重置服务/记录/列表的重置方法。
  6. 更新/使用当前记录/列表的 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

FactoryControllerServiceIFactoryControllerService 的抽象实现。它包含了所有样板代码。大部分代码是不言自明的。

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 操作样板代码的抽象类来构建数据服务。我故意将错误检查放在代码的最低限度,以使其更易于阅读。您可以根据需要实现。

一些需要注意的关键点

  1. 尽可能使用异步代码。数据访问函数都是异步的。
  2. 泛型使得许多样板化成为可能。它们会增加复杂性,但值得付出努力。
  3. 接口对于依赖注入和 UI 样板化至关重要。

如果您在未来很久之后阅读本文,请查看存储库中的自述文件以获取本文集的最新版本。

历史

* 2020 年 9 月 15 日:初始版本。

* 2020 年 10 月 2 日:小的格式更新和拼写错误修复。

* 2020年11月17日:Blazor.CEC 库重大更改。ViewManager 更改为 Router,以及新的 Component 基类实现。

* 2021 年 3 月 28 日:服务、项目结构和数据编辑的重大更新。

© . All rights reserved.