Home > Software design >  How to return the same request scoped value from a class in a thread safe way?
How to return the same request scoped value from a class in a thread safe way?

Time:08-06

I need to create a wrapper class for IAzureMediaServicesClient which if injected as a scoped service (in a single http request) can return same client object to the callers.

This is the current wrapper code that needs to be fixed.

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly IConfiguration _configuration;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task<IAzureMediaServicesClient> GetClient()
    {
        ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
            _configuration[ConfigurationConstants.AadTenantId],
            _configuration[ConfigurationConstants.AmsAadClientId],
            _configuration[ConfigurationConstants.AmsAadSecret]);

        return new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
        {
            SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
        };
    }
}

The class is registered as a Scoped service in DI

public static IServiceCollection AddAzureMediaServiceClient(this IServiceCollection serviceCollection)
{
      return serviceCollection.AddScoped<IAzureMediaServicesClientProvider, AzureMediaServicesClientProvider>();
}

and a sample usage in code

public async Task<Job> CreateJobAsync(string transformName, Job job)
{
    IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();

    return await client.Jobs.CreateAsync(_resourceGroupName, _accountName, transformName, job.Name, job);
}

public async Task<Job> GetJobAsync(string transformName, string jobName)
{
    IAzureMediaServicesClient client = await _azureMediaServicesClientFactory.GetClient();

    return await client.Jobs.GetAsync(_resourceGroupName, _accountName, transformName, jobName);
}

Now the methods GetJobAsync and CreateJobAsync can be used in the same request and currently in such scenario for each of them a new client would be created. How can the provider class be rewritten so that in a single request same client object would be returned ? (I know I could inject it in a higher level and just pass the value to these methods but this is a simplified example and the real world use case would require a lot of refactoring to achieve this).

public async Task TestMethod()
{
    var job = await GetJobAsync(...);

    // Do some code modifications

    await CreateJobAsync(...);

    // How can we make sure here that both GetJobAsync and
    // CreateJobAsync used the same client AzureMediaServicesClient instance ?
}

Below sample shows the intent but wouldn't be thread safe if I understand correctly ?

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly IConfiguration _configuration;
    private IAzureMediaServicesClient _client;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task<IAzureMediaServicesClient> GetClient()
    {
        if (_client == null)
        {
            ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
                _configuration[ConfigurationConstants.AadTenantId],
                _configuration[ConfigurationConstants.AmsAadClientId],
                _configuration[ConfigurationConstants.AmsAadSecret]);

            _client = new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
            {
                SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
            };
        }

        return _client;
    }
}

CodePudding user response:

You can use the AsyncLazy<T> from the package Microsoft.VisualStudio.Threading:

public class AzureMediaServicesClientProvider : IAzureMediaServicesClientProvider
{
    private readonly IConfiguration _configuration;
    private readonly AsyncLazy<IAzureMediaServicesClient> _lazyClient;

    public AzureMediaServicesClientProvider(IConfiguration configuration)
    {
        _configuration = configuration;
        _lazyClient  = new AsyncLazy<IAzureMediaServicesClient>(CreateClient);
    }

    public Task<IAzureMediaServicesClient> GetClient()
    {
        return _lazyClient.GetValueAsync();
    }

    private async Task<IAzureMediaServicesClient> CreateClient()
    {
        ServiceClientCredentials credentials = await ApplicationTokenProvider.LoginSilentAsync(
            _configuration[ConfigurationConstants.AadTenantId],
            _configuration[ConfigurationConstants.AmsAadClientId],
            _configuration[ConfigurationConstants.AmsAadSecret]);

        return new AzureMediaServicesClient(new Uri(_configuration[ConfigurationConstants.ArmEndpoint]), credentials)
        {
            SubscriptionId = _configuration[ConfigurationConstants.SubscriptionId],
        };
    }
}

AsyncLazy<T> is thread-safe for all members.

  • Related