Edit action CQRS pattern using MediatR

Viewed 1127

I have a .Net Core 3.1 MVC application and I am trying to use CQRS pattern. I am new in MediatR and CQRS pattern.

My commmand/query structure like that:

enter image description here

  • Categories
    • Commands
      • DeleteCategory
      • UpsertCategory
    • Queries
      • GetCategoriesList
        • CategoryListVm
        • CategoryDto
        • GetCategoriesListQuery
        • GetCategoriesListQueryHandler

I want to use same view to create and update operation because my category has only two properties. (One of them is Id)

My CategoryController.cs file

[HttpGet]
    public IActionResult Upsert(long? id) {
        if (id == null) {
            //Create new, working correctly.
            return View();
        }
        //Update existing not working.
        return View(new UpsertCategoryCommand() { Id = id });
    }

    [HttpPost]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesDefaultResponseType]
    public async Task<IActionResult> Upsert(UpsertCategoryCommand command) {
        if (ModelState.IsValid) {
            await Mediator.Send(command);
            return RedirectToAction(nameof(Index));
        } else {
            return View(command);
        }
    }

My Category/Index view call upsert method in here.

<tbody>
        @foreach (var item in Model.Categories) {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.ActionLink("Edit", "Upsert", new { Id = item.Id }) |
                    @Html.ActionLink("Delete", "Delete", new { Id = item.Id})
                </td>
            </tr>
        }
    </tbody>

I want to send only Id information to controller and then how/where I map/get to UpsertCategoryCommand.

All other models:

    //UpsertCategoryCommand.cs
    namespace ...Categorys.Commands.UpsertCategory {
        public class UpsertCategoryCommand : IRequest<long> {
            public long? Id { get; set; }

            public string Name { get; set; }

            public class UpsertCategoryCommandHandler : IRequestHandler<UpsertCategoryCommand, long> {
                private readonly ITestDbContext _context;

                public UpsertCategoryCommandHandler(ITestDbContext context) {
                    _context = context;
                }

                public async Task<long> Handle(UpsertCategoryCommand request, CancellationToken cancellationToken) {
                    Category entity;

                    if (request.Id.HasValue) {
                        entity = await _context.Categories.FindAsync(request.Id.Value);
                    } else {
                        entity = new Category();

                        _context.Categories.Add(entity);
                    }

                    entity.Name = request.Name;

                    await _context.SaveChangesAsync(cancellationToken);

                    return entity.Id;
                }
            }
        }
    }

    //UpsertCategoryCommandValidator.cs
    namespace ...Categories.Commands.UpsertCategory {
        public class UpsertCategoryCommandValidator : AbstractValidator<UpsertCategoryCommand> {
            private readonly ITestDbContext _context;

            public UpsertCategoryCommandValidator(ITestDbContext context) {
                _context = context;

                RuleFor(x => x.Name).MaximumLength(100).NotEmpty(); 
                RuleFor(x => x.Name)
                    .Must(UniqueName)
                    .WithMessage("Category name must be unique."); ;
            }

            private bool UniqueName(UpsertCategoryCommand category, string name) {
                var dbCategory = _context.Categories
                                    .Where(x => x.Name.ToLower() == name.ToLower())
                                    .SingleOrDefault();

                if (dbCategory == null)
                    return true;

                return dbCategory.Id == category.Id;
            }
        }
    }

    //CategoryDto.cs
    namespace ...Categories.Queries.GetCategoryList {
        public class CategoryDto : IMapFrom<Category> {
            public long Id { get; set; }

            public string Name { get; set; }

            public void Mapping(Profile profile) {
                profile.CreateMap<Category, CategoryDto>();
            }
        }
    }

What is the best practies to doing same page for create and edit and also mapping? Could I use CategoryDto in my commands? Define to any common Dto for commands and queries, is good?

1 Answers

I would create separate commands for CreateCategory and UpdateCategory, to be more clear about the intent of the user. I also think the response from these commands should be separate types and try to avoid reusing classes between the commands.

I would also only include the really necessary fields in each command, and not try to reuse the CategoryDto in the various commands.

So you would have a CreateCategoryReponse and UpdateCategoryResponse type. I also think Jimmy discussed this in one of his more recent talks.

Related