Introduction
In modern application development, querying data is essential, but equally important is the ability to create, update, and delete entities. This article will focus on how to handle these operations using commands within a feature-based architecture, leveraging MediatR and AutoMapper. We will walk through the code for creating an event and extend that to updating and deleting events.
To understand the core principles behind the structure of our system, you may want to explore the following foundational concepts:
CQRS and SOLID Principles: My article on Enhancing .NET Applications with CQRS and SOLID Principles explains the benefits of CQRS and how it aligns with SOLID principles.
The Mediator Pattern: For a deeper dive into how the Mediator pattern simplifies communication between components, refer to my post on Understanding the Mediator Pattern in .NET.
AutoMapper: Finally, if you are curious about how and when to use AutoMapper, take a look at AutoMapper in .NET: When to Use It and When to Avoid It.
Creating an Event with CreateEventCommand
Let’s begin by looking at how to create an event in our application.
Step 1: Define the CreateEventCommand
The first step is to define a command class. The command is a message that encapsulates all the data needed to create the event. We use MediatR's IRequest
interface to wrap this command.
public class CreateEventCommand : IRequest<Guid>
{
public string Name { get; set; }
public int Price { get; set; }
public string Artist { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public Guid CategoryId { get; set; }
public override string ToString()
{
return $"Event name: {Name}; Price: {Price}; By: {Artist}; On: {Date.ToShortDateString()}; Description: {Description}";
}
}
Here, CreateEventCommand
contains the properties for the new event. It implements IRequest<Guid>
, which indicates that the command will return the GUID of the newly created event.
Step 2: Handle the Command with CreateEventCommandHandler
Next, we need a handler to process the command and create the event in the data store. The handler implements IRequestHandler<CreateEventCommand, Guid>
:
public class CreateEventCommandHandler : IRequestHandler<CreateEventCommand, Guid>
{
private readonly IEventRepository _eventRepository;
private readonly IMapper _mapper;
public CreateEventCommandHandler(IEventRepository eventRepository, IMapper mapper)
{
_eventRepository = eventRepository;
_mapper = mapper;
}
public async Task<Guid> Handle(CreateEventCommand request, CancellationToken cancellationToken)
{
var eventEntity = _mapper.Map<Event>(request);
await _eventRepository.AddAsync(eventEntity);
return eventEntity.EventId;
}
}
In this handler:
- The incoming command is mapped to an
Event
entity using AutoMapper. - The event is saved to the repository using the
AddAsync
method. - Finally, the handler returns the
GUID
of the newly created event.
Step 3: Mapping with AutoMapper
Since we are mapping from the CreateEventCommand
to an Event
entity, we need to define the mapping in our AutoMapper profile:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<CreateEventCommand, Event>();
}
}
This ensures that AutoMapper knows how to translate between the command and the entity.
Updating an Event with UpdateEventCommand
Next, let's look at how we can update an event.
Step 1: Define the UpdateEventCommand
For updating an event, we create another command:
public class UpdateEventCommand : IRequest
{
public Guid EventId { get; set; }
public string Name { get; set; }
public int Price { get; set; }
public string Artist { get; set; }
public DateTime Date { get; set; }
public string Description { get; set; }
public string ImageUrl { get; set; }
public Guid CategoryId { get; set; }
}
Unlike the creation command, this command includes the EventId of the event that needs to be updated. In this case, no return type is needed since updates typically do not return a value.
Step 2: Handle the Command with UpdateEventCommandHandler
Here’s how we implement the handler for updating an event:
public class UpdateEventCommandHandler : IRequestHandler<UpdateEventCommand>
{
private readonly IEventRepository _eventRepository;
private readonly IMapper _mapper;
public UpdateEventCommandHandler(IEventRepository eventRepository, IMapper mapper)
{
_eventRepository = eventRepository;
_mapper = mapper;
}
public async Task<Unit> Handle(UpdateEventCommand request, CancellationToken cancellationToken)
{
var eventEntity = await _eventRepository.GetByIdAsync(request.EventId);
if (eventEntity == null) throw new NotFoundException(nameof(Event), request.EventId);
_mapper.Map(request, eventEntity);
await _eventRepository.UpdateAsync(eventEntity);
return Unit.Value;
}
}
In this handler:
- The event is fetched from the repository by its
EventId
. - The command is mapped onto the existing event entity using AutoMapper.
- The updated event is saved back to the repository using
UpdateAsync
.
Deleting an Event with DeleteEventCommand
Finally, let’s implement the logic to delete an event.
Step 1: Define the DeleteEventCommand
We only need to pass the EventId
of the event to be deleted:
public class DeleteEventCommand : IRequest
{
public Guid EventId { get; set; }
}
Step 2: Handle the Command with DeleteEventCommandHandler
The handler for deleting an event is simple:
public class DeleteEventCommandHandler : IRequestHandler<DeleteEventCommand>
{
private readonly IEventRepository _eventRepository;
public DeleteEventCommandHandler(IEventRepository eventRepository)
{
_eventRepository = eventRepository;
}
public async Task<Unit> Handle(DeleteEventCommand request, CancellationToken cancellationToken)
{
var eventEntity = await _eventRepository.GetByIdAsync(request.EventId);
if (eventEntity == null) throw new NotFoundException(nameof(Event), request.EventId);
await _eventRepository.DeleteAsync(eventEntity);
return Unit.Value;
}
}
This handler:
- Retrieves the event by its
EventId
. - Deletes the event using
DeleteAsync
if found.
Validation and Return Types
While we focused on the core logic, it’s important to note that validation is a crucial part of handling commands. You can easily integrate FluentValidation to validate incoming commands before processing them.
In terms of return types, you can choose to return a simple boolean
for success, or more detailed information, such as the GUID
of a newly created entity. For updates and deletions, returning nothing (Unit
) is common practice when the operation is successful.
Conclusion
In this article, we walked through how to create, update, and delete entities using commands in a feature-based architecture. By organizing our code into distinct features, and leveraging MediatR for command handling, we achieve a clean, modular structure that is easy to maintain and extend.
With these patterns in place, your application will be well-structured and scalable, capable of handling more complex business logic as it evolves.
Top comments (0)