-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Add ServiceProviderAccessor to allow access to IServiceProvider from Interceptor #364
base: main
Are you sure you want to change the base?
Conversation
@@ -0,0 +1,16 @@ | |||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adapted from ASP.NET's IHttpContextAccessor
.
@@ -0,0 +1,42 @@ | |||
using System; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adapted from ASP.NET's HttpContextAccessor
.
6b175cf
to
886015d
Compare
@@ -68,6 +68,14 @@ public static ActivityDefinition CreateTemporalActivityDefinition( | |||
#else | |||
var scope = provider.CreateScope(); | |||
#endif | |||
IServiceProviderAccessor? serviceProviderAccessor = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inspiration taken from ASP.NET's DefaultHttpContextFactory
.
/// </summary> | ||
public class ServiceProviderAccessor : IServiceProviderAccessor | ||
{ | ||
private static readonly AsyncLocal<ServiceProviderHolder> ServiceProviderCurrent = new(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A System.Collections.Concurrent.ConcurrentDictionary
that is keyed on the Activity's unique ID could be a simpler replacement for this and would work just as well since it would depend on the AsyncLocal
holding the current Activity
.
886015d
to
b657fe0
Compare
Thanks! Hrmm, I am unsure we want to provide an async local form of what users can provide themselves. .NET expects, if you want access to the service provider, you inject it like anything else. Why not just accept service provider in the activity class constructor if you need it? Something like: public class MyActivities
{
private readonly IServiceProvider serviceProvider;
private readonly IWhateverElse whateverElse;
public MyActivities(IServiceProvider serviceProvider, IWhateverElse whateverElse)
{
this.serviceProvider = serviceProvider;
this.whateverElse = whateverElse;
}
[Activity]
public string DoMyActivity(string someParam)
{
// serviceProvider available here
throw new NotImplementedException("TODO");
}
} This way |
@cretz, thanks for the suggestion, but how do I that with an interceptor? The issue I'm trying to find a solution for relates to handling a cross-cutting concern that would need to be handled in every Activity. For example, consider adding support for OpenTelemetry in every Activity compared to adding support for OpenTelemetry via an interceptor that allows each Activity to be agnostic of OpenTelemetry and to instead focus on its actual job. Support for OpenTelemetry avoids needing DI because diagnostic activities don't require a reference to the specific trace provider. But if that weren't the case, if supporting OpenTelemetry required the interceptor to have a reference to the specific trace provider and that trace provider were only accessible via the service provider, how would support for OpenTelemetry be handled other than by adding logic to every Activity to handle OpenTelemetry concerns via the injected Put another way, if there's some common, scoped service registered with the Put yet another way, if I were making a library for use with Temporal, how could I provide a library that depended on a scoped service from |
An interceptor cannot control what an activity implementer chooses to dependency inject. This same concern occurs with anything to dependency inject not just service provider.
DI is optional in .NET and I believe the reason that OpenTelemetry and .NET diagnostic activities use a shared tracer provider and shared .NET diagnostic activity sources is because the per-DI-scope design is too limiting for general purpose use. I wonder if your use case could not rely on optional DI and scoping the same way OTel does not.
Because it is a specific need in your case to have implicit access to the DI container in a non-DI instantiated interceptor. One approach may be leveraging public class MyServiceScopeFactory : IServiceScopeFactory
{
public static readonly AsyncLocal<IServiceProvider> CurrentServiceProvider = new();
private readonly IServiceScopeFactory underlying;
public MyServiceScopeFactory(IServiceScopeFactory underlying) => this.underlying = underlying;
public IServiceScope CreateScope()
{
var scope = underlying.CreateScope();
CurrentServiceProvider.Value = scope.ServiceProvider;
return scope;
}
} And change host builder to have something like The other option we'd entertain is a way to make the activity inbound interceptor be created in the current scope. My concern is exposing a static based on async local if we don't have to, but maybe we can have some |
Hey @cretz , thanks for the thorough response. I like your I'm out of time for this today, but will respond to your other comments later this week. ❤️ |
Interesting. So I am now starting to lean towards providing the service provider in the interceptor only. Meaning I wonder if we should keep this async local namespace Temporalio.Extensions.Hosting;
interface IWorkerInterceptorWithServiceProvider : IWorkerInterceptor
{
ActivityInboundInterceptor InterceptActivity(ActivityInboundInterceptor nextInterceptor) =>
InterceptActivity(WhateverInternalAsyncLocal.Value, nextInterceptor);
ActivityInboundInterceptor InterceptActivity(IServiceProvider serviceProvider, ActivityInboundInterceptor nextInterceptor) =>
nextInterceptor;
} My main concern is I am not wanting people to generally use an async local to access the scope or service provider inside the activity if we can help it. I agree that constructor injection doesn't work with interceptors, so maybe this can work around it this way (still using async local internally though). |
What was changed
This PR is a sketch of a solution for #363 that leverages a pattern that's used in ASP.NET for managing access to the
HttpContext
instance that is appropriate for a given execution context.I don't think this PR is done, but I wanted to sketch something for discussion before putting more effort in.
Some things to note:
IServiceProviderAccessor
instance with theIServiceCollection
.IServiceProvider
from anActivityInboundInterceptor
possible withoutActivityInboundInterceptor
needing to know anything aboutIServiceProvider
.Why?
I want to be able to use the scoped
IServiceProvider
instance used to construct the currentActivity
from anActivityInboundInterceptor
that is dealing with a cross-cutting concern that would benefit from access to the DI service provider.For example, I have an
ActivityInboundInterceptor
, call itUserInfoInterceptor
that interrogates theActivity
to figure out what user work is being performed for and then makes that information available to other DI services that care about that context using a service registered with theIServiceProvider
. I'd now like to add a newActivityInboundInterceptor
, call itLogDecoratorInterceptor
, that decorates the log context with details about the user. This change would allow me to use the mechanism thatUserInfoInterceptor
provides viaIServiceProvider
instead of having to recompute the user or do other black magic.Checklist
Closes #363
How was this tested:
Not tested, just a proposal (though I do expect that it works)
Any docs updates needed?
I would think an example would be needed because even with this accessor, things are more convoluted than might be ideal. Consider:
During the build phase of the application, it would be necessary to create an instance of
ServiceProviderAccessor
and provide it to anyIWorkerInterceptor
that cares about accessing the scopedIServiceProvider
instance later. The code would then need to register theServiceProviderAccessor
instance with theServiceCollection
being constructed as aSingleton
so thatServiceProviderExtensions.CreateTemporalActivityDefinition
could fetch theServiceProviderAccessor
instance later to register the scopedIServiceProvider
instance.To try to put that in code, here's a hypothetical
Program.cs