-
Notifications
You must be signed in to change notification settings - Fork 111
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
Feature Manager DI Scoping #258
Comments
Hello @samgibsonmoj It sounds like a workaround is:
I'm curious if you're able to pass this state when you construct your HTTPContextAccessor (create a new Accessor that accepts it as a parameter) so it could instead use that state instead of the normal HttpContext. Might not make sense with the lifecycle of Blazor apps, as I haven't used Blazor yet. Otherwise I think your fork is the correct solution for now. I'll check with others about adding an injection option for Blazor. |
Thanks @rossgrambo, I appreciate the fast response.
I'm not 100% sure this would work for what I'm looking to achieve. From what I can see, passing these values via the root component would make them available to other child components, but not within the Filter itself. Please let me know if I have misunderstood. For some further clarification, the filter looks like this:
I understand you can use an
I'll likely continue with this fork for now, and see what you and the others come back with. Even exposing these types as part of the public api would probably suffice. From there, we can pick and choose how we'd like to use/register them in our applications. |
I would think a solution here would be to define an accessor for your UserService like: public class UserContextAccessor: ITargetingContextAccessor
{
private readonly UserService _userService;
public UserContextAccessor(UserService userService)
{
this._userService = userService;
}
public ValueTask<TargetingContext> GetContextAsync()
{
TargetingContext targetingContext = new TargetingContext
{
UserId = "" + _userService.getTenantId("abc"),
Groups = new List<string>()
};
return new ValueTask<TargetingContext>(targetingContext);
}
} Once you register that, a call to TargetingFilter.IsEnabled() would use that context. Or if you wanted to stick with a custom filter, you could grab the accessor yourself public CustomFilter(ITargetingContextAccessor contextAccessor) {
...
TargetingContext targetingContext = await _contextAccessor.GetContextAsync().ConfigureAwait(false); The accessor could also use httpcontext to determine what the resulting targetingcontext should be. Let me know if that makes sense, I may be misunderstanding. |
Thanks for coming back to me @rossgrambo. Unfortunately, using an accessor doesn't avoid the issue I'm seeing here either. To clarify, the While it may then make logical sense to therefore register the UserContextAccessor as a Scoped dependency, this isn't possible as the (Singleton) Filter has a dependency on a Scoped Context Accessor:
This circles back to how the feature manager chooses to register its dependencies (as singletons). Hope that makes more sense. |
@samgibsonmoj Thank you for the explanation. I finally understand the issue #15. But I'm very willing to hear about what changes do you think we can make to solve your problem?
Could you provide a simple demo (maybe a public repo) to illustrate the problem better? |
Hi @zhiyuanliang-ms. Glad that cleared some things up.
Please see ServiceCollectionExtensions.cs, where the Feature Manager DI extension methods are exposed. Exposing these dependencies (ConfigurationFeatureDefinitionProvider, FeatureManager, EmptySessionManager, FeatureManagerSnapshot) as part of the public api. From here, we can pick and choose how we would like to register these dependencies, e.g:
I hope that makes a little bit more sense. If necessary, I can construct a demonstration repository. However, this is more of an issue with the way dependency injection has been implemented, as opposed to a fundamental issue with any feature management functionality. |
This has also been requested here: #126 |
See #3 from my fork for the proposed changes. |
Continuing discussions here for visibility. Comment below was left by @zhiyuanliang-ms on my Pull Request.
Thank you for taking a look, I'm interested to see what you come back with. Is there any reason you have decided against making these types available as part of the public api? In general, it allows the developer more flexibility in how each of the services should be registered, instead of forcing them to register everything as singletons... Imposing this global state is generally bad practice and seems like a misuse of DI in this case. |
@samgibsonmoj Hi, we are considering provide an option to register feature manager and feature filter as scoped services. Currently, we do not want to expose feature manager class to public. |
Why not? What is gained by gatekeeping the constructors? |
Current thinking is not so much about gatekeeping but moreso to keep the ease of services.AddFeatureManagement(), which adds multiple things. Something like services.AddScopedFeatureManagement(); And then we can expect that FeatureManager is a scoped service and can depend on scoped services. |
@jimmyca15 I'm a bit confused, can't you have public constructors while having a services.AddFeatureManagement() call for ease of service? |
Thanks for the response @jimmyca15. As @McDoit has correctly stated above, having the public constructor(s) wouldn't have any effect on the current AddFeatureManagement() extension method. This method would remain unaffected, and available for those who wish to continue to use it. I can appreciate the thought behind adding an alternative AddScopedFeatureManagement() extension method, but transient services will then also have to be considered... To me, the simplest and best solution is to expose public constructors, allowing us to register these types as necessary. I don't foresee any further changes required outside of this. |
@samgibsonmoj We are discussing how to expose the FeatureManager class. We are considering how to simplify the constructor of FeatureManager. The required parameters will be only IFeatureDefinitionProvider and FeatureManagmentOptions. |
@zhiyuanliang-ms Any progress in those discussions? |
@McDoit public class FeatureManager : IFeatureManager, IDisposable, IVariantFeatureManager
{
private readonly TimeSpan ParametersCacheSlidingExpiration = TimeSpan.FromMinutes(5);
private readonly TimeSpan ParametersCacheAbsoluteExpirationRelativeToNow = TimeSpan.FromDays(1);
private readonly IFeatureDefinitionProvider _featureDefinitionProvider;
private readonly FeatureManagementOptions _options;
private readonly ConcurrentDictionary<string, IFeatureFilterMetadata> _filterMetadataCache;
private readonly ConcurrentDictionary<string, ContextualFeatureFilterEvaluator> _contextualFeatureFilterCache;
private class ConfigurationCacheItem
{
public IConfiguration Parameters { get; set; }
public object Settings { get; set; }
}
public FeatureManager(
IFeatureDefinitionProvider featureDefinitionProvider,
FeatureManagementOptions options)
{
_featureDefinitionProvider = featureDefinitionProvider;
_options = options ?? throw new ArgumentNullException(nameof(options));
_filterMetadataCache = new ConcurrentDictionary<string, IFeatureFilterMetadata>();
_contextualFeatureFilterCache = new ConcurrentDictionary<string, ContextualFeatureFilterEvaluator>();
}
public IEnumerable<IFeatureFilterMetadata> FeatureFilters;
public IMemoryCache ParametersCache;
public IEnumerable<ISessionManager> SessionManagers;
public ILogger featureManagerLogger;
public IEnumerable<ITelemetryPublisher> TelemetryPublishers { get; init; }
public IConfiguration Configuration { get; init; }
public ITargetingContextAccessor TargetingContextAccessor { get; init; }
public TargetingEvaluationOptions AssignerOptions;
// Methods
} The AddFeatureManagement method provided by the lib will be like: public static IFeatureManagementBuilder AddFeatureManagement(this IServiceCollection services)
{
services.AddLogging();
services.AddMemoryCache();
//
// Add required services
services.TryAddSingleton<IFeatureDefinitionProvider, ConfigurationFeatureDefinitionProvider>();
services.AddSingleton(sp => new FeatureManager(
sp.GetRequiredService<IFeatureDefinitionProvider>(),
sp.GetRequiredService<IOptions<FeatureManagementOptions>>()?.Value)
{
FeatureFilters = sp.GetService<IEnumerable<IFeatureFilterMetadata>>(),
ParametersCache = sp.GetService<IMemoryCache>(),
SessionManagers = sp.GetService<IEnumerable<ISessionManager>>(),
featureManagerLogger = sp.GetService<ILoggerFactory>().CreateLogger<FeatureManager>(),
Configuration = sp.GetService<IConfiguration>(),
TargetingContextAccessor = sp.GetService<ITargetingContextAccessor>(),
TelemetryPublishers = sp.GetService<IOptions<FeatureManagementOptions>>()?.Value.TelemetryPublisherFactories?
.Select(factory => factory(sp))
.ToList(),
AssignerOptions = sp.GetService<IOptions<TargetingEvaluationOptions>>()?.Value
});
services.TryAddSingleton<IFeatureManager>(sp => sp.GetRequiredService<FeatureManager>());
services.TryAddSingleton<IVariantFeatureManager>(sp => sp.GetRequiredService<FeatureManager>());
services.AddSingleton<ISessionManager, EmptySessionManager>();
services.AddScoped<FeatureManagerSnapshot>();
services.TryAddScoped<IFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
services.TryAddScoped<IVariantFeatureManagerSnapshot>(sp => sp.GetRequiredService<FeatureManagerSnapshot>());
var builder = new FeatureManagementBuilder(services);
//
// Add built-in feature filters
builder.AddFeatureFilter<PercentageFilter>();
builder.AddFeatureFilter<TimeWindowFilter>();
builder.AddFeatureFilter<ContextualTargetingFilter>();
return builder;
} Besides, we also want to provide a method called AddScopedFeatureManagement() which will register these services as scoped for you. Currently, I am investigating the other DI containers in .NET including Autofac, Unity, Ninject and Castle Windsor. So far, I found that the proposed change of Feature Manager can be supported by all of DI libs mentioned above. More discussion is still needed to finalize the change. Supporting Blazor and other DI containers has been placed in our plan, but I am not very sure when it will be published. Sorry for the inconvinence. |
Update: |
Thank you. |
Fixed in release 3.1.0 |
I have a net7 Blazor Server application and have integrated the Feature Management package into the application, registering various filters.
One filter, the 'TenantFilter' is responsible for enabling/disabling various areas of the system based on current users tenancy. This filter makes use of my 'ICurrentUserService', which is a scoped service that gives contextual information based on the user (e.g. tenant id, user id, user name).
I've noted that the Feature Manager (and other dependencies) are registered as a Singleton... There's no way around this scoping, as these types aren't made available outside of the library.
I'm unable to get any of the users contextual information via the HttpContextAccessor, as the HttpContext isn't guaranteed to be available via this interface.
While I understand there may be no enhancements planned for Blazor Server, I was curious on your take on this, and any potential solutions/workarounds. Currently, I've had to fork this repository, and register the services as Scoped.
Thanks.
The text was updated successfully, but these errors were encountered: