0

We have a common architecture for many of our projects, and this architecture requires some amount of boilerplate that is generic for every project. I'm trying to tie all this boilerplate into a single reusable NuGet package to make maintenance easier, but am running into issues with getting the DI to work with me.

Specifically, I'm struggling with the concept of services. In the NuGet, I'll have to define basic service interfaces so I can hook some pipelines to use these services. However, every application that will be using this NuGet will need to be able to extend these services with application specific methods.

Let's go over an example with the "User authentication pipeline", which should answer common questions like "Is this user in role x" and application specific questions like "Can this user modify z based on its owner y".

First, our application layer is structured based on CQRS using a common interface, which is implemented by every Query and Command:

    public interface IApplicationRequestBase<TRet> : IRequest<TRet> { //IRequest from MediatR
        Task<bool> Authorize(IUserServiceBase service, IPersistenceContextBase ctx);
        void Validate();
    }

IUserServiceBase is an interface providing access to the current user (I'm skipping the IPersistenceContextBase, which is just an empty interface):

    public interface IUserServiceBase {
        string? CurrentUserExternalId { get; }
        bool IsUserInRole(params string[] roleNames);
...

And in the authentication pipeline

    public class RequestAuthorizationBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
            where TRequest : IApplicationRequestBase<TResponse> { //MediatR IPipelineBehavior
        private readonly IUserServiceBase _userService;
        private readonly IPersistenceContextBase _ctx;

        public RequestAuthorizationBehaviour(IUserServiceBase userService, IPersistenceContextBase ctx) {
            _userService = userService;
            _ctx = ctx;
        }

        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) {
            if (await request.Authorize(_userService, _ctx)) {
                return await next();
            }

            throw new UnauthorizedAccessException();
        }
    }
}

And finally the NuGet DI definition:

    public static class DependencyInjection {
        public static IServiceCollection AddApplicationInfra(this IServiceCollection services) {
...
            services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestAuthorizationBehaviour<,>));

            return services;
        }
    }

All well and good in the NuGet side, now the application. This approach has me trying to extend the interfaces directly, and this is the easiest way to visualize what I wish to accomplish.

The application has a bunch of app-specific authorization checks, so we have a custom interface for that:

    public interface IUserService : IUserServiceBase {
        public string LocalUserIdClaimKey { get; }
        Guid CurrentUserLocalId { get; }
        /// <summary>
        /// Shortcut for checking if the user has any role allowing read access to notifications
        /// </summary>
        bool CurrentUserCanReadNotifications { get; }
...

The UserService class implements all the functionality required in the IUserService interface, meaning the IUserServiceBase methods as well. It is defined in a different project (Infrastructure) than the interface (Application).

    public class UserService : IUserService {
        private readonly IHttpContextAccessor _contextAccessor;
        public UserService(IHttpContextAccessor contextAccessor) {
            _contextAccessor = contextAccessor;
        }

        public string? CurrentUserExternalId {
            get {
                var user = _contextAccessor.HttpContext.User;
                if (user != null) {
                    return user.FindFirst(JwtClaimTypes.Subject)?.Value;
                }

                return null;
            }
        }
...

And finally, in our Command, where it all should come together:

    public class UpdateSubsequentTreatmentFacilitiesCommand : IApplicationRequestBase<int> {
        public async Task<bool> Authorize(IUserService service, IPersistenceContext ctx) {
            //Application specific authorization check
        }

        public void Validate() {
        }

Now, here we get a build error, stating that 'UpdateSubsequentTreatmentFacilitiesCommand' does not implement interface member 'IApplicationRequestBase<int>.Authorize(IUserServiceBase, IPersistenceContextBase)'. This is probably what I'm encountering here (though I still can't figure out why exactly...).

So, to reiterate:

  • Goal is to package common project boilerplate to a single NuGet
  • We need to be able to extend the services defined in the NuGet with application specific functionality
Simopaa
  • 449
  • 5
  • 8
  • What about you add another generic parameter in `IApplicationRequestBase` to define the `IUserServiceBase` type; something like https://dotnetfiddle.net/BCj2CC – user1672994 Aug 30 '20 at 08:12
  • I got pretty far with passing generic parameters, but ran into a wall with actually getting the implementing type. (I clarified this in the opening) the UserService implementation is in a different project than the interface definition, so in order to get the DI working I would have to dig the implementation class during runtime which AFAICT is not such a good idea. `services.AddApplicationInfra()` – Simopaa Aug 31 '20 at 05:39

1 Answers1

0

IApplicationRequestBase defines the type of the service parameter as IUserServiceBase, but UpdateSubsequentTreatmentFacilitiesCommand tried to use IUserService. OO programming and inheritance doesn't let you change method signatures.

If you can change IApplicationRequestBase, adding a TService generic parameter will let you get around it:

public interface IApplicationRequestBase<TRet, TService> : IRequest<TRet>
    where TService is IUserServiceBase
{
    Task<bool> Authorize(TService service, IPersistenceContextBase ctx);
    void Validate();
}

public class UpdateSubsequentTreatmentFacilitiesCommand : IApplicationRequestBase<int, IUserService>
{
    public async Task<bool> Authorize(IUserService service, IPersistenceContext ctx)
    {
        // method body
    }

    // rest of class
}

However, given that IUserService is an interface, if it is the only thing that extends/implements IUserServiceBase, then it sounds like a case of overengineering. There's a saying that perfection is the enemy of good. In other words, attempting to be too generic, too reusable, where it's not actually needed, is just slowing down progress. By all means, strive to have a high quality codebase, but you also need to be pragmatic.

If other apps that use IApplicationRequestBase have their own user service, not the same IUserService as your app, then you'll need to find another approach, given that C# is a strongly typed language. You could just typecast the IUserServiceBase into an IUserService in the method body. Rather than extending the interface, you could have an extension method. If you're creative, you might think of other approaches as well.

However, looking at IUserService, my guess is that it exists only to improve performance of checking certain commonly used roles. If I'm wrong and it's about convenience and not performance, then an extension method should be sufficient. If the concern is performance, then make sure that the implementation of IsUserInRole does caching. Looking up a string still won't be as fast as returning a property's backing field. But changing your software architecture to improve performance for something you haven't profiled to confirm that it is a performance bottleneck is the definition of premature optimization. If IsUserInRole does basic caching, you'll probably find the the performance is good enough, and helper/extension methods solve whatever readability/code quality issue you're trying to solve.

zivkan
  • 12,793
  • 2
  • 34
  • 51
  • Thanks for the thoughtful response! I tried to clarify some points in the opener, but mainly the goal of this whole refactoring is to reduce the repetition of generic code between applications. I added a sample of the implementing UserService class in the opening to show how exactly this current application would be getting the auth details. I hadn't thought about using extension methods for the app specific functionality, which might actually get me pretty close to where I want to be. I'll check it out – Simopaa Aug 31 '20 at 05:47
  • Follow-up: extension methods would work fine for the auth flow, since the default interface provides access for the common principals. The persistence access service is another story, since it does not. Basically the IPersistenceContextBase defines our EF Core DBSets, which of course are all application specific. See https://github.com/jasontaylordev/NorthwindTraders/blob/master/Src/Application/Common/Interfaces/INorthwindDbContext.cs – Simopaa Sep 01 '20 at 06:26