Model Binding

模型绑定最佳实践 实体对象是 EF 中的概念, 每个实体对象对应数据库中的一张表。模型对象是 MVC 中的概念,是 HTTP 请求和响应的数据结构。HTTP 请求中通过 URL 参数、表单、标头、 JSON 数据等方式传递数据,这些数据最终会被绑定为模型对象中,模型经过转换为实体对象后,被持久化到数据库中。 public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class ProductModel { public string Name { get; set; } public decimal Price { get; set; } } public class ProductController : Controller { private readonly ShopDbContext _context; public ProductController(ShopDbContext context) { _context = context; } [HttpPost] public async Task<IActionResult> Create(ProductModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var product = new Product { Name = model.Name, Price = model.Price }; _context.Products.Add(product); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(Get), new { id = product.Id }, product); } } 针对不同 HTTP 操作,应该使用不同的模型对象,比如创建模型、更新模型、查询模型、删除模型等,这样的好处是可以更好的区分模型对象的职责,比如创建模型只需要包含创建所需的字段,更新模型只需要包含更新所需的字段,查询模型只需要包含查询所需的字段,删除模型只需要包含删除所需的字段,职责单一,维护性好,还可以针对不同的模型提供不同的验证规则。

2026年3月10日

PostgreSQL 高可用性和读写分离

基本概念 一个操作系统可以安装多个 PostgreSQL 实例,每个实例都有自己的配置文件、数据目录和端口号。每个实例中可以创建多个数据库,每个数据库中可以创建多个表。使用 Docker 可以方便的创建多个 PostgreSQL 实例。 安装主从服务器 docker run --name postgres1 -e POSTGRES_PASSWORD=postgres -d -p 5431:5432 -v ${pwd}/postgres1/data:/var/lib/postgresql/data postgres docker run --name postgres2 -e POSTGRES_PASSWORD=postgres -d -p 5432:5432 -v ${pwd}/postgres2/data:/var/lib/postgresql/data postgres 逻辑复制 逻辑复制是根据复制标识(通常是主键)复制数据对象及其更改的一种方法。 我们使用术语逻辑与物理复制相比,逻辑复制使用发布和订阅模型, 其中一个或多个订阅者订阅发布者节点上的一个或多个发布。 订阅者从他们订阅的发布中提取数据, 并可能随后重新发布数据以允许级联复制或更复杂的配置。 创建主数据库和从数据库 在 postgres1 中创建数据库 mydb1 数据库。 CREATE DATABASE mydb1; 在 postgres2 中创建数据库 mydb2 数据库。 CREATE DATABASE mydb2; 在 mydb1 和 mydb2 数据库中创建结构相同的表 mytable。 CREATE TABLE mytable (id int PRIMARY KEY, name text NOT NULL); 将服务器日志级别设置为逻辑 在 postgres1 的 mydb1 数据库中设置服务器日志级别为逻辑。 ...

2026年3月10日

Readme

朋友们,零度新一代基架今日正式开始搭建。我们非常激动地宣布,新一代基架起名为 HelloShop 项目, 这个基架将演示新一代 .NET 技术栈开发架构,之所以起名为 HelloShop 是以为简单的商店应用能够演示出一个系统的所有技术,当然,这个商城系统也可以是其它系统,只要你能够理解这个基架的设计思想,你就能够快速搭建一个系统,以下文档由 AI 生成,可能会有一些语法错误,我们会在后期进行修正。 业务场景 本商店应用旨在演示一个系统的所有技术,帮助开发者以最小的依赖和代码搭建一个系统,将不同的技术置于微服务架构中。开发者可以根据自己的业务需求快速复用代码并搭建一个系统,而不是花费大量时间在一个通用的框架上。 我们的目标是提供细粒度的微服务架构和最佳实践,让您可以自行组合各种技术。统一的身份认证和授权系统,服务发现,负载均衡,容错,分布式跟踪,分布式缓存,分布式事务,分布式消息,分布式日志,分布式配置,分布式定时任务等等。 此外,我们还提供了界面,包括基于 Blazor 的 Web 应用和基于 MAUI 的混合应用,可以在不同的平台上运行,包括桌面应用,安卓应用和 IOS 应用。 {class=“img-responsive w-100”} 所含技术 Visual Studio 2022、 .NET 8、C# 12.0、Aspire、ASP.NET Core,EF Core、WebApi、gPRC、Blazor、MAUI、PostgreSQL、MonngDB、Redis、SignalR、Identity、Orleans、日志记录、密钥管理、后台定时任务、服务发现、发布订阅、分布式跟踪、健康检查、性能指标探测、配置管理、容器化技术、单元测试和集成测试、基于 Roslyn 的源代码自动生成器和增量生成器。 仓库结构 在仓库的结构上,我们跟随了微软惯用的风格,目录结构如下: assets 静态资源,包括图片,图标,视频,音频等。 build 构建脚本,包括编译脚本,打包脚本,发布脚本等。 docs 相关文档,包括设计文档,架构文档,开发文档,部署文档等。` samples 演示示例,包括代码示例,配置示例,数据示例,文档示例等。 src 源代码,包括源代码,配置文件,资源文件,脚本文件等。 tests 测试代码,包括单元测试,集成测试,端到端测试等。 tools 项目所使用的一些工具。 代码仓库 国外仓库:https://github.com/bit365/hello-shop{target="_blank"} 国内仓库:https://gitee.com/bit365/hello-shop{target="_blank"} 后续计划 我们刚刚还注册了 helloshopnet.com 和 helloshopnet.cn 两个域名用于后期的基架部署,也算是给 HelloShop 安一个家,目前还是正在备案。 技术探讨 本套基架会有一些配套视频在 www.xcode.me 上发布。如果你想要获取最新的视频可以关注我们的微信订阅号 zerostack,我们会在公众号上发布最新的动态,如果你觉得这套基架对你有帮助,你可以给我们一个 Star,这是对我们最大的鼓励。 最后我们希望这套基架能帮助到你,如果你有任何问题可以在 Github 上提出我们会尽快回复,坚持优雅的编码是我们一直所追求的,希望你也能够参与这种优雅。

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日