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

在 Blazor 中构建数据库应用程序 - 第一部分 - 项目结构和框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (12投票s)

2020年9月15日

CPOL

10分钟阅读

viewsIcon

50316

downloadIcon

270

如何构建和结构化 Blazor 数据库应用程序

引言

这组文章介绍了一个用于在 Blazor 中构建和组织数据库应用程序的框架。

这只是一个框架。我不做任何推荐:你可以使用它,也可以滥用它。这是我在我的项目中使用的方法。它是一种轻度主观的框架:尽可能使用开箱即用的 Blazor/Razor/DotNetCore 系统和工具包。CSS 框架是 Bootstrap 的一个轻度定制版本。

共有 6 篇文章介绍了该框架的各个方面和使用的编码模式

  1. 项目结构和框架 - 略作介绍。
  2. 服务 - 构建 CRUD 数据层。
  3. 视图组件 - UI 中的 CRUD 编辑和查看操作。
  4. UI 组件 - 构建 HTML/CSS 控件。
  5. 视图组件 - UI 中的 CRUD 列表操作。

文章自最初发布以来已发生巨大变化

  1. 整个框架的主观性大大降低。我放弃了许多在 Blazor/SPA 中处理某些问题的激进方法。
  2. 随着我对服务器和 WASM 项目如何共存的理解不断加深,库已重新组织。
  3. 所有内容都已更新到 Net5。
  4. 存储库主页已移动。
  5. 服务器和 WASM SPA 现在从同一个站点托管和运行。

它们不是

  1. 定义最佳实践的尝试。
  2. 最终产品。

第一篇文章概述了框架和解决方案架构。

存储库和数据库

这些文章的存储库已移至 Blazor.Database 存储库。所有之前的存储库都已过时,并将很快被删除。

存储库中的 /SQL 目录下有一个用于构建数据库的 SQL 脚本。

现在服务器和 WASM 已合并,演示站点也已更改。站点以服务器模式启动 - https://cec-blazor-server.azurewebsites.net/

设计理念

Data

项目的后端是松散地按照三层模型 - 数据层、逻辑层和表示层 - 来组织的。数据层针对数据库实体实现标准的 CRUDL - 创建/读取/更新/删除/列表 - 操作。

应用程序库包含两个 DbContext

  1. LocalWeatherDbContext 使用标准的 SQL 数据库,连接字符串在 AppSettings 中定义。
  2. InMemoryWeatherDbContext 使用内存中的 SQLite 数据库用于测试和演示目的。

DbContext 服务是通过 AddDBContextFactory 服务扩展实现的 DBContextFactory 创建的。数据服务使用其 IDbContextFactory<TDbContext> 接口。

基本数据层由 IFactoryDataService 接口定义,FactoryDataService 是该接口的抽象实现。有三个通用数据服务实现了大部分样板代码

  1. FactoryServerDataService 用于普通的 SQL 数据库。所有操作都是 Async 的,并使用 IDbContextFactory 在每次事务中获取 DbContext 实例。
  2. FactoryServerInMemoryDataService。SQLite 内存数据库只能存在于单个 DbContext 中。此数据服务在启动时创建一个 DbContext 实例,并将其用于所有事务。
  3. FactoryWASMDataService 用于 WASM SPA。此数据服务向 API 服务器控制器发出远程 API 调用。

为了演示通用服务中实现的样板代码量,本地数据库数据服务的声明如下所示

public class LocalDatabaseDataService : FactoryServerDataService<LocalWeatherDbContext>
{
    public LocalDatabaseDataService(IConfiguration configuration, IDbContextFactory<LocalWeatherDbContext> dbContext) : base(configuration, dbContext) {}
}

以及 WeatherForecast 数据类的控制器服务

public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast>
{
    public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { }
}

我们将在第二篇文章中详细讨论这些内容,以及针对 Weather 应用程序的具体实现。

UI 接口层,我称之为控制器服务,由 IFactoryControllerService 定义,并在 FactoryControllerService 中有一个基本的抽象实现。同样,我们将在第二篇和第三篇文章中详细讨论这些内容。

数据服务通过依赖注入访问,可以直接访问或通过其接口访问。

UI

我在 SPA 中使用“Page”这个词时遇到了一些问题。我认为这是 Microsoft(和其他 SPA 框架)可以引入一些新术语的一个领域。我只将“Pages”目录用于真正的网页。SPA 不是网站,因此要理解它们的工作原理,我们需要跳出网页范式。Blazor UI 是基于组件的;将其视为包含“Pages”会延续这种范式。唯一的网页是服务器上的启动页。一旦 SPA 启动,应用程序就会切换组件以在视图之间过渡。我构建了一个没有任何路由器或 URL 的 SPA - 就像一个桌面应用程序。

在这些文章中,我将使用以下术语

  1. Page - 网站上的启动网页。SPA 中唯一的页面。
  2. RouteView/Routed Component。这些是不同人用来描述伪页面的术语。我使用 RouteViews 这个术语。这是在 Layout 的内容区域显示的,通常由定义的路由决定。我们将在本文后面更详细地讨论这些。
  3. Forms。Forms 是控件的逻辑集合,这些控件显示在视图或模态对话框中。列表、查看表单、编辑表单都是经典的表单。Forms 包含控件而不是 HTML。
  4. Controls。Controls 是显示某些内容的组件:它们发出 HTML 代码。例如,一个编辑框、一个下拉列表、一个按钮...一个 Form 是一个控件的集合。

应用程序配置为构建和部署 SPA 的服务器版本和 WASM 版本,并将两者托管在同一个网站上。基本的解决方案架构是

  1. Core Razor Library - 包含可以部署到任何应用程序的代码。这些可以作为 Nuget 包构建和部署。
  2. Web Assembly Razor Library - 包含 SPA 的应用程序特定代码以及 Web Assembly 特定代码。
  3. ASPNetCore Razor Web Project。宿主项目,包含 WASM 和服务器 SPA 的启动页面、Blazor Server Hub 的服务以及 WASM SPA 的服务器端 API 控制器。

解决方案结构

我使用 Visual Studio,因此 Github 存储库包含一个具有五个项目的解决方案。它们是

  1. Blazor.SPA - 核心库,包含所有可以进行样板化处理并在任何项目中重用的内容。
  2. Blazor.Database - 这是其他项目共享的 WASM/服务器库。几乎所有的项目代码都保存在这里。例如 EF DB Context、模型类、模型特定的 CRUD 组件、Bootstrap SCSS、Views、Forms 等。
  3. Blazor.Database.Web - ASPNetCore 服务器宿主。

此时您可能已经注意到,没有服务器项目。您不需要一个。

UI 结构

应用程序采用了结构化的 UI 方法。这使得更容易停止在应用程序中重复相同的 Razor/Html 标记,构建可重用的组件并将代码从应用程序移到库中。

页数

Pages 是充当应用程序宿主的网页。每个应用程序有一个。

RouteViews

RouteViews 是加载到根 App 组件中的组件,通常由 Router 通过 Layout 加载。它们不一定如此。您可以编写自己的 View Manager,也不必使用 Layouts。视图的唯一两个要求是

  1. 它必须被声明为一个 razor 组件。
  2. 它必须使用 @page 指令声明一个或多个路由。

FetchData 视图声明如下。Razor 标记极少,只有 WeatherForecastListForm。代码处理 List Form 中各种操作的发生。

@page "/fetchdata"
<WeatherForecastListForm EditRecord="this.GoToEditor" ViewRecord="this.GoToViewer" NewRecord="this.GoToNew" ExitAction="Exit"></WeatherForecastListForm>

@code {
    [Inject] NavigationManager NavManager { get; set; }
    private bool _isWasm => NavManager?.Uri.Contains("wasm", StringComparison.CurrentCultureIgnoreCase) ?? false;

    public void GoToEditor(int id)
    => this.NavManager.NavigateTo($"weather/edit/{id}");

    public void GoToNew()
    => this.NavManager.NavigateTo($"weather/edit/-1");

    public void GoToViewer(int id)
    => this.NavManager.NavigateTo($"weather/view/{id}");

    public void Exit()
    {
        if (_isWasm)
            this.NavManager.NavigateTo($"/wasm");
        else
            this.NavManager.NavigateTo($"/");
    }
}

RouteView 的目的是声明 Router 组件在 SPA 启动时可以找到的路由。根组件 App 如下所示,它声明了 Router 组件。AppAssembly="@typeof(WeatherApp).Assembly" 将路由器指向它用于查找路由声明的程序集。在这种情况下,它指向包含根组件的程序集。

<Router AppAssembly="@typeof(WeatherApp).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(WASMLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(WASMLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

请注意,显示组件名为 RouteView,这就是 RouteViews 的来源。

Layouts

Layouts 是开箱即用的 Blazor Layouts。Router 渲染 Layout,并将 RouteView 作为子内容。在 Router 定义中有一个默认的 Layout。我在这里跳过 Layouts,它们已经存在很长时间了,并且在其他地方得到了充分的介绍。

表单

Form 是组件层次结构中的一个中等层级单元。RouteViews 包含一个或多个 Forms。

. Forms 是控件的逻辑集合,这些控件显示在视图或模态对话框中。列表、查看表单、编辑表单都是经典的表单。Forms 包含控件而不是 HTML。

控件

Control 是低层级组件。它是构建 HTML 代码的地方。Controls 可以包含其他 Controls 以构建更复杂的 Controls。

您多久会在一个 Razor 组件中重复相同的 HTML 代码。您在 Razor 中所做的,在 C# 代码中您可能不会想到。您会编写一个辅助方法。为什么不在组件中做同样的事情。

// mylist.razor
<td class="px-1 py-2">xxxx</td>
....  10 times

看起来可能比

// UiListRow.razor
<td class="px-1 py-2">@childContent</td>

// mylist.razor
<UiListRow>xxxx</UiListRow>
....  10 times

但以组件方法更改应用程序中的填充很简单,而以标记更改则很麻烦。

// UiListRow.razor
<td class="px-1 py-1">@childContent</td>

Blazor.Database 项目

Blazor.Database 项目包含所有项目特定的 Blazor 代码以及 WASM 应用程序的启动代码和 Web Assembly 代码。

Program.cs

Program 是 WASM 应用程序的入口点,包含服务定义和对根组件的引用。

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<WeatherApp>("#app");

    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    builder.Services.AddWASMApplicationServices();

    await builder.Build().RunAsync();
}

每个项目/库的服务在 IServiceCollection 扩展中指定。

ServiceCollectionExtensions.cs

站点特定的服务加载的是控制器服务 WeatherForecastControllerService 和数据服务,作为 IFactoryDataService 接口加载 FactoryWASMDataService

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddWASMApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IFactoryDataService, FactoryWASMDataService>();
        services.AddScoped<WeatherForecastControllerService>();
        return services;
    }
}

WASM 项目的最终设置是在项目文件中设置 StaticWebAssetBasePath。这将允许我们将 WASM 版本和服务器版本一起运行在“Web”项目上。

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <StaticWebAssetBasePath>WASM</StaticWebAssetBasePath>
  </PropertyGroup>

CSS

所有 CSS 都是共享的,因此位于 Blazor.Database.Web 中。我使用 Bootstrap,并使用 SASS 进行了一些定制。我在 Visual Studio 中安装了 WEB COMPILER 扩展,可以即时编译 SASS 文件。

Blazor.Database.Web 项目

CSS

该项目使用 SCSS 构建自定义版本的 Bootstrap,具有一些颜色和小格式差异。我不会在此处介绍设置 - 搜索 shaun curtis blazor css frameworks 找到我写的一篇关于此主题的文章。

页数

我们有两个真实的页面 - 用于启动 Blazor Server SPA 的标准 _Host.cshtml 和用于启动 WASM SPA 的 _WASM.cshtml

_Host.cshtml

标准的 Blazor Server 启动页面。请注意

  1. 对自定义 CSS 和组件 CSS 的样式表引用。
  2. blazor.server.js 文件脚本引用。
  3. 对根组件的 component 引用 - 在本例中是 Blazor.Database.Server.Components.WeatherApp。根组件位于 Blazor.Database 库中。
@page "/"
@namespace Blazor.Database.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Blazor.Database.Web</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/site.min.css" />
    <link href="Blazor.Database.Web.styles.css" rel="stylesheet" />
    <link href="/wasm/Blazor.Database.styles.css" rel="stylesheet" />
</head>
<body>
    <component type="typeof(Blazor.Database.Components.WeatherApp)" render-mode="ServerPrerendered" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred.  This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred.  See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="/wasm/site.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>
</html>

_WASM.cshtml

这只是 WASM *index.html 的服务器版本。

  1. 相同的 CSS 引用和服务器文件。
  2. 相同的 site.js
  3. <base href> 设置为 WASM 子目录。
  4. 引用的 blazor.webassembly.js 到子目录。
@page "/WASM"
@namespace Blazor.Database.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazor.DataBase.WASM</title>
    <base href="/wasm/" />
    <link rel="stylesheet" href="/css/site.min.css" />
    <link href="/Blazor.Database.Web.styles.css" rel="stylesheet" />
    <link href="/wasm/Blazor.Database.styles.css" rel="stylesheet" />
</head>
<body>
    <div id="app">
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;"><h4>Web Application Loading</h4></div>
        </div>
    </div>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="/wasm//site.js"></script>
    <script src="/wasm/_framework/blazor.webassembly.js"></script>
</body>
</html>

Startup.cs

本地服务和 Blazor.SPA 库服务已添加。它

  1. 添加 Blazor 服务器端服务
  2. 配置两个中间件通道,取决于 URL。

我不会在这里详细介绍 - 您可以在另一篇文章中阅读有关多 SPA 托管的更多信息 - 搜索 shaun curtis blazor hydra

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services){
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddControllersWithViews();

        // services.AddApplicationServices(this.Configuration);
        services.AddInMemoryApplicationServices(this.Configuration);
            
        // Server Side Blazor doesn't register HttpClient by default
        // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
        if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
        {
            // Setup HttpClient for server side in a client side compatible fashion
            services.AddScoped<HttpClient>(s =>
            {
                // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
                var uriHelper = s.GetRequiredService<NavigationManager>();
                return new HttpClient
                {
                    BaseAddress = new Uri(uriHelper.BaseUri)
                };
            });
        }
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/WASM"), app1 =>
        {
            app1.UseBlazorFrameworkFiles("/wasm");
            app1.UseRouting();
            app1.UseEndpoints(endpoints =>
            {
                endpoints.MapFallbackToPage("/wasm/{*path:nonfile}", "/_WASM");
            });
        });

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapBlazorHub();
            endpoints.MapRazorPages();
            endpoints.MapFallbackToPage("/Server/{*path:nonfile}","/_Host");
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}

ServiceCollectionExtensions.cs

有两个服务集合扩展方法。一个用于普通 SQL 数据库,第二个用于测试内存数据库。

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;
}

总结

本节到此结束。这是一个概述,后面还有更多细节。希望它能展示 Blazor 项目可以达到的抽象级别。下一节将介绍服务和数据层的实现。

一些需要注意的关键点

  1. 您可以使用服务器和 WASM 项目的通用代码来构建您的代码。通过谨慎,您可以编写一个可以以任何一种方式部署的应用程序,就像本项目的案例一样。
  2. WASM 和服务器都可以从同一个网站运行,您可以轻松地在两者之间切换。
  3. 非常小心术语。理解“Page”的不同含义。

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

历史

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

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

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

© . All rights reserved.