现代 CQRS 和在 @ .NET@NET @:_ 正确操作中的事件间隔 (中文 (Chinese Simplified))

现代 CQRS 和在 @ .NET@NET @:_ 正确操作中的事件间隔

Thursday, 13 November 2025

//

14 minute read

注注注注@: 这是一篇我忘记发表#.的老文章,

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; }
}

模式@: @%

  1. 业务方法验证和返回活动
  2. 应用方法更新内部状态
  3. 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);
    }
}

流 :

  1. 通过重放事件来加载总和@ ( @ or create new)\
  2. 调用商业方法 @(validates 和返回事件@)
  3. 将事件附加到溪流
  4. 保存更改

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

继续阅读

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.