DEV Community

mohamed Tayel
mohamed Tayel

Posted on

What is Clean Architecture: Part 12-Creating, Updating, and Deleting Entities Using Commands

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:


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}";
     }
 }
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
 }
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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; }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)