# CQRS moderno y abastecimiento de eventos en .NET: Hacerlo correctamente

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

> **NOTA**: Este es un viejo artículo que olvidé publicar.

Aquí está, ¡disfrútenlo!

Lo actualicé, pero pueden ser algunos problemas que me perdí.

## CQRS y Event Sourcing - dos patrones que a menudo se mencionan juntos, pero con frecuencia mal entendido.

En este artículo, voy a mostrarles cómo implementarlas correctamente utilizando herramientas .NET modernas: Marten para la obtención de eventos, Dapper para consultas optimizadas y MediatR para mantener todo organizado.

- **También te mostraré la alternativa basada en caché "mediana" y te explicaré por qué intentar mezclar Event Sourcing con la invalidación manual de caché es una idea terrible.**Introducción
- **CQRS (Command Query Responsibility Segregation) y Event Sourcing son dos patrones distintos que funcionan excepcionalmente bien juntos:**CQRS

- Separar sus modelos de lectura de sus modelos de escritura

- Abastecimiento de eventos
- - Almacenar cada cambio de estado como una secuencia inmutable de eventos
- Cuando se hace correctamente con herramientas como Marten, se obtiene:
- Seguimiento completo de cada cambio en la auditoría

Capacidad de reconstruir el estado en cualquier momento**Proyecciones automáticas de modelos de lectura**Ajuste natural con diseño basado en el dominio

[TOC]

## Este artículo se centra en hacerlo

correctamente

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



## Si quieres el enfoque basado en caché rápido y sucio, lo cubriré brevemente al final - pero no es realmente CQRS, y no te da los beneficios de Event Sourcing.

¿Qué es el 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);
```

En esencia, CQRS significa utilizar diferentes modelos para leer y escribir datos:

## El lado de la escritura se centra en la lógica de negocios y la validación.

**El lado de lectura está desnormalizado y optimizado para la visualización.**Lo suficientemente simple, pero el CQRS verdadero significa que son rutas completamente separadas a través de su aplicación.

**¿Qué es la Abastecimiento de Eventos?**En lugar de almacenar el estado actual, almacena los eventos que llevaron a ese estado:

**El estado actual se deriva de la repetición de eventos.**Esto te da una historia completa de todo lo que ha pasado en tu sistema.

**¿Por qué usar Abastecimiento de Eventos con CQRS?**Seguimiento completo de la auditoría

**: Cada cambio se registra.**Perfecto para sistemas financieros, atención médica o en cualquier lugar que necesite para demostrar lo que pasó y cuándo.

### Consultas temporales

**: "¿Cómo se veía este blog el martes pasado?" se convierte en trivial - sólo repetir eventos hasta ese punto.**Depuración

**: Reproducir errores reproduciendo la secuencia exacta de los eventos que los causaron.**Inteligencia empresarial

**: Construir nuevos informes a partir de datos históricos sin ejecutar migraciones.**Los acontecimientos ya están allí.

**Ajustar CQRS natural**: Los eventos naturalmente separan las escrituras (append events) de las lecturas (proyecciones de petición).

## Cuando no deberías

CRUD simple[: Si sólo estás almacenando y recuperando datos, Event Sourcing es ridículo.](https://martendb.io/)Pequeño equipo sin experiencia

: La curva de aprendizaje es empinada.

- Sin requisitos de auditoría
- : Si sólo te preocupas por el estado actual, no almacenes la historia.
- Grandes datos binarios
- : Los eventos funcionan mal con imágenes, videos, archivos.
- Abastecimiento de Eventos Modernos en .NET: Marten

### La mejor herramienta para la Abastecimiento de Eventos en .NET en 2025 es

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

### Es una tienda de eventos construida en PostgreSQL, mantenida activamente por Jeremy Miller, y simplemente funciona.

¿Por qué 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
```

Construido en PostgreSQL (ya lo sabes)

- **Proyecciones automáticas de eventos a modelos de lectura**Demonio de Async para procesamiento de proyecciones
- **Capacidades de consulta ricas**Preparados para la producción y probados
- **Configuración de Marten**Instalar los paquetes:
- Configurar en

## La arquitectura

Así es como todo encaja:

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

Puntos clave:

- **Comandos**añadir eventos a la tienda de eventos
- **Demonio de async**procesa eventos y actualizaciones lee modelos
- **Consultas**leer de los modelos de lectura desnormalizados

## ¡No se necesita invalidación manual de caché!

Definir los acontecimientos

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

Los eventos son registros inmutables que describen las cosas que han sucedido:

1. Los eventos deben ser:
2. Inmutable
3. - una vez escrito, nunca cambiado

## Tiempo pasado

- describen lo que pasó.

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

Rico en significado de negocios

1. - "BlogPostTitleChanged" no "PropertyUpdated"
2. Crear agregados
3. Los agregados son el modelo de escritura.
4. Validan las reglas de negocio y producen eventos:

El patrón:

## Los métodos de negocio validan y devuelven eventos

Aplicar métodos actualizar estado interno

```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 se encarga de la persistencia y repetición de eventos

## Manipuladores de órdenes (escritura lateral)

Los comandos se manejan añadiendo eventos a las secuencias:

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

El flujo:

## Cargar agregado reproduciendo eventos (o crear nuevos)

Call business method (valida y devuelve el evento)

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

## Añadir evento a transmitir

Guardar cambios

```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 se encarga del resto - almacenar eventos, activar proyecciones, etc.

Leer modelos y proyecciones

Las proyecciones convierten los eventos en modelos de lectura desnormalizados:

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

El demonio async de Marten procesa eventos en segundo plano y mantiene actualizados los modelos de lectura.

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

## Usted no escribe ningún código de invalidación de caché - es automático.

El lado de la pregunta con Dapper

### Ahora consultamos los modelos de lectura usando Dapper para obtener el máximo rendimiento:

- Sin código de caché.
- No hay lógica de invalidación.
- Marten mantiene los modelos de lectura sincronizados automáticamente.
- Contralores

Con MediatR, los controladores son muy simples:

### El flujo completo

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

### Así es como funciona todo juntos:

- Coherencia eventual
- Una cosa a entender: con proyecciones asíncronas, hay un pequeño retraso entre la escritura de un evento y la actualización del modelo de lectura.
- Normalmente milisegundos, pero está ahí.
- Si necesita coherencia inmediata para una operación específica, utilice proyecciones en línea:

### O consulta directamente el flujo de eventos para los escenarios de lectura después de escribir:

**Parte 2: El enfoque a mitad de camino (invalidación de la caché)**Bien, así que has leído sobre Event Sourcing apropiado y estás pensando "eso es mucho trabajo".

**Me parece justo.**Aquí está el enfoque "CQRS informal" que la mayoría de los equipos realmente utilizan.

**La configuración**Los comandos escriben en la base de datos (usando EF o Dapper)

**Consultas leídas de IMemoryCache o IDistributedCache**Las órdenes invalidan las entradas de caché relevantes después de escribir

**Sin fuentes de eventos, sin proyecciones, sin demonios asíncronos**No es verdad CQRS.

No se obtiene el rastro de auditoría, consultas temporales o proyecciones automáticas.

## Pero se obtiene un rendimiento con menos complejidad.

Ejemplo rápido

### Cuando esto funciona

Aplicaciones sencillas sin requisitos de auditoría complejos

Necesitas rendimiento pero no puedes justificar la complejidad de la Abastecimiento de Eventos

### Pequeño equipo que no quiere aprender Event Sourcing

**La rancio de unos segundos es aceptable**Los problemas

**La invalidación de caché es difícil**: Falta una clave de caché y usted sirve datos rancios.

**Cada comando necesita saber qué cachés invalidar.**Sin pista de auditoría

**: Sólo tienes estado actual.**No puedo probar lo que pasó o cuándo.

**Sin consultas temporales**: No se puede preguntar "¿cómo era el sistema ayer?"

### Escalado

: Con IMemoryCache, cada servidor tiene su propia caché.

- Actualización en el servidor A, el servidor B todavía tiene datos rancios.
- IDistributedCache resuelve esto pero añade Redis.
- Acoplamiento apretado
- : Los comandos se acoplan a las teclas de caché.

Cambiar una consulta, podría romper una orden.

- Funciona, pero estás intercambiando simplicidad por capacidades perdidas.
- A veces eso es lo correcto.
- A menudo no lo es.

Parte 3: La peor idea - Abastecimiento de eventos + Invalidación manual de caché

### Ahora para la idea verdaderamente terrible: usando Event Sourcing pero también tratando de invalidar cachés manualmente.

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

Por qué la gente intenta esto

## Quieren el rastro de auditoría de Event Sourcing y las consultas temporales, pero están preocupados por la consistencia eventual.

Así que piensan: "¡Solo añadiré la invalidación de caché para que las lecturas sean más rápidas y coherentes!"

### No hagas esto.

**Por qué es terrible**

- Has derrotado el propósito.
- : Event Sourcing ya construye modelos de lectura a través de proyecciones.
- Agregar la invalidación manual de caché significa que estás evitando ese sistema.
- Doble complejidad

**: Ahora tienes dos sistemas que mantienen los modelos de lectura sincronizados - las proyecciones de Marten Y la invalidación manual de su caché.**

- Van a entrar en conflicto.
- Estado inconsistente
- : El demonio async de Marten actualiza la base de datos.

### Su caché de invalidación se ejecuta inmediatamente.

**Están fuera de sincronía.**

- ¿Cuál es la correcta?
- Beneficios perdidos
- : Event Sourcing todo el punto es que las proyecciones se derivan de eventos.
- La invalidación manual de caché rompe ese modelo.

**Depurar pesadilla**

- : ¿Los datos rancios porque las proyecciones no se han ejecutado?
- ¿O porque olvidaste invalidar una clave de caché?
- ¿O porque la caché invalidaba pero la proyección no se había ejecutado todavía?

### Buena suerte.

**El enfoque correcto**

- Si necesita Abastecimiento de Eventos:

Utilice las proyecciones de Marten (sincronización o inline)

Consultar directamente los modelos de lectura[Aceptar la consistencia eventual (por lo general está bien)](/blog/addingentityframeworkforblogpostspt1)Utilice proyecciones en línea si realmente necesita consistencia inmediata

## Si usted no puede aceptar la consistencia eventual:

- [No use Event Sourcing](https://martendb.io/)Utilice el enfoque basado en caché de la Parte 2
- [O simplemente use CRUD tradicional con EF Core](https://www.eventstore.com/)No intentes hacer las dos cosas.
- [Terminarás con la complejidad de Event Sourcing más la fragilidad de la invalidación manual de la caché.](https://github.com/DapperLib/Dapper)Ejemplo de lo que no se debe hacer
- [Sólo usa las proyecciones de Marten.](https://github.com/jbogard/MediatR)Están diseñados para esto.
- [Conclusión](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs)Tres enfoques, tres casos de uso:
- CQRS adecuado con Abastecimiento de Eventos (Marten)[Usar cuando:](/blog/addingentityframeworkforblogpostspt1)Necesita una pista de auditoría completa