ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 3 MediatR and FluentValidation)

Now that we have done all the setup that we need from previous article Private: ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 2 Auditing in EF Core with CreatedBy and LastModifiedBy). We are now ready create our API.

We will follow CQRS (Command Query Responsibility Segregation) approach in handling our data. We will use MediatR to handle the mappings of request to response and FluentValidation for validation.

Using Nuget Package Manager install the following libraries inInvoiceManagementApp.Application.

  • FluentValidation
  • FluentValidation.DependencyInjectionExtensions
  • MediatR.Extensions.Microsoft.DependencyInjection
Advertisements

In InvoiceManagementApp.Application create a new folder and call it Invoices. Then inside the Invoices folder create a new folder called Commands.

Inside the Commands folder add a new class CreateInvoiceCommand

public class CreateInvoiceCommand: IRequest<int>
    {
        public CreateInvoiceCommand()
        {
            this.InvoiceItems = new List<InvoiceItemVm>();
        }
        public string InvoiceNumber { get; set; }
        public string Logo { get; set; }
        public string From { get; set; }
        public string To { get; set; }
        public DateTime Date { get; set; }
        public string PaymentTerms { get; set; }
        public DateTime? DueDate { get; set; }
        public double Discount { get; set; }
        public DiscountType DiscountType { get; set; }
        public double Tax { get; set; }
        public TaxType TaxType { get; set; }
        public double AmountPaid { get; set; }
        public IList<InvoiceItemVm> InvoiceItems { get; set; }
    }

CreateInvoiceCommand looks like an ordinary class with properties, except it inherit IRequest from MediatR. The IRequest<int> in CreateInvoiceCommand means that this class is a request that will return an int data type.

Advertisements

Instead of using InvoiceItem in CreateInvoiceCommand, we will replace it of a view model. Inside the Invoices folder create a new folder called ViewModels.

In ViewModels folder add a class InvoiceItemVm

public class InvoiceItemVm
    {
        public long Id { get; set; }
        public string Item { get; set; }
        public double Quantity { get; set; }
        public double Rate { get; set; }
        public double Amount
        {
            get
            {
                return Quantity * Rate;
            }
        }
    }

Add another folder in Invoices folder and call it Handlers. Then create a new class CreateInvoiceCommandHandler

public class CreateInvoiceCommandHandler : IRequestHandler<CreateInvoiceCommand, int>
    {
        private readonly IApplicationDbContext _context;

        public CreateInvoiceCommandHandler(IApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<int> Handle(CreateInvoiceCommand request, CancellationToken cancellationToken)
        {
            var entity = new Invoice
            {
                AmountPaid = request.AmountPaid,
                Date = request.Date,
                DueDate = request.DueDate,
                Discount = request.Discount,
                DiscountType = request.DiscountType,
                From = request.From,
                InvoiceNumber = request.InvoiceNumber,
                Logo = request.Logo,
                PaymentTerms = request.PaymentTerms,
                Tax = request.Tax,
                TaxType = request.TaxType,
                To = request.To,
                InvoiceItems = request.InvoiceItems.Select(i => new InvoiceItem
                {
                    Item = i.Item,
                    Quantity = i.Quantity,
                    Rate = i.Rate
                }).ToList()
            };

            _context.Invoices.Add(entity);
            await _context.SaveChangesAsync(cancellationToken);
            return entity.Id;
        }
    }

The handler contains the logic on what we want to do in the request. In this case we save the invoice data in the database.

Advertisements

Now that we have a request and response, we can now use this in API, but how about data validation.

For data validation we will make use of MediatR Pipeline behavior. The pipeline behavior is like the events in MediatR. Using Pipeline behavior we can run some logic before the handlers handle the request.

Add a folder inside of Common folder and call it Behaviors. Then inside Behaviors folder add a new class and call it ValidationBehavior.

public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
	{
		private readonly IEnumerable<IValidator<TRequest>> _validators;

		public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
		{
			_validators = validators;
		}

		public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
		{
			var context = new ValidationContext(request);
			var failures = _validators
				.Select(v => v.Validate(context))
				.SelectMany(result => result.Errors)
				.Where(f => f != null)
				.ToList();

			if (failures.Count != 0)
			{
				throw new ValidationException(failures);
			}

			return next();
		}
	}

Go back to Invoices folder then add a new folder called Validators. Inside the Validators folder add a new class CreateInvoiceCommandValidator. This class will inherit AbstractValidator and we will use this to set validation rule for CreateInvoiceCommand.

public class CreateInvoiceCommandValidator : AbstractValidator<CreateInvoiceCommand>
    {
        public CreateInvoiceCommandValidator()
        {
            RuleFor(v => v.AmountPaid).NotNull();
            RuleFor(v => v.Date).NotNull();
            RuleFor(v => v.From).NotEmpty().MinimumLength(3);
            RuleFor(v => v.To).NotEmpty().MinimumLength(3);
            RuleFor(v => v.InvoiceItems).SetValidator(new MustHaveInvoiceItemPropertyValidator());
        }
    }

For InvoiceItems property we need to create a custom property validator to check if the list is not null and not empty.

Add a new class in Validators folder and call it MustHaveInvoiceItemPropertyValidator

public class MustHaveInvoiceItemPropertyValidator : PropertyValidator
    {
        public MustHaveInvoiceItemPropertyValidator()
            : base("Property {PropertyName} should not be an empty list.")
        {
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            var list = context.PropertyValue as IList<InvoiceItemVm>;
            return list != null && list.Any();
        }
    }
Advertisements

Add a new class in the root of InvoiceManagementApp.Application and call it DependencyInjection. We will register our Application layer dependency injection in this class.

public static class DependecyInjection
    {
        public static IServiceCollection AddApplication(this IServiceCollection services)
        {
            services.AddMediatR(Assembly.GetExecutingAssembly());
            services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
            services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
            return services;
        }
    }

Then add the Application layer dependency injection in the Startup services of InvoiceManagementApp.Api.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplication();
            services.AddInfrastructure(Configuration);

            services.AddScoped<ICurrentUserService, CurrentUserService>();

            services.AddControllersWithViews();
            services.AddRazorPages();

            // In production, the React files will be served from this directory
            services.AddSpaStaticFiles(configuration =>
            {
                configuration.RootPath = "ClientApp/build";
            });
        }
Advertisements

Add a new controller in InvoiceManagementApp.Api called ApiController. We will use this as our base controller for controllers that will use MediatR.

[ApiController]
    [Route("api/[controller]")]
    public abstract class ApiController : ControllerBase
    {
        private IMediator _mediator;

        protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
    }

Add a new controller called InvoicesController. This will inherit ApiController.

Then add an action in InvoicesController to create an invoice.

[Authorize]
    public class InvoicesController : ApiController
    {

        [HttpPost]
        public async Task<ActionResult<int>> Create(CreateInvoiceCommand command)
        {
            return await Mediator.Send(command);
        }
    }

The action accepts a CreateInvoiceCommand then using the function Mediator.Send the request will be sent to MediatR. Then MediatR will handle the request.

Run the application. Login to the app then click the fetch data. In the network tab, copy the authorization token.

Open postman, click headers tab. Add Authorization header then paste the token in the value column.

Now try to post a new invoice. If everything works, it should return the id of the new invoice.

Advertisements

To create a Query request. Create a new folder called Queries inside the Invoices folder in InvoiceManagementApp.Application.

In Queries folder add a new class GetUserInvoicesQuery

public class GetUserInvoicesQuery : IRequest<IList<InvoiceVm>>
    {
        public string User { get; set; }        
    }

Add a new class in ViewModels folder inside the Invoices folder. Name the class InvoiceVm.

public class InvoiceVm
    {
        public InvoiceVm()
        {
            this.InvoiceItems = new List<InvoiceItemVm>();
        }

        public long Id { get; set; }
        public string InvoiceNumber { get; set; }
        public string Logo { get; set; }
        public string From { get; set; }
        public string To { get; set; }
        public DateTime Date { get; set; }
        public string PaymentTerms { get; set; }
        public DateTime? DueDate { get; set; }
        public DiscountType DiscountType { get; set; }
        public double Discount { get; set; }
        public TaxType TaxType { get; set; }
        public double Tax { get; set; }
        public double AmountPaid { get; set; }
        public IList<InvoiceItemVm> InvoiceItems { get; set; }
    }

Then add new class in Handlers folder GetUserInvoicesQueryHandler

public class GetUserInvoicesQueryHandler : IRequestHandler<GetUserInvoicesQuery, IList<InvoiceVm>>
    {
        private readonly IApplicationDbContext _context;

        public GetUserInvoicesQueryHandler(IApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<IList<InvoiceVm>> Handle(GetUserInvoicesQuery request, CancellationToken cancellationToken)
        {
            var invoices = await _context.Invoices.Include(i => i.InvoiceItems)
                .Where(i => i.CreatedBy == request.User).ToListAsync();
            var vm = invoices.Select(i => new InvoiceVm
            {
                AmountPaid = i.AmountPaid,
                Date = i.Date,
                Discount = i.Discount,
                DiscountType = i.DiscountType,
                DueDate = i.DueDate,
                From = i.From,
                Id = i.Id,
                InvoiceNumber = i.InvoiceNumber,
                Logo = i.Logo,
                PaymentTerms = i.PaymentTerms,
                Tax = i.Tax,
                TaxType = i.TaxType,
                To = i.To,
                InvoiceItems = i.InvoiceItems.Select(i => new InvoiceItemVm
                {
                    Id = i.Id,
                    Item = i.Item,
                    Quantity = i.Quantity,
                    Rate = i.Rate
                }).ToList()
            }).ToList();
            return vm;
        }
    }

Add Get action in InvoicesController.

[Authorize]
    public class InvoicesController : ApiController
    {
        private readonly ICurrentUserService _currentUserService;

        public InvoicesController(ICurrentUserService currentUserService)
        {
            _currentUserService = currentUserService;
        }

        [HttpPost]
        public async Task<ActionResult<int>> Create(CreateInvoiceCommand command)
        {
            return await Mediator.Send(command);
        }

        [HttpGet]
        public async Task<IList<InvoiceVm>> Get()
        {
            return await Mediator.Send(new GetUserInvoicesQuery { User = _currentUserService.UserId });
        }
    }

Using Postman try to send a get request in invoices end point. You also need to add authorization header for get request.

Advertisements

Now we have a command and query request using MediatR. There are some things we can improve in this code.

For example instead of using the command as a parameter in actions, we can use a view model then change it to our command before sending it to MediatR. The problem is our CreateInvoiceCommand have a lot of properties, so mapping them one by one might be a time consuming and can make our code look bulky.

To resolve that, we can use AutoMapper. In the next article ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 4 AutoMapper – Map object properties to another object ) we will improve our code using AutoMapper.

Advertisements

RELATED ARTICLES:

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 1)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 2 Auditing in EF Core with CreatedBy and LastModifiedBy)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 3 MediatR and FluentValidation)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 4 AutoMapper – Map object properties to another object)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 5 NSwag – Setting up Swagger and Auto generate API client code)

ASP.NET Core 3.1 Clean Architecture – Invoice Management App (Part 6 React – How To Convert ReactJs To Typescript)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s