初识 Masa Framework
初识 Masa Framework
MASA Framework
全新的.NET现代应用开发,提供分布式应用运行时–基于Dapr云原生最佳实践,能够快速实现分布式、微服务、DDD,SaaS等现代应用开发。官方文档参阅
先决条件
开发计算机上应安装以下工具:
一个集成开发环境 (比如: Visual Studio) 它需要支持 .NET 6.0 的开发.
环境配置
MacOS dotnet环境配置
1 | # 下载对应脚本进行安装 https://dotnet.microsoft.com/zh-cn/download/dotnet/scripts |
Quickstarts
Project Creation
create a blank solution
1 | dotnet new sln -n Masa.EShop.Demo |
Add Contracts Project
1 | dotnet new classlib -n Masa.EShop.Contracts.Catalog -o Contracts/Masa.EShop.Contracts.Catalog -f net6.0 |
Add Services Project
1 | dotnet new web -n Masa.EShop.Service.Catalog -o Services/Masa.EShop.Service.Catalog -f net6.0 |
Create and use MiniAPIs in Services
1 | cd Services/Masa.EShop.Service.Catalog |
To use
MinimalAPIs
, change fileProgram.cs
:use
var app = builder.AddServices();
replacevar app = builder.AddServices();
create
Services
folder,add classHealthService
, inherit fromServiceBase
1
2
3
4public class HealthService : ServiceBase
{
public IResult Get() => Results.Ok("success");
}
Domain
在前面的章节中, 使用MinimalAPIs
提供最小依赖项的HTTP API
对于本篇文档, 我们将要展示创建一个充血模型的商品模型, 并实现领域驱动设计 (DDD)的最佳实践
领域层是项目的核心,我们建议您按照以下结构来存放:
Domain
: 领域层 (可以与主服务在同一项目, 也可单独存储到一个独立的类库中)Aggregates
: 聚合根及相关实体Events
: 领域事件 (建议以DomainEvent
结尾)Repositories
: 仓储 (仅存放仓储的接口)Services
: 领域服务EventHandlers
: 进程内领域事件处理程序 (建议以DomainEventHandler
结尾)
包引入
1 | dotnet add package Masa.Contrib.Ddd.Domain --prerelease |
program.cs
注册 Mapster
映射器
1 | builder.Services.AddMapster(); |
聚合
选中 Aggregates
文件夹, 我们将新建包括 CatalogItem
、CatalogBrand
的聚合根以及 CatalogType
枚举类, 并在初始化商品时添加商品领域事件
商品
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34public class CatalogItem : FullAggregateRoot<Guid, int>
{
public string Name { get; private set; } = default!;
public string Description { get; private set; } = default!;
public decimal Price { get; private set; }
public string PictureFileName { get; private set; } = default!;
private int _catalogTypeId;
public CatalogType CatalogType { get; private set; } = default!;
private Guid _catalogBrandId;
public CatalogBrand CatalogBrand { get; private set; } = default!;
public int AvailableStock { get; private set; }
public int RestockThreshold { get; private set; }
public int MaxStockThreshold { get; private set; }
public CatalogItem(Guid id, Guid catalogBrandId, int catalogTypeId, string name, string description, decimal price, string pictureFileName) : base(id)
{
_catalogBrandId = catalogBrandId;
_catalogTypeId = catalogTypeId;
Name = name;
Description = description;
Price = price;
PictureFileName = pictureFileName;
}
}商品类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public class CatalogType : Enumeration
{
public static CatalogType Cap = new Cap();
public static CatalogType Mug = new(2, "Mug");
public static CatalogType Pin = new(3, "Pin");
public static CatalogType Sticker = new(4, "Sticker");
public static CatalogType TShirt = new(5, "T-Shirt");
public CatalogType(int id, string name) : base(id, name)
{
}
public virtual decimal TotalPrice(decimal price, int num)
{
return price * num;
}
}
public class Cap : CatalogType
{
public Cap() : base(1, "Cap")
{
}
public override decimal TotalPrice(decimal price, int num)
{
return price * num * 0.95m;
}
}商品品牌
1
2
3
4
5
6
7
8
9public class CatalogBrand : FullAggregateRoot<Guid, int>
{
public string Brand { get; private set; } = null!;
public CatalogBrand(string brand)
{
Brand = brand;
}
}
领域事件
我们将创建商品的领域事件, 它将在创建商品成功后被其它服务所订阅
创建商品的领域事件属于集成事件, 为保证订阅事件的重用以及订阅事件所属类库的最小依赖, 我们将其拆分为
CatalogItemCreatedIntegrationDomainEvent
、CatalogItemCreatedIntegrationEvent
两个类
选中 Masa.EShop.Contracts.Catalog
类库,添加nuget包
1 | # 注意使用命令的时候切换到对应项目文件夹 |
新增 IntegrationEvents
文件夹,新建文件 CatalogItemCreatedIntegrationEvent
1 | public record CatalogItemCreatedIntegrationEvent : IntegrationEvent |
新建创建商品集成事件 CatalogItemCreatedIntegrationEvent
集成事件在规约层存储, 后期可将规约层通过nuget方式引用, 以方便其它服务订阅事件使用 (
IntegrationEvent
选中项目 Masa.EShop.Service.Catalog
的领域事件 (Events
)文件夹, 新建创建商品集成领域事件 CatalogItemCreatedIntegrationDomainEvent
并在该项目添加对类库 Masa.EShop.Contracts.Catalog
的引用
1 | public record CatalogItemCreatedIntegrationDomainEvent : CatalogItemCreatedIntegrationEvent, IIntegrationDomainEvent |
1 | # 注意使用命令的时候切换到对应项目文件夹 |
领域事件可以在聚合根或领域服务中发布, 例如:
1 | public class CatalogItem : FullAggregateRoot<Guid, int> |
对象映射功能为
CatalogItem
类转换为CatalogItemCreatedIntegrationDomainEvent
提供了帮助, 具体可查看对象映射文档
仓储
选中领域层 Masa.EShop.Service.Catalog
中的 Repositories
文件夹并创建 ICatalogItemRepository
接口, 继承 IRepository<CatalogItem, Guid>
, 可用于扩展商品仓储
1 | public interface ICatalogItemRepository : IRepository<CatalogItem, Guid> |
对于新增加继承
IRepository<CatalogItem, Guid>
的接口, 我们需要在Repository<CatalogDbContext, CatalogItem, Guid>
的基础上扩展其实现, 由于实现并不属于领域层, 这里我们会在后面的文档实现这个Repository
领域服务
选中领域层 Masa.EShop.Service.Catalog
中的 Services
文件夹并创建 商品领域服务
1 | public class CatalogItemDomainService : DomainService |
- 继承
DomainService
的类会自动完成服务注册, 无需手动注册
最终解决方案结构类似下图
Save Or Get Data
在开发中, 我们需要用到数据库, 以便对数据能进行存储或读取, 下面例子我们将使用Sqlite数据库进行数据的存储与读取, 如果你的业务使用的是其它数据库, 可参考文档选择与之匹配的数据库包
前提
选中领域层 Masa.EShop.Service.Catalog
,在项目根目录下,新建如下层次文件夹
- Infrastructure
- Repositories
- Middleware
- Extensions
- EntityConfigurations
安装 Masa.Contrib.Data.EFCore.Sqlite
、Masa.Contrib.Data.Contracts
包
1 | dotnet add package Masa.Contrib.Data.EFCore.Sqlite --prerelease |
Masa.Contrib.Data.Contracts
提供了数据过滤的能力, 但它不是必须的
使用
在
Infrastructure
文件夹新建数据上下文CatalogDbContext
- 数据上下文的格式 :
XXXDbContext
, 并继承MasaDbContext<XXXDbContext>
1
2
3
4
5
6
7
8
9
10
11
12
13public class CatalogDbContext : MasaDbContext<CatalogDbContext>
{
public CatalogDbContext(MasaDbContextOptions<CatalogDbContext> options) : base(options)
{ss
}
protected override void OnModelCreatingExecuting(ModelBuilder builder)
{
builder.ApplyConfigurationsFromAssembly(typeof(CatalogDbContext).Assembly);
base.OnModelCreatingExecuting(builder);
}
}数据库迁移时将执行
OnModelCreatingExecuting
方法, 我们可以在其中配置与数据库表的映射关系, 为避免出现流水账式的数据库映射记录, 我们通常会将不同表的映射情况分别写到不同的配置对象中去, 并在OnModelCreatingExecuting
指定当前上下文映射的程序集.- 数据上下文的格式 :
配置数据库中商品表与
CatalogItem
的映射关系,在文件夹EntityConfigurations
新建CatalogItemEntityTypeConfiguration
类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class CatalogItemEntityTypeConfiguration
: IEntityTypeConfiguration<CatalogItem>
{
public void Configure(EntityTypeBuilder<CatalogItem> builder)
{
builder.ToTable("Catalog");
builder.Property(ci => ci.Id)
.IsRequired();
builder.Property(ci => ci.Name)
.IsRequired()
.HasMaxLength(50);
builder.Property(ci => ci.Price)
.IsRequired();
builder.Property(ci => ci.PictureFileName)
.IsRequired(false);
builder
.Property<Guid>("_catalogBrandId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("CatalogBrandId")
.IsRequired();
builder
.Property<int>("_catalogTypeId")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.HasColumnName("CatalogTypeId")
.IsRequired();
builder.HasOne(ci => ci.CatalogBrand)
.WithMany()
.HasForeignKey("_catalogBrandId");
builder.HasOne(ci => ci.CatalogType)
.WithMany()
.HasForeignKey("_catalogTypeId");
}
}配置数据库连接字符串
通常情况下数据库链接字符串配置信息存储在本地配置文件中, 框架支持在不同的配置文件中存放不同环境下使用的数据库链接字符串, 而不需要修改任何代码
1
2
3
4
5{
"ConnectionStrings": {
"DefaultConnection": "Data Source=./Data/Catalog.db;"
}
}如果你的项目使用了配置中心, 数据库链接字符串也在配置中心存储, 那么请跳过步骤3, 它不会对你有任何的帮助
在
Program.cs
里注册数据上下文1
2
3
4
5
6builder.Services.AddMasaDbContext<CatalogDbContext>(dbContextBuilder =>
{
dbContextBuilder
.UseSqlite() //使用Sqlite数据库
.UseFilter(); //数据数据过滤
});UseSqlite
方法由Masa.Contrib.Data.EFCore.Sqlite
提供, 我们建议在使用时不传入数据库字符串
继承 MasaDbContext
的数据库默认使用 ConnectionStrings
节点下的 DefaultConnection
配置, 想了解更多关于链接字符串相关的知识可查看 文档, 除了使用本地配置文件存放数据库链接字符串之外, 它还支持其它方式, 详细请查看 文档
其它
MasaFramework
并未约束项目必须使用 Entity Framework Core
, 查看已支持的ORM框架
自定义仓储实现
虽然框架已经提供了仓储功能, 但它的功能是有限的, 当默认仓储提供的功能不足以满足我们的需求时, 我们就需要在默认仓储的基础上进行扩展或者重写, 自定义仓储的接口与实现是一对一的, 它们必须是成对出现的
前提2
安装 Masa.Contrib.Ddd.Domain.Repository.EFCore
1 | dotnet add package Masa.Contrib.Ddd.Domain.Repository.EFCore --prerelease |
如果后续考虑可能更换ORM框架, 建议将仓储的实现可以单独存储到一个独立的类库中
使用2
在文件夹里 Infrastructure/Repositories
新建 CatalogItemRepository
用于实现 ICatalogItemRepository
1 | public class CatalogItemRepository : Repository<CatalogDbContext, CatalogItem, Guid>, ICatalogItemRepository |
自定义仓储实现可以继承 Repository<CatalogDbContext, CatalogItem, Guid>
, 我们只需要在默认仓储实现的基础上扩展新扩展的方法即可, 如果你不满意默认实现, 也可重写父类的方法, 默认仓储支持了很多功能, 查看详细文档
无论是直接使用框架提供的仓储能力, 还是基于默认仓储提供的能力基础上进行扩展, 都需要我们在Program
中进行注册, 否则仓储将无法正常使用, 例如:
1 | builder.Services.AddDomainEventBus(options => |
框架是如何完成自动注册, 为何项目提示仓储未注册, 点击查看文档
如果不在默认仓储的的基础上扩展, 而是完全自定义仓储, 则可以使用按约定自动注册功能简化服务注册
事件总线
通过事件总线帮助我们解耦不同架构层次, 根据事件类型我们将事件总线划分为:
必要条件
进程内事件总线
1
dotnet add package Masa.Contrib.Dispatcher.Events --prerelease // 支持进程内事件
进程内事件总线的实现由
Masa.Contrib.Dispatcher.Events
提供集成事件总线
1
2
3
4
5
6
7
8
9
10dotnet add package Masa.Contrib.Dispatcher.IntegrationEvents --prerelease
# 使用具有发件箱模式的集成事件
dotnet add package Masa.Contrib.Dispatcher.IntegrationEvents.Dapr --prerelease
# 使用dapr提供的pubsub能力
dotnet add package Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EFCore --prerelease
# 本地消息表
dotnet add package Masa.BuildingBlocks.Data.UoW --prerelease
#提供uow
dotnet add package Masa.Contrib.Ddd.Domain.Repository.EFCore --prerelease
#提供Efcore Repository而后续发送集成事件的类所在类库只需引用
Masa.BuildingBlocks.Dispatcher.IntegrationEvents
即可 (如果当前类库已经引用了Masa.Contrib.Dispatcher.IntegrationEvents.*
, 则无需重复引用Masa.BuildingBlocks.Dispatcher.IntegrationEvents
)集成事件总线的实现由
Masa.Contrib.Dispatcher.IntegrationEvents.Dapr
、Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EFCore
提供
使用事件总线
注册集成事件与进程内事件, 修改
Program
1
2
3
4
5
6builder.Services
.AddIntegrationEventBus(integrationEventBus =>
integrationEventBus
.UseDapr()
.UseEventLog<CatalogDbContext>()
.UseEventBus())由于我们的项目使用了DDD, 我们可以将领域事件总线与进程内事件总线、集成事件总线注册代码简写为:
1
2
3
4
5
6
7
8
9
10builder.Services
.AddDomainEventBus(options =>
{
options.UseIntegrationEventBus(integrationEventBus =>
integrationEventBus
.UseDapr()
.UseEventLog<CatalogDbContext>())
.UseEventBus()
.UseRepository<CatalogDbContext>();
});中间件
进程内事件支持
AOP
, 提供与ASP.NET Core
类似的中间件
的功能, 例如: 记录所有事件的日志- 自定义日志中间件
文件夹
Infrastructure/Middleware
新建日志中间件LoggingEventMiddleware
, 并继承EventMiddleware<TEvent>
1
2
3
4
5
6
7
8
9
10
11
12public class LoggingEventMiddleware<TEvent> : EventMiddleware<TEvent>
where TEvent : IEvent
{
private readonly ILogger<LoggingEventMiddleware<TEvent>> _logger;
public LoggingEventMiddleware(ILogger<LoggingEventMiddleware<TEvent>> logger) =>_logger = logger;
public override async Task HandleAsync(TEvent @event, EventHandlerDelegate next)
{
_logger.LogInformation("----- Handling command {CommandName} ({@Command})", @event.GetType().GetGenericTypeName(), @event);
await next();
}
}修改注册进程内事件代码, 指定需要执行的中间件, 修改
Program
1
2
3
4
5
6
7
8
9
10builder.Services
.AddDomainEventBus(options =>
{
options.UseIntegrationEventBus(integrationEventBus =>
integrationEventBus
.UseDapr()
.UseEventLog<CatalogDbContext>())
.UseEventBus(eventBusBuilder => eventBusBuilder.UseMiddleware(typeof(LoggingEventMiddleware<>))) //指定需要执行的中间件
.UseRepository<CatalogDbContext>();
});进程内事件总线的中间件是先进先执行
除此之外, 进程内事件还支持[Handler编排]、[Saga]等, 查看详细文档
- 验证中间件
在
Masa.Contrib.Dispatcher.Events.FluentValidation
中我们提供了基于FluentValidation
的验证中间件, 它可以帮助我们在发送进程内事件后自动调用验证, 协助我们完成对参数的校验安装
Masa.Contrib.Dispatcher.Events.FluentValidation
、FluentValidation.AspNetCore
1
2dotnet add package Masa.Contrib.Dispatcher.Events.FluentValidation --prerelease
dotnet add package FluentValidation.AspNetCore指定进程内事件使用
FluentValidation
的中间件1
2
3
4
5
6
7
8
9
10
11
12builder.Services
.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()) //添加指定程序集下的`FluentValidation`验证器
.AddDomainEventBus(options =>
{
options.UseIntegrationEventBus(integrationEventBus =>
integrationEventBus
.UseDapr()
.UseEventLog<CatalogDbContext>())
.UseEventBus(eventBusBuilder => eventBusBuilder.UseMiddleware(new[] { typeof(ValidatorMiddleware<>), typeof(LoggingEventMiddleware<>) })) //使用验证中间件、日志中间件
.UseUoW<CatalogDbContext>() //使用工作单元, 确保原子性
.UseRepository<CatalogDbContext>();
});
基于FluentValidation的验证部分代码将在下面会讲到
Application 应用服务层
我们在应用服务层, 存放事件以及事件处理程序, 使用CQRS
模式我们将事件分为命令端 (Command
)、查询端 (Query
)
前提3
选中规约层 Masa.EShop.Contracts.Catalog
, 新建文件夹 Request
,并安装 Masa.BuildingBlocks.ReadWriteSplitting.Cqrs
选中领域层 Masa.EShop.Service.Catalog
,在项目根目录下新建文件夹结构如下:
- Application
- Catalogs
- Commands
- Queries
- Catalogs
1 | dotnet add package Masa.ReadWriteSplitting.Cqrs --prerelease |
命令端(Command)
选中领域层
Masa.EShop.Service.Catalog
, 在Commands
文件夹中新建CatalogItemCommand
类并继承Command
1
2
3
4
5
6
7
8
9
10
11
12
13
14public record CatalogItemCommand : Command
{
public string Name { get; set; } = null!;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public string PictureFileName { get; set; } = string.Empty;
public Guid CatalogBrandId { get; set; }
public int CatalogTypeId { get; set; }
}在
Commands
文件夹中新建CatalogItemCommandValidator
类并继承AbstractValidator<CatalogItemCommand>
我们建议使用FluentValidation提供的验证功能, 为每个 Command
定义对应的验证类, 排除那些参数不符合规定的请求进入 Handler
, 如果不需要使用它, 可跳过此步骤
自定义验证提供了很多验证方法, 比如NotNull
、Length
等, 更多使用技巧查看文档
1 | public class CatalogItemCommandValidator : AbstractValidator<CatalogItemCommand> |
除此之外, 我们还扩展了其它验证方法, 例如:
中文验证
、手机号验证
、身份证验证
等, 查看文档
查询端(Query)
选中规约层
Masa.EShop.Contracts.Catalog
, 在文件夹Request
新建类ItemsQueryBase
, 并继承Query
1
2
3
4
5
6public abstract record ItemsQueryBase<TResult> : Query<TResult>
{
public virtual int Page { get; set; } = 1;
public virtual int PageSize { get; set; } = 20;
}选中领域层
Masa.EShop.Service.Catalog
, 在Queries
文件夹中新建CatalogItemQuery
类并继承ItemsQueryBase
1 | public record CatalogItemQuery: ItemsQueryBase<PaginatedListBase<CatalogListItemDto>> |
验证类不是必须的, 根据业务情况选择性创建即可, 并没有强制性要求每个事件都必须有对应一个的事件验证类, 通常情况下查询端可以忽略参数校验
处理程序(Handler)
选中规约层 Masa.EShop.Contracts.Catalog
,新建文件夹 Dto
, 并新建类 CatalogListItemDto
1 | public class CatalogListItemDto |
选中领域层 Masa.EShop.Service.Catalog
,在 Infrastructure 文件夹新建类 GlobalMappingConfig
1 | using Mapster; |
选中领域层 Masa.EShop.Service.Catalog
, 在 Program.cs
文件里注册 GlobalMappingConfig
1 | GlobalMappingConfig.Mapping();//指定自定义映射 |
选中领域层 Masa.EShop.Service.Catalog
, 在 Catalogs
文件夹中新建 CatalogItemHandler
类,用于存放商品事件的处理程序
1 | using System.Linq.Expressions; |
多级缓存(MultilevelCache)
随着业务的增长, 访问系统的用户越来越多, 直接读取数据库的性能也变得越来越差, IO读取出现瓶颈, 这个时候我们可以有两种选择:
使用IO读写更快的磁盘, 比如: 使用固态磁盘代替读写速度差一点的机械磁盘
- 优点: 无需更改代码
- 缺点: 读写速度更高的磁盘意味着更大的成本压力, 且提升是有限的
使用缓存技术代替直接读取数据库
- 优点: 服务器硬件成本未上涨, 但可以带来十倍的性能提升
- 缺点: 针对读大于写的场景更为实用, 不可用于复杂查询
而多级缓存是由分布式缓存与内存缓存的组合而成, 它可以给我们提供比分布式缓存更强的读取能力, 下面我们将使用多级缓存技术, 用于提升获取 商品
详情的速度
MultilevelCache前提
选中领域层 Masa.EShop.Service.Catalog
1 | dotnet add package Masa.Contrib.Caching.MultilevelCache --prerelease |
MultilevelCache 使用
配置分布式
Redis
缓存配置信息, 修改appsettings.json
1
2
3
4
5
6
7
8
9
10
11{
"RedisConfig": {
"Servers": [
{
"Host": "localhost",
"Port": 6379
}
],
"DefaultDatabase": 0
}
}配置多级缓存中内存缓存的配置信息, 修改
appsettings.json
1
2
3
4
5
6
7
8{
"MultilevelCache": {
"CacheEntryOptions": {
"AbsoluteExpirationRelativeToNow": "72:00:00", //绝对过期时间(从当前时间算起)
"SlidingExpiration": "00:05:00" //滑动到期时间(从当前时间开始)
}
}
}注册缓存, 修改
Program.cs
1
2
3
4builder.Services.AddMultilevelCache(distributedCacheOptions =>
{
distributedCacheOptions.UseStackExchangeRedisCache();
});重写
FindAsync
, 优先从缓存中获取数据, 缓存不存在时读取数据库1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36public class CatalogItemRepository : Repository<CatalogDbContext, CatalogItem, Guid>, ICatalogItemRepository
{
/// <summary>
/// 使用多级缓存
/// </summary>
private readonly IMultilevelCacheClient _multilevelCacheClient;
public CatalogItemRepository(CatalogDbContext context, IUnitOfWork unitOfWork, IMultilevelCacheClient multilevelCacheClient) : base(context, unitOfWork)
{
_multilevelCacheClient = multilevelCacheClient;
}
public override async Task<CatalogItem?> FindAsync(Guid id, CancellationToken cancellationToken = default)
{
TimeSpan? timeSpan = null;
var catalogInfo = await _multilevelCacheClient.GetOrSetAsync(id.ToString(), () =>
{
//仅当内存缓存、Redis缓存都不存在时执行, 当db不存在时此数据将在5秒内被再次访问时将直接返回`null`, 如果db存在则写入`redis`, 写入内存缓存 (并设置滑动过期: 5分钟, 绝对过期时间: 3小时)
var info = Context.Set<CatalogItem>()
.Include(catalogItem => catalogItem.CatalogType)
.Include(catalogItem => catalogItem.CatalogBrand)
.AsSplitQuery()
.FirstOrDefaultAsync(catalogItem => catalogItem.Id == id, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult();
if (info != null)
return new CacheEntry<CatalogItem>(info, TimeSpan.FromDays(3))
{
SlidingExpiration = TimeSpan.FromMinutes(5)
};
timeSpan = TimeSpan.FromSeconds(5);
return new CacheEntry<CatalogItem>(info);
}, timeSpan == null ? null : new CacheEntryOptions(timeSpan));
return catalogInfo;
}
}
多级缓存与分布式缓存相比, 它有更高的性能, 对Redis集群的压力更小, 但当缓存更新时, 多级缓存会有1-2秒左右的刷新延迟, 详细可查看文档
全局异常和国际化(Exception & I18n)
我们建议在项目中使用全局异常处理, 它对外提供了统一的响应信息, 这将使得我们的项目体验更好
Exception前提
选中领域层 Masa.EShop.Service.Catalog
1 | dotnet add package Masa.Contrib.Exceptions --prerelease |
全局异常
使用全局异常处理, 修改Program.cs
1 | app.UseMasaExceptionHandler(); |
针对未处理的异常, 将返回Internal service error
, 自定义异常处理可参考文档
框架提供了友好异常、参数校验异常等, 我们可以通过抛出友好异常来中断请求, 并输出友好的错误提示信息, 还支持与多语言配合输出本地化的错误信息
Source Code Download
see it on github