# Moderne CQRS en Event Sourcing in .NET: Doing It Properly

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

> **NOOT**: Dit is een oud artikel dat ik vergat uit te brengen.

Hier is het, geniet ervan!

Ik heb het bijgewerkt, maar misschien heb ik wat problemen gemist.

## CQRS en Event Sourcing - twee patronen die vaak samen worden genoemd maar vaak verkeerd worden begrepen.

In dit artikel ga ik je laten zien hoe je ze goed kunt implementeren met behulp van moderne .NET tools: Marten voor event sourcing, Dapper voor geoptimaliseerde queries en MediatR om alles georganiseerd te houden.

- **Ik zal je ook het "half-assed" cache-gebaseerde alternatief laten zien, en uitleggen waarom proberen Event Sourcing te mengen met handmatige cache-invalidatie een vreselijk idee is.**Inleiding
- **CQRS (Command Query Responsibility Segregation) en Event Sourcing zijn twee verschillende patronen die uitzonderlijk goed samenwerken:**CQRS

- Scheiden van je leesmodellen van je schrijfmodellen

- Event Sourcing
- - Het opslaan van elke staat verandering als een onveranderlijke opeenvolging van gebeurtenissen
- Wanneer goed gedaan met gereedschap zoals Marten, krijg je:
- Complete audit trail van elke verandering

Mogelijkheid om de staat op elk moment te herbouwen**Automatische leesmodelprojecties**Natuurlijke pasvorm met domeingestuurd ontwerp

[TOC]

## Dit artikel richt zich op het doen

correct

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



## Als je de snel-en-vuile cache-gebaseerde aanpak wilt, zal ik dat kort behandelen aan het einde - maar het is niet echt CQRS, en het geeft je niet de voordelen van Event Sourcing.

Wat is CQRS?

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

In de kern, CQRS betekent het gebruik van verschillende modellen voor het lezen en schrijven van gegevens:

## De schrijfkant richt zich op bedrijfslogica en validatie.

**De leeszijde is gedenormaliseerd en geoptimaliseerd voor display.**Simpel genoeg, maar echte CQRS betekent dat ze volledig gescheiden paden zijn door uw toepassing.

**Wat is Event Sourcing?**In plaats van de huidige toestand op te slaan, slaat u de gebeurtenissen op die tot die toestand leidden:

**De huidige toestand wordt afgeleid door gebeurtenissen te herhalen.**Dit geeft je een complete geschiedenis van alles wat er ooit in je systeem is gebeurd.

**Waarom Event Sourcing met CQRS gebruiken?**Complete audit trail

**: Elke verandering wordt geregistreerd.**Perfect voor financiële systemen, gezondheidszorg, of waar dan ook moet je bewijzen wat er gebeurd is en wanneer.

### Tijdelijke zoekopdrachten

**: "Hoe zag deze blogpost er afgelopen dinsdag uit?" wordt triviaal - gewoon gebeurtenissen tot op dat punt afspelen.**Debuggen

**: Reproduceer bugs door de exacte volgorde van gebeurtenissen die hen veroorzaakt.**Bedrijfsintelligentie

**: Bouw nieuwe rapporten uit historische gegevens zonder migraties uit te voeren.**De gebeurtenissen zijn er al.

**Natuurlijke CQRS Fit**: Gebeurtenissen scheiden natuurlijk schrijfsels (evenementen toevoegen) van lezen (query projecties).

## Als je dat niet zou moeten doen

Eenvoudige CRUD[: Als je alleen gegevens opslaat en ophaalt, is Event Sourcing belachelijk.](https://martendb.io/)Klein team zonder ervaring

: De leercurve is steil.

- Geen auditvereisten
- : Als je alleen maar om de huidige staat geeft, bewaar dan geen geschiedenis.
- Grote binaire gegevens
- : Evenementen werken slecht met afbeeldingen, video's, bestanden.
- Modern Event Sourcing in .NET: Marten

### De beste tool voor Event Sourcing in .NET in 2025 is

Marten

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

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

### Het is een evenementenwinkel gebouwd op PostgreSQL, actief onderhouden door Jeremy Miller, en het werkt gewoon.

Waarom Marten?

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

Gebouwd op PostgreSQL (je weet het al)

- **Automatische projecties van gebeurtenissen om modellen te lezen**Async daemon voor projectieverwerking
- **Rijke zoekmogelijkheden**Productie-klaar en slag-getest
- **Opzetten van Marten**Installeer de pakketten:
- Instellen

## De architectuur

Dit is hoe alles bij elkaar past:

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

Belangrijkste punten:

- **Commando's**agendanotities toevoegen aan de agendanotitie-opslag
- **Async-daemon**verwerkt gebeurtenissen en updates leesmodellen
- **Vragen**lees uit de gedenormaliseerde leesmodellen

## Geen handmatige cache ongeldigheid nodig!

Gebeurtenissen definiëren

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

Evenementen zijn onveranderlijke records die dingen beschrijven die zijn gebeurd:

1. Gebeurtenissen moeten zijn:
2. Onveranderlijk
3. - één keer geschreven, nooit veranderd

## Verleden tijd

Ze beschrijven wat er gebeurd is.

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

Rijk aan zakelijke betekenis

1. - "BlogPostTitleChanged" niet "PropertyUpdated"
2. Aanmaken van geaggregeerde gegevens
3. Aggregaties zijn het schrijfmodel.
4. Zij valideren de bedrijfsregels en produceren evenementen:

Het patroon:

## Bedrijfsmethoden valideren en retourneren evenementen

Toepassen methoden update interne status

```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 behandelt event persistentie en replay

## Commandohandlers (Schrijfzijde)

Opdrachten worden behandeld door gebeurtenissen toe te voegen aan stromen:

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

De stroom:

## Geaggregeerd laden door gebeurtenissen te herhalen (of nieuwe aan te maken)

Call business methode (validates and returns event)

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

## Agendanotitie toevoegen aan stream

Wijzigingen opslaan

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

## Marten behandelt de rest - het opslaan van gebeurtenissen, het triggeren van projecties, enz.

Lees Modellen en Projecties

Projecties veranderen gebeurtenissen in gedenormaliseerde leesmodellen:

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

Martens async daemon verwerkt gebeurtenissen op de achtergrond en houdt modellen up-to-date.

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

## Je schrijft geen cache ongeldigheidscode - het is automatisch.

Zoekzijde met dapper

### Nu vragen we de gelezen modellen met behulp van Dapper voor maximale prestaties:

- Geen caching code.
- Geen ongeldigheidslogica.
- Marten houdt de leesmodellen automatisch synchroon.
- Controllers

Met MediatR zijn controllers heel eenvoudig:

### De volledige stroom

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

### Dit is hoe het allemaal werkt:

- Eventuele samenhang
- Een ding te begrijpen: met async projecties, is er een kleine vertraging tussen het schrijven van een evenement en het lezen model wordt bijgewerkt.
- Meestal milliseconden, maar het is er.
- Als u onmiddellijke consistentie nodig heeft voor een specifieke operatie, gebruik dan inline projecties:

### Of vraag direct naar de activiteitsstream voor lees-na-schrijfscenario's:

**Deel 2: De Half-Assed-benadering (Cache-invalidatie)**Juist, dus je hebt gelezen over de juiste Event Sourcing en je denkt "dat is veel werk."

**Eerlijk genoeg.**Hier is de "informele CQRS" benadering die de meeste teams eigenlijk gebruiken.

**De set-up**Opdrachten schrijven naar database (met behulp van EF of Dapper)

**Vragen gelezen van IMemoryCache of IDistributedCache**Commando's ongeldige relevante cache-items na het schrijven

**Geen event sourcing, geen projecties, geen async daemons**Het is niet waar CQRS.

Je krijgt geen audit trail, temporal queries, of automatische projecties.

## Maar je krijgt wel prestaties met minder complexiteit.

Snel voorbeeld

### Wanneer dit werkt

Eenvoudige toepassingen zonder complexe auditvereisten

Je hebt prestaties nodig, maar kan de complexiteit van Event Sourcing niet rechtvaardigen

### Klein team dat geen Event Sourcing wil leren

**Staliteit van enkele seconden is aanvaardbaar**De problemen

**Cache ongeldigheid is moeilijk**: Miss one cache key and you server mule data.

**Elk commando moet weten welke caches ongeldig moeten worden gemaakt.**Geen auditspoor

**: Je hebt alleen de huidige staat.**Ik kan niet bewijzen wat er gebeurd is of wanneer.

**Geen tijdelijke vragen**"Hoe zag het systeem er gisteren uit?"

### Schalen

: Met IMemoryCache heeft elke server zijn eigen cache.

- Update op server A, server B heeft nog steeds oude gegevens.
- IDistributedCache lost dit op, maar voegt Redis toe.
- Strakke koppeling
- : Commands zijn gekoppeld aan cachesleutels.

Verander een query, kan een commando breken.

- Het werkt, maar je ruilt eenvoud voor verloren mogelijkheden.
- Soms is dat de juiste ruil.
- Vaak niet.

Deel 3: Het slechtste idee - Event Sourcing + Handmatige Cache-validatie

### Nu voor het werkelijk verschrikkelijke idee: het gebruik van Event Sourcing maar ook proberen handmatig ongeldig caches.

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

Waarom mensen dit proberen

## Ze willen Event Sourcing's audit trail en tijdelijke vragen, maar ze zijn bezorgd over uiteindelijke consistentie.

Dus ze denken: "Ik voeg gewoon cache ongeldigheid toe om lezen sneller en consistenter te maken!"

### Doe dit niet.

**Waarom het verschrikkelijk is**

- Je hebt het doel verslagen.
- : Event Sourcing bouwt al leesmodellen door middel van projecties.
- Het toevoegen van handmatige cache ongeldigheid betekent dat je dat systeem omzeilt.
- Dubbele complexiteit

**: Je hebt nu twee systemen die leesmodellen synchroon houden - Marten's projecties en je handmatige cache ongeldigheid.**

- Ze zullen conflicteren.
- Inconsistente toestand
- : Martens async daemon werkt de database bij.

### Uw cache ongeldigheid loopt onmiddellijk.

**Ze zijn niet synchroon.**

- Wat is juist?
- Verloor uitkeringen
- : Event Sourcing's hele punt is dat projecties zijn afgeleid van gebeurtenissen.
- Handmatige cache ongeldigheid breekt dat model.

**Debug nachtmerrie**

- : Is de oude data omdat de projecties niet zijn uitgevoerd?
- Of omdat je vergat een cachesleutel ongeldig te maken?
- Of omdat de cache ongeldig was, maar de projectie nog niet was uitgevoerd?

### Succes.

**De juiste aanpak**

- Als u Event Sourcing nodig heeft:

Gebruik Marten's projecties (async of inline)

De gelezen modellen direct opvragen[Accepteer uiteindelijke consistentie (het is meestal prima)](/blog/addingentityframeworkforblogpostspt1)Gebruik inline projecties als je echt onmiddellijke consistentie nodig hebt

## Als je uiteindelijke consistentie niet kunt accepteren:

- [Niet gebruiken Event Sourcing](https://martendb.io/)Gebruik de op cache gebaseerde benadering van deel 2
- [Of gebruik gewoon traditionele CRUD met EF Core](https://www.eventstore.com/)Probeer niet beide te doen.
- [Je zult eindigen met de complexiteit van Event Sourcing plus de kwetsbaarheid van handmatige cache ongeldigheid.](https://github.com/DapperLib/Dapper)Voorbeeld van wat niet te doen
- [Gebruik gewoon Marten's projecties.](https://github.com/jbogard/MediatR)Ze zijn hiervoor ontworpen.
- [Conclusie](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)Drie benaderingen, drie gebruikscases:
- Juiste CQRS met Event Sourcing (Marten)[Gebruiken wanneer:](/blog/addingentityframeworkforblogpostspt1)Je hebt volledige audit trail nodig.