-
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). |
hey @cretz , sorry for not getting back to you. With the US holiday and some travel for work, I've been AFK for a few weeks. I'd be fine if only the interceptor got the I think I'd confused myself on what library Is this something you'd like me to take a stab at or do you have a plan of attack in mind? |
/// <summary> | ||
/// Gets the async local current value. | ||
/// </summary> | ||
public static readonly AsyncLocal<IServiceProvider?> AsyncLocalCurrent = 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.
Due to the need to access this across packages, this can't be internal unless Temporalio
is modified to make internals available to Temporalio.Extensions.Hosting
. I don't want this to be public
, but need some guidance on the right way to hide this value from the public.
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.
There is also some tension here because an interface
implementation of IWorkerInterceptorWithServiceProvider
would actually require that this field (or a wrapping property) is public because applications that are not NETCOREAPP3_0_OR_GREATER
would need to be able to access this field to replicate this logic
ActivityInboundInterceptor InterceptActivity(ActivityInboundInterceptor nextInterceptor) =>
InterceptActivity(ActivityServiceProviderAccessor.Current, nextInterceptor);
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.
I feel like the interface
approach to this is a bit clunky. Perhaps a class
would be better?
Hrmm, now that I'm thinking about it, this may not work either. The interceptor is invoked before the service provider is created. The scope and service provider is created as part of the last interceptor in the chain. But you want a service provider before then. I'm now thinking of an alternative approach where you get a service provider in your interceptor and create a scope yourself if needed. I now think you have to do something like this: resultOfAddHostedTemporalWorkers.ConfigureOptions().PostConfigure<IServiceProvider>((options, provider) =>
{
// Create an instance of the interceptor.
// Could also do provider.GetRequiredService<MyInterceptor> if it was registered on the provider.
var myInterceptor = new MyInterceptor(provider);
// Add to interceptor list (extra code for checking if list exists may not be needed for you)
var newInterceptors = new List<IWorkerInterceptor>();
if (options.Interceptors is { } existing)
{
newInterceptors.AddRange(existing.Interceptors);
}
newInterceptors.Add(myInterceptor);
worker.Interceptors = newInterceptors;
}); Then public class MyInterceptor : IWorkerInterceptor
{
private readonly IServiceProvider serviceProvider;
public MyInterceptor(IServiceProvider serviceProvider) =>
this.serviceProvider = serviceProvider;
public ActivityInboundInterceptor InterceptActivity(ActivityInboundInterceptor next) =>
new MyActivityInboundInterceptor(serviceProvider, next);
}
public class MyActivityInboundInterceptor : ActivityInboundInterceptor
{
private readonly IServiceProvider serviceProvider;
public MyActivityInboundInterceptor(IServiceProvider serviceProvider,
ActivityInboundInterceptor next)
: base(next) => this.serviceProvider = serviceProvider;
public override Task<object?> ExecuteActivityAsync(ExecuteActivityInput input)
{
using scope = serviceProvider.CreateScope();
// Do stuff with scope.ServiceProvider here...
return await base.ExecuteActivityAsync(input)
}
} Is this acceptable? (untested, just typed in here) |
@cretz, I explored taking a stab at this (PR updated). My approach was to adapt my proposed Points of friction:
|
79c2d00
to
8362d65
Compare
8362d65
to
7413e4d
Compare
@tdg5 - take a peek at my suggestion above your comment. I don't think the |
@cretz , just saw your comment. Catching up now. |
@cretz, IMHO, what you've suggested wouldn't work for what I'm trying to achieve. The same kind of thing could be achieved using a The trick is getting access to the specific scoped A second scoped I think allowing But all that said, it doesn't really seem like this can be achieved as currently implemented since the creation of the |
Hrmm. To confirm, you need the exact same instance of scoped things in an activity that you have in the interceptor? You cannot use a separate outer scope for your interceptor needs? The interceptor is invoked out of scope. Yeah, if you must use interceptors and they must be in the same scope as the activity, we have to refactor because that is not the current case. Otherwise, out-of-scope interceptors may not bee acceptable for your in-scope needs. If we can do it in a compatible way, I am ok instantiating the scope before the interceptor. The problem is that we intentionally don't give pre-activity-task/activation hooks to users (this separate library is a "user" in this sense). So we would now, in addition to having a concept of intercepting/wrapping activity work, need to have a concept of intercepting/wrapping the interception/wrapping of activity work. At some level we are always going to have to expose out-or-scope interception to external users so they do work like this DI library does create-scope. I am open to designs here, though hopefully we can avoid another layer of wrapping/interception/hooks even higher than interceptors. |
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