# Modernes CQRS und Event Sourcing in .-NET-:-Tun Sie es richtig

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

> **HINWEIS**Dies ist ein alter Artikel, den ich vergessen habe zu veröffentlichen.

CQRS und Event Sourcing 2 Muster, die oft zusammen erwähnt werden, aber häufig missverstanden werden, . In diesem Artikel zeigen I,im Ihnen, wie man sie richtig mit modernen Tools von . NET umsetzt.:. Marten für Event-Sourcing,.Dapper für optimierte Abfragen,. und MediatR alles organisiert zu halten

I'-ll zeigt Ihnen auch die .", halbherzige -, besorgt . ", cache ?- basierte Alternative ., und erklären, warum der Versuch, Event Sourcing mit manuellen Cache Ungültigkeit zu mischen ist eine schreckliche Idee .

## Einleitung

CQRS -(-Command Query Responsibility Segregation und Event Sourcing sind zwei unterschiedliche Muster, die außergewöhnlich gut zusammen arbeiten.

- **CQRS** Abtrennen Ihrer Lesemodelle von Ihren Schreibmodellen
- **Event Sourcing** Alle Zustandsänderungen als unveränderliche Abfolge von Ereignissen speichern

Wenn Sie richtig mit Werkzeugen wie Marten.,, erhalten Sie

- Vollständiger Prüfpfad jeder Änderung
- Fähigkeit, den Zustand jederzeit wieder aufzubauen
- Automatische Lesemodellprojektionen
- Natürliche Passform mit domäne-angetriebenem Design

Dieser Artikel konzentriert sich auf es zu tun **richtig**Wenn Sie wollen, dass die quick--und--Tirty-Cache .-Basic approach . , . I' .ll decken, dass kurz am Ende ?- , aber es ist nicht wirklich CQRS . , und es doesn't geben Ihnen die Vorteile von Event Sourcing .

[TOC]

## Was ist CQRS-??

CQRS bedeutet im Kern, verschiedene Modelle zum Lesen und Schreiben von Daten zu verwenden.

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

Die Schreibseite konzentriert sich auf Geschäftslogik und Validierung.Die Leseseite ist für Displays entnormiert und optimiert.Einfach genug, aber wahre CQRS bedeutet, dass sie durch Ihre Anwendung völlig getrennte Pfade finden.

## Was ist Ereignis Sourcing?

Anstatt den aktuellen Zustand zu speichern, speichern Sie die Ereignisse, die zu diesem Zustand geführt haben.

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

Der aktuelle Zustand wird durch das Abspielen von Ereignissen abgeleitet.Dies gibt Ihnen eine vollständige Geschichte von allem, was jemals in Ihrem System passiert ist.

## Warum Event Sourcing mit CQRS-? verwenden

**Vollständiger Prüfpfad**Jede Änderung wird aufgezeichnet...... Perfekt für Finanzsysteme.......... oder überall, wo Sie beweisen müssen, was passiert ist und wann....

**Zeitliche Anfragen**Wie sah dieser Blog-Post am letzten Dienstag aus??" , wird trivial.- , nur Replay Ereignisse bis zu diesem Punkt.

**Debugging**Fehler reproduzieren, indem Sie die genaue Reihenfolge der Ereignisse wiederholen, die sie verursacht haben.

**Unternehmensinformationen**Erstellen Sie neue Berichte aus historischen Daten, ohne Migrationen zu laufen.Die Ereignisse sind bereits da.

**Natürliche CQRS Fit**Ereignisse natürlich trennen schreibt ,( , anhängen Ereignisse , ) , von liest , ( , Querprojektionen , ).

### Wenn Sie sollten

**Einfacher CRUD**Wenn du gerade Daten speicherst und wiederholst, ist das Event Sourcing lächerlich Overhead.

**Kleines Team ohne Erfahrung**Die Lernkurve ist steil.

**Keine Prüfungsanforderungen**Wenn du dich nur um den aktuellen Zustand kümmerst

**Große binäre Daten**Ereignisse funktionieren schlecht mit Bildern, Videos und Dateien

## Modernes Event Sourcing in .-NET-MARTEN

Das beste Werkzeug für Event Sourcing in -.-NET ist [Marten](https://martendb.io/)Es ist ein Event-Store, der auf PostgreSQL.,' gebaut wurde und aktiv von Jeremy Miller gepflegt wird.

Warum Marten-?

- Gebaut auf PostgreSQL, du weißt es bereits
- Automatische Projektionen von Ereignissen zum Lesen von Modellen
- Async-Daemon für Projektionsverarbeitung
- Reiche Abfragemöglichkeiten
- Produktion – --ready und Kampf –--getestet

### Marten einrichten

Installieren Sie die Pakete

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

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

### Die Architektur

Hier ist ', wie alles zusammenpasst.

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

Wesentliche Punkte

- **Befehle** Veranstaltungen an den Event-Store anhängen
- **Assync-Daemon** Prozesse Ereignisse und Updates Lesemodelle
- **Anfragen** gelesen von den entnormierten Lesemodellen
- Keine manuelle Cache-Ungültigkeit erforderlich

## Festlegung von Ereignissen

Ereignisse sind unveränderliche Aufzeichnungen, die Dinge beschreiben, die geschehen sind

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

Ereignisse sollten sein

- **Unveränderbar** Einmal geschrieben, nicht geändert.
- **Vergangenheit** Sie beschreiben, was passiert ist
- **Reich an geschäftlicher Bedeutung** Weitere Informationen finden Sie unter: http:europa.eu.int

## Aggregate erstellen

Aggregate sind das Schreibmodell. Sie bestätigen Geschäftsregeln und erzeugen Ereignisse.

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

Das Muster

1. Geschäftsmethoden validieren und Ereignisse zurückgeben
2. Methoden anwenden, die den internen Zustand aktualisieren
3. Marten kümmert sich um Event Persistenz und Wiederholung

## Befehlshandler Seite schreiben

Befehle werden durch Anhängen von Ereignissen an Streams behandelt.

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

Der Durchfluss

1. Aggregat laden, indem Ereignisse wiederholt werden
2. Call Business-Methode bestätigt und liefert Ereignis
3. Event zum Streamen anhängen
4. Änderungen speichern

Marten kümmert sich um den Rest der Speicherung von Ereignissen, die , auslösende Projektionen, , usw.

## Modelle und Projektionen lesen

Projektionen verwandeln Ereignisse in entnormierte Lesemodelle

```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 verarbeitet Ereignisse im Hintergrund und hält Modelle auf dem neuesten Stand...Sie schreiben keinen Cache-Ungültigkeitscode.

## Seite mit Dapper abfragen

Jetzt fragen wir die Lesemodelle mit Dapper nach maximaler Performance.

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

Kein Caching-Code... keine Ungültigkeitslogik... Marten hält die Lesemodelle automatisch synchron.

## Steuergeräte

Mit MediatR-,-Controllern sind schön einfach

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

## Der vollständige Fluss

Hier-', wie alles zusammen funktioniert

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

## Eventuelle Konsistenz

Eine Sache mit async Projektionen zu verstehen ,.' ist eine kleine Verzögerung zwischen dem Schreiben eines Ereignisses und dem gelesenen Modell aktualisiert wird normalerweise Millisekunden ,, aber es ist '.

Wenn Sie sofortige Konsistenz für einen bestimmten Betrieb benötigen, verwenden Sie Inline-Projektionen.

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

Oder Abfrage des Ereignisstroms direkt für lese--.after--.write scenarios-:.

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

## Teil: Die Halbzeit--Bestätigter Ansatz -Cache Invalidation -)

Rechts-,, so dass Sie lesen über die richtige Ereignis Sourcing und you ?'re denken, dass -' ist eine Menge Arbeit - ". - Fair genug, um hier-MSC7 ist der informelle CQRSC9-Ansatz, den die meisten Teams tatsächlich verwenden -MSC10

### Das Setup

- Befehle schreiben in die Datenbank mit EF oder Dapper.
- Anfragen aus IMemoryCache oder IDistributedCache
- Befehle ungültig machen relevante Cache-Einträge nach dem Schreiben
- Keine Event-Sourcing-Beispiele keine Projektionen keine Async-Daemons

Es ist nicht wahr CQRS... Sie erhalten die Audit-Trail nicht ,. Temporale Abfragen oder automatische Projektionen, aber Sie bekommen Leistung mit weniger Komplexität.

### Schnelles Beispiel

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

### Wenn das funktioniert

- Einfache Anwendungen ohne komplexe Audit-Anforderungen
- Sie benötigen Leistung, aber kann ?'?t rechtfertigen Event Sourcing Komplexität
- Kleines Team, das nicht will Event Sourcing lernen
- Standfestigkeit von ein paar Sekunden ist akzeptabel

### Die Probleme

**Cache Ungültigkeit ist schwer**Verpassen Sie einen Cache-Schlüssel und Sie dienen veraltete Daten.Alle Befehle müssen wissen, welche Caches ungültig sind.

**Kein Prüfpfad**Sie haben nur den aktuellen Zustand , . , kann nicht beweisen, was passiert ist oder wenn ,. .

**Keine zeitlichen Abfragen**Wie sah das System gestern aus?

**Skalierung**Mit IMemoryCache, ,, hat jeder Server seinen eigenen Cache, ., Update auf dem Server, A-,, Server B hat noch veraltete Daten, ., IDistributedCache löst dies aber fügt Redis, MSC5 hinzu.

**Enge Kopplung**Befehle werden mit Cache-Schlüsseln gekoppelt..Ändern Sie eine Abfrage, die einen Befehl brechen könnte.

Es funktioniert, ,, aber du handelst Einfachheit für verlorene Fähigkeiten, ., Manchmal, dass -', der richtige Handel, MSC5, oft ist es ', nicht .

## Teil - Die schlimmste Idee - Event Sourcing - Manual Cache Invalidation

Jetzt für die wirklich schreckliche Idee mit Event Sourcing, sondern auch versuchen, manuell zu entkräften Caches

### Warum Menschen dies versuchen

Sie wollen Event Sourcing-' s Audit Trail und zeitliche Abfragen... aber sie machen sich Sorgen wegen eventueller Konsistenz.... also denken sie, dass "... ' ll nur Cache-Ungültigkeit hinzufügt, um Lesevorgänge schneller und konsistenter zu machen.

Tun Sie dies nicht

### Warum es Schrecklich ist

**Sie haben den Zweck besiegt**Event Sourcing baut bereits Lesemodelle durch Projektionen auf, indem Sie manuelle Cache-Ungültigkeit hinzufügen, bedeutet, dass Sie das System umgehen.

**Doppelte Komplexität**Sie haben jetzt zwei Systeme, die lesende Modelle in synchron halten.... MARTEN... M's Projektionen und Ihre manuelle Cache-Ungültigkeit....

**Inkonsistenter Zustand**:-Marten'-Async Daemon aktualisiert die Datenbank .-Ihr Cache ungültig läuft sofort .-Sie sind aus dem sync .-Das ist richtig ?-

**Verlorene Leistungen**Event Sourcing's ganzer Punkt ist, dass Projektionen von Ereignissen abgeleitet werden.Manuelle Cache-Ungültigkeit bricht dieses Modell

**Debugging Alptraum**Ist die veraltete Daten, weil Projektionen haben ?'?t run ??? Oder weil du vergessen hast, einen Cache-Schlüssel zu entkräften ??? Oder da der Cache ungültig, aber die Projektion hatte noch laufen ? ?? Good luck -.?

### Der richtige Weg

Wenn Sie Event Sourcing benötigen

- Verwenden Sie Marten-'-Projektionen (-async oder Inline-)-
- Die Lesemodelle direkt abfragen
- Akzeptieren Sie eventuelle Konsistenz (.it.'.s in der Regel fein
- Verwenden Sie Inline-Projektionen, wenn Sie wirklich sofortige Konsistenz benötigen

Wenn du eine eventuelle Konsistenz akzeptieren kannst

- Verwende nicht Ereignis Sourcing
- Den cache-basierten Ansatz von Part verwenden
- Oder nutzen Sie einfach traditionelle CRUD mit EF Core

Don.'.t versuchen, beides zu tun. .. You.'.ll endet mit der Komplexität von Event Sourcing plus der Fragilität der manuellen Cache-Ungültigkeit.

### Beispiel für das, was nicht zu tun ist

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

Verwenden Sie einfach Marten-'-Projektionen, die für diese entworfen wurden.

## Schlußfolgerung

Drei Ansätze – drei Anwendungsfälle – :

### Richtiger CQRS mit Event Sourcing

**Verwenden Sie, wenn -:**

- Sie benötigen einen kompletten Prüfpfad
- Zeitliche Abfragen sind wertvoll
- Komplexe Domäne mit reicher Geschäftslogik
- Finanz-, Gesundheits- und Gesundheitswesen

**Nicht verwenden, wenn**

- Einfache CRUD-Anwendung
- Team fehlt Event Sourcing Erfahrung
- Keine Prüfungsanforderungen

### Informelles CQRS mit Caching

**Verwenden Sie, wenn -:**

- Leistungssteigerung auf Lesarten benötigen
- Prüfungspfad nicht erforderlich
- Einfacher als Event Sourcing
- Eventuelle Konsistenz akzeptabel

**Nicht verwenden, wenn**

- Prüfpfad erforderlich
- Zeitliche Abfragen benötigen
- Ungültigkeits-Komplexität überwiegt Vorteile

### Ereignis-Surcing-Beschwerde-Ungültigkeit von Manual Cache

**Verwenden Sie, wenn -:**

- Nein, nur nicht.

Wählen Sie das richtige Werkzeug für Ihren Anwendungsfall aus..-Event Sourcing mit Marten ist leistungsstark, wenn Sie es benötigen.MSC1-Cache.--based CQRS ist einfacher, wenn man sie don.MSSK3-t-.-Mixing ist ein Fehler.

Für die meisten Anwendungen starten Sie mit der traditionellen CRUD-Serie (. [Hierher](/blog/addingentityframeworkforblogpostspt1)Wenn Sie Performance-Probleme treffen, dann fügen Sie Caching hinzu..... Nur für volle Event Sourcing erreichen, wenn Sie seine spezifischen Fähigkeiten wirklich benötigen.

## Literaturhinweise

- [Dokumentation von Marten](https://martendb.io/) Sehr gut lesenswert
- [EventStoreDB](https://www.eventstore.com/) Alternativer Event-Store, wenn Postgres Ihre Sache ist
- [Dapper Dokumentation](https://github.com/DapperLib/Dapper) Schnelle SQL-Abfragen
- [MediatR](https://github.com/jbogard/MediatR) Befehlsversendung der /
- [Microsoft auf CQRS](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs) Amtliche Anleitung
- Meine Entity Framework-Serie beginnt [Hierher](/blog/addingentityframeworkforblogpostspt1) Traditioneller Ansatz