天天看點

在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架構。

繼續閱讀