Search code examples
asp.net-mvcdependency-injectionsimple-injector

Passing run-time data to services that are injected with Dependency Injection


My ASP.NET MVC application uses Dependency Injection to inject services to the controllers.

I need to find some way of passing run-time data to the services, because as far as I know it's anti-pattern to send run-time data to the constructors using DI.

In my case I have four different services that all rely on access tokens, which can be re-used between the services. However, that access token can expire so something needs to take care of issuing new access token when it expires.

The services (independent NuGet packages) are all clients for various services, that require access token for every request made. One example would be the AddUserAsync method in the IUserServiceBusiness, it basically POSTs to an endpoint with JSON data and adds Authorization header with bearer access token.

My current solution is to accept access token as a parameter in all of the methods in the services, which means that the web application takes care of handling the access tokens and passing them when needed. But this solution smells, there has to be a better way of doing this.

Here's an example on how it's done currently.

The RegisterContainer method where all of the implementations are registered.

public static void RegisterContainers()
{
    // Create a new Simple Injector container
    var container = new Container();
    container.Options.DefaultScopedLifestyle = new WebRequestLifestyle();

    SSOSettings ssoSettings = new SSOSettings(
        new Uri(ConfigConstants.SSO.FrontendService), 
        ConfigConstants.SSO.CallbackUrl, 
        ConfigConstants.SSO.ClientId, 
        ConfigConstants.SSO.ClientSecret, 
        ConfigConstants.SSO.ScopesService);

    UserSettings userSettings = new UserSettings(
            new Uri(ConfigConstants.UserService.Url));

    ICacheManager<object> cacheManager = CacheFactory.Build<object>(settings => settings.WithSystemRuntimeCacheHandle());

    container.Register<IUserBusiness>(() => new UserServiceBusiness(userSettings));
    container.Register<IAccessTokenBusiness>(() => new AccessTokenBusiness(ssoSettings, cacheManager));

    container.RegisterMvcControllers(Assembly.GetExecutingAssembly());
    container.RegisterMvcIntegratedFilterProvider();

    container.Verify();

    DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));
}

Implementation of IUserBusiness and IAccessTokenBusiness are injected to AccountController.

    private readonly IUserBusiness _userBusiness;
    private readonly IAccessTokenBusiness _accessTokenBusiness;

    public AccountController(IUserBusiness userBusiness, IAccessTokenBusiness accessTokenBusiness)
    {
        _userBusiness = userBusiness;
        _accessTokenBusiness = accessTokenBusiness;
    }

Example endpoint in AccountController that updates the user's age:

    public ActionResult UpdateUserAge(int age)
    {
        // Get accessToken from the Single Sign On service
        string accessToken = _accessTokenBusiness.GetSSOAccessToken();
        bool ageUpdated = _userBusiness.UpdateAge(age, accessToken);

        return View(ageUpdated);
    }

And here are some ideas that I've thought of:

  1. Pass the access token to the services with a setter, in the constructor of the controllers. For example:

    public HomeController(IUserBusiness userBusiness, IAccessTokenBusiness accessTokenBusiness) 
    {
        _userBusiness = userBusiness;
        _accessTokenBusiness = accessTokenBusiness;
        string accessToken = _accessTokenBusiness.GetAccessToken();
        _userBusiness.setAccessToken(accessToken);
    }
    

    I don´t like this idea because then I would have to duplicate this code in every controller.

  2. Pass the access token with every method on the services (currently doing this). For example:

    public ActionResult UpdateUser(int newAge)
    {
        string accessToken = _accessTokenBusiness.GetAccessToken();
        _userBusiness.UpdateAge(newAge, accessToken);
    }
    

    Works, but I don't like it.

  3. Pass implementation of IAccessTokenBusiness to the constructor of the services. For example:

    IAccessTokenBusiness accessTokenBusiness = new AccessTokenBusiness();
    
    container.Register<IUserBusiness>(() => new IUserBusiness(accessTokenBusiness));
    

    But I'm unsure how I would handle caching for the access tokens. Perhaps I can have the constructor of AccessTokenBusiness accept some generic ICache implementation, so that I'm not stuck with one caching framework.

I would love to hear how this could be solved in a clean and clever way.

Thanks!


Solution

  • As I see it, the requirement of having this access token for communication with external services is an implementation detail to the class that actually is responsible of calling that service. In your current solution you are leaking these implementation details, since the IUserBusiness abstraction exposes that token. This is a violation of the Dependency Inversion Principle that states:

    Abstractions should not depend on details.

    In case you ever change this IUserBusiness implementation to one that doesn't require an access token, it would mean you will have to make sweeping changes through your code base, which basically means you voilated the Open/close Principle.

    The solution is to let the IUserBusiness implementation take the dependency on IAccessTokenBusiness itself. This means your code would look as follows:

    // HomeController:
    public HomeController(IUserBusiness userBusiness)
    {
        _userBusiness = userBusiness;
    }
    
    public ActionResult UpdateUser(int newAge)
    {
        bool ageUpdated = _userBusiness.UpdateAge(newAge);
        return View(ageUpdated);
    }
    
    // UserBusiness
    public UserBusiness(IAccessTokenBusiness accessTokenBusiness)
    {
        _accessTokenBusiness = accessTokenBusiness;
    }
    
    public bool UpdateAge(int age)
    {
        // Get accessToken from the Single Sign On service
        string accessToken = _accessTokenBusiness.GetSSOAccessToken();
    
        // Call external service using the access token
    }
    

    But I'm unsure how I would handle caching for the access tokens.

    This is neither a concern of the controller nor the business logic. This is either a concern of the AccessTokenBusiness implementation or a decorator around IAccessTokenBusiness. Having a decorator is the most obvious solution, since that allows you to change caching independently of generation of access tokens.

    Do note that you can simplify your configuration a bit by making use of the container's auto-wiring abilities. Instead of registering your classes using a delegate, you can let the container analyse the type's constructor and find out itself what to inject. Such registration looks as follows:

    container.Register<IUserBusiness, UserServiceBusiness>();
    container.Register<IAccessTokenBusiness, AccessTokenBusiness>();
    
    ICacheManager<object> cacheManager = 
        CacheFactory.Build<object>(settings => settings.WithSystemRuntimeCacheHandle());
    
    container.RegisterSingleton<ICacheManager<object>>(cacheManager);
    

    Further more, a decorator for IAccessTokenBusiness can be added as follows:

    container.RegisterDecorator<IAccessTokenBusiness, CachingAccessTokenBusinessDecorator>();