# Modern CQRS och sekvensering av händelser i .NET: gör det på rätt sätt

<!--category-- ASP.NET, Architecture, CQRS, Event Sourcing, Marten, Performance -->
<datetime class="hidden">2025-11-13T12:00</datetime>

> **NOTA**: Det här är en gammal artikel som jag glömde att publicera . Här är den , glädjas av~ ! Jag uppdaterade den men det kan vara några problem som jag missade.

CQRS och Event Sourcing - två mönster som ofta nämns tillsammans men som ofta missförstås.

I' ska jag också visa er den "halfM SK2assed " kass, -baserade alternativen och förklara varför att mixa Event Sourcing med manuell cache-invalidering är en hemsk idé.

## Inleiding

CQRS (Kommandofrågans ansvarsskiktning Segregering ) och Event Sourcing är två olika mönster som fungerar exceptionellt bra tillsammans

- **CQRS** - Separerar dina läsmodeller från dina skrivmodeller
- **Event Sourcing** - Att spara varje statförändring som en förändringslös sekvens av händelser

När man gör det ordentligt med verktyg som Marten, får man

- Complete audit trail of every change
- Förmågor att bygga om tillstånd vid vilken tidpunkt som helst
- Automatiska projektioner av modeller
- Naturlig tillpassning med domain-driven design

Detta artikel fokuserar på att göra det **på rätt sätt**. Om du vill ha den snabbaM SK1 och - smutsiga cachen-baserade ansatsen, m.s.k.,, så tar jag den kort i slutet, men det är inte riktigt CQRS, ,, och det ger dig inga fördelar med Event Sourcing.

[TOC]

## Vad är CQRS?

I grunden betyder CQRS att använda olika modeller för att läsa och skriva data

```csharp
// 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; }
}
```

Den skrivna sidan fokuserar på affärslogik och validering. Den läsa sidan är denormaliserad och optimerad för att se ut

## Vad är händelsernas efterfrågan?

Istället för att lagra den nuvarande tillstånden, lagar du in händelserna som ledde till det tillståndet.

```csharp
// 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);
```

Den nuvarande tillstånden kommer från att spela upp händelser igen. Det ger dig en komplett historia av allt som någonsin hänt i ditt system

## Varför använda Event Sourcing med CQRS?

**komplett Audit Trail**: Varje förändring är registrerad . Perfekt för finansiella system

**Tydliga frågor**: " Hur såg den här bloggposten ut förra tiistaina? ♫ ?" ♫ blir trivial ♫

**Debugging**: Reproduera bugs genom att åter spela in den exakta sekvensen av händelser som orsakade dem

**Ekonomisk intelligens**: Att bygga nya rapporter från historiska data utan att göra migrationer

**Pass för naturlig CQRS**: Erfarenheter som är naturligt separerade skriver ♫ ( ♫ ansluter händelser ♫ ) ♫ från svar ♫ МSK3 ♫ sökprojektioner ♫

### När man ska göra det

**Enkel CRUD**: Om du

**En liten grupp utan erfarenhet**: Inlärningskurvan är hög

**Inga revīzijasbehov**: Om du bara bryr dig om det nuvarande tillståndet

**Stora binära data**: Erfarenheter fungerar dåligt med bilder

## Moderna händelser i .NET: Marten

Det bästa verktyget för att hitta händelser i .NET i ♫ 2025 ♫ är [Marten](https://martendb.io/). Det är en eventbutik byggd på PostgreSQL , som aktivt underhålls av Jeremy Miller

Varför Marten?

- Inbyggt på PostgreSQL ( du känner till det redan)
- Automatiska prognoser från händelser för att läsa modeller
- Async-daemon för projektionsprocessering
- Rika sökkrafter
- Tillverkning - redo och striden-testat

### Att sätta upp Marten

Installera packagerna:

```bash
dotnet add package Marten
dotnet add package Marten.AspNetCore
```

Konfigurera i `Program.cs`:

```csharp
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();
```

### Architekturen

Här är hur allt passar ihop.

```mermaid
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
```

Nyckelpunkter:

- **Kommandor** lägga till händelser i event-butiken
- **Async-daemon** bearbetar händelser och uppdaterar modeller
- **Frågor** läsa från denormaliserade läsande modellerna
- Ingen manuell cache-invalidering krävs!

## Definierade händelser

Erfarelser är oföränderliga rekord som beskriver saker som har hänt.

```csharp
// 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
);
```

Erfarenheter borde vara:

- **Oänderliga** - en gång skrivetM SK1 aldrig ändrats
- **Förflutna** - beskriver de vad som hände
- **Den rika affärsmässiga betydelsen** -  "BlogPostTitleChanged

## Att skapa agregater

Aggregater är skrivmodellen, de validerar affärsregler och producerar händelser.

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

Pattern:

1. affärsmetoder validerar och återskapar händelser
2. Applicera metoder för att uppdatera inre tillstånd
3. Marten hanterar händelsernas persistens och återuppspelning

## Kommandohandler (skriv sida)

Kommandon hanteras genom att lägga till händelser i strömmar:

```csharp
// 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);
    }
}
```

Flöden:

1. Lade agregat genom att spela om händelser ( eller skapa nyaM SK1
2. Fråga affärsmetoden (Validerar och återskapar händelsen)
3. Lägg till händelse i ström
4. spara ändringar

Marten hanterar resten - lagring av händelser , aktivering av förutsägelser

## Läs modeller och förutsägelser

Prognoser omvandlar händelser till denormaliserade läsade modeller:

```csharp
// 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 daemon bearbetar händelser i bakgrunden och håller uppdatet läsade modeller.

## Fråga sida med dapper

Nu frågar vi de läsade modellerna med hjälp av Dapper för maxpresterande

```csharp
// 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;
    }
}
```

Inget kaskande kode . Ingen invalideringslogik. Marten håller läsade modellerna i synkronisering automatiskt

## Controller

Med MediatR är kontrollerna vackert enkla

```csharp
[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();
    }
}
```

## Den kompletta strömmen

Här är hur allt fungerar tillsammans.

```mermaid
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
```

## Till slutlig konsistens

En sak att förstå: med asyncka förutsägelser, där ' är en liten fördröjning mellan att skriva en händelse och att läsas modellen som håller på att göras uppdateradM SK3 Vanligtvis milsekunder , men det

Om du behöver en direkt konsistens för en specifik opération, använder du inlineprojektioner.

```csharp
builder.Services.AddMarten(options =>
{
    // This projection runs synchronously
    options.Projections.Add<CriticalDataProjection>(ProjectionLifecycle.Inline);

    // This projection runs async
    options.Projections.Add<BlogPostProjection>(ProjectionLifecycle.Async);
});
```

Eller sök upp händelserna direkt för att läsa

```csharp
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(id);
```

## Del 2: Den halva | - | Avlägsna tillvägagångssätt |( | Maskinvalidering | МSK3

Okej, så ni har läst om korrekta Event Sourcing och ni tänker att ' är mycket arbete. ". är rättvist.. Här är ' den informella CQRS-metoden

### Konfiguration

- Kommandon skriver till database (usande EF eller Dapper)
- Frågor som lästs från IMemoryCache eller IDistributedCache
- Kommandon gör relevanta cache-Einträg ogültade efter att skriva
- Inget uppfinning av händelser , inga förutsägelser, inga async-daemoner

Det är inte sant med CQRS.

### Ett snabbt exempel

```csharp
// 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;
    }
}
```

### När det fungerar

- Enkela applikationer utan komplexa revīzijasbehov
- Du behöver prestation men kan inte rättfärdiga Event Sourcing komplexitet
- En liten grupp som inte vill lära sig Event Sourcing
- Enkelhet av några sekunder är acceptabel

### Problemet

**Cache-invalidering är svårt**: Missar en kasknyck och du serverar baklagda data . Varje kommando behöver veta vilka kasker man ska avskaffa

**Ingen revīzijasspår**: Du har bara det nuvarande tillståndet . Kan inte bevisa vad som hände eller när

**Inga tidiga frågor**"" Hur såg systemet ut igår? ""

**Skalering**: Med IMemoryCache

**Attight coupling**: Kommandor är kopplade till kasknycklar

Det fungerar.,, men du, ', är enkelhet i att handla om förlorade kapaciteter.., Ibland är det det rätta.

## Del 3: Den värsta idén - Erfarenhetssökningen | + | Hand Cache Invalidation

Nu till den verkligt hemska idén, att använda Event Sourcing, men också att försöka avfärda kaser manuell.

### Varför försöker folk detta?

De vill ha Event Sourcing, auditspår och temporella frågor, men de är oroliga för slutgiltiga jämlikhet. Så de tror att "" ", I, ' "" ska bara lägga till cache-validation för att göra läsningar snabbare och mer jämlika.

gör inte det här

### Varför är det hemskt?

**Du ' har misslyckats med målet**: Event Sourcing bygger redan upp läsande modeller genom förutsägelser

**Dubbel komplexitet**: Du har nu två system som håller läsade modellerna i synkronisering - Marten M SK2s prognoser Och din manuella cache-invalidering . De kommer att konfliktera

**Inkonsistent tillstånd**: Marten

**Förlust av förmåner**: Event Sourcing ' hela poängen är att förutsägelser kommer från händelser

**Debuggen av mardröm**: Är den föråldrade datan för att projektionerna inte fungerat ' inte fungerade ? Eller för att du glömde bort att avskaffa ett kassetts nyckel M SK3 Eller eftersom kaset avskadades men projektionen inte fungerad MSC4 inte ännu fungerade

### Den rätta vägen

Om du behöver Event Sourcing:

- Använd Martens prognoser
- Fråga de läsade modellerna direkt
- Acceptera den slutgiltiga konsistensen (it
- Använd inline-projektioner om du verkligen behöver direkt konsistens

Om du kan acceptera konsistens

- Don' inte använda Event Sourcing
- Använd cache-baserat angreppet från del 2
- Eller bara använda traditionell CRUD med EF Core

Att inte försöka göra båda två.

### Ett exempel på vad man inte ska göra

```csharp
// 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
    }
}
```

Bara använda Martens prognoser. De är designade för den här.

## Slutsatsen

Tre tillvägagångssätt

### Rätt CQRS med sekvensering av händelser (MartenM SK1

**Använd när:**

- Du behöver en komplett auditspårning
- Tydliga sökningar är värdefulla
- Kompleks domain med rik affärslogik
- Finansiell1 , healthcare 1 MSK eller reglerade system

**Använd inte när**

- En enkel CRUD-applikation
- Team saknar erfarenhet av tävling
- Inga revīzijasbehov

### Informellt CQRS med Caching

**Använd när:**

- Man behöver förbättra prestationen vid läsningar
- Revideringsspår inte nödvändiga
- Enklare än att leta efter händelser
- Till slut accepterad konsekventhet

**Använd inte när**

- Revideringsspår
- Det behövs temporära sökningar
- Cache-invaliderings komplexitet överväger fördelarna

### Event Sourcing + Hand Cache Invalidation

**Använd när:**

- Aldrig

Välj det rätta verktyget för ditt användandesfall. Event Sourcing med Marten är kraftfullt när du behöver det . Cache, -baserad CQRS är enklare när du inte gör det

För de flesta tillämpningar , börjar med traditionell CRUD ( se min EF Core-serie [här](/blog/addingentityframeworkforblogpostspt1)). När du stöter på prestationens problem , lägga till kassering . nå bara till full Event Sourcing när du verkligen behöver dess specifika egenskaper M SK3

## Fortsättning avläsning

- [Marten Dokumentation](https://martendb.io/) - Utmärkta dokument , bra att läsa
- [EventStoreDB](https://www.eventstore.com/) - Alternativt eventbutik om Postgres inte är
- [Dapper Dokumentation](https://github.com/DapperLib/Dapper) - snabba SQL-sökningar
- [MediatR](https://github.com/jbogard/MediatR) - Kommando
- [Microsoft på CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) - officiella instruktioner
- Min entitets ramverk serie börjar [här](/blog/addingentityframeworkforblogpostspt1) - Traditionell tillvägagångssätt