C
C#5mo ago
baristaner

Implementing MediatR

How can i implement MediatR library to redirect the request relevant Service class (like GenerateCodeService, CreateProductService) And i don't wanna use CQRS pattern
35 Replies
Pobiega
Pobiega5mo ago
Makes no sense to use mediatr or similar if you're gonna use service classes anyways MediatR works fine without an explicit CQRS split (commands vs queries), but it still relies heavily on the idea that a request has a handler
baristaner
baristaner5mo ago
My folder structure for this thing is like this
|-- Features
| |-- Products
| |-- Add
| |-- AddProductCommand.cs
| |-- AddProductCommandHandler.cs
| |-- Validators
| |-- ProductValidator.cs
|-- Features
| |-- Products
| |-- Add
| |-- AddProductCommand.cs
| |-- AddProductCommandHandler.cs
| |-- Validators
| |-- ProductValidator.cs
ProductService.cs
c#
using fastwin.Entities;
using fastwin.Interfaces;
using FluentValidation;
using System.Diagnostics;

namespace fastwin.Services
{
public class ProductService
{
private readonly IRepository<Product> _productRepository;
private readonly IValidator<Product> _productValidator;

public ProductService(IRepository<Product> productRepository, IValidator<Product> productValidator)
{
_productRepository = productRepository;
_productValidator = productValidator;
}

public async Task<Product> AddProductAsync(Product product)
{
try
{
await _productValidator.ValidateAndThrowAsync(product);
await _productRepository.AddAsync(product);
Debug.WriteLine("Product added successfully in the service.");
return product;
}
catch (ValidationException ex)
{
Debug.WriteLine($"Validation failed: {ex.Message}");
throw; // Rethrow the exception to propagate it up
}
catch (Exception ex)
{
Debug.WriteLine($"Error adding product: {ex.Message}");
throw; // Rethrow the exception to propagate it up
}
}



}
}
c#
using fastwin.Entities;
using fastwin.Interfaces;
using FluentValidation;
using System.Diagnostics;

namespace fastwin.Services
{
public class ProductService
{
private readonly IRepository<Product> _productRepository;
private readonly IValidator<Product> _productValidator;

public ProductService(IRepository<Product> productRepository, IValidator<Product> productValidator)
{
_productRepository = productRepository;
_productValidator = productValidator;
}

public async Task<Product> AddProductAsync(Product product)
{
try
{
await _productValidator.ValidateAndThrowAsync(product);
await _productRepository.AddAsync(product);
Debug.WriteLine("Product added successfully in the service.");
return product;
}
catch (ValidationException ex)
{
Debug.WriteLine($"Validation failed: {ex.Message}");
throw; // Rethrow the exception to propagate it up
}
catch (Exception ex)
{
Debug.WriteLine($"Error adding product: {ex.Message}");
throw; // Rethrow the exception to propagate it up
}
}



}
}
ProductValidator.cs
c#
using fastwin.Entities;
using FluentValidation;

namespace fastwin.Features.Products.Add.Validators
{
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product Name is required.")
.MaximumLength(3)
.WithMessage("Product Name Only Can Be 3 Characters Length");

RuleFor(product => product.Category)
.Must(BeAValidEnumValue)
.WithMessage("Invalid Category, please provide a valid category name.");
}

private bool BeAValidEnumValue(Category category)
{
return Enum.IsDefined(typeof(Category), category);
}
}
}
c#
using fastwin.Entities;
using FluentValidation;

namespace fastwin.Features.Products.Add.Validators
{
public class ProductValidator : AbstractValidator<Product>
{
public ProductValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Product Name is required.")
.MaximumLength(3)
.WithMessage("Product Name Only Can Be 3 Characters Length");

RuleFor(product => product.Category)
.Must(BeAValidEnumValue)
.WithMessage("Invalid Category, please provide a valid category name.");
}

private bool BeAValidEnumValue(Category category)
{
return Enum.IsDefined(typeof(Category), category);
}
}
}
AddProductCommand.cs
c#
using fastwin.Requests;
using MediatR;

public class AddProductCommand : IRequest<Unit>
{
public ProductRequestDto ProductDto { get; }

public AddProductCommand(ProductRequestDto productDto)
{
ProductDto = productDto;
}
}
c#
using fastwin.Requests;
using MediatR;

public class AddProductCommand : IRequest<Unit>
{
public ProductRequestDto ProductDto { get; }

public AddProductCommand(ProductRequestDto productDto)
{
ProductDto = productDto;
}
}
Pobiega
Pobiega5mo ago
Your structure doesnt include a ProductService.cs
baristaner
baristaner5mo ago
It does but i just didn't sent it
Pobiega
Pobiega5mo ago
if you just rename your "service" to AddProductCommandHandler you're doing things correctly.
baristaner
baristaner5mo ago
The thing is wait i'll explain
Pobiega
Pobiega5mo ago
do not mix and match services with command handlers. thats just silly.
baristaner
baristaner5mo ago
AddProductCommandHandler.cs
c#
using fastwin.Entities;
using fastwin.Requests;
using fastwin.Services;
using MediatR;

public class AddProductCommandHandler : IRequestHandler<AddProductCommand, Unit>
{
private readonly ProductService _productService;

public AddProductCommandHandler(ProductService productService)
{
_productService = productService;
}

public async Task<Unit> Handle(AddProductCommand request, CancellationToken cancellationToken)
{
var product = ConvertToProduct(request.ProductDto);

_productService.AddProductAsync(product);


return Unit.Value;
}

private Product ConvertToProduct(ProductRequestDto productRequest)
{
return new Product
{
Name = productRequest.Name,
Description = productRequest.Description,
Category = Enum.Parse<Category>(productRequest.Category),
};
}
}
c#
using fastwin.Entities;
using fastwin.Requests;
using fastwin.Services;
using MediatR;

public class AddProductCommandHandler : IRequestHandler<AddProductCommand, Unit>
{
private readonly ProductService _productService;

public AddProductCommandHandler(ProductService productService)
{
_productService = productService;
}

public async Task<Unit> Handle(AddProductCommand request, CancellationToken cancellationToken)
{
var product = ConvertToProduct(request.ProductDto);

_productService.AddProductAsync(product);


return Unit.Value;
}

private Product ConvertToProduct(ProductRequestDto productRequest)
{
return new Product
{
Name = productRequest.Name,
Description = productRequest.Description,
Category = Enum.Parse<Category>(productRequest.Category),
};
}
}
ProductController.cs
c#
namespace fastwin.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IMediator _mediator;

public ProductController(IMediator mediator)
{
_mediator = mediator;
}

[HttpPost("add-product")]
public async Task<IActionResult> AddProduct([FromBody] ProductRequestDto productDto)
{
try
{
var result = await _mediator.Send(new AddProductCommand(productDto));

return Ok(result);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
}
c#
namespace fastwin.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductController : ControllerBase
{
private readonly IMediator _mediator;

public ProductController(IMediator mediator)
{
_mediator = mediator;
}

[HttpPost("add-product")]
public async Task<IActionResult> AddProduct([FromBody] ProductRequestDto productDto)
{
try
{
var result = await _mediator.Send(new AddProductCommand(productDto));

return Ok(result);
}
catch (Exception ex)
{
return StatusCode(500, $"Internal server error: {ex.Message}");
}
}
}
}
It returns ok but it's empty and it gives this error on console
fail: Microsoft.EntityFrameworkCore.Database.Connection[20004]
An error occurred using the connection to database 'CodeGeneratorDB' on server 'TR-ADN-B5CN4D3\SQLEXPRESS01'.
fail: 1/4/2024 16:12:26.592 RelationalEventId.ConnectionError[20004] (Microsoft.EntityFrameworkCore.Database.Connection)
An error occurred using the connection to database 'CodeGeneratorDB' on server 'TR-ADN-B5CN4D3\SQLEXPRESS01'.
fail: Microsoft.EntityFrameworkCore.Database.Connection[20004]
An error occurred using the connection to database 'CodeGeneratorDB' on server 'TR-ADN-B5CN4D3\SQLEXPRESS01'.
fail: 1/4/2024 16:12:26.592 RelationalEventId.ConnectionError[20004] (Microsoft.EntityFrameworkCore.Database.Connection)
An error occurred using the connection to database 'CodeGeneratorDB' on server 'TR-ADN-B5CN4D3\SQLEXPRESS01'.
But i'm %100 sure that my db connection string works Because it works on other endponits
Pobiega
Pobiega5mo ago
You are overengineering this massively. And using generic repositories over EF Core is redudant and bad practice. Sounds odd that EF would throw that error if the connection worked, so I can only guess that its related to your service lifetime?
baristaner
baristaner5mo ago
How can i simplify this like as i said my supervisor asked me to "implement MediatR library to redirect the request relevant Service class (like GenerateCodeService, CreateProductService)"
Pobiega
Pobiega5mo ago
You did not say that, fyi. This is the first time you mentioned a supervisor or being ordered to do something this way. Still, this is an anti-pattern. But other than being a bad pattern, I don't see any obvious problems in the code you have shared that would explain why EF can't connect specifically for this endpoint.
baristaner
baristaner5mo ago
She also said Injecting repositories into the controller directly is not suitable. We can build a new structure like this: Controller > Service Class > Repository I mean that works like a charm But when i tried to use MediatR library to redirect the request relevant Service class it's so messy So i'm stuck...
Pobiega
Pobiega5mo ago
That part is correct, you should not have business logic in a controller. What Im trying to get across is that you are doubling up on the intermediate layer.
Pobiega
Pobiega5mo ago
No description
Pobiega
Pobiega5mo ago
this is what it should look like
baristaner
baristaner5mo ago
So do you suggest should i remove all the Features folder use service instead
Pobiega
Pobiega5mo ago
No description
Pobiega
Pobiega5mo ago
and this is what you are doing
baristaner
baristaner5mo ago
And bytw i'm not working on a real project this is my internship assessment
Pobiega
Pobiega5mo ago
I actually suggest skipping the Services layer, as thats the least useful comand handlers are almost the exact same thing, but more granular services are just multiple handlers grouped together for no purpose so skip the service, use the handlers. figure out why your database isnt working
baristaner
baristaner5mo ago
it is kinda odd error is this the correct way to inject mediart? Program.cs
c#
builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
c#
builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
Pobiega
Pobiega5mo ago
seems fine yeah assuming all your handlers are actually in that assembly
baristaner
baristaner5mo ago
Here is my full program.cs
c#
using fastwin;
using Microsoft.EntityFrameworkCore;
using fastwin.Repository.Repositories;
using fastwin.Interfaces;
using FastWIN.API.Converters;
using FluentValidation.AspNetCore;
using FluentValidation;
using fastwin.Services;
using fastwin.Features.Products.Add.Validators;
using System.Reflection;
using MediatR;
using Microsoft.AspNetCore.Hosting;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
});

builder.Services.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters()
.AddValidatorsFromAssemblyContaining<ProductValidator>();


builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();



builder.Services.AddDbContext<CodeDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.LogTo(Console.WriteLine, LogLevel.Information);
});


builder.Services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<ProductService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);



app.UseAuthorization();

app.MapControllers();

app.Run();
c#
using fastwin;
using Microsoft.EntityFrameworkCore;
using fastwin.Repository.Repositories;
using fastwin.Interfaces;
using FastWIN.API.Converters;
using FluentValidation.AspNetCore;
using FluentValidation;
using fastwin.Services;
using fastwin.Features.Products.Add.Validators;
using System.Reflection;
using MediatR;
using Microsoft.AspNetCore.Hosting;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter());
});

builder.Services.AddFluentValidationAutoValidation()
.AddFluentValidationClientsideAdapters()
.AddValidatorsFromAssemblyContaining<ProductValidator>();


builder.Services.AddMediatR(cfg => {
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();



builder.Services.AddDbContext<CodeDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.LogTo(Console.WriteLine, LogLevel.Information);
});


builder.Services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<ProductService>();


var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment() || app.Environment.IsProduction())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
);



app.UseAuthorization();

app.MapControllers();

app.Run();
Pobiega
Pobiega5mo ago
new JsonDateTimeConverter()? is this .net 6? also, I personally prefer the services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Startup>()); syntax but they do the same thing
baristaner
baristaner5mo ago
nope 8.0
Pobiega
Pobiega5mo ago
then you shouldn't need that line STJ has full support for DateOnly and TimeOnly since .net 7
baristaner
baristaner5mo ago
It didn't convert datetime utc to json
Pobiega
Pobiega5mo ago
? DateTimeOffset? it absolutely supports that, even since .net 5
baristaner
baristaner5mo ago
btw my validators also not working
Pobiega
Pobiega5mo ago
and if you are using anything other than DateTimeOffset, you are doing something wrong DateTime for example should never be used
baristaner
baristaner5mo ago
Honestly Wait i just clicked execute again and sql worked first time it didnt
Pobiega
Pobiega5mo ago
why are you using sqlexpress anyways please use a more appropriate database. sqlserver, postgres, sqlite...
baristaner
baristaner5mo ago
It creates the product in db but
Response body is {}
Pobiega
Pobiega5mo ago
well.. uh.. yes?
return Unit.Value;
return Unit.Value;
your command returns Unit lol Unit is MediatRs version of void, aka nothing so this is a comand that has no return value
baristaner
baristaner5mo ago
oh yeah sorry i'm kinda new