For example: I want to delete an item or return 404 if it doesn't exist from a controller action. Am I breaking any rules? Commands are still separated from queries.
[ApiController]
public class PostsController : ControllerBase
{
[HttpDelete("/posts/{postId}")]
public async Task<IActionResult> DeletePost(Guid postId)
{
var postDTO = await _mediator.Send(new GetPostByIdQuery(postId)); // query
if (postDTO == null)
{
return NotFound();
}
await _mediator.Send(new DeletePostCommand(postId)); // command
return NoContent();
}
}
CodePudding user response:
Am I breaking any rules?
Not really specific to CQRS, but maybe?
If you are in the context of a controller, then you are in a world where we can reasonably expect that lots of different requests are being handled concurrently.
So we have to be aware that unlocked data can change out from under us while our process is running.
var postDTO = await _mediator.Send(new GetPostByIdQuery(postId)); // query
// now some other thread comes along, and does work that
// changes the result we would get from GetPostByIdQuery
// when we resume, our if test takes the wrong branch....
if (postDTO == null)
{
return NotFound();
}
await _mediator.Send(new DeletePostCommand(postId)); // command
For something like a Delete, where we suspect that interest in a given postId is only happening in one place, then concurrency collisions are going to be rare, and maybe we don't need to worry about it.
In general... there's the potential for a real problem here.
Part of the problem here: your design here violates Tell, don't ask
you should endeavor to tell objects what you want them to do; do not ask them questions about their state, make a decision, and then tell them what to do.
Better might be a design that looks like:
var deleteResult = await _mediator.Send(new DeletePostCommand(postId)); // command
if (deleteResult == null)
{
return NotFound();
}
return NoContent();
This allows you to ensure that the read and the write are happening within the same transaction, which means you get the locking right.
CodePudding user response:
Are CQRS principles violated ?
CQRS is Command Query Responsibility Segregation. It means that your system should segregate commands and queries in two different subsystems, and clients should either interact with one or another, but not both at the same time. Now, there are two cases here :
CQRS as internal API architecture
If CQRS is implemented inside your API, then you are not violating any CQRS principles proper. Your API controller is the CQRS client, and it either interacts with the query subsystem or the command subsystem. However, if you do not want state integrity problems, your command subsystem must verify the post existence himself and not rely on the client having checked for post existence prior. Also this validation should not depend on your query subsystem (see below).
CQRS as external API architecture
Yes, you are breaking a rule here.
Whether you segregate your queries and commands at controller level or controller action level is an implementation details, but clients should atomically interact, either with your command subsystem, or your query subsystem. If your client sends a single HTTP request which results in execution of code in both subsystems, then commands and queries are not segregated and your system violates CQRS principles.
Whether this is good or bad, relevant or not, is up to you, no judgement.
Why using your query subsystem is bad
What does the user wants the system to do ? Does it want to know about the post existence ? No. It wants to delete a post. This means your user wants to alter the system's state. A CQRS client can perfectly query the system prior to sending commands in order to limit stress on the command subsystem, this is a good way to secure performance. However, your system MUST NOT rely on the query subsystem to ensure state integrity. Why ?
One idea behind CQRS was that, if you validate application state on every write operation, you do not need to validate application state for read operations. This allowed to design application with three subsystems :
- A command subsystem, designed for integrity first and write performance second
- A query subsystem, designed for read performance only
- An eventual consistency subsystem, which propagates application state changes to the query model
In a CQRS applications, the query subsystem does not need to know about the application state, business rules, or anything integrity-related. It has a persistence storage which holds a query-state, and everything in that system is optimized for read performance. Since you don't require integrity in that subsystem, you can accept violations in the query-model, as long as consistency with application state can be enforced eventually. This also means the query-model is updated after the command subsystem.
Since application state changes at different times on both subsystem, you need to decide which change is considered the source of truth for your application state. Since application state integrity is enforced by the command subsystem, and query subsystem does not know about integrity, this cannot be the latter. That is why the command subsystem model is considered the source of truth regarding application state in a CQRS application. This means, you cannot trust the query subsystem to make decisions about the application state.
How to do it properly
Your command subsystem is the part of your CQRS system that is responsible for application state integrity and business rules enforcement. This means this subsystem is responsible to maintain knowledge of the application state so it can validate commands, and should persist that model for continuity purposes. There are two classic approaches of modeling your command persistence schema:
You can use a traditional persistence design, using a technology such as an ORM to store your object modeled application state to database / disk. Command subsystems usually use RDBMS for persistence since this is the most secure way to do so, integrity wise. This storage can be easily read from or written to, like any classic layered application. The reads are not considered queries in the sense of CQRS terminology, simply a domain model rehydration. You would implement this like that:
using(var tx = new context.Database.BeginTransaction());
var entity = context.Posts.Find(postId);
if(entity == null) { return NotFound(); }
context.Posts.Remove(entity);
context.SaveChanges();
NotifyEventualConsistency();
tx.Commit();
return NoContent();
An alternative is to use event sourcing, which consists of storing events rather than state. However, the command subsystem is still responsible for rehydrating the application state and ensuring business integrity. With event sourcing, you can have a "classic" object domain model, which usually involves a lot of "translation" between the persistence layer and the domain model. Or you can also rewrite your business rules based on events invariants. In that case, you could imagine implementing your business rule like this:
using(var tx = new context.Database.BeginTransaction());
if(PostStatus.Created != context.PostStatusChanged.AsNoTracking()
.OrderByDesc(event => event.Date)
.Where(event => event.PostId == postId)
.Select(event => event.NewState)
.FirstOrDefault())
{
return NotFound();
}
context.PostStatusChanged.Add(new PostStatusChanged {
PostId = postId,
NewState = PostStatus.Deleted
});
context.SaveChanges();
NotifyEventualConsistency();
tx.Commit();
return NoContent();