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

目录

存储库和数据库

目标

服务

泛型

数据访问

DbTaskResult

数据类

WeatherForecast

实体框架层

WeatherForecastDBContext

LocalWeatherDbContext

InMemoryWeatherDbContext

DbContextExtensions

IFactoryDataService

FactoryDataService

FactoryServerDataService

API控制器

FactoryServerInMemoryDataService

控制器服务

IFactoryControllerService

FactoryControllerService

WeatherForecastControllerService

总结


这是构建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. 作用域服务对象存在于客户端应用程序会话的生命周期内——请注意客户端而不是服务器。任何应用程序重置,例如F5或离开应用程序的导航,都会重置所有范围服务。浏览器中的重复选项卡会创建一个新应用程序和一组新的范围服务。
  2. 范围服务可以进一步限定为代码中的单个对象。OwningComponentBase组件类都有功能来限制范围的服务的生命到组件的寿命。

Services是Blazor IOC [控制反转]容器。服务实例声明如下:

  1. 在Blazor服务器Startup.cs的ConfigureServices中
  2. 在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;
}

要点:

  1. 每个项目/库都有一个IServiceCollection扩展方法来封装项目所需的特定服务。
  2. 只有数据层服务不同。由Blazor服务器和WASM API服务器使用的服务器版本与数据库和实体框架接口。它的范围是单例。
  3. 一切都是异步的,使用DbContextFactory和在使用时管理DbContext实例。WASM客户端版本使用HttpClient(这是一个作用域服务)来调用API,因此是作用域的。
  4. 实现IFactoryDataService的IFactoryDataService通过泛型处理所有数据请求。TRecord定义检索和返回哪个数据集。工厂服务样板所有核心数据服务代码。
  5. 既有真正的SQL数据库,也有内存中的SQLite DbContext。

泛型

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

  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框架实体,例如警报和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。

  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数据实体的数据类。

要点:

  1. 用于属性标签的实体框架属性。
  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;
    }

实体框架层

该应用程序实现了两个实体框架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 定义基本表单代码使用的公共接口。

注意:

  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

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操作样板代码的抽象类来构建数据服务。我特意将代码中的错误检查保持在最低限度,以使其更具可读性。您可以根据需要尽可能少地或尽可能多地实现。

需要注意的一些关键点:

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

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

https://www.codeproject.com/Articles/5279596/Building-a-Database-Application-in-Blazor-Part-2-S

上一篇:04 elasticsearch学习笔记-基本CRUD


下一篇:sdu项目实训初步完成基础模块的CRUD