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






4.78/5 (12投票s)
如何构建和结构化 Blazor 数据库应用程序
引言
这组文章介绍了一个用于在 Blazor 中构建和组织数据库应用程序的框架。
这只是一个框架。我不做任何推荐:你可以使用它,也可以滥用它。这是我在我的项目中使用的方法。它是一种轻度主观的框架:尽可能使用开箱即用的 Blazor/Razor/DotNetCore 系统和工具包。CSS 框架是 Bootstrap 的一个轻度定制版本。
共有 6 篇文章介绍了该框架的各个方面和使用的编码模式
- 项目结构和框架 - 略作介绍。
- 服务 - 构建 CRUD 数据层。
- 视图组件 - UI 中的 CRUD 编辑和查看操作。
- UI 组件 - 构建 HTML/CSS 控件。
- 视图组件 - UI 中的 CRUD 列表操作。
文章自最初发布以来已发生巨大变化
- 整个框架的主观性大大降低。我放弃了许多在 Blazor/SPA 中处理某些问题的激进方法。
- 随着我对服务器和 WASM 项目如何共存的理解不断加深,库已重新组织。
- 所有内容都已更新到 Net5。
- 存储库主页已移动。
- 服务器和 WASM SPA 现在从同一个站点托管和运行。
它们不是
- 定义最佳实践的尝试。
- 最终产品。
第一篇文章概述了框架和解决方案架构。
存储库和数据库
这些文章的存储库已移至 Blazor.Database 存储库。所有之前的存储库都已过时,并将很快被删除。
存储库中的 /SQL 目录下有一个用于构建数据库的 SQL 脚本。
现在服务器和 WASM 已合并,演示站点也已更改。站点以服务器模式启动 - https://cec-blazor-server.azurewebsites.net/。
设计理念
Data
项目的后端是松散地按照三层模型 - 数据层、逻辑层和表示层 - 来组织的。数据层针对数据库实体实现标准的 CRUDL - 创建/读取/更新/删除/列表 - 操作。
应用程序库包含两个 DbContext
类
LocalWeatherDbContext
使用标准的 SQL 数据库,连接字符串在AppSettings
中定义。InMemoryWeatherDbContext
使用内存中的 SQLite 数据库用于测试和演示目的。
DbContext
服务是通过 AddDBContextFactory
服务扩展实现的 DBContextFactory 创建的。数据服务使用其 IDbContextFactory<TDbContext>
接口。
基本数据层由 IFactoryDataService
接口定义,FactoryDataService
是该接口的抽象实现。有三个通用数据服务实现了大部分样板代码
FactoryServerDataService
用于普通的 SQL 数据库。所有操作都是Async
的,并使用IDbContextFactory
在每次事务中获取DbContext
实例。FactoryServerInMemoryDataService
。SQLite 内存数据库只能存在于单个DbContext
中。此数据服务在启动时创建一个DbContext
实例,并将其用于所有事务。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 - 就像一个桌面应用程序。
在这些文章中,我将使用以下术语
- Page - 网站上的启动网页。SPA 中唯一的页面。
- RouteView/Routed Component。这些是不同人用来描述伪页面的术语。我使用 RouteViews 这个术语。这是在 Layout 的内容区域显示的,通常由定义的路由决定。我们将在本文后面更详细地讨论这些。
- Forms。Forms 是控件的逻辑集合,这些控件显示在视图或模态对话框中。列表、查看表单、编辑表单都是经典的表单。Forms 包含控件而不是 HTML。
- Controls。Controls 是显示某些内容的组件:它们发出 HTML 代码。例如,一个编辑框、一个下拉列表、一个按钮...一个 Form 是一个控件的集合。
应用程序配置为构建和部署 SPA 的服务器版本和 WASM 版本,并将两者托管在同一个网站上。基本的解决方案架构是
- Core Razor Library - 包含可以部署到任何应用程序的代码。这些可以作为 Nuget 包构建和部署。
- Web Assembly Razor Library - 包含 SPA 的应用程序特定代码以及 Web Assembly 特定代码。
- ASPNetCore Razor Web Project。宿主项目,包含 WASM 和服务器 SPA 的启动页面、Blazor Server Hub 的服务以及 WASM SPA 的服务器端 API 控制器。
解决方案结构
我使用 Visual Studio,因此 Github 存储库包含一个具有五个项目的解决方案。它们是
- Blazor.SPA - 核心库,包含所有可以进行样板化处理并在任何项目中重用的内容。
- Blazor.Database - 这是其他项目共享的 WASM/服务器库。几乎所有的项目代码都保存在这里。例如 EF DB Context、模型类、模型特定的 CRUD 组件、Bootstrap SCSS、Views、Forms 等。
- Blazor.Database.Web - ASPNetCore 服务器宿主。
此时您可能已经注意到,没有服务器项目。您不需要一个。
UI 结构
应用程序采用了结构化的 UI 方法。这使得更容易停止在应用程序中重复相同的 Razor/Html 标记,构建可重用的组件并将代码从应用程序移到库中。
页数
Pages 是充当应用程序宿主的网页。每个应用程序有一个。
RouteViews
RouteViews 是加载到根 App
组件中的组件,通常由 Router 通过 Layout 加载。它们不一定如此。您可以编写自己的 View Manager,也不必使用 Layouts。视图的唯一两个要求是
- 它必须被声明为一个 razor 组件。
- 它必须使用
@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 启动页面。请注意
- 对自定义 CSS 和组件 CSS 的样式表引用。
blazor.server.js
文件脚本引用。- 对根组件的
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 的服务器版本。
- 相同的 CSS 引用和服务器文件。
- 相同的 site.js。
<base href>
设置为 WASM 子目录。- 引用的 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
库服务已添加。它
- 添加 Blazor 服务器端服务
- 配置两个中间件通道,取决于 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 项目可以达到的抽象级别。下一节将介绍服务和数据层的实现。
一些需要注意的关键点
- 您可以使用服务器和 WASM 项目的通用代码来构建您的代码。通过谨慎,您可以编写一个可以以任何一种方式部署的应用程序,就像本项目的案例一样。
- WASM 和服务器都可以从同一个网站运行,您可以轻松地在两者之间切换。
- 非常小心术语。理解“Page”的不同含义。
如果您在未来很久之后阅读本文,请查看存储库中的自述文件以获取本文集的最新版本。
历史
* 2020年9月15日:初始版本
* 2020年11月17日:Blazor.CEC 库重大更改。ViewManager 更改为 Router,以及新的 Component 基类实现。
* 2021年3月26日:服务、项目结构和数据编辑重大更新。