视频讲解
第5章〓书山有路勤为径〓学海无涯苦作舟——MAUI数据访问
5.1本地数据库
5.1.1环境搭建
第2章中讲述了Sqlite数据库,作为一款轻量级的关系数据库,其资源占用小,对资源有限的智能手机来说,Sqlite数据库特别适合嵌入式开发的场景。
搭建Sqlite数据库环境非常简单,安装Sqlite相关的软件包即可。NuGet作为Visual Studio的扩展,是一款开源的包管理开发工具,使用该工具可以非常方便地将第三方的组件库整合至项目中,类似于Maven和Npm等工具。
第一种安装程序包的方法是采用命令行的方式进行。在Visual Studio中选择“工具”→“NuGet 包管理器(N)”→“程序包管理控制台(O)”选项后,打开PowerShell控制台执行安装命令安装相关库。
【例51】使用PowerShell 安装软件包。
1Install-Package sqlite-net-pcl
2Install-Package SQLitePCLRaw.bundle_green
第二种安装程序包的方法是采用图形化界面的方式进行。在Visual Studio中选择“工具”→“NuGet包管理器(N)”→“管理解决方案的NuGet程序包(N)”选项,打开NuGet管理向导后,选择相应的项目,搜索需要安装的软件包安装即可。管理解决方案的NuGet程序包如图51所示。
图51管理解决方案的NuGet 程序包
第三种安装程序包的方法是采用修改工程配置文件的方式进行。双击项目打开MAUIDemo.csproj 配置文件,找到相应的配置节点并追加如下配置。建议安装最新稳定版本。
【例52】工程方式配置Sqlite。
MAUIDemo.csproj代码如下:
1
2
3
4
5.1.2功能封装
Sqlite数据库支持多种编程语言,本节采用C#进行相关功能的封装。软件工程的核心思想之一是封装,封装的目的是便于后期各种工程复用。对于任何包含主键的表,需要定义基本模型。
【例53】Sqlite数据库功能封装。
BaseModel.cs代码如下:
1using SQLite;
2
3namespace MAUISDK.Models
4{
5public class BaseModel
6{
7[PrimaryKey, AutoIncrement]
8public long Id
9{
10get;
11set;
12}
13}
14}
上述基本模型仅有一个字段,Id代表数据库索引,所以添加注解PrimaryKey表示主键,注解AutoIncrement表示自动增长,这样无须用户手工维护主键索引。基本模型能够满足大部分建表需求。定义业务对象时,只需要继承基本模型即可。
MAUILearning.cs代码如下:
1namespace MAUISDK.Models
2{
3public class MAUILearning : BaseModel
4{
5public string ?Learning
6{
7get;
8set;
9}
10}
11}
MAUILearning类作为Sqlite数据库操作示例模型,继承BaseModel,为简化起见,仅定义一个Learning字段。
IMAUIRepository.cs代码如下:
1using MAUISDK.Models;
2
3namespace MAUISDK.Interfaces
4{
5public interface IMAUIRepository where T : BaseModel
6{
7bool Exist(long Id);
8T Find(long Id);
9void Insert(T Item);
10void Update(T Item);
11void Remove(long Id);
12}
13}
在接口文件夹中定义IMAUIRepository接口,仓储接口定义了增、删、查、改4个基本操作,同时定义了Exist()方法判断搜索对象是否存在。为保证封装的通用性,这里采用泛型机制,添加 where T : BaseModel 约束表明泛型对象需要继承BaseModel类。
MAUIRepository.cs代码如下:
1using MAUISDK.Interfaces;
2using MAUISDK.Models;
3
4namespace MAUISDK.Services
5{
6public class MAUIRepository : IMAUIRepository where T : BaseModel
7{
8public List ItemList;
9public MAUIRepository()
10{
11ItemList = new();
12}
13public void Remove(long Id)
14{
15ItemList.Remove(Find(Id));
16}
17public bool Exist(long Id)
18{
19return ItemList.Any(item => item.Id == Id);
20}
21public T Find(long Id)
22{
23return ItemList.FirstOrDefault(item => item.Id == Id)!;
24}
25public void Insert(T Item)
26{
27ItemList.Add(Item);
28}
29public void Update(T Item)
30{
31int index = ItemList.IndexOf(Find(Item.Id));
32ItemList.RemoveAt(index);
33ItemList.Insert(index, Item);
34}
35}
36}
上述代码实现了IMAUIRepository接口,对Lambda表达式还不熟悉的读者可以参阅本书1.4节中的相关内容。仓储中的核心数据结构是ItemList 列表,用于维护一系列对象。
BaseViewModel.cs代码如下:
1using System.ComponentModel;
2using System.Runtime.CompilerServices;
3
4namespace MAUIDemo.ViewModels
5{
6public class BaseViewModel : IQueryAttributable, INotifyPropertyChanged
7{
8public event PropertyChangedEventHandler PropertyChanged;
9public virtual void ApplyQueryAttributes(IDictionary query)
10{
11}
12protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
13{
14PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
15}
16}
17}
与基本模型相对应,上述代码中定义了基本视图模型BaseViewModel。BaseViewModel继承了IQueryAttributable和INotifyPropertyChanged接口,目的是监听数据动态变化,及时通知视图层。实现了接口的OnPropertyChanged()方法,定义事件委托PropertyChangedEventHandler 调用Invoke()方法触发属性变更动作。
MAUILearningDAO.cs代码如下:
1using MAUISDK.Core;
2using MAUISDK.Models;
3using SQLite;
4
5namespace MAUIDemo.Core
6{
7public class MAUILearningDAO
8{
9private SQLiteAsyncConnection Database;
10public MAUILearningDAO()
11{
12}
13public async Task LogAsync()
14{
15await Database.EnableWriteAheadLoggingAsync();
16}
17public async Task Init()
18{
19if (Database is not null)
20return;
21Database = new(GlobalValues.LocalDatabasePath, GlobalValues.Flags);
22_ = await Database.CreateTableAsync();
23}
24public async Task> GetItemsAsync()
25{
26await Init();
27return await Database.Table().ToListAsync();
28}
29public async Task GetItemAsync(int Id)
30{
31await Init();
32return await Database.Table().Where(i => i.Id == Id).FirstOrDefaultAsync();
33}
34public async Task SaveItemAsync(MAUILearning model)
35{
36await Init();
37return await Database.InsertAsync(model);
38}
39public async Task DeleteItemAsync(object primaryKey)
40{
41await Init();
42return await Database.DeleteAsync(primaryKey);
43}
44}
45}
DAO(Data Access Objects,数据访问对象)属于面向对象中的一种设计模式,该设计模式将底层的数据访问逻辑与上层的业务逻辑进行分离。数据访问逻辑专注于数据访问相关的操作,业务逻辑专注于业务执行流程。MAUILearningDAO是针对MAUILearning类对应的数据表进行操作。内部引入SQLiteAsyncConnection Sqlite数据库异步连接对象,Init()方法完成初始化。GlobalValues.LocalDatabasePath是自定义MAUIDemo.db3文件的路径。MAUIDemo.db3文件是用于存储数据的Sqlite数据库文件,数据库相关的所有操作均会影响此文件内容。Gitee的.gitignore文件可添加 *.db3配置,屏蔽上传Sqlite数据文件。与本地文件相对应的是本地数据库缓存路径,本地数据库缓存路径是Sqlite数据库操作默认缓存文件的位置,针对不同的操作系统有一定的差异性。笔者的本地数据库缓存路径是
C:\Users\Administrator\AppData\Local\Packages\***\LocalState。
MAUILearningViewModel.cs代码如下:
1using MAUIDemo.Core;
2using MAUISDK.Models;
3using System.Collections.ObjectModel;
4using System.Windows.Input;
5
6namespace MAUIDemo.ViewModels
7{
8public class MAUILearningViewModel : BaseViewModel
9{
10private MAUILearningDAO dao;
11public ObservableCollection MAUILearnings
12{
13get;
14set;
15}
16public ICommand OnSaveAsync
17{
18set;
19get;
20}
21public ICommand OnDeleteAsync
22{
23set;
24get;
25}
26public ICommand OnFlushAsync
27{
28set;
29get;
30}
31public MAUILearningViewModel()
32{
33MAUILearnings = new();
34dao = new();
35OnSaveAsync = new Command(async (text) =>
36{
37if (text != null && !String.IsNullOrWhiteSpace(text.ToString()))
38{
39MAUILearning Item = new()
40{
41Learning = text.ToString()
42};
43await dao.SaveItemAsync(Item);
44}
45OnFlushAsync.Execute(null);
46});
47OnDeleteAsync = new Command(async (text) =>
48{
49if (text != null)
50{
51int del = -1;
52for (int i = 0; i < MAUILearnings.Count; i++)
53{
54if (MAUILearnings[i].Id.Equals(long.Parse(text.ToString())))
55{
56del = i;
57break;
58}
59}//for
60if (del != -1)
61{
62await dao.DeleteItemAsync(MAUILearnings[del]);
63}
64}
65OnFlushAsync.Execute(null);
66});
67OnFlushAsync = new Command(async () =>
68{
69var items = await dao.GetItemsAsync();
70MainThread.BeginInvokeOnMainThread(() =>
71{
72MAUILearnings.Clear();
73foreach (var item in items)
74{
75MAUILearnings.Add(item);
76}
77});
78OnPropertyChanged("MAUILearnings");
79});
80}
81}
82}
MAUILearningViewModel类继承BaseViewModel类。内部的核心成员MAUILearningDAO用于实际数据库操作。MAUILearnings列表是ObservableCollection可观察的列表,底层使用观察者设计模式。观察者设计模式也称为发布订阅模式,多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知并且会被自动更新。观察者设计模式是一种数据更新的触发机制,可以降低目标与观察者之间的耦合关系。MAUI视图模型中大量使用观察者设计模式,以保证数据的双向绑定。
MAUILearningViewModel类相关方法如下。
OnSaveAsync()。用于新增数据对象。当界面输入数据不为空时,构造MAUILearning对象,调用DAO层封装的SaveItemAsync()方法完成新增数据对象逻辑。最后调用OnFlushAsync()方法实现界面层数据刷新。
OnDeleteAsync()。用于删除数据对象。依次遍历观察者列表中的所有内容,如果满足删除条件,调用DAO层封装的DeleteItemAsync()方法完成删除数据对象逻辑。最后调用OnFlushAsync()方法实现界面层数据刷新。
OnFlushAsync()。用于刷新数据对象,同步界面层数据显示。首先调用GetItemsAsync()方法获取当前数据库中的全部对象,因为这里涉及多线程间互操作界面更新问题。多线程间互操作修改线程数据会导致线程不安全,MAUI提供了解决此问题的一种思路,采用主线程方式调用MainThread.BeginInvokeOnMainThread()方法。该方法的参数是一个委托,委托内部完成MAUILearnings 可观察数据对象的更新操作,最后调用父类的OnPropertyChanged()方法完成属性更新的通知操作。
MAUILearningConfig.cs代码如下:
1using MAUISDK.Models;
2using Microsoft.EntityFrameworkCore;
3using Microsoft.EntityFrameworkCore.Metadata.Builders;
4
5namespace MAUISDK.Configs
6{
7public class MAUILearningConfig : IEntityTypeConfiguration
8{
9public void Configure(EntityTypeBuilder builder)
10{
11builder.Property(e=> e.Learning).IsRequired();
12}
13}
14}
上述MAUILearningConfig 配置类完成数据表的自动生成,Configure()方法完成数据表的自动创建过程。内部根据需求配置表中的各个字段,如IsRequired()表示该字段必须设置,不能为空。至此,完成了数据库相关的功能封装。
5.1.3应用调用
应用调用逻辑在SQLitePage页面演示。
【例54】Sqlite应用调用。
SQLitePage.xaml代码如下:
1
2
9
10
11
12
13
22
28
34
35
36
37
42
43
44
45
46
50
51
55
59
60
61
SQLitePage页面说明如下。
ContentPage.BindingContext 配置节中定义了绑定的视图模型为MAUILearningViewModel。
ContentPage.Resources 配置节中定义了Label、Button和Entry控件的通用样式。DataTemplate定义了数据模板,数据模板名称为data,后面引入时使用此名称。该数据模板使用了GestureRecognizers 手势操作。当Tapped 触摸事件触发时,执行OnTapped 事件逻辑。
界面部分以VerticalStackLayout垂直布局方式进行展示,内部嵌套VerticalStackLayout垂直布局用于展示数据列表,BindableLayout.ItemTemplate绑定的数据模板ItemTemplate引用了前面的数据模板资源 data。BindableLayout.ItemsSource绑定的数据源绑定了可观察列表MAUILearnings。接着定义了输入信息输入条目控件和两个按钮控件。两个按钮控件绑定的参数引用了上方的输入条目控件,Path属性指明了输入条目控件的Text属性,这样输入条目控件的Text属性发生变化时,作为绑定参数传递给相应的事件。
SQLitePage.xaml.cs代码如下:
1namespace MAUIDemo.Pages;
2
3public partial class SQLitePage : ContentPage
4{
5public SQLitePage()
6{
7InitializeComponent();
8}
9private async void OnTapped(object sender, EventArgs e)
10{
11await DisplayAlert("选择", ((Label)sender).Text, "确认");
12}
13}
页面逻辑大部分通过视图模型进行控制,仅有数据模板中用于展示列表数据信息的OnTapped()事件方法需要上述代码进行实现。这里直接使用异步调用DisplayAlert()方法展示用户选中的信息,sender 传递对象强制转换为Label标签控件,获取其Text文本参数进行弹框显示。本地操作数据库的运行效果如图52所示。
图52本地操作数据库的运行效果
5.2.NET Core Web API
5.2.1.NET Core 最小化API
.NET Core 最小化API(minimal API)是从.NET 6开始引入的。不同于传统Program.cs的静态Main()方法,最小化API去除了Main()方法,直接采用顶级语句进行编程。这样大大简化了主流程的书写过程,配置过程全部通过顶级语句完成。读者可以使用最少的代码行数搭建出自己心仪的网站。下面进行最小化API演示。
在Visual Studio中选择“解决方案”→“添加”→“新建项目”选项,在“添加新项目”窗口中,选择项目类型为ASP.NET Core Web API,如图53所示。
图53新建项目
单击 “下一步”按钮,进入如图54所示的“配置新项目”窗口,在其中输入项目名称WebAPI,单击“下一步”按钮。
图54“配置新项目”窗口
配置其他信息,选择框架为“.NET 8.0(长期支持)”,根据需求配置HTTPS等选项,单击 “创建”按钮。其他信息如图55所示。
图55其他信息
创新项目后,项目默认结构如图56所示。
图56项目默认结构
运行项目,Swagger启动界面如图57所示。
图57Swagger启动界面
在浏览器中输入URL,访问相应的控制器,运行效果如图58所示。
图58运行效果
launchSettings是启动配置文件。
【例55】最小化ASP.NET Core Web API。
launchSettings.json代码如下:
1{
2"$schema": "http://json.schemastore.org/launchsettings.json",
3"iisSettings": {
4"windowsAuthentication": false,
5"anonymousAuthentication": true,
6"iisExpress": {
7"applicationUrl": "http://localhost:49874",
8"sslPort": 44386
9}
10},
11"profiles": {
12"http": {
13"commandName": "Project",
14"dotnetRunMessages": true,
15"launchBrowser": true,
16"launchUrl": "swagger",
17"applicationUrl": "http://localhost:5281",
18"environmentVariables": {
19"ASPNETCORE_ENVIRONMENT": "Development"
20}
21},
22"https": {
23"commandName": "Project",
24"dotnetRunMessages": true,
25"launchBrowser": true,
26"launchUrl": "swagger",
27"applicationUrl": "https://localhost:7211;http://localhost:5281",
28"environmentVariables": {
29"ASPNETCORE_ENVIRONMENT": "Development"
30}
31},
32"IIS Express": {
33"commandName": "IISExpress",
34"launchBrowser": true,
35"launchUrl": "swagger",
36"environmentVariables": {
37"ASPNETCORE_ENVIRONMENT": "Development"
38}
39}
40}
41}
上述代码是JSON结构。
iisSettings配置节。IIS(Internet Information Services,因特网信息服务)相关的配置: windowsAuthentication(Windows验证选项)、anonymousAuthentication(匿名验证选项)、applicationUrl(应用访问路径)、sslPort SSL(Secure Socket Layer,安全套接字层端口)。
profiles配置节。针对不同协议进行的配置。
http配置节。commandName(命令名称)、dotnetRunMessages(是否运行消息)、launchBrowser(是否启动浏览器)、launchUrl(启动URL)、environmentVariables(环境变量),环境变量的子节点是键值对。
https配置节。与http类似,不同的是applicationUrl(应用访问路径)的协议采用https。
IIS Express配置节。与http配置节和https配置节类似,不同的是commandName(命令名称)为IISExpress。
下面是WeatherForecast(天气预报)数据结构代码。
WeatherForecast.cs代码如下:
1namespace WebAPI
2{
3public class WeatherForecast
4{
5public DateOnly Date { get; set; }
6public int TemperatureC { get; set; }
7public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
8public string? Summary { get; set; }
9}
10}
WeatherForecast类的各个字段说明如下。
Date。当前日期。从.NET 6开始,引入两种新的数据结构DateOnly和TimeOnly,将原来的日期对象划分为日期部分和时间部分。
TemperatureC。摄氏温度。
TemperatureF。华氏温度。
Summary。信息汇总。
WeatherForecastController.cs代码如下:
1using Microsoft.AspNetCore.Mvc;
2
3namespace WebAPI.Controllers
4{
5[ApiController]
6[Route("[controller]")]
7public class WeatherForecastController : ControllerBase
8{
9private static readonly string[] Summaries = new[]
10{
11"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
12};
13private readonly ILogger _logger;
14public WeatherForecastController(ILogger logger)
15{
16_logger = logger;
17}
18[HttpGet(Name = "GetWeatherForecast")]
19public IEnumerable Get()
20{
21return Enumerable.Range(1, 5).Select(index => new WeatherForecast
22{
23Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
24TemperatureC = Random.Shared.Next(-20, 55),
25Summary = Summaries[Random.Shared.Next(Summaries.Length)]
26})
27.ToArray();
28}
29}
30}
针对WeatherForecast对象构造WeatherForecastController控制器。该控制器中的Summaries成员变量是只读的数据模板。构造方法通过依赖注入方式注入了ILogger日志对象。Get()方法获取当天的天气描述数据。内部通过Enumerable.Range(1,5)随机产生了5条相关数据。模拟产生WeatherForecast对象,将该对象的3个属性进行填充。填充时使用随机产生器Random.Shared.Next()随机生成。最后调用ToArray()方法转换为IEnumerable类型进行返回。WeatherForecastController控制器使用ApiController 注解,代表
RESTful API方式。接口Get()方法注解HttpGet的参数name指明了路由。访问路径需要在https://localhost:7211/后面追加GetWeatherForecast。
Program.cs代码如下:
1var builder = WebApplication.CreateBuilder(args);
2// Add services to the container.
3builder.Services.AddControllers();
4// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
5builder.Services.AddEndpointsApiExplorer();
6builder.Services.AddSwaggerGen();
7var app = builder.Build();
8// Configure the HTTP request pipeline.
9if (app.Environment.IsDevelopment())
10{
11app.UseSwagger();
12app.UseSwaggerUI();
13}
14app.UseHttpsRedirection();
15app.UseAuthorization();
16app.MapControllers();
17app.Run();
Program.cs是最小化API的入口类,省略了Main()方法,全部采用顶级语句。WebApplication.CreateBuilder()构造builder对象。对builder对象进行配置,builder对象配置逻辑如下。
builder.Services.AddControllers()。添加控制器中间件。
builder.Services.AddEndpointsApiExplorer()。引入端点路由。
builder.Services.AddSwaggerGen()。添加Swagger生成器。Swagger是一款API的文档管理工具。
builder.Build()。完成应用构建,返回应用对象。
app对象配置逻辑如下。
app.Environment.IsDevelopment()。判断是否为开发模式。
app.UseSwagger()。使用Swagger中间件。
app.UseSwaggerUI()。使用SwaggerUI(Swagger图形用户界面)中间件。
app.UseHttpsRedirection()。使用Https重定向中间件。完成HTTPS协议的重定向机制。
app.UseAuthorization()。使用授权机制中间件。通过策略和授权中间件的配置来决定是否允许请求继续执行。
app.MapControllers()。开启控制器映射。
app.Run()。运行应用。
appsettings是应用程序配置文件。Logging 配置节配置了日志相关信息。Default 默认记录日志等级Information 信息级别。AllowedHosts允许访问主机配置项,这里采用正则表达式表明全部允许。
appsettings.json代码如下:
1{
2"Logging": {
3"LogLevel": {
4"Default": "Information",
5"Microsoft.AspNetCore": "Warning"
6}
7},
8"AllowedHosts": "*"
9}
.NET 8项目中还新增了WebAPI.http文件。花括号插值运算符引入URL变量WebAPI_HostAddress。
WebAPI.http代码如下:
1@WebAPI_HostAddress = http://localhost:5281
2GET {{WebAPI_HostAddress}}/weatherforecast/
3Accept: application/json
4###
至此介绍完了.NET Core 最小化API项目结构。MAUI框架与其有很多相通之处,如依赖注入、配置、中间件等机制。
5.2.2.NET Core Web API管道模型
.NET Core Web API管道模型是责任链设计模式的变体。责任链设计模式(Chain of Responsibility Pattern)属于行为设计模式,多个处理者构成链式结构,允许将请求沿着处理者链进行发送。每个处理者接收请求后,均可对请求进行处理,或者处理后将其传递给链上的下个处理者。管道模型可以提升系统可维护性、可测试性,避免代码冗余,能够并发执行,而且可以方便地增加和删除管道中的处理者,动态根据需求进行配置。.NET Core内置了多个中间件,不同中间件完成不同的需求。.NET Core Web API中间件管道模型如图59所示。
图59.NET Core WebAPI中间件管道模型
.NET Core Web API管道模型内置中间件如下。
app.UseAntiforgery()。使用防伪造中间件。防止恶意的第三方进行篡改。
app.UseAuthentication()。使用鉴权机制中间件。通过鉴权机制控制准入逻辑。
app.UseAuthorization()。使用授权机制中间件。通过策略和授权中间件的配置来决定是否允许请求继续执行。
app.UseCertificateForwarding()。使用证书转发中间件。寻找证书请求解码,更新HttpContext.Connection.ClientCertificate参数。
app.UseCookiePolicy()。使用Cookie 策略中间件。Cookie是网站为了辨别用户身份,进行会话跟踪而存储在客户端上的数据。Cookie 策略中间件用于修改Cookie 相关的策略。
app.UseCors()。使用跨域中间件。配置策略,允许跨域请求。
app.UseDefaultFiles()。使用默认文件中间件。开启当前路径的默认映射。
app.UseDeveloperExceptionPage()。使用开发异常页面中间件。在开发环境中,使用该中间件针对异步或同步方法抛出的异常产生用户自定义友好的HTML页面。
app.UseDirectoryBrowser()。使用目录访问中间件。开启当前路径目录访问功能。
app.UseEndpoints()。使用终端中间件。执行匹配的端点。与路由机制中间件相分离,增强灵活性。
app.UseExceptionHandler()。使用异常处理中间件。抛出异常后进行日志记录,并重新执行其他中间件,如果响应已经开始则不执行其他中间件。
app.UseFileServer()。使用文件服务中间件。除目录服务器外,启用所有的静态文件。
app.UseForwardedHeaders()。使用转发头中间件。用于处理转发请求字段相匹配的请求。
app.UseHealthChecks()。使用健康检查中间件。
app.UseHostFiltering()。使用主机过滤中间件。过滤不满足条件的主机。
app.UseHsts()。使用严格的传输安全头部中间件。该中间件用于开启HTTP严格传输安全(StrictTransportSecurity,STS)相关的功能。
app.UseHttpLogging()。使用HTTP日志中间件。
app.UseHttpMethodOverride()。使用HTTP方法重写中间件。
app.UseHttpsRedirection()。使用HTTPS重定向中间件。完成HTTPS重定向机制。
app.UseOutputCache()。使用输出缓存中间件。
app.UseMiddleware()。使用中间件。
app.UseMvc()。使用MVC请求中间件。使用.NET Core模型、视图、控制器框架。
app.UseMvcWithDefaultRoute()。使用MVC请求中间件且为默认路由。
app.UsePathBase()。使用基路径中间件。
app.UseRateLimiter()。使用限流中间件。限流和削峰的目的是降低QPS(QueriesPerSecond,每秒查询率),延缓用户请求,防止高并发带来的服务器压力过大导致宕机。
app.UseRequestDecompression()。使用请求压缩中间件。
app.UseRequestLocalization()。使用请求本地化中间件。
app.UseRequestTimeouts()。使用请求超时中间件。
app.UseResponseCaching()。使用响应缓存中间件。
app.UseResponseCompression()。使用响应压缩中间件。
app.UseRewriter()。使用重定向中间件。
app.UseRouter()。使用路由机制中间件。特化IRouter实例。
app.UseRouting()。使用路由机制中间件。特化IApplicationBuilder实例。
app.UseSession()。使用会话中间件。Session是利用对象存储特定用户会话所需的属性及配置信息的对象。该方法封装了会话相关的管理。
app.UseStaticFiles()。使用静态文件中间件。通过配置静态文件中间件,产生类似IIS中虚拟目录的效果。
app.UseStatusCodePages()。使用状态码中间件。
app.UseStatusCodePagesWithRedirects()。使用状态码转发中间件。
app.UseStatusCodePagesWithReExecute()。使用状态码执行中间件。
app.UseSwagger()。使用Swagger中间件。
app.UseSwaggerUI()。使用SwaggerUI(Swagger图形用户界面)中间件。
app.UseW3CLogging()。使用W3C日志中间件。
app.UseWebSockets()。使用WebSocket 网络通信协议。WebSocket协议是单TCP连接上的全双工通信协议,仅需一次握手即可创建持久性连接,简化客户端和服务器端之间的数据交互。
app.UseWelcomePage()。使用欢迎页面中间件。
app.UseWhen()。使用条件判断中间件。
5.2.3EFCore
EntityFrameworkCore(EFCore)是轻量级的数据访问技术,可用作对象关系映射(Object Relational Mapping,ORM)
程序,类似Java世界中大名鼎鼎的Hibernate框架。EntityFrameworkCore 最方便之处在于代码优先或数据优先模式能够自动完成对应部分的生成。如代码优先(Code First)方式,用户仅需写C#代码,PowerShell 执行命令或应用程序启动时会自动在数据库中生成相应的数据表结构。同理,数据优先(DataBase First)方式,用户仅需写SQL代码,应用层自动完成C#相关代码同步。
1. EFCore框架的主要优点
易切换。跨数据库能力强大,用户仅需要修改相应的配置即可完成数据库之间的切换。
易使用。EFCore提供强大的模型设计器功能,支持复杂表之间的关联关系,以及灵活的导航属性机制。
高效率。用户无须编写复杂的SQL语句,强大的
ORM机制自动完成相关操作。对于复杂的查询和修改操作需要通过EFCore的ExecuteSqlCommand 完成相关操作。
延迟查。延迟查询加载和懒加载机制提升性能。
2. EFCore框架的主要缺点
性能低。与大部分ORM固有的缺点一样,针对大批量的数据操作效率低,尤其是复杂查询的性能会下降。
启动慢。首次预热时启动慢,对象关系映射加载至内存后性能会提升。
NuGet安装EFCore框架相关组件或者按照如下方式对项目文件进行配置。如果使用其他类型的数据库,进行相应的修改即可。
【例56】安装EFCore框架相关组件。
代码如下:
1
2
3
4
5
6
3. EFCore常用命令
AddMigration。增加迁移,每次迁移前需要先执行此命令。后面的参数指定迁移的操作名称。
UpdateDatabase。更新数据库,包括数据库中的表和字段。
ScriptMigration。生成从开始到最新迁移的SQL语句。
5.3网络数据库
5.3.1核心层
实际工程中网络数据库的开发大部分需要使用ORM框架。核心层主要完成数据操作逻辑的封装,为上层业务应用提供相应的接口。
【例57】网络数据库核心层。
EFCoreDbContext.cs代码如下:
1using MAUISDK.Core;
2using MAUISDK.Models;
3using Microsoft.EntityFrameworkCore;
4using Microsoft.Extensions.Logging;
5
6namespace MAUISDK.Repositories
7{
8public class EFCoreDbContext : DbContext
9{
10public DbSet MAUILearning
11{
12get;
13set;
14}
15///
16/// 构造方法
17///
18/// EFCore选项
19public EFCoreDbContext(DbContextOptions options)
20: base(options!)
21{
22}
23///
24/// 模型构建
25///
26/// 构建器
27protected override void OnModelCreating(ModelBuilder builder)
28{
29var cfg = builder.Entity();
30cfg.Property(p => p.Learning).IsRequired();
31}
32///
33/// 构建
34///
35/// 构建器
36protected override void OnConfiguring(DbContextOptionsBuilder builder)
37{
38string connection = GlobalValues.ConnectionStr;
39builder.UseSqlite(connection)
40.LogTo(Console.WriteLine, LogLevel.Information);
41}
42}
43}
定义EFCoreDbContext是EFCore框架数据库上下文类,继承DbContext数据库上下文类。成员变量MAUILearning是DbSet类型,对应数据库中的一张表。重写OnModelCreating()方法,构造MAUILearning数据表以及内部字段。重写OnConfiguring()方法,构建数据库连接配置信息。调用构建器的UseSqlite()方法,配置Sqlite数据库连接字符串信息。
IEFCoreRepository.cs代码如下:
1///
2/// EFCore仓储接口
3///
4/// 数据泛型
5/// 仓储上下文
6public interface IEFCoreRepository : IDisposable
7where T : class
8where Context : DbContext
9{
10}
IEFCoreRepository定义了仓储接口,泛型参数T表示数据表,泛型参数Context表示数据上下文。
EFCoreRepository.cs代码结构如下:
1using Microsoft.EntityFrameworkCore;
2using System.Data;
3using System.Linq.Expressions;
4
5namespace MAUISDK.Repositories
6{
7///
8/// EFCore仓储
9///
10/// 数据泛型
11/// 仓储上下文
12public class EFCoreRepository : IEFCoreRepository
13where T : class
14where Context : DbContext
15{
16#region 构造析构
17// 此处省略相关代码
18#endregion
19#region 事务操作
20// 此处省略相关代码
21#endregion
22#region 同步版本
23// 此处省略相关代码
24#endregion
25#region 异步版本
26// 此处省略相关代码
27#endregion
28}
29}
EFCoreRepository EFCore仓储继承了之前定义的IEFCoreRepository接口,涉及EFCoreRepository的代码非常多,为了从宏观上进行把握,分为四大类代码,分别用C#区域运算符进行划分。构造析构类方法完成EFCore仓储的构造析构操作,事务操作完成EFCore仓储的保障数据业务完整性和一致性的事务类操作,同步版本完成EFCore 仓储涉及数据库同步类的相关操作,异步版本完成EFCore仓储涉及数据库异步类的相关操作。
EFCoreRepository.cs构造析构部分代码如下:
1#region 构造析构
2private Context dbContext;
3private readonly DbSet dbSet;
4///
5/// 构造方法
6///
7/// 数据库上下文
8public EFCoreRepository(Context dbContext)
9{
10this.dbContext = dbContext;
11dbSet = dbContext.Set();
12}
13///
14/// 释放资源
15///
16public void Dispose()
17{
18if (dbContext != null)
19dbContext.Dispose();
20}
21#endregion
EFCore仓储的构造部分初始化数据表,析构部分释放数据表相关资源。
EFCoreRepository.cs事务操作部分代码如下:
1///
2/// 开始事务
3///
4/// 隔离级别
5public void BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
6{
7if (dbContext.Database.CurrentTransaction == null)
8{
9dbContext.Database.BeginTransaction(isolationLevel);
10}
11}
12///
13/// 提交事务
14///
15public void Commit()
16{
17var transaction = dbContext.Database.CurrentTransaction;
18if (transaction != null)
19{
20try
21{
22transaction.Commit();
23}
24catch (Exception)
25{
26transaction.Rollback();
27throw;
28}
29}
30}
31///
32/// 回滚事务
33///
34public void Rollback()
35{
36if (dbContext.Database.CurrentTransaction != null)
37{
38dbContext.Database.CurrentTransaction.Rollback();
39}
40}
EFCore仓储的事务操作部分封装了下述3个核心方法。
BeginTransaction()。开始事务。根据事务的隔离级别参数IsolationLevel进行配置。
Commit()。提交事务。获取当前事务对象,如果提交成功则不进行回滚; 如果提交不成功则回滚后抛出异常供上层程序处理。
Rollback()。回滚事务。手动调用此方法完成当前事务的回滚操作。
EFCore仓储的同步和异步操作包含很多方法。同步操作指所有的逻辑都执行完,才最终返回给用户,可能导致用户在线等待的时间过长,造成一种阻塞或死机的体验。异步操作是将用户请求放入消息队列后,直接返回,一个任务不需要等待另一个任务的完成就能够继续执行,这种方式不会造成任务阻塞,可以提高系统的响应速度。下面仅介绍几个常用的方法,其余的读者自行查看代码,根据需要也可以自行进行扩充和完善。
根据条件分页查询代码如下:
1///
2/// 根据条件分页查询
3///
4/// 排序约束
5/// 过滤条件
6/// 排序条件
7/// 当前页码
8/// 每页记录条数
9/// 返回总条数
10/// 是否倒序
11/// 查询结果集
12public IEnumerable Where(Func @where, Func order, int pageIndex, int pageSize, out int count, bool isDesc = false)
13{
14count = Count();
15if (isDesc)
16{
17return dbSet.Where(@where).OrderByDescending(order).Skip((pageIndex - 1) * pageSize).Take(pageSize);
18}
19else
20{
21return dbSet.Where(@where).OrderBy(order).Skip((pageIndex - 1) * pageSize).Take(pageSize);
22}
23}
根据条件分页查询是数据库的常用操作之一。泛型参数TOrder是排序要求,函数委托参数 @where是过滤条件,函数委托参数 order是排序规则,pageIndex为页码,pageSize为每页大小,count 作为输出参数返回总条数,isDesc表示是否进行降序。通过链式调用方式实现,Skip()方法跳过指定记录,Take()方法获取指定记录数,如果不足则返回剩余所有。返回的结果集是IEnumerable 可枚举类型。
插入实体对象代码如下:
1///
2/// 插入实体对象
3///
4/// 实体对象
5/// 是否保存
6/// 实体对象
7public async Task AddAsync(T entity, bool save = true)
8{
9await dbSet.AddAsync(entity);
10if (save)
11{
12await this.SaveChangesAsync();
13}
14return entity;
15}
数据库的插入操作是数据库的常用操作之一。通过save参数判断是否需要同步至实体数据库。
删除实体对象代码如下:
1///
2/// 删除实体对象
3///
4/// 实体对象
5/// 是否保存
6public async Task DeleteAsync(T entity, bool save = true)
7{
8dbSet.Remove(entity);
9if (save)
10{
11await this.SaveChangesAsync();
12}
13}
数据库的删除操作是数据库的常用操作之一。通过save参数判断是否需要同步至实体数据库。
根据编号查询对象代码如下:
1///
2/// 根据编号查询对象
3///
4/// 字段类型
5/// 编号
6///
7public async Task GetByIdAsync(Ttype id)
8{
9return await dbSet.FindAsync(id);
10}
数据库的查询操作是数据库的常用操作之一。上述代码实现了基于主键的查询。
修改对象代码如下:
1///
2/// 修改对象
3///
4/// 实体对象
5/// 是否保存
6public async Task UpdateAsync(T entity, bool save = true)
7{
8dbSet.Update(entity);
9if (save)
10{
11await this.SaveChangesAsync();
12}
13}
数据库的修改操作是数据库的常用操作之一。通过save参数判断是否需要同步至实体数据库。
5.3.2服务层
核心层完成了底层数据交互的逻辑封装,服务层则进行业务逻辑的封装。首先定义RESTful API操作风格的接口。
【例58】网络数据库服务层。
IRestService.cs代码如下:
1using MAUISDK.Models;
2
3namespace MAUISDK.Interfaces
4{
5public interface IRestService where T : BaseModel
6{
7Task> QueryAsync();
8Task SaveAsync(T Item, bool newData = true);
9Task RemoveAsync(long Id);
10}
11}
IRestService接口的泛型参数继承了BaseModel 基本模型类,需要符合BaseModel 基本模型类的规范。下面是其中的3个方法。
QueryAsync()。数据查询接口。
SaveAsync()。数据保存接口。参数Item是待保存的对象,参数 newData指明是否为新增数据,用于区分新增操作还是修改操作。
RemoveAsync()。数据删除接口。参数Id 对应数据对象的主键。
RestService.cs代码如下:
1using MAUISDK.Interfaces;
2using MAUISDK.Models;
3using System.Text.Json;
4using System.Text;
5using MAUISDK.Core;
6
7namespace MAUISDK.Services
8{
9public class RestService : IRestService where T : BaseModel
10{
11private HttpClient client;
12private JsonSerializerOptions serializerOptions;
13private IHttpsClientHandlerService httpsClientHandlerService;
14public List ItemList;
15public RestService(IHttpsClientHandlerService httpsClientHandlerService)
16{
17#if DEBUG
18this.httpsClientHandlerService = httpsClientHandlerService;
19HttpMessageHandler handler =
20httpsClientHandlerService.GetPlatformMessageHandler();
21if (handler != null)
22client = new(handler);
23else
24client = new();
25#else
26client = new HttpClient();
27#endif
28serializerOptions = new JsonSerializerOptions
29{
30PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
31WriteIndented = true
32};
33}
34public async Task> QueryAsync()
35{
36ItemList = [];
37Uri uri = new(GlobalValues.RestUrl);
38try
39{
40HttpResponseMessage response = await client.GetAsync(uri);
41if (response.IsSuccessStatusCode)
42{
43string content = await response.Content.ReadAsStringAsync();
44ItemList = JsonSerializer.Deserialize>(content, serializerOptions)!;
45}
46}
47catch
48{
49}
50return ItemList;
51}
52
53public async Task SaveAsync(T Item, bool newData = true)
54{
55Uri uri = new(GlobalValues.RestUrl);
56try
57{
58string json = JsonSerializer.Serialize(Item, serializerOptions);
59StringContent content = new(json, Encoding.UTF8, "application/json");
60HttpResponseMessage response;
61if (newData)
62{
63response = await client.PostAsync(uri, content);
64}
65else
66{
67response = await client.PutAsync(uri, content);
68}
69}
70catch
71{
72}
73}
74public async Task RemoveAsync(long Id)
75{
76Uri uri = new(String.Format(GlobalValues.RemoveItem, Id));
77try
78{
79HttpResponseMessage response = await client.DeleteAsync(uri);
80}
81catch
82{
83}
84}
85}
86}
RestService 继承了IRestService接口。构造方法完成IHttpsClientHandlerService对象实例的依赖注入,并构造出HttpClient与服务器交互的对象,同时完成了JsonSerializerOptions JSON序列化对象选项配置。PropertyNamingPolicy JSON属性名称策略指定为CamelCase(骆驼拼写法),根据单词的大小写拼写复合词的方法。骆驼拼写法又分为小骆驼拼写法和大骆驼拼写法。小骆驼拼写法是指第一个词的首字母小写,后面单词首字母大写,单词除首字母后的字母均为小写。大骆驼拼写法是指第一个词的首字母大写,后面单词首字母大写,单词除首字母后的字母均为小写。WriteIndented参数指明了JSON是否采用格式缩进。
QueryAsync()。数据查询方法。通过HttpClient对象获取结果后,因为服务器的控制器返回的是JSON格式,所以需要调用JsonSerializer类的Deserialize()反序列化方法转换为对象列表。
SaveAsync()。数据保存方法。提交参数时,根据后台的[FromForm] 注解,配套使用StringContent对象进行提交POST请求。
RemoveAsync()。数据删除方法。直接调用HttpClient对象的DeleteAsync()方法。
5.3.3控制层
控制层主要由控制器实现对服务器的封装。配置了路由参数 [Route("api/[controller]")],通过控制器类型进行路由匹配。
【例59】网络数据库控制层。
MAUILearningController.cs代码如下:
1[ApiController]
2[Route("api/[controller]")]
3public class MAUILearningController : ControllerBase
4{
5private readonly IEFCoreRepository repository;
6public MAUILearningController(IEFCoreRepository repository)
7{
8this.repository = repository;
9}
10[HttpGet]
11public IActionResult List()
12{
13return Ok(repository.GetAll());
14}
15[HttpPost]
16public IActionResult Create([FromBody] MAUILearning Item)
17{
18try
19{
20if (Item == null || !ModelState.IsValid)
21{
22return BadRequest("InValid");
23}
24repository.AddAsync(Item);
25}
26catch (Exception)
27{
28return BadRequest("Error");
29}
30return Ok(Item);
31}
32[HttpPut]
33public IActionResult Update([FromBody] MAUILearning Item)
34{
35try
36{
37if (Item == null || !ModelState.IsValid)
38{
39return BadRequest("InValid");
40}
41repository.UpdateAsync(Item);
42}
43catch (Exception)
44{
45return BadRequest("Error");
46}
47return NoContent();
48}
49[HttpDelete("{Id}")]
50public IActionResult Remove(long Id)
51{
52try
53{
54repository.DeleteAsync(Id);
55}
56catch (Exception)
57{
58return BadRequest("Error");
59}
60return NoContent();
61}
62}
控制器的构造方法通过依赖注入机制完成仓储对象的初始化,增、删、查、改操作均是通过调用之前仓储对象封装的方法完成。控制器主要完成异常检测功能,如果抛出异常,返回BadRequest类型。IActionResult是控制器方法执行后返回的结果类型,Ok()方法返回的结果类型是正常服务返回的类型,BadRequest()方法返回的结果类型是异常服务返回的类型,NoContent()方法返回的结果类型是无内容类型。
客户端界面层定义WebPage.xaml代码如下:
1
2
7
8
14
20
21
22
23
27
31
32
37
38
39
40
45
46
47
48
49
50
资源定义部分定义了Entry和Button控件的页面级通用样式。主体部分通过VerticalStackLayout垂直布局定义了输入条目、保存和删除按钮。CollectionView控件用于展示数据信息。DataTemplate内部使用Grid布局展示标签,标签绑定了数据的Learning属性。
WebPage.xaml.cs代码如下:
1using MAUISDK.Interfaces;
2using MAUISDK.Models;
3using System.Collections.ObjectModel;
4using System.Windows.Input;
5
6namespace MAUIDemo.Pages;
7
8public partial class WebPage : ContentPage
9{
10private readonly IRestService restService;
11public ObservableCollection MAUILearnings
12{
13get;
14set;
15}
16public ICommand OnSaveAsync
17{
18get;
19set;
20}
21public ICommand OnRemoveAsync
22{
23get;
24set;
25}
26public ICommand OnFlushAsync
27{
28get;
29set;
30}
31public WebPage(IRestService restService)
32{
33this.restService = restService;
34InitializeComponent();
35OnSaveAsync = new Command(async (arg) =>
36{
37MAUILearning Item = new()
38{
39Learning = entry.Text.Trim()
40};
41await restService.SaveAsync(Item);
42await DisplayAlert("信息", "保存成功", "确认");
43OnFlushAsync.Execute(null);
44});
45OnRemoveAsync = new Command(async (arg) =>
46{
47long Id = long.Parse(arg.ToString());
48await restService.RemoveAsync(Id);
49await DisplayAlert("信息", "删除" + Id.ToString() + "成功", "确认");
50OnFlushAsync.Execute(null);
51});
52OnFlushAsync = new Command(async (arg) =>
53{
54var Items = await restService.QueryAsync();
55MainThread.BeginInvokeOnMainThread(() =>
56{
57MAUILearnings = new();
58foreach (var Item in Items)
59{
60MAUILearnings.Add(Item);
61}
62});
63OnPropertyChanged("MAUILearnings");
64collectionView.ItemsSource = MAUILearnings;
65});
66BindingContext = this;
67}
68protected override void OnAppearing()
69{
70base.OnAppearing();
71OnFlushAsync.Execute(null);
72}
73}
与本地数据库界面逻辑代码实现完全类似,刷新操作也使用了MAUI涉及界面多线程更新数据时保障线程安全的技巧。网络操作数据库的示例效果如图510所示。
图510网络操作数据库的示例效果
本示例以极简的方式完成了MAUI网络数据库编程相关的全流程操作,起到演示作用。作为抛砖引玉,
相信读者能够开发出比MAUI网络数据库示例更为复杂的业务场景。