Pass Through Data Over IServiceProvider.CreateScope()
- Panot Thaiuppathum
- Programming , /thcategories/programming Microsoft /thcategories/microsoft
- 17 Jul, 2023
[ASP.NET] In some cases you may encounter the situation that you need to pass through some particular data over a new scope of Service Provider.
For instance, when you implement a solution that integrate to a third party that has webhook callbacks. There might be a data from the webhook request that is necessary to be resolved by each service function.
The payload of the request contains identity like tenant id. API Controller received the request then it make a request to the services. Most cases we can carry the payload over to the service request via function parameter but for more complex project structure there might be a middleware, filters, service calling another service, attribute, etc. where they should be able to resolve the identifier (like tenant id in this sample) via either a normal dependency injection way or IServiceProvider.GetService
This might be a rare use case but I post this for anyone who already encounter the same situation and my technique may offer you one more workaround option.
To make sure the necessary data is passed through everywhere from the API controller endpoint throughout all classes that are registered to IServiceCollection.
-
Create a class that carry the identifier data.
public class TenantIdentifier { public string? TenantId { get; set; } }
-
Register this class to IServiceCollection with AddScoped.
// Register identifier carrier object builder.Services.AddScoped<TenantIdentifier>();
-
Create an extension method for IScopeProvider named CreateServiceScope(). We are going to enforce all developers on the same project to use IScopeProvider.CreateServiceScope() instead of IScopeProvider.CreateScope(). The IScopeProvider.CreateServiceScope() has additional step to transfer TenantId from current scope to the new scope. Also put the extension of the CreateScope() to break the build (ambiguous error) if someone tries to use CreateScope() still. This technique is a workaround when we need to override extension method but we definitely couldn’t by its design.
namespace PassThroughScopeSample { public static class ServiceProviderExtension { public static IServiceScope CreateServiceScope(this IServiceProvider provider) { var newScope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope(); // Copy tenant identifier values from original scope to the new scope var originalTenantIdentifier = provider.GetRequiredService<TenantIdentifier>(); var newTenantIdentifier = newScope.ServiceProvider.GetRequiredService<TenantIdentifier>(); newTenantIdentifier.TenantId = originalTenantIdentifier.TenantId; return newScope; } } } namespace Microsoft.Extensions.DependencyInjection { /// <summary> /// This is only to interfere and break the build if someone tries to use the standard .NET CreateScope() extension function. /// We need to enforce usage of CreateServiceScope() custom extension function to make sure the necessary pass through variables are managed. /// </summary> public static class ServiceProviderServiceExtensions { [Obsolete("This will throw error use CreateServiceScope instead.")] public static IServiceScope CreateScope(this IServiceProvider provider) { throw new NotSupportedException("Do not use CreateScope() "); } } }
-
Time to apply usage of the new extension method.
Add dependency injection of TenantIdentifer class and make sure to set TenantId from the payload when the endpoint is being requested. And whenever you need to create a new scope just use IServiceProvider.CreateServiceScope() instead of IServiceProvider.CreateScope().
```c#
[ApiController]
[Route("[controller]")]
public class WebhookController : ControllerBase
{
private readonly TenantIdentifier _tenantIdentifier;
private readonly IServiceProvider _serviceProvider;
public WebhookController(TenantIdentifier tenantIdentifier, IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_tenantIdentifier = tenantIdentifier;
}
[HttpPost]
public async Task Post([FromBody]WebhookPayload webhookPayload)
{
_tenantIdentifier.TenantId = webhookPayload.TenantId;
// Using Task.Run to fire and forget logging operation which is not too important
_ = Task.Run(async () => // This can be one-liner just expose to explain
{
// Create a new scope to avoid scope is disposed sooner than this to be completed
var scope = _serviceProvider.CreateServiceScope(); // Create a new scope using CreateServiceScope() instead of CreateScope() to carry data
var logService = scope.ServiceProvider.GetRequiredService<ILogService<WebhookController>>();
await logService.TenantAuditAsync(webhookPayload.Action);
});
await Task.Delay(500); // simmulate actual long-operation
}
}
```
With this workaround we no longer have to pass through the request identifier via function parameter and worry-free that the service of the new scope will call what other services.
The full (runnable) source code sample can be found at panot-hong/PassThroughScopeSample (github.com)