Search code examples
c#design-patternsdependency-injectionmicrosoft-graph-apiasp.net-core-webapi

C# Two GraphServiceClient singleton instances


In my application (.NET Web API), I need to call Microsoft Graph API using two different credentials set (completely different tenant). When I was using only one, the case was simple - I just did a setup in DI part of Program.cs:

var clientSecretCredential = new ClientSecretCredential(b2cSettings.TenantId, b2cSettings.ClientId, b2cSettings.ClientSecret, tokenCredentialOptions);

// Use a single client instance for the lifetime of the application
services.AddSingleton(sp =>
{
    return new GraphServiceClient(clientSecretCredential, new[] { "https://graph.microsoft.com/.default" });
});

which allowed me to just inject it to whichever service I needed, like so:

    public GraphApiClient(
        GraphServiceClient graphServiceClient)
    {
        _graphServiceClient = graphServiceClient;
    }

Obviously, there is no way to re-use that for second set of credentials - it will just pick the last registered GraphServiceClient instance.

It smells like some common pattern, but it does not seem to be a good candidate for factory (I want only TWO instances across app lifetime - each with different credentials). I also wonder how would service "client" (class using it) specify which GraphServiceClient to retrieve in runtime.

I imagine some "aggregate" class with methods like:

RegisterGraphApiClientWithCredentials(string tenantId, string clientId, string clientSecret); // <- this would be done once, in Program.cs

RetrieveGraphApiClientWithCredentials(string tenantId, string clientId, string clientSecret); // <- this would be done whenever some service would need actual client instance

How should I approach this problem? Microsoft advise single instance for the lifetime of the application. Will this approach collide with that?

Thank You in advance!


Solution

  • In my use case I was reading/writing data from/to two tenants.

    I've registered as a singleton a class that holds GraphServiceClient instances accessed by tenant id

    public class GraphServiceClientProvider : IDisposable
    {
        private bool disposedValue;
    
        private readonly IDictionary<string, GraphServiceClient> _clients;
    
        public GraphServiceClient this[string tenantId] => _clients[tenantId];
    
        public GraphServiceClientProvider(IDictionary<string, GraphServiceClient> clients)
        {
            _clients = clients;
        }       
    
        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    foreach (var client in _clients.Values)
                    {
                        client.Dispose();
                    }
                }
                disposedValue = true;
            }
        }
    
        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }
    }
    

    in Program.cs

    var tokenCredentialOptions = new ClientSecretCredentialOptions();
    builder.Services.AddSingleton(x =>
    {
        var scopes = new[] { "https://graph.microsoft.com/.default" };
        var dict = new Dictionary<string, GraphServiceClient>
        {
            { "tenantId1", new GraphServiceClient(new ClientSecretCredential("tenantId1", "clientId1", "clientSecret1", tokenCredentialOptions), scopes) },
            { "tenantId2", new GraphServiceClient(new ClientSecretCredential("tenantId2", "clientId2", "clientSecret2", tokenCredentialOptions), scopes) }
        };
        return new GraphServiceClientProvider(dict);
    });
    builder.Services.AddSingleton<IUsersService, UsersService>();
    

    Example of usage

    public class UserService : IUserService
    {
        private readonly GraphServiceClientProvider _clientProvider;
    
        public UserService(GraphServiceClientProvider clientProvider)
        {
            _clientProvider = clientProvider;
        }
    
        public List<User> GetUsers(string tenantId)
        {
            var client = _clientProvider[tenantId];
            ...
        }
    }