Search code examples
c#.netdependency-injectionsimple-injector

Simple Injector - Delayed Initialization


Could someone assist me with one question please?

I have two services.

GAuth:

public class GAuth : IGAuth
{
    public async Task<UserCredential> AuthorizeAsync(ClientSecrets clientSecrets)
    {
        using (var cts = new CancellationTokenSource())
        {
            var localServerCodeReceiver = new LocalServerCodeReceiver();

            cts.CancelAfter(TimeSpan.FromMinutes(1));

            return await GoogleWebAuthorizationBroker.AuthorizeAsync(
                clientSecrets,
                _scopes,
                User,
                cts.Token,
                new FileDataStore(string.Empty), localServerCodeReceiver
            );
        }
    }
}

GDrive:

public class GDrive : IGDrive
{
    private readonly DriveService _driveService;

    public GDrive(UserCredential userCredential)
    {
        _driveService = new DriveService(new BaseClientService.Initializer
        {
            HttpClientInitializer = userCredential,
            ApplicationName = string.Empty
        });
    }
}

And registration part:

container.Register<IGAuth, GAuth>(Lifestyle.Singleton);
container.Register<IGDrive, GDrive>(Lifestyle.Singleton);

As you can see GAuth service returns UserCredentials object after authorization. And this UserCredentials is required for GDrive service. It's simplified example, but in general I need to initialize GDrive service with correct UserCredentials object after user press Auth button on the application form.

Is there any way to do that?

Thanks in advance.


Solution

  • From a design perspective, injection constructors should be simple, fast and reliable. This isn't the case in your case, since the building of the object graph depends on I/O.

    Instead, you should postpone the IO till after the constructor has ran.

    There are two things that come to my mind that you can do. Either you inject the IGAuth into the GDrive and make sure it is called after the constructor has ran, or you inject a lazy async UserCredential that can be requested after the constructor has ran.

    Here's an example of the latter:

    public class GDrive : IGDrive
    {
        private readonly Lazy<Task<DriveService>> _driveService;
    
        public GDrive(Lazy<Task<UserCredential>> userCredential)
        {
            _driveService = new Lazy<Task<DriveService>>(async () =>
                new DriveService(new BaseClientService.Initializer
                {
                    HttpClientInitializer = await userCredential.Value,
                    ApplicationName = string.Empty
                }));
        }
    
        public async Task SomeMethod()
        {
            var service = await _driveService.Value;
    
            service.DoSomeStuff();
        }
    }
    

    You can configure this GDrive as follows:

    var auth = new GAuth();
    var credentials = new Lazy<Task<UserCredential>>(
        () => auth.AuthorizeAsync(new ClientSecrets()));
    
    container.RegisterSingleton<IGDrive>(new GDrive(credentials));
    

    The other option is to inject the GAuth into the GDrive. That would result in something as follows:

    public class GDrive : IGDrive
    {
        private readonly Lazy<Task<DriveService>> _driveService;
    
        public GDrive(IGAuth auth)
        {
            _driveService = new Lazy<Task<DriveService>>(async () =>
                new DriveService(new BaseClientService.Initializer
                {
                    HttpClientInitializer = await auth.AuthorizeAsync(new ClientSecrets()),
                    ApplicationName = string.Empty
                }));
        }
    
        public async Task SomeMethod()
        {
            var service = await _driveService.Value;
    
            service.DoSomeStuff();
        }
    }
    

    Note that in both cases a Lazy<Async<T>> is created that will ensure that the asynchronous operation is only triggered when Lazy<T>.Value is called for the first time.