Closed shandysawyer closed 9 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?
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?
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?
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.
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:
ForceClientFactory
itself contains the logic authenticateAuthenticationClient
, it is responsible for disposing of it.CreateClient
would be fast, as the call to UsernamePasswordAsync
is skipped.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.
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.
You're very welcome.
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:
AuthenticationClient
is a Scoped dependency, whileForceClient
is a Transient dependencyWiring 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?:
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?