使用 gRPC 实现购物车服务

什么是 gRPC 通信框架 gRPC 是一个高性能、开源和通用的 RPC 框架,由 Google 开发,基于 HTTP/2 协议,支持多种语言(如 Go、Java、Python、C++、Node.js、Ruby、C#、Objective-C、PHP 和 Dart)。 gRPC 使用 Protocol Buffers 作为接口定义语言(IDL),Protocol Buffers 是一种轻便高效的结构化数据序列化方法,类似于 XML 或 JSON,但更小、更快、更简单。 ASP.NET Core 中使用 gRPC 实现服务 定义一个 gRPC 服务,需要创建一个 gRPC 服务定义文件(.proto 文件),然后使用 gRPC 工具生成服务端和客户端代码。 带有 OpenAPI 的 gRPC 服务 dotnet add package Microsoft.AspNetCore.Grpc.Swagger <IncludeHttpRuleProtos>true</IncludeHttpRuleProtos> var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc().AddJsonTranscoding(); builder.Services.AddGrpcSwagger(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "gRPC transcoding", Version = "v1" }); }); var app = builder.Build(); app.UseSwagger(); if (app.Environment.IsDevelopment()) app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); } app.MapGrpcService<GreeterService>(); app.Run(); // My amazing greeter service. service Greeter { // Sends a greeting. rpc SayHello (HelloRequest) returns (HelloReply) { option (google.api.http) = { get: "/v1/greeter/{name}" }; } } message HelloRequest { // Name to say hello to. string name = 1; } message HelloReply { // Hello reply message. string message = 1; } 单元测试 gRPC 服务 dotnet add package moq public class GreeterServiceTests { [Fact] public async Task SayHello_ReturnsHello() { // Arrange var mockLogger = new Mock<ILogger<GreeterService>>(); var service = new GreeterService(mockLogger.Object); var request = new HelloRequest { Name = "Unit Test" }; var context = new ServerCallContext(); // Act var reply = await service.SayHello(request, context); // Assert Assert.Equal("Hello Unit Test", reply.Message); } } 集成测试 gRPC 服务 dotnet add package Microsoft.AspNetCore.TestHost public class GreeterServiceIntegrationTests { private readonly TestServer _server; private readonly Greeter.GreeterClient _client; public GreeterServiceIntegrationTests() { _server = new TestServer(new WebHostBuilder() .ConfigureServices(services => { services.AddGrpc(); }) .Configure(app => { app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGrpcService<GreeterService>(); }); })); _client = new Greeter.GreeterClient(_server.CreateGrpcChannel()); } [Fact] public async Task SayHello_ReturnsHello() { // Arrange var request = new HelloRequest { Name = "Integration Test" }; // Act var reply = await _client.SayHelloAsync(request); // Assert Assert.Equal("Hello Integration Test", reply.Message); } }

2026年3月10日

使用 HybridCache 混合缓存库

混合缓存可以同时使用内存缓存和分布式缓存,它可以提高缓存的命中率,减少对分布式缓存的访问,从而提高性能。 当从缓存中获取数据时,它首先会从内存缓存中获取数据,如果内存缓存中没有数据,它会从分布式缓存中获取数据。 当向缓存中写入数据时,它会同时向内存缓存和分布式缓存中写入数据。 Service 1 ----> HybridCache ----> MemoryCache ----> DistributedCache https://learn.microsoft.com/zh-cn/azure/architecture/best-practices/caching 安装程序包 dotnet add package Microsoft.Extensions.Caching.Hybrid 配置服务 builder.Services.AddHybridCache(); 使用服务 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; } } 按标记移除缓存条目 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); } }

2026年3月10日

使用 JwtBearer 令牌进行身份验证

介绍 JwtBearer 令牌身份验证是一种基于 JSON Web 令牌的身份验证方法, 用于验证用户的身份, 它是一种无状态的身份验证方法, 适用于 Web API 和 Web 应用程序。 安装 NuGet 包 dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer 自建 Identity Api 终结点 builder.Services.AddIdentity<User, Role>(options => { options.SignIn.RequireConfirmedAccount = false; options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireUppercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequiredLength = 5; }).AddEntityFrameworkStores<IdentityServiceDbContext>(); 使用 JwtBearer 验证令牌 builder.Services.AddAuthentication(options => { options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultSignInScheme = CustomJwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.TokenValidationParameters.ValidateIssuer = false; options.TokenValidationParameters.ValidateAudience = false; options.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.Default.GetBytes(issuerSigningKey)); }) 自定义身份认证处理程序 public class CustomJwtBearerDefaults public class CustomJwtBearerOptions public class CustomJwtBearerHandler public class CustomJwtBearerExtensions 配置令牌生成 builder.Services.AddAuthentication().AddJwtBearer().AddCustomJwtBearer(options => { options.IssuerSigningKey = issuerSigningKey; options.SecurityAlgorithm = SecurityAlgorithms.HmacSha256; }); 在容器中运行 pgAdmin 管理工具 docker pull dpage/pgadmin4 docker run --name pgadmin -e PGADMIN_DEFAULT_EMAIL=test@test.com -e PGADMIN_DEFAULT_PASSWORD=test -e TZ=Asia/Shanghai -d -p 5050:80 dpage/pgadmin4

2026年3月10日

使用 OpenAPI 规范生成 API 文档

Swagger 是一个与语言无关的规范,用于描述 REST API。 它使计算机和用户无需直接访问源代码即可了解 REST API 的功能,Swagger 项目已于捐赠给 OpenAPI 计划,自此它被称为 OpenAPI 规范。 Swashbuckle 是一个用于 .NET Core 的开源项目,它是 OpenAPI 工具的集合,用于生成 OpenAPI 规范的文档。 它可以自动生成 API 文档,包括 API 的描述、请求和响应的格式、参数的描述等。 安装 Swashbuckle.AspNetCore 在 .NET Core 项目中,可以通过 NuGet 安装 Swashbuckle.AspNetCore 包。 dotnet add package Swashbuckle.AspNetCore 配置 Swashbuckle 在 Startup.cs 文件中,可以通过以下方式配置 Swagger: builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }

2026年3月10日

使用 PostgreSQL 数据库存储数据

在 Docker 中 启动 PostgreSQL 数据库 https://www.postgresql.org docker pull postgres docker run --name postgres -e POSTGRES_PASSWORD=postgres -e TZ=Asia/Shanghai -d -p 5432:5432 postgres 使用 PgAdmin 连接 PostgreSQL 数据库 https://www.pgadmin.org SHOW timezone; EfCore 使用 PostgreSQL 数据库 dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL 定义实体类型 public class User { public int Id { get; set; } public string UserName { get; set; } public string PasswordHash { get; set; } public DateTimeOffset CreationTime { get; set; } = DateTimeOffset.UtcNow; } 配置实体类型 public class UserEntityTypeConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.ToTable("Users"); builder.HasKey(x => x.Id); builder.Property(x => x.UserName).IsRequired().HasMaxLength(50); builder.Property(x => x.PasswordHash).IsRequired().HasMaxLength(50); builder.Property(x => x.CreationTime); } } 创建 DbContext 上下文 public class IdentityServiceDbContext : DbContext { public IdentityServiceDbContext(DbContextOptions<IdentityServiceDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); AppContext.SetSwitch("Npgsql.DisableDateTimeInfinityConversions", true); } } 数据库连接字符串 { "ConnectionStrings": { "IdentityDatabase": "Host=localhost;Port=5432;Database=IdentityService;Username=postgres;Password=postgres" } } 注册数据库上下文 builder.Services.AddDbContext<IdentityServiceDbContext>(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("IdentityDatabase")); }); 迁移数据库 dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.Design dotnet ef migrations add InitialCreate --output-dir EntityFrameworks/Migrations dotnet ef database update PostgreSQL 数据库命名约定 https://github.com/efcore/EFCore.NamingConventions ...

2026年3月10日

使用 TimeProvider 获取时间

传统方式获取时间 我们通常使用 DateTime.Now 和 DateTimeOffset.Now 来获取当前时间,但是这样的代码很难测试,时间依赖于系统时间,很难控制和重现,只能修改系统时间。 var now1 = DateTime.Now; var nowUtc1 = DateTime.UtcNow; var now2 = DateTimeOffset.Now; var nowUtc2 = DateTimeOffset.UtcNow; 最新的 TimeProvider 类型 为了解决上述问题,在 .NET 8 中新增了 TimeProvider 类型,可以用来获取时间,并且可以在测试中轻松地控制时间。 var now = TimeProvider.System.GetLocalNow(); var nowUtc = TimeProvider.System.GetUtcNow(); 重写 TimeProvider 模拟时间 我们可以重写 TimeProvider 类型,模拟时间,方便测试。 public class MyTimeProvider : TimeProvider { public override DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow; } 依赖注入 TimeProvider 实现 默认实现 TimeProvider.System 使用系统时间。 builder.Services.AddSingleton(TimeProvider.System); 注入 MyTimeProvider 使用自定义时间。 builder.Services.AddSingleton<TimeProvider, MyTimeProvider>(); 使用 TimeProvider 的代码 public class MyService(TimeProvider timeProvider) { public void DoSomething() { var now = timeProvider.GetLocalNow(); var nowUtc = timeProvider.GetUtcNow(); } } 演示零度框架中使用 TimeProvider 代码。 关于时间的单元测试 Microsoft.Extensions.TimeProvider.Testing

2026年3月10日

使用 TimeProvider 类注入时间

System.TimeProvider 是一种时间抽象,它以 DateTimeOffset 类型的形式提供时间点。 通过使用 TimeProvider,可确保代码可测试且可预测。 TimeProvider 已在 .NET 8 中引入。 默认实现 默认情况下,TimeProvider 使用 DateTimeOffset.UtcNow 作为时间源。 Console.WriteLine($"Local: {TimeProvider.System.GetLocalNow()}"); Console.WriteLine($"Utc: {TimeProvider.System.GetUtcNow()}"); 自定义实现 public class CustomTimeProvider: TimeProvider { public override DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow.AddHours(1); } builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton<TimeProvider, CustomTimeProvider>(); public class MyService { private readonly TimeProvider _timeProvider; public MyService(TimeProvider timeProvider) { _timeProvider = timeProvider; } public void DoSomething() { var now = _timeProvider.Now; Console.WriteLine(now); } } FakeTimeProvider 实现 dotnet add package Microsoft.Extensions.TimeProvider.Testing FakeTimeProvider fakeTimeProvider = new(); fakeTimeProvider.SetUtcNow(fakeTimeProvider.GetUtcNow().AddHours(1)); MyService service = new (timeProvider); service.DoSomething();

2026年3月10日

使用分布式锁解决并发问题

单进程应用程序中的锁 在单机环境下,我们可以使用线程锁来解决并发问题,但是在分布式系统中,线程锁无法解决并发问题,因为分布式系统中的线程锁只能锁住当前进程,无法锁住其他进程。 在 .NET 中,我们可以使用 lock 关键字来实现线程锁。 https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/lock private static readonly object _lock = new object(); public void DoSomething() { lock (_lock) { // 业务逻辑 } } C# 13 引入了新的线程同步类型 System.Threading.Lock,它通过作用域管理的方式简化了锁的使用,使代码更加清晰可靠。 using System.Threading.Lock; private static readonly Lock _lock = new Lock(); public void DoSomething() { using (_lock.EnterScope()) { // 业务逻辑 } } 开源分布式锁的实现 DistributedLock 是一个 .NET 库,它基于各种底层技术提供强大且易于使用的分布式互斥锁、读写器锁和信号量。 https://github.com/madelson/DistributedLock 零度框架中的分布式锁 零度框架中提供分布式锁的基本抽象,并基于 Dapr 实现了分布式锁。 https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview Redis 分布式锁原理 Redis 分布式锁的实现原理是通过 SETNX 命令,SETNX 是 Redis 的一个原子性操作,它会在键不存在时设置键的值,如果键已经存在,则不做任何操作。

2026年3月10日

使用缓存提高系统性能

缓存是一种常见的技术,目标是提高系统的性能和伸缩性。 为缓存数据,它会暂时会经常访问的数据复制到位置靠近应用程序的快速存储。 如果这种快速数据存储比原始源更靠近应用程序,则缓存可以通过更快速提供数据,大幅改善客户端应用程序的响应时间。 缓存类型 私有专用缓存 专用缓存是指缓存服务运行在应用程序的同一台服务器上,它是应用程序的一部分,通常是一个库或模块。专用缓存通常是一个内存缓存,如 MemoryCache 或 ConcurrentDictionary,它是一个轻量级的缓存,适用于小型应用程序。专用缓存的优点是简单易用,缺点是无法跨服务器共享数据,不适用于大型应用程序,使用 IMemoryCache 接口可以在 .NET 中使用专用缓存。 services.AddMemoryCache(); public class MyService(IMemoryCache cache) : IService { public async Task Test() { cache.Set("key", "value"); var value = cache.Get("key"); } } 分布式共享缓存 分布式缓存是指缓存服务运行在多台服务器上,它是一个独立的服务,可以跨服务器共享数据。分布式缓存通常是一个内存缓存,如 Redis、Memcached、Garnet 等,它是一个高性能、低延迟的缓存,适用于大型应用程序。分布式缓存的优点是高性能、低延迟,支持大规模数据存储和访问,缺点是复杂度高,需要额外的服务器资源,使用 IDistributedCache 接口可以在 .NET 中使用分布式缓存。 services.AddMemoryDistributedCache(); public class MyService(IDistributedCache cache) : IService { public async Task Test() { await cache.SetStringAsync("key", "value"); var value = await cache.GetStringAsync("key"); } } 使用 Redis 分布式缓存 Redis 是一个高性能、开源的分布式缓存系统,它是一个内存数据库,支持多种数据结构,如字符串、列表、哈希表、集合等,Redis 的设计目标是提供一个高性能、低延迟的分布式缓存系统,以支持大规模的数据存储和访问,同时提供高可用性和可扩展性。Redis 采用 RESP 协议,使用 C 语言编写,支持多种语言,如 C、C++、C#、Java、Python、Node.js 等,可以在 .NET 中使用 StackExchange.Redis 开源库对缓存进行操作。 Windows 下 Redis 不再维护,可以使用 WSL2 安装 Redis,或者使用 Docker 安装 Redis。也可使用 Memurai 替代 Redis,Memurai 是一个 Windows 下的 Redis 服务,支持 Redis 的大部分功能,可以在 Windows 下运行 Redis 服务。 ...

2026年3月10日

数据库对象命名约定

不同数据库对象的命名约定不同,EF Core 为了适配不同数据库,提供了一些命名约定的配置选项。 常见的命名方法 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 可使用 Humanizr 库将字符串转换为不同的命名风格。 表名使用单数还是复数 EF Core 默认情况下,表名使用单数形式,可以通过约定器或者手动指定复数表名,社区对单数和复数表名的争论一直存在,没有统一的标准,但零度推荐使用单数形式。 手动指定表名和列名 表名 默认情况下,EF Core 使用实体类型的名称作为表名。可以通过重写 OnModelCreating 方法来修改表名: protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().ToTable("blogs"); } 列名 默认情况下,EF Core 使用属性名作为列名。可以通过 HasColumnName 方法来修改列名: protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().Property(b => b.Url).HasColumnName("blog_url"); } PostgreSQL 命名约定 PostgreSQL 的命名约定采用小写蛇形命名法,即单词之间使用下划线分隔,如 blog_url,表名使用单数形式。 使用 EFCore.NamingConventions 库 EFCore.NamingConventions 库提供了一些命名约定的配置选项,可以方便地配置表名、列名等。 安装 EFCore.NamingConventions 库 dotnet add package EFCore.NamingConventions 配置命名约定 使用 OnConfiguring 方法配置命名约定: ...

2026年3月10日