现代 CQRS 和在 @ .NET@NET @:_ 正确操作中的事件间隔
注注注注@: 这是一篇我忘记发表#.的老文章,
CQRS 和事件 Sourcing {-} 经常被一起提及但常常被误解的两种模式}.在本篇文章中, <I,}IMS K3}%将展示您如何使用现代@.NET 工具来正确执行它们_: Marten 用于活动源源<,> Dapper 以获取最佳查询}MPK7}和MidaltR 以保存一切组织好.
并解释为何试图将事件与手动缓存失效混为一谈,
一. 导言导言 导言 导言 导言
CQRS {(Command Query Responsibility Segionation_)}和事件抛售是两种不同的模式,它们特别有效
- CQRS 质化器 - 将阅读模型与写作模型分隔开来
- 事件时间间隔 “-” 存储每个状态变化, 作为不可改变的事件序列
当用像Marten,这样的工具正确完成时, 你会得到\ :
- 每项变动的全部审计线索
- 在任何时候重建国家的能力
- 自动读取模型预测
- 自然适合域名@-}驱动设计
本条的重点是这样做 适当.如果你想要快速的- 和-dirty缓存-}基于速效的方法 <I,}#I'}在结尾部分可以简短地覆盖到它 但是它不是CQRSMS K8, 它并没有给您 事件抛售的惠益{'>_
什么是CQRS ?
CQRS意指使用不同的读写模型来读写数据 :
// Write Model - Commands that change state
public record CreateBlogPostCommand(string Title, string Content, string AuthorId);
// Read Model - DTOs optimised for display
public class BlogPostListItemDto
{
public Guid Id { get; set; }
public string Title { get; set; }
public string AuthorName { get; set; }
public DateTime PublishedDate { get; set; }
public int CommentCount { get; set; }
}
写入的侧面侧重于商业逻辑和验证@. @ 读的侧边已经解除了正常状态, 为显示“ .} 简单化” ,{ 但真正的 CQRS 意味着它们='} 是您应用程序中完全独立的路径 *.}
什么是事件苏尔辛 ?
您不是保存当前状态@ ,, 而是存储导致该状态的事件 @ MS K1
// Traditional: Store current state
public class BlogPost
{
public Guid Id { get; set; }
public string Title { get; set; } // Current title
public bool IsPublished { get; set; } // Current status
}
// Event Sourcing: Store the events
public record BlogPostCreated(Guid Id, string Title, string Content, DateTime CreatedAt);
public record BlogPostTitleChanged(Guid Id, string OldTitle, string NewTitle, DateTime ChangedAt);
public record BlogPostPublished(Guid Id, DateTime PublishedAt);
当前状态来自重播事件\ . 这将为您提供在您的系统中发生的每件事情的完整历史@' @
为什么使用 CQRS @ ? 来使用事件间隔@% @
完整的审计拖车“: ” 每个变化都被记录下来,“. ”对金融系统来说是完美的 ” “MS K2 $Helpchurgation ” 或“任何您需要证明所发生的事情和时间的任何地方 ”
时间查询@:_ @ "}这个博客文章看起来像上星期二的什么?
调试“:”通过重播造成它们的事件的精确序列来复制错误“.”
商业情报: 根据历史数据建立新报告 不运行迁移}MS K1 事件已经存在
CQRS 适合自然@:_事件自然而然地分开,
当你应该做 ' @%t
简单 CRUD:如果你只是存储和检索数据...
缺乏经验的小型团队学习曲线是陡峭的.
无审计要求: 如果你只关心当前状态...
大型二进制数据@:_事件对图像效果不佳@,}视频#,}文件=.}
@ .@NET @:_Marten中的现代事件
在 @2025中的 .+NET 中事件抛出的最佳工具是@ info: tooltip 马尔坦由Jeremy Miller , 积极维护,
为什么是Marten ??
- 建在PostgreSQL @(#你已经知道它@)}
- 从事件到阅读模型的自动预测
- 用于投影处理的 Async 守护程序
- 丰富查询能力
- 制作@-#ready and fights @-_测试
设置Marten
安装软件包@: @% 1
dotnet add package Marten
dotnet add package Marten.AspNetCore
配置在 Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(options =>
{
options.Connection(builder.Configuration.GetConnectionString("Marten")!);
// Register event types
options.Events.AddEventType<BlogPostCreated>();
options.Events.AddEventType<BlogPostPublished>();
options.Events.AddEventType<BlogPostTitleChanged>();
options.Events.AddEventType<CommentAdded>();
// Configure async projections
options.Projections.Add<BlogPostProjection>(ProjectionLifecycle.Async);
});
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
var app = builder.Build();
建筑结构
这里的'}一切都如何相配 : *
flowchart TB
subgraph Client["Client Application"]
UI[User Interface]
end
subgraph Commands["Command Side (Writes)"]
CMD[Commands] --> CMDH[Command Handlers]
CMDH --> MARTEN[Marten Session]
MARTEN --> EVENTS[(Event Store)]
end
subgraph Background["Async Processing"]
EVENTS -.->|Event Stream| DAEMON[Marten Async Daemon]
DAEMON --> PROJ[Projections]
PROJ --> READDB[(Read Models)]
end
subgraph Queries["Query Side (Reads)"]
QRY[Queries] --> QRYH[Query Handlers with Dapper]
QRYH --> READDB
end
UI -->|Commands| CMD
UI -->|Queries| QRY
classDef commandStyle fill:none,stroke:#e63946,stroke-width:3px
classDef queryStyle fill:none,stroke:#457b9d,stroke-width:3px
classDef dataStyle fill:none,stroke:#2a9d8f,stroke-width:3px
class CMD,CMDH,MARTEN commandStyle
class QRY,QRYH queryStyle
class EVENTS,READDB dataStyle
关键点 @:
- 命令 事件存储的附加事件
- Async 守护程序 读取模型
- 查询 读取自非正常阅读模型
- 没有需要手动缓存失效@ ! @
界定事件
事件是无法改变的记录 描述已经发生的事情:
// Always past tense - these things have happened
public record BlogPostCreated(
Guid BlogPostId,
string Title,
string Content,
string AuthorId,
DateTime CreatedAt
);
public record BlogPostPublished(
Guid BlogPostId,
DateTime PublishedAt
);
public record BlogPostTitleChanged(
Guid BlogPostId,
string OldTitle,
string NewTitle,
DateTime ChangedAt
);
public record CommentAdded(
Guid BlogPostId,
Guid CommentId,
string Author,
string Content,
DateTime CreatedAt
);
事件应该是:
- 不可变 @- 曾经写过@, 从未改变
- 过去时态 @-他们描述发生了什么事。
- 商业意义丰富 - MS K1 Blog Polost Title changed" 不是" property updated}"
创建总计
它们验证业务规则, 并产生事件/:_
public class BlogPost
{
// Marten requires an Id property
public Guid Id { get; set; }
// Current state (private setters)
public string Title { get; private set; } = string.Empty;
public string Content { get; private set; } = string.Empty;
public string AuthorId { get; private set; } = string.Empty;
public bool IsPublished { get; private set; }
public DateTime? PublishedDate { get; private set; }
private readonly List<Comment> _comments = new();
public IReadOnlyList<Comment> Comments => _comments.AsReadOnly();
// Apply methods - called by Marten when replaying events
public void Apply(BlogPostCreated e)
{
Id = e.BlogPostId;
Title = e.Title;
Content = e.Content;
AuthorId = e.AuthorId;
}
public void Apply(BlogPostPublished e)
{
IsPublished = true;
PublishedDate = e.PublishedAt;
}
public void Apply(BlogPostTitleChanged e)
{
Title = e.NewTitle;
}
public void Apply(CommentAdded e)
{
_comments.Add(new Comment
{
Id = e.CommentId,
Author = e.Author,
Content = e.Content,
CreatedAt = e.CreatedAt
});
}
// Business logic methods that produce events
public static BlogPostCreated Create(string title, string content, string authorId)
{
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("Title is required");
return new BlogPostCreated(
Guid.NewGuid(),
title,
content,
authorId,
DateTime.UtcNow
);
}
public BlogPostPublished Publish()
{
if (IsPublished)
throw new InvalidOperationException("Post is already published");
return new BlogPostPublished(Id, DateTime.UtcNow);
}
public BlogPostTitleChanged ChangeTitle(string newTitle)
{
if (string.IsNullOrWhiteSpace(newTitle))
throw new ArgumentException("Title cannot be empty");
if (newTitle == Title)
throw new InvalidOperationException("New title is the same as current title");
return new BlogPostTitleChanged(Id, Title, newTitle, DateTime.UtcNow);
}
}
public class Comment
{
public Guid Id { get; set; }
public string Author { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
模式@: @%
- 业务方法验证和返回活动
- 应用方法更新内部状态
- Marten 处理事件持续和重放
命令处理器 @ ( @ write side@ MS K1
命令通过将事件附加到串流@:
// Define commands
public record CreateBlogPostCommand(
string Title,
string Content,
string AuthorId
) : IRequest<Guid>;
public record PublishBlogPostCommand(Guid BlogPostId) : IRequest;
public record ChangeBlogPostTitleCommand(
Guid BlogPostId,
string NewTitle
) : IRequest;
// Handler for creating a blog post
public class CreateBlogPostHandler : IRequestHandler<CreateBlogPostCommand, Guid>
{
private readonly IDocumentSession _session;
public CreateBlogPostHandler(IDocumentSession session)
{
_session = session;
}
public async Task<Guid> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
{
// Create the event
var created = BlogPost.Create(
request.Title,
request.Content,
request.AuthorId
);
// Start a new event stream
_session.Events.StartStream<BlogPost>(created.BlogPostId, created);
await _session.SaveChangesAsync(cancellationToken);
return created.BlogPostId;
}
}
// Handler for publishing
public class PublishBlogPostHandler : IRequestHandler<PublishBlogPostCommand>
{
private readonly IDocumentSession _session;
public PublishBlogPostHandler(IDocumentSession session)
{
_session = session;
}
public async Task Handle(PublishBlogPostCommand request, CancellationToken cancellationToken)
{
// Load the aggregate by replaying its events
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(
request.BlogPostId,
token: cancellationToken
);
if (blogPost == null)
throw new InvalidOperationException($"Blog post {request.BlogPostId} not found");
// Business logic produces new event
var published = blogPost.Publish();
// Append event to the stream
_session.Events.Append(request.BlogPostId, published);
await _session.SaveChangesAsync(cancellationToken);
}
}
// Handler for changing title
public class ChangeBlogPostTitleHandler : IRequestHandler<ChangeBlogPostTitleCommand>
{
private readonly IDocumentSession _session;
public ChangeBlogPostTitleHandler(IDocumentSession session)
{
_session = session;
}
public async Task Handle(ChangeBlogPostTitleCommand request, CancellationToken cancellationToken)
{
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(
request.BlogPostId,
token: cancellationToken
);
if (blogPost == null)
throw new InvalidOperationException($"Blog post {request.BlogPostId} not found");
var titleChanged = blogPost.ChangeTitle(request.NewTitle);
_session.Events.Append(request.BlogPostId, titleChanged);
await _session.SaveChangesAsync(cancellationToken);
}
}
流 :
- 通过重放事件来加载总和@ ( @ or create new)\
- 调用商业方法 @(validates 和返回事件@)
- 将事件附加到溪流
- 保存更改
Marten 处理其余的 - 存储事件@, 触发预测 @, 等#.}
阅读模型和预测
预测将事件变成变异的阅读模型@: @%
// Read model - optimised for queries
public class BlogPostReadModel
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string AuthorId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public bool IsPublished { get; set; }
public int CommentCount { get; set; }
}
// Projection - tells Marten how to build read models from events
public class BlogPostProjection : MultiStreamProjection<BlogPostReadModel, Guid>
{
public BlogPostProjection()
{
// Identity tells Marten which stream each event belongs to
Identity<BlogPostCreated>(x => x.BlogPostId);
Identity<BlogPostPublished>(x => x.BlogPostId);
Identity<BlogPostTitleChanged>(x => x.BlogPostId);
Identity<CommentAdded>(x => x.BlogPostId);
}
// Apply methods - Marten calls these to update read models
public void Apply(BlogPostReadModel view, BlogPostCreated e)
{
view.Id = e.BlogPostId;
view.Title = e.Title;
view.Content = e.Content;
view.AuthorId = e.AuthorId;
view.CreatedAt = e.CreatedAt;
view.IsPublished = false;
}
public void Apply(BlogPostReadModel view, BlogPostPublished e)
{
view.IsPublished = true;
view.PublishedAt = e.PublishedAt;
}
public void Apply(BlogPostReadModel view, BlogPostTitleChanged e)
{
view.Title = e.NewTitle;
}
public void Apply(BlogPostReadModel view, CommentAdded e)
{
view.CommentCount++;
}
}
Marten's async 守护进程在背景中处理事件, 并不断读取模型更新@ . 您不做 @ MS K2t 写入任何缓存失效代码 {-} it's annow *.}
与 Dapper 的查询边
现在我们使用 Dapper 来查询读取模型以获取最大性能@: @%
// Define queries
public record GetRecentBlogPostsQuery(
int Count,
bool PublishedOnly
) : IRequest<List<BlogPostListItemDto>>;
public record GetBlogPostByIdQuery(Guid Id) : IRequest<BlogPostDetailDto?>;
// DTOs for display
public class BlogPostListItemDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public int CommentCount { get; set; }
public bool IsPublished { get; set; }
}
public class BlogPostDetailDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string AuthorId { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public bool IsPublished { get; set; }
public List<CommentDto> Comments { get; set; } = new();
}
public class CommentDto
{
public Guid Id { get; set; }
public string Author { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// Query handlers
public class GetRecentBlogPostsHandler : IRequestHandler<GetRecentBlogPostsQuery, List<BlogPostListItemDto>>
{
private readonly string _connectionString;
public GetRecentBlogPostsHandler(IConfiguration config)
{
_connectionString = config.GetConnectionString("Marten")!;
}
public async Task<List<BlogPostListItemDto>> Handle(
GetRecentBlogPostsQuery request,
CancellationToken cancellationToken)
{
await using var connection = new NpgsqlConnection(_connectionString);
// Query the Marten-generated read model table
const string sql = @"
SELECT
bp.id AS Id,
bp.title AS Title,
u.name AS AuthorName,
bp.created_at AS CreatedAt,
bp.published_at AS PublishedAt,
bp.comment_count AS CommentCount,
bp.is_published AS IsPublished
FROM blog_post_read_models bp
LEFT JOIN users u ON bp.author_id = u.id
WHERE (@PublishedOnly = false OR bp.is_published = true)
ORDER BY
CASE WHEN bp.is_published THEN bp.published_at
ELSE bp.created_at
END DESC
LIMIT @Count";
var results = await connection.QueryAsync<BlogPostListItemDto>(
sql,
new
{
PublishedOnly = request.PublishedOnly,
Count = request.Count
});
return results.ToList();
}
}
public class GetBlogPostByIdHandler : IRequestHandler<GetBlogPostByIdQuery, BlogPostDetailDto?>
{
private readonly string _connectionString;
public GetBlogPostByIdHandler(IConfiguration config)
{
_connectionString = config.GetConnectionString("Marten")!;
}
public async Task<BlogPostDetailDto?> Handle(
GetBlogPostByIdQuery request,
CancellationToken cancellationToken)
{
await using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT
bp.id AS Id,
bp.title AS Title,
bp.content AS Content,
bp.author_id AS AuthorId,
u.name AS AuthorName,
bp.created_at AS CreatedAt,
bp.published_at AS PublishedAt,
bp.is_published AS IsPublished
FROM blog_post_read_models bp
LEFT JOIN users u ON bp.author_id = u.id
WHERE bp.id = @Id";
var post = await connection.QuerySingleOrDefaultAsync<BlogPostDetailDto>(
sql,
new { request.Id });
if (post == null)
return null;
// Get comments from event stream if needed
// Or maintain a separate comment read model
return post;
}
}
没有 caching code.无无效逻辑.Marten 保持读取模型同步自动 .
主计长
使用 MeditR, 控制器的操作器非常简单@ : @
[ApiController]
[Route("api/[controller]")]
public class BlogPostsController : ControllerBase
{
private readonly IMediator _mediator;
public BlogPostsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult<List<BlogPostListItemDto>>> GetRecent(
[FromQuery] int count = 10,
[FromQuery] bool publishedOnly = true)
{
var query = new GetRecentBlogPostsQuery(count, publishedOnly);
var results = await _mediator.Send(query);
return Ok(results);
}
[HttpGet("{id}")]
public async Task<ActionResult<BlogPostDetailDto>> GetById(Guid id)
{
var query = new GetBlogPostByIdQuery(id);
var result = await _mediator.Send(query);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<Guid>> Create([FromBody] CreateBlogPostCommand command)
{
var postId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id = postId }, postId);
}
[HttpPost("{id}/publish")]
public async Task<ActionResult> Publish(Guid id)
{
await _mediator.Send(new PublishBlogPostCommand(id));
return NoContent();
}
[HttpPut("{id}/title")]
public async Task<ActionResult> ChangeTitle(
Guid id,
[FromBody] ChangeBlogPostTitleCommand command)
{
if (id != command.BlogPostId)
return BadRequest();
await _mediator.Send(command);
return NoContent();
}
}
完整的流动
这里:'}所有这一切是如何一起工作的 @:
sequenceDiagram
participant Client
participant Controller
participant MediatR
participant CommandHandler
participant Marten
participant EventStore
participant AsyncDaemon
participant ReadDB
participant QueryHandler
Note over Client,ReadDB: Write Operation
Client->>Controller: POST /api/blogposts
Controller->>MediatR: Send CreateBlogPostCommand
MediatR->>CommandHandler: Handle command
CommandHandler->>CommandHandler: Validate & create event
CommandHandler->>Marten: StartStream(event)
Marten->>EventStore: Append event
EventStore-->>Marten: Success
Marten-->>CommandHandler: Success
CommandHandler-->>Controller: Return ID
Controller-->>Client: 201 Created
Note over AsyncDaemon,ReadDB: Background Processing
EventStore->>AsyncDaemon: New event available
AsyncDaemon->>AsyncDaemon: Apply projection
AsyncDaemon->>ReadDB: Update read model
ReadDB-->>AsyncDaemon: Updated
Note over Client,ReadDB: Read Operation
Client->>Controller: GET /api/blogposts
Controller->>MediatR: Send Query
MediatR->>QueryHandler: Handle query
QueryHandler->>ReadDB: SELECT with Dapper
ReadDB-->>QueryHandler: Return data
QueryHandler-->>Controller: Return DTOs
Controller-->>Client: 200 OK
最终一致性
有一件事可以理解: “: ” 与 Async professionation 和“,” 在写一个事件和正在更新阅读模型之间稍有延迟 ” @. 通常为毫秒@,,但是它“MS K5 ”
如果具体操作需要立即保持一致,请使用内线投影@:
builder.Services.AddMarten(options =>
{
// This projection runs synchronously
options.Projections.Add<CriticalDataProjection>(ProjectionLifecycle.Inline);
// This projection runs async
options.Projections.Add<BlogPostProjection>(ProjectionLifecycle.Async);
});
或者直接查询事件流以读取@ - @ after MS K1} write situations=:
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(id);
部分 @2: 半端@- Accassed approach {(} 缓存无效=)
你读到过关于适当事件搜捕的讯息, 并且你认为“"”是很多工作。 “".”很公平吗?"."这里"非正式 CQRS="}方法 多数团队实际上使用“MS K10”
设置
- 命令用 EF 或 Dapper @) 写入数据库 *\ (
- 从 IMemoryCache 或 I分布缓存中查询
- 写入后命令使相关缓存条目无效
- 没有事件源码@ , @ no project=% MS K1}没有 Async 守护进程
它不是真实的 CQRS.}你没有得到审计线索 , 时间查询 , 或自动投影*.}但是你的性能不那么复杂
快速示例
// Command handler
public class CreateBlogPostHandler : IRequestHandler<CreateBlogPostCommand, int>
{
private readonly ApplicationDbContext _context;
private readonly IMemoryCache _cache;
public async Task<int> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
{
var blogPost = new BlogPost
{
Title = request.Title,
Content = request.Content,
AuthorId = request.AuthorId,
PublishedDate = DateTime.UtcNow
};
_context.BlogPosts.Add(blogPost);
await _context.SaveChangesAsync(cancellationToken);
// Manual cache invalidation - this is the tedious bit
_cache.Remove("recent-posts");
_cache.Remove($"author-posts-{request.AuthorId}");
_cache.Remove($"post-{blogPost.Id}");
return blogPost.Id;
}
}
// Query handler
public class GetRecentPostsHandler : IRequestHandler<GetRecentPostsQuery, List<BlogPostDto>>
{
private readonly string _connectionString;
private readonly IMemoryCache _cache;
public async Task<List<BlogPostDto>> Handle(GetRecentPostsQuery request, CancellationToken cancellationToken)
{
var cacheKey = "recent-posts";
if (_cache.TryGetValue<List<BlogPostDto>>(cacheKey, out var cached))
return cached!;
// Cache miss - query with Dapper
using var connection = new NpgsqlConnection(_connectionString);
var posts = (await connection.QueryAsync<BlogPostDto>(
"SELECT id, title, author_name, published_date FROM blog_posts ORDER BY published_date DESC LIMIT 10"
)).ToList();
_cache.Set(cacheKey, posts, TimeSpan.FromMinutes(5));
return posts;
}
}
当此工程完成时
- 简单应用程序,没有复杂的审计要求
- 您需要性能, 但可以证明事件复杂程度 。
- 不想学习“事件追逐”的小团队
- 几秒钟的腐烂是可以接受的
问题
缓存无效是困难的: 错过一个缓存密钥, 您则提供 stale 数据@ MS K1 每个命令都需要知道哪些缓存可以失效 @ .
无审计线索: 您只有当前状态@. can @'}无法证明发生了什么或何时发生吗?
无时间查询{\fn黑体\fs22\bord1\shad0\3aHBE\4aH00\fscx67\fscy66\2cHFFFFFF\3cH808080}这个系统昨天长什么样?
缩缩: 使用 IMemoryCache @, 每个服务器都有自己的缓存@. 服务器 A,}服务器 B 的更新仍然有粘贴数据I Dispreditedcache 解决了这个问题,但添加 Redisi=.}
紧连结: 命令与缓存密钥连接@. 更改一个查詢, 可能中断命令 @.
它的工作原理是:,,但你为失去能力而简化交易 =. 有时,'对了贸易 =-Off}. 经常是'T.
部分 @3: 最坏的想法 @ -事件 @ I+ 手写遗漏
现在,真正可怕的想法":"使用事件递解 并尝试手动无效的缓存@. {
为什么人们尝试这个
他们想要事件 Sourcing'}审计线索和时间查询{,} 但他们担心最终的一致性_.}所以他们认为 <"IMS K5>只会添加缓存失效功能,让阅读更快、更一致!"}♪
千万不要这样
为何如此可怕
你辜负了目的添加手动缓存无效表示您正在绕过此系统@. @%
双重复杂复杂性: 您现在有两个系统保持读数模型同步 {- Marten'}预测和您的手动缓存失效@.}它们将会冲突.}
不一致状态: Marten's async damon 更新数据库@. 您的缓存失效程序立即运行 @.}他们%'re 退出同步=.}这是正确的?{
损失的养恤金@ :_ 事件Sourcing'}整点是,预测来自事件\ MS K2} 手动缓存失效符
调试恶梦: 是因为投影已停止运行, 或者因为您忘了取消一个缓存密钥@? 或因为缓存无效, 但投影已经失效了 @' t 尚未运行@MS K5}祝你好运 @MS K6 *
正确方法
如果您需要事件 Sourcing @ : @
- 使用 Marten's 投影@ (async 或内嵌 @)}
- 直接查询阅读模型
- 接受最终一致性@(it'}通常都罚款 @)
- 如果您真的需要即时一致性, 请使用内线预测
如果您能接受最终的一致@ : @
- Donó't 使用事件递解
- 使用基于 & 2\ part 的缓存@ MS K0 @ based 方针
- 或者只是使用传统的 CRUD 与 EF 核心
Don' t try to do both.你MS K2结尾的复杂事件Surcing加上手动缓存失效的脆弱性@.#
" 不做做什么 " 实例
// Don't do this!
public class PublishBlogPostHandler : IRequestHandler<PublishBlogPostCommand>
{
private readonly IDocumentSession _session;
private readonly IMemoryCache _cache; // ← BAD
public async Task Handle(PublishBlogPostCommand request, CancellationToken cancellationToken)
{
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(request.BlogPostId);
var published = blogPost.Publish();
_session.Events.Append(request.BlogPostId, published);
await _session.SaveChangesAsync(cancellationToken);
// Manually invalidating cache while using Event Sourcing ← TERRIBLE IDEA
_cache.Remove($"post-{request.BlogPostId}");
_cache.Remove("recent-posts");
// Now you have:
// 1. Event in event store
// 2. Cache invalidated
// 3. But projection hasn't run yet!
// Queries will hit database before projection completes = stale data
}
}
只要使用为此设计的 Marten ' 的 Marten @ ' @s 投影@ MS K1 @ they @ MSk2_re
结论结论
三种方法: ,+3个使用案例
正常的 CQRS 与事件缓冲 @ ( @ 马尔滕@ MS K1 @ *
使用时@ : @
- 您需要完整的审计线索
- 时间询问很有价值
- 具有丰富商业逻辑的复杂领域
- 或受管制的制度。
-=YTET -伊甸园字幕组=- 翻译:
- 简单 CRUD 应用程序
- 团队缺乏事件保护经验
- 无审计要求
带有缓存的非正式的 CQRS
使用时@ : @
- 需要提高工作表现,
- 不需要审计线索
- 比事件抛落简单
- 实际一致性可接受
-=YTET -伊甸园字幕组=- 翻译:
- 需要审计线索
- 需要时间查询
- 遗漏废弃的复杂复杂性大于效益
自动缓存无效
使用时@ : @
- 永远不要.,只是不做' ~T#.
选择合适的工具用于使用 case @ . @ 事件与 Marten 的崩溃在您需要它时非常强大@ MS K1} Cache\ -} 以 CQRS 为主的 CQRS 在您忽略了 message its is a mistake+. 的情况下更简单一些
对于大多数应用程序来说 , 以传统的 CRUD BARBAR 我的EF核心系列 在这里当您遇到性能问题时, ). @ ,} 添加 changing.} 只有当您真正需要其特定能力的时候才能达到完整事件 surcing