simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 155 forks source link

How to register third party constructor with runtime string arguments #988

Closed shandysawyer closed 7 months ago

shandysawyer commented 7 months ago

I am coming up slightly short after going through all your documentation. As you have mentioned, injecting runtime constructor arguments is a code smell. However the third party library I am using is forcing me to do just that.

This is the code I am working with:

using (IAuthenticationClient auth1 = new AuthenticationClient())
{
    // have to authenticate before initializing ForceClient constructor,
    // no public default constructor available
    await auth.UsernamePasswordAsync(consumerKey, consumerSecret,
        username, password + securityToken);
    using (IForceClient client1 = new ForceClient(
        auth1.InstanceUrl, auth1.AccessToken, auth1.ApiVersion))
        // do stuff with client1
    using (IForceClient client2 = new ForceClient(
        auth1.InstanceUrl, auth1.AccessToken, auth1.ApiVersion))
        // do stuff with client2 on the same authentication
}

AuthenticationClient is a Scoped dependency, while ForceClient is a Transient dependency

Wiring up AuthenticationClient

In the case of AuthenticationClient I did find the question for delaying the async I/O operation until after the graph is built.

I am thinking it would similar to the answered question, or could it be simpler?:

public class AuthenticationClientProvider
{
    private readonly Func<Task<IAuthenticationClient>> _authClient;
    public AuthenticationClientProvider(
        Func<Task<IAuthenticationClient>> authClient) =>
        _authClient = authClient;
}
// configuration
var authClientProvider = (InstanceProducer<IAuthenticationClient>)
    Lifestyle.Scoped.CreateProducer(
        () => new AuthenticationClient(), container);

var authenticatedClientTaskProducer =
    (InstanceProducer<Lazy<Task<IAuthenticationClient>>>)
    Lifestyle.Scoped.CreateProducer(() =>
        new Lazy<Task<IAuthenticationClient>>(async () => {
            var auth = authClientProvider.GetInstance();
            await auth.UsernamePasswordAsync(consumerKey, consumerSecret,
            username, password + securityToken);
            return auth;
        }), container);

container.RegisterSingleton(
    new AuthenticationClientProvider(() =>
        authenticatedClientTaskProducer.GetInstance().Value));

Wiring up ForceClient

However - the part I am more stuck on - how would I register IForceClient given the required runtime parameters? Is there an example you could point me to?

dotnetjunkie commented 7 months ago

Why do you think you need to register AuthenticationClient in the first place? Can't you hide all the library's logic behind a (simple) application-specified abstraction? And does the composition of the third party library's classes really have to be done by Simple Injector? Wouldn't it be simpler to just new up those classes by hand?

shandysawyer commented 7 months ago

Because I am not very good at software development apparently? I mean when you are using DI, you are under the impression all services (i.e. classes you make or classes that someone else made that you need) should be registered into the container, allowing you all the benefits IoC provides. So why should AuthenticationClient and ForceClient be any different compared to say this answer? What would you do in this scenario?

dotnetjunkie commented 7 months ago

Software development is hard; there's no single way to do things and there are many different philosophies and schools which one can follow. As you already noticed (by reading my blog posts and SO answers), I'm pretty opinionated. Even when it comes to runtime data there are multiple schools of thought, which is something I dove deeper into in the past in this series of blog posts.

When it comes to working with third-party components, I often try to hide them behind application-tailored abstractions. This is something that I learned from the Dependency Inversion Principle. But it's not just about following some arbitrary rules, I've found that this actually leads to reduced overall complexity in my applications. That said, it's not always easy to come up with an abstraction that works great for your application. I love to help you with this. But in order to do so, I need a bit more context.

In your first code example, you demonstrated how this third-party library can be used. But can you show me some typical examples of how you use this code in your application?

shandysawyer commented 7 months ago

I am using this third party library to query data from Salesforce. My plan was to encapsulate in a repository layer like so:

public class SalesforceRepository : ISalesforceRepository
{
    private Func<IForceClient> _forceClientFactory;
    public SalesforceRepository(Func<IForceClient> forceClientFactory) =>
        _forceClientFactory = forceClientFactory;

    public async Task<List<Things>> FindAllThingsAsync()
    {
        using (var client = _forceClientFactory())
        {
            var query = $@"SELECT Id FROM Table";
            var result = await client.QueryAsync<Things>(query);
            return result.Records;
        }
    }
    // other repo methods
}

And then injecting this repository layer in my business classes so that I can retrieve the data and do things with it:

public class MyBusinessClass
{
    private ISalesforceRepository _salesforceRepository;
    public MyBusinessClass(ISalesforceRepository salesforceRepository) =>
        _salesforceRepository = salesforceRepository;

    public async Task DoStuff()
    {
        var data = await _salesforceRepository.FindAllThingsAsync();
        // do stuff with the data
    }
}

Side note: I am currently working in .NET 4.8 Framework for this project.

dotnetjunkie commented 7 months ago

Thank you for this addition. This gives me a decent idea of what you're aiming at. In your case, I'd suggest the following.

Introduce an IForceClientFactory abstraction, with a single factory method returning a Task<IForceClient>:

public interface IForceClientFactory
{
    Task<IForceClient> CreateClient();
}

By making the CreateClient method async, it allows you to lazily authenticate the AuthenticationClient (will show an implementation shortly). This means you will have a small change to your SalesforceRepository:

public class SalesforceRepository : ISalesforceRepository
{
    private IForceClientFactory _factory;
    public SalesforceRepository(IForceClientFactory factory) =>
        _factory = factory;

    public async Task<List<Things>> FindAllThingsAsync()
    {
        // NOTE: Here you must await the CreateClient method
        using (var client = await _factory.CreateClient())
        {
            var query = $@"SELECT Id FROM Table";
            var result = await client.QueryAsync<Things>(query);
            return result.Records;
        }
    }
    // other repo methods
}

The interesting part of the solution I'm proposing here is inside the IForceClientFactory implementation:

public sealed class ForceAuthenticationCredentials { ... }

// This class caches AuthenticationClient. Should be register as scoped.
public sealed class ForceClientFactory : IForceClientFactory, IDisposable
{
    private readonly ForceAuthenticationCredentials _credentials;
    private readonly AuthenticationClient _auth;

    private bool _authenticated;

    public ForceClientFactory(ForceAuthenticationCredentials credentials)
    {
        _credentials = credentials ?? throw new ArgumentNullException();
        _auth = new AuthenticationClient();
    }

    public async Task<IForceClient> CreateClient()
    {
        var auth = await GetAuthenticatedClient();

        return new ForceClient(
            auth.InstanceUrl, auth.AccessToken, auth.ApiVersion);
    }

    private async Task<AuthenticationClient> GetAuthenticatedClient()
    {
        if (!_authenticated)
        {
            await _auth.UsernamePasswordAsync(
                _credentials.ConsumerKey,
                _credentials.ConsumerSecret,
                _credentials.UserName,
                _credentials.Password + _credentials.SecurityToken);

            _authenticated = true;
        }

        return _auth;
    }

    public void Dispose() => _auth.Dispose();
}

Here a few interesting things are happening:

While it is possible to completely remove ForceClientFactory and have all this logic crammed inside delegates that are registered inside of Simple Injector (or any other Container), I believe you'll find the existence of the ForceClientFactory to provide an easier to grasp solution. If you like, you can place the ForceClientFactory inside your Composition Root; in other words, close to the place where you register Simple Injector. This keeps the code close to the place you initially wanted it to be.

The only part missing from the equation now is of course the registrations. Considering all classes you posted, the following set of Simple Injector registrations would probably do the trick:

container.RegisterInstance(new ForceAuthenticationCredentials(...));
container.Register<IForceClientFactory, ForceClientFactory>(Lifestyle.Scoped);
container.Register<ISalesforceRepository, SalesforceRepository>();
container.Register<MyBusinessClass>();

One last note: Considering that the SalesforceRepository is about communicating with Salesforce, you could consider the IForceClientFactory optional. You could SalesforceRepository depend directly on the ForceClientFactory implementation instead. Doing that, however, does mean that you won't be able to place the implementation inside the Composition Root any longer.

I hope this helps.

shandysawyer commented 7 months ago

Thank you for taking time to demonstrate what can be done in my scenario. This is very elegant, well thought out, and best of all works. I agree that the registration I initially posted is difficult to grasp, and is probably not doing any favors to future programmers that will inherit this code. Additionally, this translates very well amongst the various DI frameworks.

dotnetjunkie commented 7 months ago

You're very welcome.