版权

版权 通常,.NET 基金会中项目的版权声明如下: “Copyright (c) .NET Foundation and Contributors. All Rights Reserved”. 版权声明应放在项目的 LICENSE 文件中,因此 MIT 许可项目的 LICENSE 开头如下: The MIT License (MIT) Copyright (c) .NET Foundation and Contributors All Rights Reserved Permission is hereby granted... Apache 2.0 许可项目的 LICENSE 则以如下内容开头: Copyright (c) .NET Foundation and Contributors All Rights Reserved Apache License, Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions... 在项目中引入第三方开源代码时,任何第三方的版权声明都应随代码一起保留。一种很好的做法是,把项目中需要展示的第三方声明汇总到项目根目录下名为 NOTICE.md(或 ThirdPartyNotices.txt)的文件中。 文件头 以下文件头格式用于 .NET Foundation 项目。请在新文件中使用该格式,并酌情更新现有代码文件的文件头。注意,项目的一般版权声明在项目的 LICENSE 文件中给出,但任何第三方的版权声明都应保留。 ...

2026年3月10日

创建代码仓库

仓库命名规范 在 Github 上创建一个名为 HelloShop 的代码仓库,关于代码仓库的命名规范使用,使用 SnakeCaseLower 命名法。 Naming policy Original Converted PascalCase HelloShop HelloShop CamelCase HelloShop helloShop SnakeCaseLower HelloShop hello_shop SnakeCaseUpper HelloShop HELLO_SHOP KebabCaseLower HelloShop hello-shop KebabCaseUpper HelloShop HELLO-SHOP 仓库文件夹结构 在仓库中创建一个名为 src 的文件夹,用于存放源代码。 说明文件 在仓库中创建一个名为 README.md 的文件,用于存放仓库的说明文档,当然如果可能每个文件夹都应该有一个说明文档。 忽略文件 gitignore 文件的作用是指定不需要提交到代码仓库的文件,例如编译后的文件、日志文件等。 https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 属性文件 gitattributes 文件的作用是指定文件的属性,例如文件的换行符、文件的编码等。 目录结构 assets 静态资源,包括图片,图标,视频,音频等。 build 构建脚本,包括编译脚本,打包脚本,发布脚本等。 docs 相关文档,包括设计文档,架构文档,开发文档,部署文档等。` samples 演示示例,包括代码示例,配置示例,数据示例,文档示例等。 src 源代码,包括源代码,配置文件,资源文件,脚本文件等。 tests 测试代码,包括单元测试,集成测试,端到端测试等。 tools 项目所使用的一些工具。 仓库分支 仓库分支用于管理仓库的版本,例如 master,develop,release,hotfix, feature 等。 仓库标签 仓库标签用于标记仓库的版本,例如 v1.0.0,v1.0.1,v1.1.0,v2.0.0 等。

2026年3月10日

订单微服务架构概述

订单部分使用了很多技术点,这包括 CQRS 模式、MediatR 本地命令分发、本地事件处理、分布式事件的抽象和实现、发件箱模式、分布式锁等技术。 订单服务架构图 微服务通信 客户端部分 客户端部分主要包括了订单的创建、支付、取消、查询等功能。客户端通过 API Gateway 调用订单微服务,订单微服务通过 MediatR 处理命令,然后通过事件总线发布事件,事件总线将事件分发到事件处理器,事件处理器处理事件,然后将事件持久化到数据库。 订单 WebApi 部分 使用 WebApi 作为订单微服务的入口,接收客户端的请求,然后通过 MediatR 处理命令,然后通过事件总线发布事件。 Queries 部分 使用从库来处理查询,从库是一个独立的数据库,用于处理查询,从库的数据是通过流式复制从主库同步过来的,从库的数据是只读的,从库的数据是最终一致的。 Commands 部分 使用命令模式处理订单的创建、支付、取消等命令,命令模式是一个行为设计模式,命令数据最终会被持久化到主数据库。 本地事件 本地事件是指在微服务内部的事件,本地事件是通过 MediatR 发布的,只能在当前微服务内部使用。 分布式事件 分布式事件是指在微服务之间的事件,分布式事件是通过事件总线发布的,可以在多个微服务之间使用,事件的发布和处理是异步的,使用 Dapr 的 Pub/Sub 组件实现事件的发布和订阅,可使用很多种消息队列来实现。 使用分布式锁 使用分布式锁来处理并发问题,分布式锁是通过 Dapr 组件实现的,分布式锁是一个分布式系统中的同步工具,用于解决并发问题,目前 Dapr 的分布式锁是基于 Redis 实现的。

2026年3月10日

多租户应用程序设计

什么是多租户应用程序 多租户应用程序是一种软件架构设计,允许单个实例的软件服务多个客户,每个客户被称为一个租户,租户之间的数据自动隔离的,租户之间的数据不会相互影响。 零度编程官网搜索「多租户」查看多租户设计视频教程。 单表字段隔离租户数据 单表多租户设计是指在一个表中存储多个租户的数据,通过在表中增加一个租户 TenantId 字段来区分不同租户的数据。 多租户数据查询 public class FieldIsolationServiceDbContext(DbContextOptions<FieldIsolationServiceDbContext> options, ICurrentTenant currentTenant) : DbContext(options) { protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().Property(p => p.Name).HasMaxLength(32); foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) { if (entityType.ClrType.IsAssignableTo(typeof(IMultiTenant))) { modelBuilder.Entity(entityType.ClrType).AddQueryFilter<IMultiTenant>(e => e.TenantId == currentTenant.TenantId); } } base.OnModelCreating(modelBuilder); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.AddInterceptors(new TenantSaveChangesInterceptor(currentTenant)); base.OnConfiguring(optionsBuilder); } } 保存数据时自动设置租户编号 private void MultiTenancyTracking(DbContext dbContext) { IEnumerable<EntityEntry<IMultiTenant>> multiTenancyEntries = dbContext.ChangeTracker.Entries<IMultiTenant>().Where(entry => entry.State == EntityState.Added || entry.State == EntityState.Modified); multiTenancyEntries?.ToList().ForEach(entityEntry => { entityEntry.Entity.TenantId ??= currentTenant.TenantId; }); } 多数据库隔离租户数据 多数据库多租户设计是指为每个租户创建一个独立的数据库,通过数据库连接字符串来区分不同租户的数据。 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (currentTenant.TenantId != null) { string? connectionString = configuration.GetConnectionString(currentTenant.TenantId); optionsBuilder.UseNpgsql(connectionString); } base.OnConfiguring(optionsBuilder); } ZeroFramework TenantDbConnectionInterceptor 基于 Schema 的多租户设计 基于 Schema 的多租户设计是指为每个租户创建一个独立的 Schema,通过 Schema 来区分不同租户的数据。 protected override void OnModelCreating(ModelBuilder modelBuilder) { if (currentTenant.TenantId != null) { modelBuilder.HasDefaultSchema(currentTenant.TenantId); } base.OnModelCreating(modelBuilder); } 目前 EF Core 不支持在迁移时自动创建 Schema,需要手动创建 Schema。 ...

2026年3月10日

分页排序和多条件查询

分页参数 请求应使用 GET 方法 http://localhost:8080/api/products?keyword=test&pagenumber=1&pagesize=5&orderby=id desc,price asc 响应返回如下 { "totalCount": 100, "items": [ { "id": 1, "name": "test", "price": 100 }, { "id": 2, "name": "test", "price": 200 } ] } 分页请求模型 public class PagedAndSortedRequest : PagedRequest { public string? OrderBy { get; init; } } 分页响应模型 public class PagedResponse<T>(IReadOnlyList<T> items, int totalCount) { public IReadOnlyList<T> Items { get; init; } = items; public int TotalCount { get; init; } = totalCount; } 扩展 IQueryable 以便通过属性名称排序 QueryableOrderByExtensions.cs 将字符串条件转化为排序表达式 IOrderedQueryable<TSource> OrderBy(IQueryable<TSource> source, string propertyName) 扩展 IQueryable 以便搜索和排序 QueryableExtensions.cs 扩展分页和排序方法 IQueryable<TEntity> SortBy<TEntity>(this IQueryable<TEntity> query, string? orderBy = null); IQueryable<TEntity> PageBy<TEntity>(this IQueryable<TEntity> query, PagedRequest pagedRequest) IQueryable<TEntity> SortAndPageBy<TEntity>(this IQueryable<TEntity> query, PagedAndSortedRequest? pagedAndSortedRequest = null); 再扩展一个 WhereIf 条件查询 IQueryable<TSource> WhereIf<TSource>(this IQueryable<TSource> source, bool condition, Expression<Func<TSource, bool>> predicate); 在 API 控制器中使用扩展方法 [HttpGet] [Authorize(IdentityPermissions.Users.Default)] public async Task<ActionResult<PagedResponse<UserListItem>>> GetUsers([FromQuery] UserListRequest model) { IQueryable<User> users = dbContext.Set<User>(); if (model.Keyword is not null) { users = users.Where(e => e.UserName != null && e.UserName.Contains(model.Keyword)); } users = users.WhereIf(model.PhoneNumber is not null, e => e.PhoneNumber == model.PhoneNumber); var pagedUsers = users.SortAndPageBy(model); var list = new List<UserListItem>(); return new PagedResponse<UserListItem>(mapper.Map<List<UserListItem>>(await pagedUsers.ToListAsync()), await users.CountAsync()); } 实现灵活的复杂查询 可以使用 OData 或者 GraphQL 来实现更复杂的查询。 ...

2026年3月10日

负载测试和压力测试

负载测试 测试应用是否可以在特定情况下处理指定的用户负载,同时仍满足响应目标,应用在正常状态下运行。 压力测试 在极端条件下(通常为长时间)运行时测试应用的稳定性,测试会对应用施加高用户负载(峰值或逐渐增加的负载)或限制应用的计算资源。压力测试可确定压力下的应用是否能够从故障中恢复,并正常返回到预期的行为,在压力下,应用在异常高的压力下运行。 测试工具 云端测试 和 Apache JMeter 是两种流行的负载测试工具。负载测试和压力测试应在发布和生产模式下完成,而不是在调试和开发模式下进行。 使用 Apache JMeter 工具 Apache JMeter 是一个开源的 Java 应用程序,用于执行负载测试、功能测试和性能测试。JMeter 可以模拟多种类型的负载,包括负载测试、压力测试、功能测试、基准测试等。 提供强大的图形化界面,可以通过图形化界面创建测试计划,然后执行测试计划。JMeter 支持多种协议,包括 HTTP、HTTPS、FTP、JMS、SOAP、TCP 和 LDAP,还可以通过插件支持其他协议。 https://jmeter.apache.org/usermanual/build-web-test-plan.html 基准测试 基准测试是一种测试方法,用于确定应用程序的性能基准,以便在应用程序的生命周期中进行性能优化。使用 BenchmarkDotNet 可以在 .NET 应用程序中进行基准测试。 dotnet add package BenchmarkDotNet public class Md5VsSha256 { [Benchmark] public byte[] Sha256() => sha256.ComputeHash(data); [Benchmark] public byte[] Md5() => md5.ComputeHash(data); } public class Program { public static void Main(string[] args) { BenchmarkRunner.Run<Md5VsSha256>(); } }

2026年3月10日

更新 .NET 8.0 到 .NET 9.0

更新 Visual Studio 最新预览版 下载并安装 Visual Studio 预览版,安装 .NET 9.0 SDK。 更新项目文件 打开项目文件,将 <TargetFramework>net8.0</TargetFramework> 更新为 <TargetFramework>net9.0</TargetFramework>。 更新 NuGet 包 打开 NuGet 包管理器,更新所有 NuGet 包到最新版本。 更新 Aspire 和 MAUI 工作负载 打开 Aspire 工作负载,更新所有 Aspire 包到最新版本。 dotnet workload install aspire android ios maccatalyst maui-android maui-desktop maui-mobile maui-windows 若要将 Tab 自动补全添加到适用于 .NET CLI 的 PowerShell https://learn.microsoft.com/zh-cn/dotnet/core/tools/enable-tab-autocomplete

2026年3月10日

关于 Aspire 应用宿主的编排

通过 Add 方法添加应用组件 通过 AddProject、AddContainer、AddExecutable、AddParameter,AddConnectionString 等方法,可以将应用宿主编排为一个完整的应用。 配置显式资源启动 builder.AddProject<Projects.MyApp>("dbmigration").WithExplicitStart(); 使用 WithExplicitStart 方法,可以配置显式资源启动,显式资源启动的资源需要手动启动。 资源的引用通过 WithReference 方法 builder.AddProject<Projects.MyApp>("myapp").WithReference(postgresdb); 被引用资源可以是一个终结点,连接字符串,或者其他资源,所引用的资源会通过环境变量的方式传递给引用资源。 等待资源启动和完成 builder.AddProject<Projects.MyApp>("myapp").WithWaitFor(postgresdb); builder.AddProject<Projects.MyApp>("myapp").WithWaitForCompletion("dbmigration"); 自定义容器的启动参数 var cache = builder.AddRedis("cache").WithImageTag("latest"); 容器资源生存期 var cache1 = builder.AddRedis("cache1").WithLifetime(ContainerLifetime.Persistent); var cache2 = builder.AddRedis("cache1").WithLifetime(ContainerLifetime.Session); Persistent 表示容器资源的生存期为持久,Session 表示容器资源的生存期为会话。 外部参数 在 Host 项目的 appsettings.json 配置文件中添加如下节点。 { "Parameters": { "myvalue": "local-value" } } var value1 = builder.AddParameter("myvalue"); var value2 = builder.AddParameter("myvalue", secret: true); builder.AddProject<Projects.ApiService>("api").WithEnvironment("EXAMPLE_VALUE", value1); 外部连接字符串 在 Host 项目的 appsettings.json 配置文件中添加如下节点。 { "ConnectionStrings": { "myconnection": "Server=myserver;Database=mydb;User=myuser;Password=mypassword;" } } var connection = builder.AddConnectionString("myconnection"); builder.AddProject<Projects.ApiService>("api").WithReference(connection); 终结点引用 var builder = DistributedApplication.CreateBuilder(args); var customContainer = builder.AddContainer("myapp", "mycustomcontainer").WithHttpEndpoint(port: 9043, name: "endpoint"); var endpoint = customContainer.GetEndpoint("endpoint"); var apiservice = builder.AddProject<Projects.AspireApp_ApiService>("apiservice").WithReference(endpoint); 在 Redis 组件上启用管理扩展 var redis = builder.AddRedis("redis").WithRedisInsight(); var redis = builder.AddRedis("redis").WithRedisCommander(); 注册生命周期钩子 using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; var builder = DistributedApplication.CreateBuilder(args); builder.Services.AddLifecycleHook<LifecycleLogger>(); builder.Build().Run(); class LifecycleLogger(ILogger<LifecycleLogger> logger): IDistributedApplicationLifecycleHook { public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) { logger.LogInformation("BeforeStartAsync"); return Task.CompletedTask; } public Task AfterEndpointsAllocatedAsync( DistributedApplicationModel appModel, CancellationToken cancellationToken = default) { logger.LogInformation("AfterEndpointsAllocatedAsync"); return Task.CompletedTask; } public Task AfterResourcesCreatedAsync( DistributedApplicationModel appModel, CancellationToken cancellationToken = default) { logger.LogInformation("AfterResourcesCreatedAsync"); return Task.CompletedTask; } } 网络模型和多副本启动 builder.AddProject<Projects.Networking_Frontend>("frontend") .WithHttpEndpoint(port: 5066, IsExternal: true, IsProxied: true) .WithReplicas(2); IsExternal 和 IsProxied 属性决定了终结点的管理和公开方式,IsExternal 为 true 时,终结点会被公开,IsProxied 为 true 时,终结点会被代理。 ...

2026年3月10日

关于 EditorConfig 文件

EditorConfig 有助于为跨不同编辑器和 IDE 处理同一项目的多个开发人员保持一致的编码风格。 EditorConfig 项目由用于定义编码样式的文件格式和文本编辑器插件集合组成,这些插件使编辑器能够读取文件格式并遵循定义的样式。EditorConfig 文件易于读取,并且与版本控制系统配合得很好。 https://editorconfig.org Visual Studio 2019 及更高版本支持 EditorConfig 文件,可以在 Visual Studio 中使用 EditorConfig 文件来定义和维护代码样式设置。 https://github.com/dotnet/aspnetcore/blob/main/.editorconfig // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the <<LICENSE_TYPE>> license. // See the LICENSE file in the project root for more information. 在 Visual Studio 中使用 EditorConfig 文件添加文件头版权信息。 ...

2026年3月10日

基于策略和资源的授权机制

身份认证完成后 HttpContext.User 中包含了用户的身份信息,但是用户的身份信息并不包含用户的权限信息。用户的权限信息需要通过授权系统来获取。 ASP.NET Core 中的授权系统 ASP.NET Core 中的授权系统是基于策略的授权系统,可以通过声明式的方式来定义授权策略。授权策略可以基于角色,也可以基于资源,也可以基于其他的条件。授权策略可以通过声明式的方式来定义,也可以通过代码的方式来定义。 使用 IAuthorizationService 接口 IAuthorizationService 接口是 ASP.NET Core 中的授权服务接口,可以通过该接口来进行授权操作。IAuthorizationService 接口提供了多个授权方法,可以通过这些方法来进行授权操作。 public interface IAuthorizationService { Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName); Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, AuthorizationPolicy policy); } AuthorizationResult 是授权结果,包含了授权的结果和失败的原因。 实现授权策略提供者 使用 OperationAuthorizationRequirement 表示授权条件,将上一期视频中的权限定义转成授权条件提供给系统,一个策略中本可以有多个条件,但为了简单这里的策略中只加一个条件。 需要注意的是,多个策略是 OR 的关系,多个条件是 AND 的关系。 public class CustomAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options) 实现资源表示 public interface IAuthorizationResource { string ResourceType => GetType().Name; string ResourceId => GetType().GetProperty("Id")?.GetValue(this)?.ToString() ?? throw new NotImplementedException(); } 设计一个表示资源的记录类 一般来说,实体类型就是一种资源,可以直接从 IAuthorizationResource 接口实现,但为了方便表示资源,可以设计一个表示资源的记录类。 public record struct ResourceInfo : IAuthorizationResource 实现授权条件处理器 public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement> public class ResourcePermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement, IAuthorizationResource> protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement); protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, TResource resource); 在授权处理器中使用 IPermissionChecker public class PermissionRequirementHandler(IPermissionChecker permissionChecker) : AuthorizationHandler<OperationAuthorizationRequirement> { private readonly IPermissionChecker _permissionChecker; public PermissionRequirementHandler(IPermissionChecker permissionChecker) { _permissionChecker = permissionChecker; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement) { if (await _permissionChecker.IsGrantedAsync(context.User, requirement.Name)) { context.Succeed(requirement); } } }

2026年3月10日