I'm playing around with using Dapper with Mediatr in a .NET Core API. It's all working nicely.
Its a learning project for me so I am just putting together a system that will hold user details against some training qualifications/certifications.
I am just building out the api's first and then will have a look at a UI. This initial code doesn't make sense in regards to the specifics around the user I am really just trying to work out whether fundamentally this approach of no repository is flawed. I haven't used Dapper before but have used EF so dropped repositories as they offered little benefit
Have been reading lots of blog posts from Jimmy Bogard around dropping repositories favouring query objects and posts from Mark Seemann around design smells.
However, I am not sure about best practice around using Dapper and although it's working, I'd like to know if there is a better approach or something I am missing.
I am thinking about using a Repository
but not sure it's going to add much value.
Should I be injecting the connection into the handler? Or sharing between requests somehow? At the moment it's just the connection string that is injected in.
As I add more handlers to the project, would the project benefit from having some sort of base class for the handlers where the IDbConnection
would be made available from?
Any feedback gratefully received.
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, ValidatedResponse>
{
private readonly IConnectionStringProvider config;
public GetUserQueryHandler(IConnectionStringProvider config)
{
this.config = config;
}
public async Task<ValidatedResponse> Handle(GetUserQuery request, CancellationToken cancellationToken)
{
using (IDbConnection conn = new SqlConnection(config.ConnectionString))
{
var query = "SELECT ID, first_name, last_name FROM [user] WHERE ID = @ID";
conn.Open();
var result = conn.QuerySingleOrDefault<User>(query, new { ID = request.UserId });
return new ValidatedResponse(result);
}
}
}
public class User
{
private readonly int id;
private readonly string first_name;
private readonly string last_name;
public User(int id, string first_name, string last_name)
{
this.id = id;
this.first_name = first_name;
this.last_name = last_name;
}
public int Id => id;
public string Firstname => first_name;
public string Surname => last_name;
}
public class GetUserQuery : IRequest<ValidatedResponse>
{
private readonly string _name;
public GetUserQuery(string name)
{
_name = name;
}
public string Name { get { return _name; } }
}
public class GetApplicationQueryValidator : AbstractValidator<GetUserQuery>
{
public GetApplicationQueryValidator()
{
RuleFor(m => m.Name).Must(m => m == null || m.Length > 2).WithMessage("Name must be greater than 5, long");
}
}
public class ValidatedResponse
{
private readonly IList<string> _messages = new List<string>();
public IEnumerable<string> Errors { get; }
public object Result { get; }
public ValidatedResponse() => Errors = new ReadOnlyCollection<string>(_messages);
public ValidatedResponse(object result) : this() => Result = result;
public ValidatedResponse AddError(string message)
{
_messages.Add(message);
return this;
}
}
public class ValidatorHandler<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IRequest<TResponse>
where TResponse : class
{
private readonly IEnumerable<IValidator> _validators;
public ValidatorHandler(IEnumerable<IValidator<TRequest>> validators)
{
_validators = validators;
}
public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
var failures = _validators
.Select(v => v.Validate(request))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
return failures.Any()
? Errors(failures)
: next();
}
private static Task<TResponse> Errors(IEnumerable<ValidationFailure> failures)
{
var response = new ValidatedResponse();
foreach (var failure in failures)
{
response.AddError(failure.ErrorMessage);
}
return Task.FromResult(response as TResponse);
}
}
The Startup
class:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration.GetConnectionString("userApi");
services.AddSingleton<IConnectionStringProvider>(new ConnectionStringProvider(connectionString));
AddMediatr(services);
services.AddControllers();
}
private static void AddMediatr(IServiceCollection services)
{
const string applicationAssemblyName = "UserApi";
var assembly = AppDomain.CurrentDomain.Load(applicationAssemblyName);
AssemblyScanner
.FindValidatorsInAssembly(assembly)
.ForEach(result => services.AddScoped(result.InterfaceType, result.ValidatorType));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidatorHandler<,>));
services.AddMediatR(assembly);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
```