Compare commits
10 Commits
6e4d286f86
...
bfc15537da
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bfc15537da | ||
![]() |
b354b70f7e | ||
![]() |
16a14d3b21 | ||
![]() |
08e5adaac5 | ||
![]() |
9925eaf091 | ||
![]() |
4443d55057 | ||
![]() |
0b1c871c7e | ||
![]() |
bfd1ee0547 | ||
![]() |
28523c58b2 | ||
![]() |
44e0ba65f0 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"livePreview.defaultPreviewPath": "/notes/helloshop/index.html"
|
||||
}
|
47
blogs/abc.md
47
blogs/abc.md
@ -1,47 +0,0 @@
|
||||
# 零度框架升级 8.0 稳定版
|
||||
|
||||
随着微软 Aspire 正式版的发布,零度框架 HelloShop 8.0 版本也正式发布了,本次改进高达 180 多项,代码已推送到 Github,主要包括以下几个方面。
|
||||
|
||||
## 升级到最新稳定版
|
||||
|
||||
之前,零度框架使用的是 Aspire 的多个预览版,导致了一些不稳定的问题,现在,零度框架和所有依赖包都升级到了最新稳定版,不再引用任何预览包,卸载掉 Visual Studio 2022 预览版,并升级到 17.10.0 稳定版,卸载掉所有 .NET 9.0 预览版,安装 .NET 8.0 SDK 稳定版,卸载掉所有 Aspire 预览版,将 Aspire 工作负载升级到 8.0.0 稳定版。项目所依赖的所有 NuGet 包都升级到了最新稳定版。
|
||||
|
||||
现在,零度框架和所有依赖包都升级到了最新稳定版,不再引用任何预览包。
|
||||
|
||||
|
||||
## 远程权限检查改进
|
||||
|
||||
`PermissionsController` 和 `RemotePermissionChecker` 引入 RoleId 角色权限检查,
|
||||
|
||||
## 创建虚拟授权类用于测试
|
||||
|
||||
使用 `FakePermissionChecker` 类来模拟权限检查,方便后续单元和集成测试。
|
||||
|
||||
## 创建 Aspire 集成测试项目
|
||||
|
||||
|
||||
创建 `HelloShop.FunctionalTests` 项目,该项目使用 Aspire 模板创建,用于集成测试,包括了一个 `FirstWebApiIntegrationTest` 测试类,用于测试所有微服务 Web API 的集成。
|
||||
|
||||
## 创建产品单元测试项目
|
||||
|
||||
创建 `HelloShop.ProductService.UnitTests` 项目,该项目使用 xUnit 模板创建,用于单元测试产品微服务中的代码,包括了一个 `ProductsControllerTest` 测试类,用于演示使用内存数据库模拟数据进行单元测试。包括一个 `MockProductsControllerTest` 测试类,用于演示使用 Moq 框架模拟数据进行单元测试。
|
||||
|
||||
## 创建产品集成测试项目
|
||||
|
||||
创建 `HelloShop.ProductService.FunctionalTests` 项目,该项目使用 xUnit 模板创建,用于集成测试产品微服务中的代码,包括了一个 `BrandApiIntegrationTest` 测试类,用于演示产品品牌 API 接口的集成测试,为了提高集成测试效率,使用 SQLite 内存数据库模拟数据,同时使用 `WebApplicationFactory` 类来模拟 Web 主机,以便在集成测试中使用真实的 HTTP 请求。通过使用 `WebApplicationFactory` 类,实现自动登录和令牌传递,以便在集成测试中使用真实的用户身份。
|
||||
|
||||
## 项目启动端口配置
|
||||
|
||||
为了项目更好的同时兼容 HTTP 和 HTTPS,每个微服务 `launchSettings` 文件中添加了 HTTP 和 HTTPS 端口配置,并约定 80xx 为 HTTP 端口,81xx 为 HTTPS 端口, 任何微服务都可通过两种协议访问以便于测试。
|
||||
|
||||
## 修复由于包升级导致的编译错误
|
||||
|
||||
这些升级包括 OpenApi 界面、删除过期的代码,修复由于包升级导致的编译错误。
|
||||
|
||||
## MAUI 项目升级
|
||||
|
||||
将 MAUI 项目升级到最新稳定版,修复由于包升级导致的编译错误。
|
||||
|
||||
## 后期版本计划
|
||||
|
||||
后期内容包括:Blazor 中控界面搭建、前端认证授权, MAUI 跨平台开发、安卓、IOS、MacOS、Windows 跨平台界面制作,后台自动任务管理、gRPC 微服务最佳实践、基于 Dapr 的事件服务总线和密钥管理、基于 Orleans 的分布式计算、基于 Aspire 项目的单机部署, K8S 集群容器化部署和 CI/CD 最佳实践,使用 OpenTelemetry 实现监控和追踪,使用 Prometheus 和 Grafana 实现监控和报警,分布式日志链路跟踪,性能追踪。
|
@ -82,7 +82,7 @@ var connection = builder.AddConnectionString("myconnection");
|
||||
builder.AddProject<Projects.ApiService>("api").WithReference(connection);
|
||||
```
|
||||
|
||||
## 连接字符串和终结点引用
|
||||
## 终结点引用
|
||||
|
||||
```csharp
|
||||
var builder = DistributedApplication.CreateBuilder(args);
|
||||
@ -98,7 +98,7 @@ var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice")
|
||||
## 在 Redis 组件上启用管理扩展
|
||||
|
||||
```csharp
|
||||
var redis = builder.AddRedis("redis").WithManagement();
|
||||
var redis = builder.AddRedis("redis").WithRedisInsight();
|
||||
var redis = builder.AddRedis("redis").WithRedisCommander();
|
||||
```
|
||||
|
||||
|
25
notes/helloshop/diagrams/hybridcache.drawio
Normal file
25
notes/helloshop/diagrams/hybridcache.drawio
Normal file
@ -0,0 +1,25 @@
|
||||
<mxfile host="65bd71144e">
|
||||
<diagram id="IbUPyTJ7BBrHTTByFtqF" name="Page-1">
|
||||
<mxGraphModel dx="1261" dy="1139" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0"/>
|
||||
<mxCell id="1" parent="0"/>
|
||||
<mxCell id="8" style="edgeStyle=none;html=1;" edge="1" parent="1" source="4" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="4" value="Service 1" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="40" width="270" height="190" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="9" style="edgeStyle=none;html=1;" edge="1" parent="1" source="5" target="7">
|
||||
<mxGeometry relative="1" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="5" value="Service 2" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="40" y="290" width="270" height="190" as="geometry"/>
|
||||
</mxCell>
|
||||
<mxCell id="7" value="Redis" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
|
||||
<mxGeometry x="450" y="180" width="250" height="170" as="geometry"/>
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
84
notes/helloshop/hybrid-cache-library.md
Normal file
84
notes/helloshop/hybrid-cache-library.md
Normal file
@ -0,0 +1,84 @@
|
||||
# 使用 HybridCache 混合缓存库
|
||||
|
||||
混合缓存可以同时使用内存缓存和分布式缓存,它可以提高缓存的命中率,减少对分布式缓存的访问,从而提高性能。
|
||||
|
||||
- 当从缓存中获取数据时,它首先会从内存缓存中获取数据,如果内存缓存中没有数据,它会从分布式缓存中获取数据。
|
||||
- 当向缓存中写入数据时,它会同时向内存缓存和分布式缓存中写入数据。
|
||||
|
||||
```text
|
||||
|
||||
Service 1 ----> HybridCache ----> MemoryCache ----> DistributedCache
|
||||
|
||||
```
|
||||
|
||||
https://learn.microsoft.com/zh-cn/azure/architecture/best-practices/caching
|
||||
|
||||
|
||||
## 安装程序包
|
||||
|
||||
```shell
|
||||
dotnet add package Microsoft.Extensions.Caching.Hybrid
|
||||
```
|
||||
|
||||
## 配置服务
|
||||
|
||||
```csharp
|
||||
builder.Services.AddHybridCache();
|
||||
```
|
||||
|
||||
## 使用服务
|
||||
|
||||
```csharp
|
||||
public class SomeService(HybridCache cache)
|
||||
{
|
||||
private HybridCache _cache = cache;
|
||||
|
||||
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
|
||||
{
|
||||
return await _cache.GetOrCreateAsync( $"{name}-{id}",async cancel => await GetDataFromTheSourceAsync(name, id, cancel),cancellationToken: token );
|
||||
}
|
||||
|
||||
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
|
||||
{
|
||||
string someInfo = $"someinfo-{name}-{id}";
|
||||
return someInfo;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 按标记移除缓存条目
|
||||
|
||||
```csharp
|
||||
public class SomeService(HybridCache cache)
|
||||
{
|
||||
private HybridCache _cache = cache;
|
||||
|
||||
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
|
||||
{
|
||||
var tags = new List<string> { "tag1", "tag2", "tag3" };
|
||||
var entryOptions = new HybridCacheEntryOptions
|
||||
{
|
||||
Expiration = TimeSpan.FromMinutes(1),
|
||||
LocalCacheExpiration = TimeSpan.FromMinutes(1)
|
||||
};
|
||||
return await _cache.GetOrCreateAsync(
|
||||
$"{name}-{id}", // Unique key to the cache entry
|
||||
async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
|
||||
entryOptions,
|
||||
tags,
|
||||
cancellationToken: token
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
|
||||
{
|
||||
string someInfo = $"someinfo-{name}-{id}";
|
||||
return someInfo;
|
||||
}
|
||||
|
||||
public async Task RemoveByTagAsync(string tag)
|
||||
{
|
||||
await _cache.RemoveByTagAsync(tag);
|
||||
}
|
||||
}
|
||||
```
|
137
notes/helloshop/migration-dataseeding.md
Normal file
137
notes/helloshop/migration-dataseeding.md
Normal file
@ -0,0 +1,137 @@
|
||||
# 自动迁移和数据库初始化
|
||||
|
||||
## 生成迁移脚本
|
||||
|
||||
```shell
|
||||
dotnet ef migrations add InitialCreate --output-dir Infrastructure/Migrations
|
||||
```
|
||||
|
||||
## 手动将最新迁移脚本应用到数据库
|
||||
|
||||
```shell
|
||||
dotnet ef database update
|
||||
```
|
||||
|
||||
## 使用代码创建数据库
|
||||
|
||||
```csharp
|
||||
await dbContext.Database.EnsureCreatedAsync();
|
||||
```
|
||||
|
||||
`EnsureCreatedAsync` 会创建数据库(如果不存在),并会创建所有表,但不会应用迁移脚本,适用于开发环境。
|
||||
|
||||
## 使用代码将最新迁移脚本应用到数据库
|
||||
|
||||
```csharp
|
||||
await dbContext.Database.MigrateAsync();
|
||||
```
|
||||
|
||||
`MigrateAsync` 会应用迁移脚本,如果数据库不存在,会先创建数据库,适用于生产环境。
|
||||
|
||||
## 注意事项
|
||||
|
||||
如果先前已经使用 `EnsureCreatedAsync` 创建了数据库,再使用 `MigrateAsync` 会抛出异常,因为数据库和所有表已经存在,因为迁移中有创建表的操作脚本。
|
||||
|
||||
如果数据库不存在,使用 `MigrateAsync` 会先创建数据库,再创建迁移历史表,然后再应用迁移脚本,但这个过程中会打印一些错误警告信息,虽然可以忽略,但不够优雅。
|
||||
|
||||
## 优雅的做法
|
||||
|
||||
这里使用到一些 EF Core 的内部服务,注意他们在实现细节上的区别。
|
||||
|
||||
```csharp
|
||||
private static async ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
var dbCreator = dbContext.GetService<IRelationalDatabaseCreator>();
|
||||
var historyRepository = dbContext.GetService<IHistoryRepository>();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
if (!await dbCreator.ExistsAsync(cancellationToken))
|
||||
{
|
||||
await dbCreator.CreateAsync(cancellationToken);
|
||||
}
|
||||
await historyRepository.CreateIfNotExistsAsync();
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
1、使用 `dbCreator.ExistsAsync` 先检查数据库是否存在,不存在则仅仅创建数据库(它不会创建表),这个时候得到一个空的数据库。
|
||||
|
||||
2、再使用 `historyRepository.CreateIfNotExistsAsync` 在空数据库中创建迁移历史表,用于保存迁移记录。
|
||||
|
||||
3、最后使用 `dbContext.Database.MigrateAsync` 应用迁移脚本,因为迁移脚本中的操作会在空数据库中执行,包括创建表等等操作。
|
||||
|
||||
4、迁移完成后,数据库中会有所有表和迁移历史表,迁移历史表中登记了最新的迁移记录。
|
||||
|
||||
5、后续再使用 `dbContext.Database.MigrateAsync` 会检查迁移历史表,如果有新的迁移脚本,会应用新的迁移脚本。
|
||||
|
||||
6、这样就实现了自动迁移和数据库初始化。
|
||||
|
||||
为了以上操作都在一个事务中执行,我们使用 `dbContext.Database.CreateExecutionStrategy` 创建一个执行策略, 在策略中执行上述操作。
|
||||
|
||||
|
||||
## 数据库初始化的方式
|
||||
|
||||
在最新的 EF 9.0 建议使用 UseSeeding 和 UseAsyncSeeding 并利用初始数据对数据库进行种子设定,这些代码会在`迁移`时执行。
|
||||
|
||||
```csharp
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)=> optionsBuilder
|
||||
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
|
||||
.UseSeeding((context, _) =>
|
||||
{
|
||||
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
|
||||
if (testBlog == null)
|
||||
{
|
||||
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
|
||||
context.SaveChanges();
|
||||
}
|
||||
})
|
||||
.UseAsyncSeeding(async (context, _, cancellationToken) =>
|
||||
{
|
||||
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
|
||||
if (testBlog == null)
|
||||
{
|
||||
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
不推荐传统的种子数据方式,因为它会在每次迁移时都会执行,而且不支持异步操作。
|
||||
|
||||
```csharp
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Blog>().HasData(
|
||||
new Blog { BlogId = 1, Url = "http://sample.com" },
|
||||
new Blog { BlogId = 2, Url = "http://sample2.com" }
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 零度框架的数据库初始化
|
||||
|
||||
在某些场景下数据库初始化可能是一个耗时的操作,我们使用 IHostedService 或 BackgroundService 来异步执行数据库初始化。
|
||||
|
||||
```csharp
|
||||
public class DbInitializer : IHostedService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public DbInitializer(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
// The code to initialize the database
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
```
|
@ -1,4 +1,4 @@
|
||||
# 使用 TimeProvider 服务
|
||||
# 使用 TimeProvider 类注入时间
|
||||
|
||||
System.TimeProvider 是一种时间抽象,它以 DateTimeOffset 类型的形式提供时间点。 通过使用 TimeProvider,可确保代码可测试且可预测。 TimeProvider 已在 .NET 8 中引入。
|
||||
|
||||
@ -22,6 +22,7 @@ public class CustomTimeProvider: TimeProvider
|
||||
|
||||
```csharp
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddSingleton<TimeProvider, CustomTimeProvider>();
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
@ -12,7 +12,7 @@ Add User:test/test
|
||||
|
||||
[http://192.168.0.202:15672](http://192.168.0.202:15672)
|
||||
|
||||
[http://mqtt.yuangan:15672](http://mqtt.yuangan:15672)
|
||||
[http://mqtt.test:15672](http://mqtt.test:15672)
|
||||
|
||||
# RabbitMQ Management MQTT
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
# 物联网云平台功能说明
|
||||
|
||||
## 设备管理
|
||||
|
||||
### 产品列表
|
||||
|
||||
产品列表可以对产品进行管理,包括产品的创建、查看、编辑、删除等操作,产品属性的定义、查看、编辑、删除等操作,产品的协议定义、查看、编辑、删除等操作。
|
||||
|
||||
### 设备列表
|
||||
|
||||
可以对设备进行管理,包括设备的创建、查看、编辑、删除等操作,包括设备属性绑定、查看、编辑、删除等操作,设备的协议绑定、查看、编辑、删除等操作。
|
||||
|
||||
### 设备分组
|
||||
|
||||
## 数据统计
|
||||
|
||||
### 实时数据
|
||||
|
||||
查看设备的实时数据,包括设备的属性数据、事件数据、报警数据等,可根据产品、设备、时间等条件进行筛选查看。
|
||||
|
||||
### 历史数据
|
||||
|
||||
可通过曲线图和列表方式查看设备的历史数据,可根据产品、设备、时间、属性等条件进行筛选查看,支持历史数据导出。
|
||||
|
||||
### 统计报表
|
||||
|
||||
统计报表可对历史数据进行统计分析,通过年月日时等维度进行统计,支持统计报表导出,可根据产品、设备、时间、属性等条件进行筛选查看。
|
||||
|
||||
## 数据可视化
|
||||
|
||||
可通过地图、曲线图、仪表盘、表格等方式对数据进行可视化展示,可根据产品、设备、时间等条件进行筛选查看。
|
||||
|
||||
## 视频监控
|
||||
|
||||
支持摄像头接入,提供视频监控功能,可查看现场视频。
|
||||
|
||||
## 项目展示
|
||||
|
||||
针对不同项目提供工艺流程、设备状态、报警信息、数据统计等展示,可根据项目进行展示配置。
|
||||
|
||||
## 系统管理
|
||||
|
||||
### 组织架构
|
||||
|
||||
支持组织架构管理,包括组织的创建、查看、编辑、删除等操作,支持组织的用户管理、角色管理、权限管理等操作。
|
||||
|
||||
### 用户管理
|
||||
|
||||
支持用户的创建、查看、编辑、删除等操作,支持用户的角色管理、权限管理等操作。
|
||||
|
||||
### 角色管理
|
||||
|
||||
支持角色的创建、查看、编辑、删除等操作,支持角色的权限管理等操作。
|
||||
|
||||
## 第三方接入
|
||||
|
||||
支持第三方接入,包括设备接入、数据接入、应用接入等操作。
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user