天天看点

在Blazor中构建数据库应用程序——第2部分——服务——构建CRUD数据层介绍储存库和数据库服务总结

目录

介绍

储存库和数据库

服务

泛型

实体框架层

WeatherForecastDBContext

数据服务层

IDbRecord

IDataService

BaseDataService

BaseServerDataService

BaseWASMDataService

项目具体实现

业务逻辑/控制器服务层

WeatherForecastControllerService

WeatherForecastController

总结

介绍

本文是有关构建Blazor项目的系列文章中的第二篇:它描述了将数据和业务逻辑层抽象到库中的样板代码中的技术和方法。

  1. 项目结构与框架
  2. 服务——构建CRUD数据层
  3. View组件——UI中的CRUD编辑和查看操作
  4. UI组件——构建HTML / CSS控件
  5. View组件-UI中的CRUD列表操作
  6. 逐步详细介绍如何向应用程序添加气象站和气象站数据

储存库和数据库

CEC.Blazor GitHub存储库

存储库中有一个SQL脚本在/SQL中,用于构建数据库。

您可以在此处查看运行的项目的服务器版本。

你可以看到该项目的WASM版本运行在这里。

服务

Blazor建立在DI [依赖注入]和IOC [控制反转]的基础上。

Blazor Singleton和Transient服务相对简单。您可以在Microsoft文档中阅读有关它们的更多信息。范围要稍微复杂一些。

  1. 在客户端应用程序会话的生命周期内存在作用域服务对象——注意客户端而不是服务器。任何应用程序重置(例如F5或离开应用程序的导航)都会重置所有作用域服务。浏览器中重复的选项卡将创建一个新的应用程序和一组新的范围服务。
  2. 作用域服务可以作用域到代码中的对象。这在UI组件中是最常见的。OwningComponentBase组件类都有功能来限制作用域服务的生命周期到组件的声明周期。在另一篇文章中将对此进行更详细的介绍。

服务是Blazor IOC [Inversion of Control]容器。

在服务器模式下,服务在startup.cs中配置:

// CEC.Blazor.Server/startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    // the Services for the CEC.Blazor .
    services.AddCECBlazor();
    // the local application Services defined in ServiceCollectionExtensions.cs
    services.AddApplicationServices(Configurtion);
}
           
// CEC.Blazor.Server/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddApplicationServices
       (this IServiceCollection services, IConfiguration configuration)
{
    // Singleton service for the Server Side version of WeatherForecast Data Service 
    // services.AddSingleton<IWeatherForecastDataService, WeatherForecastDummyDataService>();
    services.AddSingleton<IWeatherForecastDataService, WeatherForecastServerDataService>();
    // Scoped service for the WeatherForecast Controller Service
    services.AddScoped<WeatherForecastControllerService>();
    // Transient service for the Fluent Validator for the WeatherForecast record
    services.AddTransient<IValidator<DbWeatherForecast>, WeatherForecastValidator>();
    // Factory that builds the specific DBContext 
    var dbContext = configuration.GetValue<string>("Configuration:DBContext");
    services.AddDbContextFactory<WeatherForecastDbContext>
    (options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton);
    return services;
}
           

和WASM模式下的program.cs:

// CEC.Blazor.WASM.Client/program.cs
public static async Task Main(string[] args)
{
    .....
    // Added here as we don't have access to builder in AddApplicationServices
    builder.Services.AddScoped(sp => new HttpClient 
    { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    // the Services for the CEC.Blazor Library
    builder.Services.AddCECBlazor();
    // the local application Services defined in ServiceCollectionExtensions.cs
    builder.Services.AddApplicationServices();
    .....
}
           
// CEC.Blazor.WASM.Client/Extensions/ServiceCollectionExtensions.cs
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
    // Scoped service for the WASM Client version of WeatherForecast Data Service 
    services.AddScoped<IWeatherForecastDataService, WeatherForecastWASMDataService>();
    // Scoped service for the WeatherForecast Controller Service
    services.AddScoped<WeatherForecastControllerService>();
    services.AddTransient<IValidator<DbWeatherForecast>, WeatherForecastValidator>();
    // Transient service for the Fluent Validator for the WeatherForecast record
    return services;
}
           

要点:

  1. 每个项目/库都有一种IServiceCollection扩展方法,用于封装项目所需的特定服务。
  2. 仅数据层服务不同。Blazor服务器和WASM API服务器都使用的Server版本与数据库和Entity Framework连接。它的作用域为Singleton——当我们运行异步时,每个查询都会创建和关闭DbContext。客户端版本使用HttpClient(是作用域服务)对API进行调用,因此本身就是作用域的。还有一个虚拟数据服务来模拟数据库。
  3. 使用代码工厂来构建特定的DBContext,并提供必要的抽象级别,以将基本数据服务代码复制到基础库中。

泛型

样板库代码在很大程度上依赖于泛型。使用的两个通用实体是:

  1. TRecord——这代表模型记录类。它必须实现IDbRecord,new()并且是一个类。
  2. TContext——这是数据库上下文,必须从DbContext类继承。

类声明如下所示:

// CEC.Blazor/Services/BaseDataClass.cs
public abstract class BaseDataService<TRecord, TContext>: 
    IDataService<TRecord, TContext>
    where TRecord : class, IDbRecord<TRecord>, new()
    where TContext : DbContext
{......}
           

实体框架层

该解决方案结合了实体框架[EF]和常规数据库访问。以前是老派(应用程序离数据表很远),我通过存储过程实现了CUD [CRUD不带读取],并通过视图实现了R [读取访问]和列表。数据层具有两层——EF数据库上下文和数据服务。

实体框架数据库使用的数据库帐户具有访问权限,只能在视图上选择并在存储过程上执行。

该演示应用程序可以在有或没有完整数据库连接的情况下运行——有一个“虚拟数据库”服务器数据服务。

所有EF代码都在共享项目CEC.Weather特定的库中实现。

WeatherForecastDBContext

DbContext有每个记录类型的DbSet。每个DbSet都链接到OnModelCreating()中的视图。WeatherForecast应用程序具有一种记录类型。

该类如下所示:

// CEC.Weather/Data/WeatherForecastDbContext.cs
public class WeatherForecastDbContext : DbContext
{
    public WeatherForecastDbContext
           (DbContextOptions<WeatherForecastDbContext> options) : base(options) { }

    public DbSet<DbWeatherForecast> WeatherForecasts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder
            .Entity<DbWeatherForecast>(eb =>
            {
                eb.HasNoKey();
                eb.ToView("vw_WeatherForecast");
            });
    }
}
           

数据服务层

IDbRecord

IDbRecord 定义所有数据库记录的公共接口。

// CEC.Blazor/Data/Interfaces/IDbRecord.cs
public interface IDbRecord<T>
{
    public int ID { get; }

    public string DisplayName { get; }

    public T ShadowCopy(); 
}
           

IDbRecord 确保:

  • 选择下拉列表的ID/Value对
  • 显示记录时在任何控件的标题区域中使用的默认名称
  • 编辑期间需要时的记录的深层副本

IDataService

IDataService接口中定义了核心数据服务功能。

// CEC.Blazor/Services/Interfaces/IDataService.cs
 public interface IDataService<TRecord, TContext> 
        where TRecord : class, IDbRecord<TRecord>, new() 
        where TContext : DbContext
     {
        /// Used by the WASM client, otherwise set to null
        public HttpClient HttpClient { get; set; }

        /// Access to the DBContext using the IDbContextFactory interface 
       public IDbContextFactory<TContext> DBContext { get; set; }

        /// Access to the application configuration in Server
        public IConfiguration AppConfiguration { get; set; }

        /// Record Configuration object that contains routing and 
        /// naming information about the specific record type
        public RecordConfigurationData RecordConfiguration { get; set; }

        /// Method to get the full Record List
        public Task<List<TRecord>> GetRecordListAsync() => 
                                   Task.FromResult(new List<TRecord>());

        /// Method to get a filtered Record List using a IFilterLit object
        public Task<List<TRecord>> GetFilteredRecordListAsync
               (IFilterList filterList) => Task.FromResult(new List<TRecord>());

        /// Method to get a single Record
        public Task<TRecord> GetRecordAsync(int id) => Task.FromResult(new TRecord());

        /// Method to get the current record count
        public Task<int> GetRecordListCountAsync() => Task.FromResult(0);

        /// Method to update a record
        public Task<DbTaskResult> UpdateRecordAsync(TRecord record) => 
               Task.FromResult(new DbTaskResult() { IsOK = false, 
               Type = MessageType.NotImplemented, Message = "Method not implemented" });

        /// Method to create and add a record
        public Task<DbTaskResult> CreateRecordAsync(TRecord record) => 
               Task.FromResult(new DbTaskResult() { IsOK = false, 
               Type = MessageType.NotImplemented, Message = "Method not implemented" });

        /// Method to delete a record
        public Task<DbTaskResult> DeleteRecordAsync(TRecord record) => 
               Task.FromResult(new DbTaskResult() { IsOK = false, 
               Type = MessageType.NotImplemented, Message = "Method not implemented" });

        /// Method to build the a list of SqlParameters for a CUD Stored Procedure.
        /// Uses custom attribute data.
        public List<SqlParameter> GetSQLParameters(TRecord item, bool withid = false) => 
               new List<SqlParameter>();
    }
           

BaseDataService

BaseDataService 实现接口:

// CEC.Blazor/Services/Interfaces
public abstract class BaseDataService<TRecord>: 
       IDataService<TRecord> where TRecord : IDbRecord<TRecord>, new()
{
    // The HttpClient used by the WASM dataservice implementation - 
    // set to null by default - set in the WASM implementation
    public HttpClient HttpClient { get; set; } = null;

    // The DBContext access through the IDbContextFactory interface - 
    // set to null by default - set in the Server implementation
    public virtual IDbContextFactory<TContext> DBContext { get; set; } = null;

    // Access to the Application Configuration
    public IConfiguration AppConfiguration { get; set; }
    
    // Record Configuration - set in each specific model implementation
    public virtual RecordConfigurationData RecordConfiguration { get; set; } = 
                                                       new RecordConfigurationData();

    // Base new
    public BaseDataService(IConfiguration configuration) => 
                                    this.AppConfiguration = configuration;
    }
           

BaseServerDataService

请参阅项目代码以获取完整的类——这相当长。

该服务实现样板代码:

  1. 实现IDataService接口CRUD方法。
  2. 用于构建“创建、更新和删除存储过程”的异步方法。
  3. 使用EF DbSet获取列表和单个记录的异步方法。

该代码依赖于

  • 使用命名约定,
  • 模型类名称DbRecordName——例如DbWeatherForecast,
  • DbContext DbSet属性命名RecordName——例如WeatherForecast,
  • 使用自定义属性
  • DbAccess——定义存储过程名称的类级别属性,或
  • SPParameter——特定于属性的特性,用于标记存储过程中使用的所有属性。

下面显示了带有自定义DbWeatherForecast属性的模型类的一小段。

[DbAccess(CreateSP = "sp_Create_WeatherForecast", 
 UpdateSP ="sp_Update_WeatherForecast", DeleteSP ="sp_Delete_WeatherForecast") ]
public class DbWeatherForecast :IDbRecord<DbWeatherForecast>
{
    [SPParameter(IsID = true, DataType = SqlDbType.Int)]
    public int WeatherForecastID { get; set; } = -1;

    [SPParameter(DataType = SqlDbType.SmallDateTime)]
    public DateTime Date { get; set; } = DateTime.Now.Date;
    ......
}
           

EF上的数据操作被实现为DBContext的扩展方法。

存储过程通过调用ExecStoredProcAsync()来运行。该方法如下所示。它使用EF DBContext获取常规的ADO数据库命令对象,然后使用Model类中的自定义属性构建的参数集执行存储过程。

// CEC.Blazor/Extensions/DBContextExtensions.cs
public static async Task<bool> ExecStoredProcAsync
       (this DbContext context, string storedProcName, List<SqlParameter> parameters)
{
    var result = false;

    var cmd = context.Database.GetDbConnection().CreateCommand();
    cmd.CommandText = storedProcName;
    cmd.CommandType = CommandType.StoredProcedure;
    parameters.ForEach(item => cmd.Parameters.Add(item));
    using (cmd)
    {
        if (cmd.Connection.State == ConnectionState.Closed) cmd.Connection.Open();
        try
        {
            await cmd.ExecuteNonQueryAsync();
        }
        catch {}
        finally
        {
            cmd.Connection.Close();
            result = true;
        }
    }
    return result;
}
           

例如使用Create

// CEC.Blazor/Services/DBServerDataService.cs
public async Task<DbTaskResult> CreateRecordAsync(TRecord record) => 
             await this.RunStoredProcedure(record, SPType.Create);
           

请参阅注释以获取信息:

// CEC.Blazor/Services/DBServerDataService.cs
protected async Task<DbTaskResult> RunStoredProcedure(TRecord record, SPType spType)
{
    // Builds a default error DbTaskResult
    var ret = new DbTaskResult()
    {
        Message = $"Error saving {this.RecordConfiguration.RecordDescription}",
        IsOK = false,
        Type = MessageType.Error
    };

    // Gets the correct Stored Procedure name.
    var spname = spType switch
    {
        SPType.Create => this.RecordInfo.CreateSP,
        SPType.Update => this.RecordInfo.UpdateSP,
        SPType.Delete => this.RecordInfo.DeleteSP,
        _ => string.Empty
    };
    
    // Gets the Parameters List
    var parms = this.GetSQLParameters(record, spType);

    // Executes the Stored Procedure with the parameters.
    // Builds a new Success DbTaskResult. In this case (Create) it retrieves the new ID.
    if (await this.DBContext.CreateDbContext().ExecStoredProcAsync(spname, parms))
    {
        var idparam = parms.FirstOrDefault
            (item => item.Direction == ParameterDirection.Output && 
            item.SqlDbType == SqlDbType.Int && item.ParameterName.Contains("ID"));
        ret = new DbTaskResult()
        {
            Message = $"{this.RecordConfiguration.RecordDescription} saved",
            IsOK = true,
            Type = MessageType.Success
        };
        if (idparam != null) ret.NewID = Convert.ToInt32(idparam.Value);
    }
    return ret;
}
           

您可以在GitHub代码文件中深入研究GetSqlParameters。

Read与List方法得到通过反射的DbSet名称,并使用EF方法和IDbRecord接口来获取数据。

// CEC.Blazor/Extensions/DBContextExtensions

public async static Task<List<TRecord>> GetRecordListAsync<TRecord>
 (this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord<TRecord>
{
    var par = context.GetType().GetProperty(dbSetName ?? IDbRecord<TRecord>.RecordName);
    var set = par.GetValue(context);
    var sets = (DbSet<TRecord>)set;
    return await sets.ToListAsync();
}

public async static Task<int> GetRecordListCountAsync<TRecord>
 (this DbContext context, string dbSetName = null) where TRecord : class, IDbRecord<TRecord>
{
    var par = context.GetType().GetProperty(dbSetName ?? IDbRecord<TRecord>.RecordName);
    var set = par.GetValue(context);
    var sets = (DbSet<TRecord>)set;
    return await sets.CountAsync();
}

public async static Task<TRecord> GetRecordAsync<TRecord>
 (this DbContext context, int id, string dbSetName = null) where TRecord : class, 
  IDbRecord<TRecord>
{
    var par = context.GetType().GetProperty(dbSetName ?? IDbRecord<TRecord>.RecordName);
    var set = par.GetValue(context);
    var sets = (DbSet<TRecord>)set;
    return await sets.FirstOrDefaultAsync(item => ((IDbRecord<TRecord>)item).ID == id);
}
           

BaseWASMDataService

有关完整的类,请参见项目代码。

该类的客户端版本相对简单,使用HttpClient可以对服务器进行API调用。同样,我们依靠命名约定来使样板工作。

例如使用Create,

// CEC.Blazor/Services/DBWASMDataService.cs
public async Task<DbTaskResult> CreateRecordAsync(TRecord record)
{
    var response = await this.HttpClient.PostAsJsonAsync<TRecord>
                   ($"{RecordConfiguration.RecordName}/create", record);
    var result = await response.Content.ReadFromJsonAsync<DbTaskResult>();
    return result;
}
           

我们将很快讨论服务器端控制器。

项目具体实现

为了抽象目的,我们定义了一个通用的数据服务接口。这没有实现任何新功能,仅指定了泛型。

// CEC.Weather/Services/Interfaces/IWeatherForecastDataService.cs
public interface IWeatherForecastDataService : 
    IDataService<DbWeatherForecast, WeatherForecastDbContext>
{
    // Only code here is to build dummy data set
}
           

WASM服务继承BaseWASMDataService并实现IWeatherForecastDataService。它定义了泛型并配置RecordConfiguration。

// CEC.Weather/Services/WeatherForecastWASMDataService.cs
public class WeatherForecastWASMDataService :
    BaseWASMDataService<DbWeatherForecast, WeatherForecastDbContext>,
    IWeatherForecastDataService
{
    public WeatherForecastWASMDataService
    (IConfiguration configuration, HttpClient httpClient) : base(configuration, httpClient)
    {
        this.RecordConfiguration = new RecordConfigurationData() 
        { RecordName = "WeatherForecast", RecordDescription = "Weather Forecast", 
        RecordListName = "WeatherForecasts", RecordListDecription = "Weather Forecasts" };
    }
}
           

Server服务继承自BaseServerDataService并实现IWeatherForecastDataService。它定义了泛型并配置RecordConfiguration。

// CEC.Weather/Services/WeatherForecastServerDataService.cs
public class WeatherForecastServerDataService :
    BaseServerDataService<DbWeatherForecast, WeatherForecastDbContext>,
    IWeatherForecastDataService
{
    public WeatherForecastServerDataService(IConfiguration configuration, 
    IDbContextFactory<WeatherForecastDbContext> dbcontext) : base(configuration, dbcontext)
    {
        this.RecordConfiguration = new RecordConfigurationData() 
        { RecordName = "WeatherForecast", RecordDescription = "Weather Forecast", 
        RecordListName = "WeatherForecasts", RecordListDecription = "Weather Forecasts" };
    }
}
           

业务逻辑/控制器服务层

控制器通常配置为作用域服务。

控制器层接口和基类是泛型的,位于CEC.Blazor库中。两个接口,IControllerService和IControllerPagingService定义所需的功能。两者都在BaseControllerService类中实现。

IControllerService,IControllerPagingService和BaseControllerService的代码太长,无法在此处显示。当我们研究UI层与控制器层的接口时,我们将介绍大多数功能。

实现的主要功能是:

  1. 用于保存当前记录和记录集及其状态的属性
  2. 属性和方法(在IControllerPagingService中定义)用于大型数据集上的UI分页操作
  3. 数据集排序的属性和方法
  4. 跟踪记录的编辑状态(Dirty/Clean)的属性和方法
  5. 通过IDataService接口实现CRUD操作的方法
  6. 记录和记录集更改触发的事件。UI用来控制页面刷新
  7. 在路由到使用相同作用域的控制器实例的新页面期间重置控制器的方法

上述功能所需的所有代码都在基类中进行了重复处理。实现基于特定记录的控制器是一项简单的任务,只需最少的编码。

WeatherForecastControllerService

  1. 实现获取所需的DI服务的类构造函数,设置基类并为db数据集分页和排序设置默认的sort列。
  2. 获取用户界面中为Outlook Enum选择框的Dictionary对象。

请注意,使用的数据服务是IWeatherForecastDataService,其是在服务中配置的。对于WASM,这是WeatherForecastWASMDataService;对于服务器或API EASM服务器,这是WeatherForecastServerDataService。

// CEC.Weather/Controllers/ControllerServices/WeatherForecastControllerService.cs
public class WeatherForecastControllerService : BaseControllerService<DbWeatherForecast, 
WeatherForecastDbContext>, IControllerService<DbWeatherForecast, WeatherForecastDbContext>
{
    /// List of Outlooks for Select Controls
    public SortedDictionary<int, string> OutlookOptionList => 
                                         Utils.GetEnumList<WeatherOutlook>();

    public WeatherForecastControllerService(NavigationManager navmanager, 
           IConfiguration appconfiguration, 
           IWeatherForecastDataService weatherForecastDataService) : 
           base(appconfiguration, navmanager)
    {
        this.Service = weatherForecastDataService;
        this.DefaultSortColumn = "WeatherForecastID";
    }
}
           

WeatherForecastController

虽然它不是一项服务,但WeatherForecastController是要覆盖的数据层的最后一部分。它使用IWeatherForecastDataService访问其数据业务,使得相同的调用的ControllerService进入DataService访问,并返回所请求的数据集。我还没有找到一种抽象的方法,因此我们需要为每个记录实现一个。

// CEC.Blazor.WASM.Server/Controllers/WeatherForecastController.cs
[ApiController]
public class WeatherForecastController : ControllerBase
{
    protected IWeatherForecastDataService DataService { get; set; }

    private readonly ILogger<WeatherForecastController> logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, 
           IWeatherForecastDataService weatherForecastDataService)
    {
        this.DataService = weatherForecastDataService;
        this.logger = logger;
    }

    [MVC.Route("weatherforecast/list")]
    [HttpGet]
    public async Task<List<DbWeatherForecast>> GetList() => 
                           await DataService.GetRecordListAsync();

    [MVC.Route("weatherforecast/count")]
    [HttpGet]
    public async Task<int> Count() => await DataService.GetRecordListCountAsync();

    [MVC.Route("weatherforecast/get")]
    [HttpGet]
    public async Task<DbWeatherForecast> 
                 GetRec(int id) => await DataService.GetRecordAsync(id);

    [MVC.Route("weatherforecast/read")]
    [HttpPost]
    public async Task<DbWeatherForecast> 
                 Read([FromBody]int id) => await DataService.GetRecordAsync(id);

    [MVC.Route("weatherforecast/update")]
    [HttpPost]
    public async Task<DbTaskResult> Update([FromBody]DbWeatherForecast record) => 
                                    await DataService.UpdateRecordAsync(record);

    [MVC.Route("weatherforecast/create")]
    [HttpPost]
    public async Task<DbTaskResult> Create([FromBody]DbWeatherForecast record) => 
                                    await DataService.CreateRecordAsync(record);

    [MVC.Route("weatherforecast/delete")]
    [HttpPost]
    public async Task<DbTaskResult> Delete([FromBody] DbWeatherForecast record) => 
                                    await DataService.DeleteRecordAsync(record);
}
           

总结

本文演示了如何将数据和控制器层代码抽象到一个库中。

需要注意的一些关键点:

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

下一节将介绍展示层/UI框架。

继续阅读