0
\$\begingroup\$

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();
        });
    }
}

```

\$\endgroup\$
  • 2
    \$\begingroup\$ oh, I believe one more last thing... we need to know what this is for and what's it doing - then I think we're good ;-) \$\endgroup\$ – t3chb0t Jul 27 at 20:11
  • \$\begingroup\$ Thanks for your prompt feedback added some more context and what I am basing this around \$\endgroup\$ – DevOverflow Jul 27 at 20:25
  • \$\begingroup\$ @t3chb0t It's a bit weird that GetUserQuery provides a Name, but the query gets UserId from it. Is this code even working as intended? \$\endgroup\$ – dfhwze Jul 27 at 20:48
  • \$\begingroup\$ The user class will be added to in terms of personal details such as date of birth and therefore hopefully will make more sense. Was just adding enough code to get Dapper working and to ask a question \$\endgroup\$ – DevOverflow Jul 27 at 20:54
  • 2
    \$\begingroup\$ @t3chb0t the initial code doesn't make sense I rest my case. \$\endgroup\$ – dfhwze Jul 27 at 20:54
1
\$\begingroup\$

Separation of Concerns

I am thinking about using a Repository but not sure it's going to add much value.

The problem with letting the handler interact to the database layer directly is that it is handling both service layer (GetUserQuery, ValidatedResponse) and data layer (SqlConnection, SqlConnection) contracts and APIs. Without separating these concepts, both layers depend on eachother. In this architecture, User is both a service as data layer contract. In more complex environments, you would have a UserDto (service layer) and UserRecord (data layer). Since we are querying, the intermediate business entity is usually omitted.

public async Task<ValidatedResponse> Handle(
    GetUserQuery request, CancellationToken cancellationToken)
{
    using (IDbConnection conn = new SqlConnection(config.ConnectionString))
    {
        // .. code left out for brevity
    }
}

I would refactor to keep GetUserQueryHandler in the service layer and create UserRepository in the data layer. Its interface IUserRepository should be stored at the business layer.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.