This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
आधुनिक सीक्यूआरएस और घटना खोज .NET: इसे सही तरीके से करना
Thursday, 13 November 2025
नोटयह एक पुराना लेख है मैं जारी करने के लिए भूल गया हूँ
CQRS और घटना सॉरसिंग - दो नमूने जो प्रायः एक साथ उल्लिखित किए जाते हैं लेकिन अक्सर गलत समझे जाते हैं।
मैं आपको यह भी दिखाता हूँ "half-assedM SK3cache,-based alternative,and explain why trying to mix Event Sourcing with manual cache invalidation is a terrible idea
परिचय
सीक्यूआरएस ( कमांड क्वेरी उत्तरदायित्व पृथक्करण) और घटना खोज दो भिन्न नमूने हैं जो एक साथ असाधारण रूप से अच्छी तरह काम करते हैं
- सीक्यूआरएस - अपने पठन मॉडलों को अपने लेखन मॉडलों से अलग करना
- घटना खोज - घटनाओं की अपरिवर्तनीय अनुक्रम के रूप में प्रत्येक स्थिति परिवर्तन को भंडारित करना
जब मार्टेन जैसे उपकरणों के साथ सही तरीके से किया जाता है तो आप मिलते हैं
- हर परिवर्तन का लेखापरीक्षा ट्रैक पूरा करें
- किसी भी समय स्थिति को पुनः निर्माण करने की क्षमता
- स्वचालित पठन मॉडल प्रक्षेपण
- डोमेन के साथ प्राकृतिक फिट
इस लेख में इसे करने पर ध्यान दिया गया है ठीक-ठीक. यदि आप त्वरित-&- & #44;गुनाहगार कैश को चाहते हैं तो->आधारित दृष्टिकोण के साथ-साथ\MSC4\I\M SK5\मैं इसे अंत में संक्षिप्त रूप से ढूंढता हूँ।
क्या है CQRS?
इसके मूल में CQRS का अर्थ होता है, डेटा को पढ़ने और लिखने के लिए विभिन्न मॉडलों का उपयोग करना
// Write Model - Commands that change state
public record CreateBlogPostCommand(string Title, string Content, string AuthorId);
// Read Model - DTOs optimised for display
public class BlogPostListItemDto
{
public Guid Id { get; set; }
public string Title { get; set; }
public string AuthorName { get; set; }
public DateTime PublishedDate { get; set; }
public int CommentCount { get; set; }
}
लेखन पक्ष व्यापारिक तर्क और सत्यापन पर ध्यान देता है. पठन पक्ष डिनॉर्मलाइज किया गया है तथा प्रदर्शन के लिए अनुकूलित किया जाता है।
घटना की खोज क्या है
मौजूदा स्थिति को भंडारित करने के बजाय आप उस स्थिति की ओर ले जाने वाले घटनाओं को संग्रहित करते हैं
// Traditional: Store current state
public class BlogPost
{
public Guid Id { get; set; }
public string Title { get; set; } // Current title
public bool IsPublished { get; set; } // Current status
}
// Event Sourcing: Store the events
public record BlogPostCreated(Guid Id, string Title, string Content, DateTime CreatedAt);
public record BlogPostTitleChanged(Guid Id, string OldTitle, string NewTitle, DateTime ChangedAt);
public record BlogPostPublished(Guid Id, DateTime PublishedAt);
मौज़ूदा स्थिति घटनाओं को फिर से बजाने के द्वारा व्युत्पन्न है
क्यों CQRS के साथ घटना खोज का उपयोग करें
पूर्ण लेखापरीक्षा मार्गप्रत्येक परिवर्तन रिकॉर्ड किया जाता है।
अस्थायी प्रश्नक्या यह ब्लॉग पोस्ट पिछले मंगलवार को कैसा दिखता था?
डिबगिंगउन घटनाओं के सही अनुक्रम को फिर से बजाकर बगों का पुनरावृत्ति करें जो उन्हें उत्पन्न करते हैं
व्यापारिक सूचना: ऐतिहासिक डेटा से नए रिपोर्टें बनाना बिना माइग्रेशन चलाने के
प्राकृतिक सीक्यूआरएस फिटघटनाएँ स्वाभाविक रूप से अलग लिखते हैं (पैंड घटनाओं को पढ़ने से)से जोड़ें
जब आपको करना चाहिए
साधारण क्रूडयदि आप केवल डेटा संग्रहित और प्राप्त कर रहे हैं
अनुभव के बिना छोटा समूहसीखने की वक्र धीमी है
कोई लेखापरीक्षा आवश्यकता नहीं: अगर आप सिर्फ मौजूदा स्थिति के बारे में सोचते हैं
बड़ा बाइनरी डेटाघटनाएँ छवियों के साथ कम काम करती हैं
आधुनिक घटना शोधन में .NET: मार्टिन
Event Sourcing के लिए सबसे अच्छा उपकरण .NET में है मार्टिनCity name (optional, probably does not need a translation)यह एक पोस्टग्रेएसक्यूएल पर बनाया गया एक घटना भंडार है
क्यों मार्टेन
- PostgreSQL पर बनाया गया (आप पहले से ही जानते हैं
- मॉडल पढ़ने के लिए घटनाओं से स्वचालित प्रक्षेप
- प्रक्षेपण प्रक्रिया के लिए अतुल्यकालित डेइमन
- समृद्ध क्वेरी क्षमताएँ
- उत्पादन-ready and battle
मार्टेन सेटअप कर रहा है
पैकेजों को संस्थापित करें
dotnet add package Marten
dotnet add package Marten.AspNetCore
में कॉन्फ़िगर करें Program.cs:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMarten(options =>
{
options.Connection(builder.Configuration.GetConnectionString("Marten")!);
// Register event types
options.Events.AddEventType<BlogPostCreated>();
options.Events.AddEventType<BlogPostPublished>();
options.Events.AddEventType<BlogPostTitleChanged>();
options.Events.AddEventType<CommentAdded>();
// Configure async projections
options.Projections.Add<BlogPostProjection>(ProjectionLifecycle.Async);
});
builder.Services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
var app = builder.Build();
वास्तुकला
यहाँ यह है कि कैसे सब कुछ एक साथ मिल जाता है
flowchart TB
subgraph Client["Client Application"]
UI[User Interface]
end
subgraph Commands["Command Side (Writes)"]
CMD[Commands] --> CMDH[Command Handlers]
CMDH --> MARTEN[Marten Session]
MARTEN --> EVENTS[(Event Store)]
end
subgraph Background["Async Processing"]
EVENTS -.->|Event Stream| DAEMON[Marten Async Daemon]
DAEMON --> PROJ[Projections]
PROJ --> READDB[(Read Models)]
end
subgraph Queries["Query Side (Reads)"]
QRY[Queries] --> QRYH[Query Handlers with Dapper]
QRYH --> READDB
end
UI -->|Commands| CMD
UI -->|Queries| QRY
classDef commandStyle fill:none,stroke:#e63946,stroke-width:3px
classDef queryStyle fill:none,stroke:#457b9d,stroke-width:3px
classDef dataStyle fill:none,stroke:#2a9d8f,stroke-width:3px
class CMD,CMDH,MARTEN commandStyle
class QRY,QRYH queryStyle
class EVENTS,READDB dataStyle
प्रमुख बिंदुएं
- कमांड घटना भंडार में घटना जोड़ें
- असमर्थ डेइमन घटनाओं और अद्यतनों को प्रक्रियाएँ मॉडल पढ़ने
- प्रश्न असामान्यकृत पठन मॉडलों से पढ़ा
- कोई हस्तचालित कैश अमान्यीकरण की आवश्यकता नहीं है!
परिभाषित घटनाएँ
घटनाएं अपरिवर्तनीय रिकार्ड हैं जो घटनाओं का वर्णन करती है
// 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
);
घटनाओं को होना चाहिए
- अपरिवर्तनीय एक बार लिखा गया - कभी नहीं बदला
- भूतकाल उन्होंने बताया कि क्या हुआ
- व्यापारिक अर्थ में समृद्ध @info: whatsthis
समूह बना रहा है
एग्रीगेट लेखन मॉडल हैं
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; }
}
पैटर्न
- व्यापार विधियों के वैधीकरण और घटनाओं को लौटाना
- विधियों को आंतरिक स्थिति अद्यतन करें
- मार्टेन घटना निरंतरता और पुनरावृत्ति को नियंत्रित करता है
कमांड हैंडलर (Write Side)
स्ट्रीम में घटनाओं को जोड़कर कमांडों का प्रबंधन किया जाता है
// 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);
}
}
प्रवाह
- घटनाओं को फिर से बजाने के द्वारा समूह लोड करें (या नया बनाएँ
- कॉल व्यापार विधि (validates and returns event
- घटना को स्ट्रीम में जोड़ें
- परिवर्तन सहेजें
मार्टेन बाकी को संभालता है - घटनाओं को भंडारित करता है, ट्रिगरिंग प्रक्षेपण करता है
मॉडल और प्रक्षेपण पढ़ें
भविष्यवाणी घटनाओं को असामान्य पठन मॉडलों में बदलती है
// 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++;
}
}
मार्टेन's async डेइमन पृष्ठभूमि में घटनाओं को प्रक्रमित करता है और पठनीय मॉडल अद्यतन रखता है
डैपर के साथ क्वेरी साइड
अब हम अधिकतम निष्पादन के लिए Dapper का उपयोग करके पठनीय मॉडलों की क्वेरी करें
// Define queries
public record GetRecentBlogPostsQuery(
int Count,
bool PublishedOnly
) : IRequest<List<BlogPostListItemDto>>;
public record GetBlogPostByIdQuery(Guid Id) : IRequest<BlogPostDetailDto?>;
// DTOs for display
public class BlogPostListItemDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public int CommentCount { get; set; }
public bool IsPublished { get; set; }
}
public class BlogPostDetailDto
{
public Guid Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public string AuthorId { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? PublishedAt { get; set; }
public bool IsPublished { get; set; }
public List<CommentDto> Comments { get; set; } = new();
}
public class CommentDto
{
public Guid Id { get; set; }
public string Author { get; set; } = string.Empty;
public string Content { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// Query handlers
public class GetRecentBlogPostsHandler : IRequestHandler<GetRecentBlogPostsQuery, List<BlogPostListItemDto>>
{
private readonly string _connectionString;
public GetRecentBlogPostsHandler(IConfiguration config)
{
_connectionString = config.GetConnectionString("Marten")!;
}
public async Task<List<BlogPostListItemDto>> Handle(
GetRecentBlogPostsQuery request,
CancellationToken cancellationToken)
{
await using var connection = new NpgsqlConnection(_connectionString);
// Query the Marten-generated read model table
const string sql = @"
SELECT
bp.id AS Id,
bp.title AS Title,
u.name AS AuthorName,
bp.created_at AS CreatedAt,
bp.published_at AS PublishedAt,
bp.comment_count AS CommentCount,
bp.is_published AS IsPublished
FROM blog_post_read_models bp
LEFT JOIN users u ON bp.author_id = u.id
WHERE (@PublishedOnly = false OR bp.is_published = true)
ORDER BY
CASE WHEN bp.is_published THEN bp.published_at
ELSE bp.created_at
END DESC
LIMIT @Count";
var results = await connection.QueryAsync<BlogPostListItemDto>(
sql,
new
{
PublishedOnly = request.PublishedOnly,
Count = request.Count
});
return results.ToList();
}
}
public class GetBlogPostByIdHandler : IRequestHandler<GetBlogPostByIdQuery, BlogPostDetailDto?>
{
private readonly string _connectionString;
public GetBlogPostByIdHandler(IConfiguration config)
{
_connectionString = config.GetConnectionString("Marten")!;
}
public async Task<BlogPostDetailDto?> Handle(
GetBlogPostByIdQuery request,
CancellationToken cancellationToken)
{
await using var connection = new NpgsqlConnection(_connectionString);
const string sql = @"
SELECT
bp.id AS Id,
bp.title AS Title,
bp.content AS Content,
bp.author_id AS AuthorId,
u.name AS AuthorName,
bp.created_at AS CreatedAt,
bp.published_at AS PublishedAt,
bp.is_published AS IsPublished
FROM blog_post_read_models bp
LEFT JOIN users u ON bp.author_id = u.id
WHERE bp.id = @Id";
var post = await connection.QuerySingleOrDefaultAsync<BlogPostDetailDto>(
sql,
new { request.Id });
if (post == null)
return null;
// Get comments from event stream if needed
// Or maintain a separate comment read model
return post;
}
}
कैशिंग कोड नहीं
नियंत्रक
MediatR के साथ कंट्रोलर सुंदर रूप से सरल हैं
[ApiController]
[Route("api/[controller]")]
public class BlogPostsController : ControllerBase
{
private readonly IMediator _mediator;
public BlogPostsController(IMediator mediator)
{
_mediator = mediator;
}
[HttpGet]
public async Task<ActionResult<List<BlogPostListItemDto>>> GetRecent(
[FromQuery] int count = 10,
[FromQuery] bool publishedOnly = true)
{
var query = new GetRecentBlogPostsQuery(count, publishedOnly);
var results = await _mediator.Send(query);
return Ok(results);
}
[HttpGet("{id}")]
public async Task<ActionResult<BlogPostDetailDto>> GetById(Guid id)
{
var query = new GetBlogPostByIdQuery(id);
var result = await _mediator.Send(query);
if (result == null)
return NotFound();
return Ok(result);
}
[HttpPost]
public async Task<ActionResult<Guid>> Create([FromBody] CreateBlogPostCommand command)
{
var postId = await _mediator.Send(command);
return CreatedAtAction(nameof(GetById), new { id = postId }, postId);
}
[HttpPost("{id}/publish")]
public async Task<ActionResult> Publish(Guid id)
{
await _mediator.Send(new PublishBlogPostCommand(id));
return NoContent();
}
[HttpPut("{id}/title")]
public async Task<ActionResult> ChangeTitle(
Guid id,
[FromBody] ChangeBlogPostTitleCommand command)
{
if (id != command.BlogPostId)
return BadRequest();
await _mediator.Send(command);
return NoContent();
}
}
पूर्ण प्रवाह
यहाँ यह सब एक साथ कैसे काम करता है
sequenceDiagram
participant Client
participant Controller
participant MediatR
participant CommandHandler
participant Marten
participant EventStore
participant AsyncDaemon
participant ReadDB
participant QueryHandler
Note over Client,ReadDB: Write Operation
Client->>Controller: POST /api/blogposts
Controller->>MediatR: Send CreateBlogPostCommand
MediatR->>CommandHandler: Handle command
CommandHandler->>CommandHandler: Validate & create event
CommandHandler->>Marten: StartStream(event)
Marten->>EventStore: Append event
EventStore-->>Marten: Success
Marten-->>CommandHandler: Success
CommandHandler-->>Controller: Return ID
Controller-->>Client: 201 Created
Note over AsyncDaemon,ReadDB: Background Processing
EventStore->>AsyncDaemon: New event available
AsyncDaemon->>AsyncDaemon: Apply projection
AsyncDaemon->>ReadDB: Update read model
ReadDB-->>AsyncDaemon: Updated
Note over Client,ReadDB: Read Operation
Client->>Controller: GET /api/blogposts
Controller->>MediatR: Send Query
MediatR->>QueryHandler: Handle query
QueryHandler->>ReadDB: SELECT with Dapper
ReadDB-->>QueryHandler: Return data
QueryHandler-->>Controller: Return DTOs
Controller-->>Client: 200 OK
अंततः संगतता
एक बात समझने के लिए, एक घटना लिखने और पठन मॉडल अद्यतन करने के बीच एक छोटा विलंब है।
यदि आप को एक विशिष्ट ऑपरेशन के लिए तत्काल संगतता की आवश्यकता है
builder.Services.AddMarten(options =>
{
// This projection runs synchronously
options.Projections.Add<CriticalDataProjection>(ProjectionLifecycle.Inline);
// This projection runs async
options.Projections.Add<BlogPostProjection>(ProjectionLifecycle.Async);
});
या घटना स्ट्रीम को सीधे पढ़ने के लिए क्वेरी करें
var blogPost = await _session.Events.AggregateStreamAsync<BlogPost>(id);
भाग 2: आधे -Assed Approach | (|Cache Invalidation |)
सही, तो आप' उचित घटना सॉरसिंग के बारे में पढ़ा है और आप ' सोच रहे हैं कि M SK3 यह '\ एक बहुत काम है".\ Fair enough\M SK6\ यहाँ\MSC7\ यह informal CQRS\MST9\ दृष्टिकोण है कि अधिकतर टीम वास्तव में उपयोग करता है
सेटअप
- कमांड डाटाबेस में लिखते हैं (EF या Dapper का उपयोग करते हुए
- IMemoryCache या IDistributedCache से पठित क्वेरी
- कमांड लेखन के बाद प्रासंगिक कैश प्रविष्टियों को अमान्य बनाता है
- कोई घटना स्रोत नहीं
यह नहीं सही है CQRS
त्वरित उदाहरण
// Command handler
public class CreateBlogPostHandler : IRequestHandler<CreateBlogPostCommand, int>
{
private readonly ApplicationDbContext _context;
private readonly IMemoryCache _cache;
public async Task<int> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
{
var blogPost = new BlogPost
{
Title = request.Title,
Content = request.Content,
AuthorId = request.AuthorId,
PublishedDate = DateTime.UtcNow
};
_context.BlogPosts.Add(blogPost);
await _context.SaveChangesAsync(cancellationToken);
// Manual cache invalidation - this is the tedious bit
_cache.Remove("recent-posts");
_cache.Remove($"author-posts-{request.AuthorId}");
_cache.Remove($"post-{blogPost.Id}");
return blogPost.Id;
}
}
// Query handler
public class GetRecentPostsHandler : IRequestHandler<GetRecentPostsQuery, List<BlogPostDto>>
{
private readonly string _connectionString;
private readonly IMemoryCache _cache;
public async Task<List<BlogPostDto>> Handle(GetRecentPostsQuery request, CancellationToken cancellationToken)
{
var cacheKey = "recent-posts";
if (_cache.TryGetValue<List<BlogPostDto>>(cacheKey, out var cached))
return cached!;
// Cache miss - query with Dapper
using var connection = new NpgsqlConnection(_connectionString);
var posts = (await connection.QueryAsync<BlogPostDto>(
"SELECT id, title, author_name, published_date FROM blog_posts ORDER BY published_date DESC LIMIT 10"
)).ToList();
_cache.Set(cacheKey, posts, TimeSpan.FromMinutes(5));
return posts;
}
}
जब यह कार्य करता है
- जटिल लेखापरीक्षा आवश्यकताओं के बिना सरल अनुप्रयोग
- आप निष्पादन की जरूरत है, लेकिन कर सकते हैं-' घटना खोज जटिलता का औचित्य नहीं
- छोटी टीम जो नहीं है, ' नहीं सीखना चाहता है घटना खोज
- कुछ सेकंड की स्थिरता स्वीकार्य है
समस्याएं
कैश अमान्यीकरण कठिन है: एक कैश कुंजी गुम हो जाती है और आप अप्रचलित डेटा सेवा करते हैं
कोई लेखापरीक्षा ट्रेल नहीं: आपके पास केवल मौजूदा स्थिति है
कोई अस्थायी क्वेरी नहींक्या पूछ सकता है
मापनIMemoryCache के साथ, प्रत्येक सर्वर का अपना कैश है. सर्वर पर अद्यतन करें AM SK3 सर्वरी B अभी भी अप्रचलित डेटा रखता है . IDistributedCache इस समस्या को हल करता है लेकिन रेडिस जोड़ता है
संकुचित कupplingकमांड कैश कुंजियों से जोड़े जाते हैं
यह काम करता है-,, लेकिन आप व्यापार सरलता के लिए खो गए क्षमताओं के लिए ' कर रहे हैं।
भाग 3: Worst Idea - Event Sourcing | +| Manual Cache Invalidation
अब सचमुच भयानक विचार के लिए-Event Sourcing का उपयोग कर रहा है लेकिन साथ ही कैशों को हस्तचालित रूप से अवैध करने की कोशिश करता है
क्यों लोग यह कोशिश करते हैं
वे घटना खोजना चाहते हैं, ' का लेखापरीक्षा ट्रैक और अस्थायी क्वेरी,,, लेकिन वे ' के बारे में चिंता कर रहे हैं अंततः सुसंगतता के लिए.
यह न करें
क्यों यह भयावह है
You'have defeated the purpose: घटना सॉरसिंग पहले से ही परियोजनाओं के माध्यम से पठन मॉडल बनाता है
दोहरा जटिलता: अब आपके पास दो प्रणालियां हैं जो पठनीय मॉडलों को समक्रमित रखते हैं।
असंगत स्थिति: Marten's async डेइमन डाटाबेस को अद्यतन करता है।
हानिकारक लाभ: घटना सॉरसिंग' पूरी बात यह है कि भविष्यवाणी घटनाओं से व्युत्पन्न हैं
डिबगिंग नाइटमेयरक्या यह अप्रचलित डेटा है क्योंकि प्रक्षेपण नहीं चला रहे हैं
सही दृष्टिकोण
अगर आप को एग्रीमेंट सोर्सिंग की जरूरत है
- मार्टेन का उपयोग करें
- पठित मॉडल सीधे क्वेरी करें
- अंतिम संगतता स्वीकार करें (it's आमतौर पर ठीक है
- इनलाइन प्रक्षेपण का उपयोग करें यदि आप वास्तव में तुरंत संगतता की आवश्यकता है
यदि आप कर सकते हैं
- Don'event sourcing का उपयोग नहीं करें
- भाग से कैश -आधारित दृष्टिकोण का उपयोग करें
- या सिर्फ EF कोर के साथ पारंपरिक CRUD का उपयोग करें
दोनो को करने की कोशिश न करें. आप'Event Sourcing की जटिलता और हस्तचालित कैश अमान्यीकरण की कमजोरी के साथ समाप्त होंगे
क्या न करें का उदाहरण
// 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
}
}
सिर्फ मार्टेन का उपयोग करें
निष्कर्ष
तीन दृष्टिकोण
घटना सॉरसिंग के साथ उचित सीक्यूआरएस (MartenM SK1
जब उपयोग करें
- आपको पूर्ण लेखापरीक्षा ट्रेल की आवश्यकता है
- अस्थायी प्रश्न मूल्यवान हैं
- समृद्ध व्यापार तर्क के साथ जटिल डोमेन
- वित्तीय, एमएसके0, स्वास्थ्य, एम एसके1 या विनियमित प्रणालियां
जब प्रयोग न करें
- साधारण क्रूड अनुप्रयोग
- टीम में घटना की कमी है
- कोई लेखापरीक्षा आवश्यकता नहीं
कैचिंग के साथ अनौपचारिक सीक्यूआरएस
जब उपयोग करें
- पठन पर कार्यक्षमता वृद्धि की आवश्यकता है
- लेखापरीक्षा ट्रेल आवश्यक नहीं है
- घटना की खोज से सरल
- अंततः सटीकता स्वीकार्य
जब प्रयोग न करें
- लेखापरीक्षा ट्रेल की आवश्यकता है
- आवधिक प्रश्नों की आवश्यकता है
- कैश अमान्यीकरण जटिलता लाभों से अधिक है
घटना खोज + मैनुअल कैश अमान्यीकरण
जब उपयोग करें
- कभी नहीं
अपने उपयोग के लिए सही उपकरण चुनें
अधिकतर अनुप्रयोगों के लिए , पारंपरिक CRUD से आरंभ करें यहाँWhen you hit performance issues, add caching. Only reach for full Event Sourcing when you genuinely need its specific capabilities
आगे पढ़ने
- मार्टेन दस्तावेज़ीकरण Excellent docs, well worth reading
- EventStoreDB - वैकल्पिक घटना भंडार यदि Postgres नहीं है
- डैपर दस्तावेज़ीकरण - फास्ट एसक्यूएल क्वेरी
- मीडियाटीआर - कमांड
- सीक्यूआरएस पर माइक्रोसॉफ्ट - आधिकारिक मार्गदर्शन
- मेरे अस्तित्व फ्रेमवर्क श्रृंखला आरंभ यहाँ - पारंपरिक दृष्टिकोण