I want to create a claim based authorization for my ASP.NET Core app:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization(options =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", "1", "2", "3", "4", "5"));
});
}
The problem is that I have a non trivial method to resolve the employee numbers (1 to 5) and I want to use a DI service:
public interface IEmployeeProvider {
string[] GetAuthorizedEmployeeIds();
}
I would like to inject this service and use it in AddPolicy, something like:
services.AddAuthorization(options =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", *employeeProvider.GetAuthorizedEmployeeIds()));
});
Note
I know that I can write my own AuthorizationHandler where I can easily inject IEmployeeProvider but I'm against this pattern because:
There is a already a handler that does exactly what I need
I need to write a new handler for each claim type and each different requirement
This is an anti pattern because the employee ids should really be part of the requirement while the handler should be generic component that handles the requirements
So I'm looking for a way to inject services when the policy is being built
To supplement the provided answer by #MichaelShterenberg, the configuration delegate can use a IServiceProvider to allow for additional dependencies
public static IServiceCollection AddAuthorization(this IServiceCollection services,
Action<AuthorizationOptions, IServiceProvider> configure) {
services.AddOptions<AuthorizationOptions>().Configure<IServiceProvider>(configure);
return services.AddAuthorization();
}
Which, based on the original example, can be used
public void ConfigureServices(IServiceCollection services) {
//...
service.AddScoped<IEmployeeProvider, EmployeeProvider>();
services.AddAuthorization((options, sp) => {
IEmployeeProvider employeeProvider = sp.GetRequiredService<IEmployeeProvider>();
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds())
);
});
//...
}
If there were other dependencies needed, they could be resolved from the service provider.
Thanks to Nkosi for the tip!
Since AddAuthorization is basically configuring AuthorizationOptions behind the scenes, I followed the same pattern only I used OptionsBuilder to configure options with dependencies
I created my own AddAuthorization method that accepts dependencies:
public static IServiceCollection AddAuthorization<TDep>(
this IServiceCollection services,
Action<AuthorizationOptions, TDep> configure) where TDep : class
{
services.AddOptions<AuthorizationOptions>().Configure<TDep>(configure);
return services.AddAuthorization();
}
And now I can use it to properly configure the requirement:
services.AddAuthorization<IEmployeeProvider>((options, employeeProvider> =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds())
);
});
You can follow the same technique if you need more dependencies (OptionsBuilder.Configure supports up to 5 dependencies)
Obviously, this solution requires extra validation when upgrading to newer ASP versions, as the underlying implementation of AddAuhtorization may change
You can build a service provider using the BuildServiceProvider() method on the IServiceCollection:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IEmployeeProvider, EmployeeProvider>();
var sp = services.BuildServiceProvider();
var employeeProvider = sp.GetService<IEmployeeProvider>();
string[] values = employeeProvider.GetAuthorizedEmployeeIds();
services.AddAuthorization(options =>
{
options.AddPolicy("Founders", policy =>
policy.RequireClaim("EmployeeNumber", employeeProvider.GetAuthorizedEmployeeIds()));
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
interface and Class
public interface IEmployeeProvider
{
string[] GetAuthorizedEmployeeIds();
}
public class EmployeeProvider : IEmployeeProvider
{
public string[] GetAuthorizedEmployeeIds()
{
var data = new string[] { "1", "2", "3", "4", "5" };
return data;
}
}
Related
This is more a general question.
But I have a question about injecting services in the startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(opt =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
opt.Filters.Add(new AuthorizeFilter(policy));
})
.AddFluentValidation(config =>
{
config.RegisterValidatorsFromAssemblyContaining<Create>();
});
services.AddApplicationServices(Configuration);
services.AddIdentityServices(Configuration);
}
So anyway I split the services in other file:
public static class StartupExtensionClass
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services,
IConfiguration Configuration)
{
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "API", Version = "v1" });
});
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddCors(opt =>
{
opt.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyMethod().AllowAnyHeader().WithOrigins("http://localhost:3000");
});
});
services.AddMediatR(typeof(List.Handler).Assembly);
services.AddAutoMapper(typeof(MappingProfiles).Assembly);
services.AddScoped<IUserAccessor, UserAccessor >();
services.AddScoped<IPhotoAccessor, PhotoAccessor>();
services.Configure<CloudinarySettings>(Configuration.GetSection("Cloudinary"));
return services;
}
But for example if you have hundreds of services. IS that not overloading the application in the start phase?
Because all the services will initialized in the beginning.
Is there not a way just to initialise some services when they will directly been called.
Like lazy loading
Thank you
I believe you are just setting up what the runtime will use when a class of the specified type is asked for....you aren't creating the classes immediately. So if your application just started up, but you never made any request to it, it isn't like 100 classes were just instantiated for no reason.
I'm trying to fully understand Dependency Injections. I'm defining a Filter and would like to read from a configuration file. Is it a better practice to instantiate Configuration inside of the filter or can this be done so globally, such as in the startup? If So, any pointers for how to do so?
public class CompanyFilter : ActionFilterAttribute
{
string _ERPUrl;
public CompanyFilter(IConfiguration iconfiguration)
{
ERPUrl = iconfiguration.GetSection("AppSettings").GetSection("ERPUrl").Value;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.Controller is Controller controller)
controller.ViewBag.ERPUrl = _ERPUrl;
//filterContext.Controller.ViewBag.Company = "Test";
}
}
Startup Class
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
....
Controllers
namespace Projects.Controllers
{
[CompanyFilter]
public class HomeController : Controller
{
....
The following error is produced.
Controllers\HomeController.cs(14,6): error CS7036: There is no argument given that corresponds to the required formal parameter 'iconfiguration' of 'CompanyFilter.CompanyFilter(IConfiguration)'
I would suggest you to use IOptions<T> to retrieve configuration from a file with all of the advantages supported by .Net Core. You can see how to do it here.
Also, to inject it to dependecy injection resolver add services.AddTransient(p => new MyService(mySettings)); to your ConfigureServices() function as transient or scoped or singleton (decide which one suits you better).
If you insist on using IConfiguration to retrieve configuration and solve the problem that you got, you should inject your IConfiguration instance like this services.AddSingleton(Configuration);. Hope this solves your problem.
Based upon some of the feedback here the following is workable by adding to Startup.cs.
services.AddMvc(options => {
options.Filters.Add(new ERPFilter(Configuration));
}
The url can be factored per the point above to improve performance.
url = ...
services.AddMvc(options => {
options.Filters.Add(new ERPFilter(url));
}
To provide an answer based on the comments provided yesterday by others & myself, it is recommended to inject IOptions<T> into your filters, or any other objects which require configuration data to be injected.
You can add your ERP settings to your appSettings.json file like so
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"Erp": {
"Url": "https://localhost"
}
}
To inject your settings into dependencies you must register it via ConfigureServices, you'll also notice that CompanyFilter is added to the IServiceCollection via AddTransient, this is to allow the ServiceFilterAttribute to resolve it at a later stage and inject any dependencies it the filter has.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.Configure<ErpSettings>(Configuration.GetSection("Erp"));
services.AddTransient<CompanyFilter>();
}
To apply your filter on your controller action, use ServiceFilterAttribute(Type)`
[HttpGet]
[ServiceFilter(typeof(CompanyFilter))]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { ViewBag.ERPUrl };
}
In the above code you'll see that I am returning ViewBag.ERPUrl, this is because your ComapnyFilter has overrided OnActionExecuting which is executed before the action is invoked, whereas OnActionExecuted is invoked after your action has finished and before the response is returned to the caller.
This is how theCompanyFilter now looks, you'll notice that the constructor now accepts IOptions<ErpSettings>
public class CompanyFilter : ActionFilterAttribute
{
private readonly ErpSettings erpSettings;
public CompanyFilter(IOptions<ErpSettings> erpSettings)
{
this.erpSettings= erpSettings.Value;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.Controller is Controller controller)
controller.ViewBag.ERPUrl = erpSettings.Url;
}
}
With all of this done, this is the response
I would like to write integration tests for my Asp .net core application, but I don't want my tests to use real implemetation of some services.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
...
services.AddTransient<IExternalService,ExternalService>();
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
}
}
public interface IExternalService
{
bool Verify(int id);
}
public class ExternalService : IExternalService
{
public bool Verify(int id)
{
//Implemetation is here.
//I want to fake this implemetation during testing.
}
}
[Fact]
public void TestCase()
{
//Stub out service
var myExtService = new Mock<IExternalService>();
//Setup response by stub
myExtService
.Setup(p => p.Verify(It.IsAny<int>()))
.Returns(false);
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
var client = server.CreateClient();
var response = client.GetAsync("/").Result;
var responseString = response.Content.ReadAsStringAsync().Result;
Assert.Contains("Your service returned: False", responseString);
}
Current injection setup in test case does not work, because ExternalService is injected over the mock.
However the test will pass when I remove services.AddTransient<IExternalService,ExternalService>; from Startup.
Most likely the one in Startup is called later and all the setup in that class is preferred by application.
What options do I have to setup some dependecies in tests, but use everything else as they are declared in Startup?
UPDATE
Application code should be unaware of tests.
Tests should be aware of:
(weakly typed) Endpoint - if this changes then test should fail
IExternalService interface
Tests should not care if application has razor pages or uses mvc or how the application is wired between endpoint and IExternalService.
Tests should not have to setup or configure application (apart from stubbing IExternalService) in order to make it work.
I understand that WebHostBuilder still has to be created, but my point is that configuration should be bare minimum in test case and majority of configuration should still be described on application side.
The only option I know of is to setup WebHostBuilder with UseEnvironment:
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureServices(services =>
{
//Setup injection
services.AddTransient<IExternalService>(provider =>
{
return myExtService.Object;
});
})
.UseEnvironment("IntegrationTest");
And then add a condition in the ConfigureServices method in the Startup:
public void ConfigureServices(IServiceCollection services)
{
if (Configuration["Environment"] != "IntegrationTest")
{
services.AddTransient<IExternalService, ExternalService>();
}
services.AddMvc();
// ...
}
UPDATE:
I did some more poking around and another option is to not use UseStartup extension method but rather configure the WebHostBuilder directly. You can do this a number of ways but I thought that you could possibly create your own extension method to create a template in your tests:
public static class WebHostBuilderExt
{
public static WebHostBuilder ConfigureServicesTest(this WebHostBuilder #this, Action<IServiceCollection> configureServices)
{
#this.ConfigureServices(services =>
{
configureServices(services);
services.AddMvc();
})
.Configure(builder =>
{
builder.UseMvc();
});
return #this;
}
}
Now your tests can be setup like the following:
var host = new WebHostBuilder()
.ConfigureServicesTest(services =>
{
//Setup injection
services.AddTransient<IInternalService>(provider =>
{
return myExtService.Object;
});
});
var server = new TestServer(host);
This means that you will have to explicitly setup all the implementations that the container will resolve for the specific endpoint you are calling. You can choose to mock or use the the concrete implementations.
The only thing yo need to change is to use ConfigureTestServices instead of ConfigureServices. ConfigureTestServices runs after your Startup, therefor you can override real implementations with mocks/stubs. ConfigureServices was newer intended for that purpose, rather, it configures "host services", which are used during the host-building phase of the application, and copied into the application's DI container.
ConfigureTestServices is available in ASP Core version 2.1 and higher.
var host = new WebHostBuilder()
.UseStartup<Startup>()
.ConfigureTestServices((services) =>
{
//Setup injection
services.AddTransient<IExternalService>((a) =>
{
return myExtService.Object;
});
});
So after hours of research I found a solution.
I could not find a way to solely use built-in dependency injection solution, so I opted to choose 3rd party DI solution - Autofac
Idea is to use WebHostBuilder (declared Main Program) and add necessary options so I can fake some services during testing.
Something that I learned:
If you use Startup as host.UseStartup<Startup> it will be created after host.ConfigureServices()
You cannot inject something to Startup like host.UseStartup<Startup>(new Dependency())
However if you have registred dependency in host.ConfigureServices(services => services.AddTransient<IDependency, MyDependency>()), then it will be resolved before Startup is created and constructor public Startup(IDependency dependency) is used to create Startup.
My application side:
public class Program
{
public static void Main(string[] args)
{
CreateWebHost(args)
.Build()
.Run();
}
public static IWebHostBuilder CreateWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureServices((services) =>
{
//Setup autofac.
services.AddAutofac();
//Register module dependency that Startup requires.
services.AddTransient<Module, MyAutofacModule>();
////It would a bit cleaner to use autofac to setup Startup dependency,
////but dependency did not get resolved for Startup.
//services.AddAutofac((builder) =>
//{
// builder.RegisterModule(new AutofacModule());
//});
})
.UseStartup<Startup>();
}
public class MyAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
//Register all application dependencies in this module.
builder.Register((c) => new ExternalService()).As<IExternalService>();
}
}
public class Startup
{
private Module applicationDIModule;
public Startup(Module applicationDIModule)
{
this.applicationDIModule = applicationDIModule;
}
public void ConfigureServices(IServiceCollection services)
{
//We can add build-in services such as mvc and authorization,
//but I would not use Add(Transient/Scoped/Singleton) here.
//You should register domain specific dependecies in MyAutofacModule,
//since it will be added after this method call.
services.AddMvc();
}
//This method is called after ConfigureServices (refer to Autofac link).
public void ConfigureContainer(ContainerBuilder builder)
{
//We will register injected module.
builder.RegisterModule(applicationDIModule);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvcWithDefaultRoute();
}
}
Test case:
public class IntegrationTests
{
[Fact]
public void TestCase()
{
//Create and setup moq object as usual.
var service = new Mock<IExternalService>();
service
.Setup(p => p.Verify(It.IsAny<int>()))
.Returns(false);
//Bundle moq objects together for registration.
var attachFakes = new Action<ContainerBuilder>((builder) =>
{
builder.Register(c => service.Object);
});
//Use host builder that application uses.
var host = Program.CreateWebHost(new string[] { })
.UseContentRoot(GetContentRoot()) //Adjust content root since testproject.csproj is not in same folder as application.csproj
.ConfigureServices((services) =>
{
//We re-configure Module registration,
//so Startup is injected with our TestModule.
services.AddTransient<Module>((a) =>
{
return new TestModule(attachFakes);
});
});
//Create server to use our host and continue to test.
var server = new TestServer(host);
var client = server.CreateClient();
var response = client.GetAsync("/").Result;
var responseString = response.Content.ReadAsStringAsync().Result;
Assert.Contains("External service result: False", responseString);
}
private static string GetContentRoot()
{
var current = Directory.GetCurrentDirectory();
var parent = Directory.GetParent(current).Parent.Parent.Parent;
return Path.Combine(parent.FullName, "src");
}
}
public class TestModule : MyAutofacModule
{
private Action<ContainerBuilder> attachFakes;
public TestModule(Action<ContainerBuilder> attachFakes)
{
this.attachFakes = attachFakes;
}
protected override void Load(ContainerBuilder builder)
{
//We register everything in MyAutoFacModule before adding our fakes.
base.Load(builder);
//We add fakes and everything that is re-registered here will be used instead.
attachFakes.Invoke(builder);
}
}
Although it feels a bit brittle, but I still prefer this solution over what #ODawg suggested. His solution would work, but I see it would cause troubles in future when new test cases are added.
Is it possible to use ASP.NET Identity without Entity Framework and Entity Framework migrations? The rest of my application will be using a Micro ORM for data access. However, the application is using the built in ASP.NET Identity Individual User accounts.
My goal is to still be able to use the built in UserManager and LoginManager classes and additionally retrieve a list of the Users using the Micro ORM and do away with anything to do with EF/Migrations. Is this possible? It doesn't seem like it is since the original database structure is created by Applying the initial migration.
If someone has a good technique for doing this, please share.
First you need to create a custom user Store:
public class UserStore : IUserStore<IdentityUser>,
IUserClaimStore<IdentityUser>,
IUserLoginStore<IdentityUser>,
IUserRoleStore<IdentityUser>,
IUserPasswordStore<IdentityUser>,
IUserSecurityStampStore<IdentityUser>
{
// interface implementations not shown
}
Then you need to register it into the dependency injection container:
// Add identity types
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddDefaultTokenProviders();
// Identity Services
services.AddTransient<IUserStore<ApplicationUser>, CustomUserStore>();
services.AddTransient<IRoleStore<ApplicationRole>, CustomRoleStore>();
This is documented here.
Asp.Net Identity has abstracted away the stores it needs and documentation on their stores is here;
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-custom-storage-providers
This is an example of a store;
public class InMemoryUserStore<TUser> :
IUserStore<TUser>,
IUserLoginStore<TUser>,
IUserClaimStore<TUser>,
IUserPasswordStore<TUser>,
IUserSecurityStampStore<TUser>,
IUserTwoFactorStore<TUser>,
IUserEmailStore<TUser>,
IUserLockoutStore<TUser>,
IUserAuthenticatorKeyStore<TUser>,
IUserTwoFactorRecoveryCodeStore<TUser>,
IUserPhoneNumberStore<TUser> where TUser: MemoryIdentityUser
{
...
}
You can also have your own User object, and it doesn't have to inherit from anything.
public class MemoryIdentityUser
{
private List<MemoryUserClaim> _claims;
private List<MemoryUserLogin> _logins;
private List<MemoryUserToken> _tokens;
...
}
Asp.Net Identity is an engine and as such is opinionated. It is that opinion that drove the abstractions of the stores. I wish the Asp.Net Identity docs has full sequence diagrams as to how it interacts with the stores. At a minimum a few reference sequences that have to be honored.
The store has some quirks where it has required methods that are only to mutate private data in the implementation and then followed up by update calls that assume you will commit that data to persistent storage.
You might want to check out this project;
https://github.com/ghstahl/AspNetCore.2.InMemoryIdentity
You can see what you need to do without the burden of having a database.
Hooking it up;
// My user is custom, so I made ApplicationUser inherit
public class ApplicationUser : MemoryIdentityUser
{
}
Startup.cs;
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IUserStore<ApplicationUser>>(provider =>
{
return new InMemoryUserStore<ApplicationUser>();
});
services.AddIdentity<ApplicationUser>(Configuration)
.AddDefaultTokenProviders();
// Add application services.
services.AddTransient<IEmailSender, EmailSender>();
services.AddMvc();
}
In AddIdentity, the following illustrates to the extent you can bring in your own implementations
public static class InMemoryIdentityServiceCollectionExtensions
{
public static IdentityBuilder AddIdentity<TUser>(this IServiceCollection services, IConfiguration configuration)
where TUser : class => services.AddIdentity<TUser>(configuration,null);
public static IdentityBuilder AddIdentity<TUser>(this IServiceCollection services, IConfiguration configuration,Action<IdentityOptions> setupAction)
where TUser : class
{
// Services used by identity
var authenticationBuilder = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme,
o => o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme)
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Hosting doesn't add IHttpContextAccessor by default
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Identity services
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), services);
}
}
There are a bunch of IUserStore implementations out there, with every type of backing database. I copied my InMemoryUserStore from another project that was using MongoDB as a backing DB.
On an ASP.NET MVC 5 application I have the following StructureMap configuration:
cfg.For(typeof(IRequestHandler<,>)).DecorateAllWith(typeof(MediatorPipeline<,>));
Does anyone know how to do this configuration with ASP.NET Core IOC?
The out of the box IoC container doesn't support decorate pattern or auto discovery, which is "by design" as far as I know.
The idea is to provide a basic IoC structure that works out of the box or where other IoC containers can be plugged in to extend the default functionality.
So if you need any advanced features (support for a specific constructor, auto-registering of all types which implement an interface or inject decorators and interceptors) you have to either write it yourself or use an IoC container which offers this functionality.
Use Scrutor. Just install the nuget package and then do the following.
services.AddSingleton<IGreeter, Greeter>();
services.Decorate<IGreeter, GreeterLogger>();
services.Decorate<IGreeter, GreeterExceptionHandler>();
The order is important. In the above, GreeterLogger decorates Greeter. And GreeterExceptionHandler decorates GreeterLogger.
If you need more info, take a look at this and this.
And of course, you can use the popular Autofac as well.
If you want to know how to configure Autofac, take a look at Ardalis Clean Arch template
In my blogpost I described how a relatively simple extension method can solve this problem easily. Here is an example from that post which shows how decorator configuration may look like:
services.AddDecorator<IEmailMessageSender, EmailMessageSenderWithRetryDecorator>(decorateeServices =>
{
decorateeServices.AddScoped<IEmailMessageSender, SmtpEmailMessageSender>();
});
This workaround doesn't apply the decorator to all instances of a type but uses extension methods to abstract the decorator logic into another file.
Defining the decorator structure like:
public static class QueryHandlerRegistration
{
public static IServiceCollection RegisterQueryHandler<TQueryHandler, TQuery, TResult>(
this IServiceCollection services)
where TQuery : IQuery<TResult>
where TQueryHandler : class, IQueryHandler<TQuery, TResult>
{
services.AddTransient<TQueryHandler>();
services.AddTransient<IQueryHandler<TQuery, TResult>>(x =>
new LoggingDecorator<TQuery, TResult>(x.GetService<ILogger<TQuery>>(), x.GetService<TQueryHandler>()));
return services;
}
}
And calling it like:
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
services.RegisterQueryHandler<FindThingByIdQueryHandler, FindThingByIdQuery, Thing>();
There's also the Scrutor package being worked on.
one of another example
services.AddTransient<Greeter>();
services.AddTransient<IGreeter>(g=>
ActivatorUtilities.CreateInstance<GreeterLogger>(g,g.GetRequiredServices<Greeter>())
);
or generic
private static void AddTransientDecorated<TInterface,TService,TDecorator>(this IServiceCollection services)
{
services.AddTransient(typeof(TService));
services.AddTransient(typeof(TInterface), p => ActivatorUtilities.CreateInstance<TDecorator>(p, p.GetRequiredService<TService>()));
}
additional information .NET Core DI, ways of passing parameters to constructor
None of these answers appear to fulfil the question - i.e. "how do we annotate a generic with a generic without specifying the type?". As the question is pretty old, this may not have been possible at the time. When you look at scrutor (from 2017) the answer is "you can't due to the underying DI framework" - https://github.com/khellang/Scrutor/issues/39
I'm really confused by this as I've managed to get this working out of the box with the Microsoft DI framework. Can anyone see any issues with this?
Thanks to those who got decorators working in the first place.
public static IServiceCollection AddDecorator(this IServiceCollection services, Type matchInterface, Type decorator, params Assembly[] assemblies)
{
Constraint.Requires(matchInterface.IsInterface, "Must be an interface to match");
Constraint.Requires(!decorator.IsInterface, "Must be a concrete type");
Constraint.Requires(assemblies.Length > 0, "Must provide at least one assembly for scanning for decorators");
var decoratedType = assemblies.SelectMany(t => t.GetTypes())
.Distinct()
.SingleOrDefault(t => t == decorator.GetGenericTypeDefinition());
if (decoratedType == null)
{
throw new InvalidOperationException($"Attempted to decorate services of type {matchInterface.Name} with decorator {decorator.Name} but no such decorator found in any scanned assemblies.");
}
foreach (var type in services
.Where(sd =>
{
try
{
return sd.ServiceType.GetGenericTypeDefinition() == matchInterface.GetGenericTypeDefinition();
}
catch (InvalidOperationException)
{
return false;
}
}).ToList())
{
var decoratedInstanceType = decoratedType.MakeGenericType(type.ServiceType.UnderlyingSystemType.GenericTypeArguments);
//Create the object factory for our decorator type, specifying that we will supply the interface injection explicitly
var objectFactory = ActivatorUtilities.CreateFactory(decoratedInstanceType, new[] {type.ServiceType});
//Replace the existing registration with one that passes an instance of the existing registration to the object factory for the decorator
services.Replace(ServiceDescriptor.Describe(
type.ServiceType,
s => objectFactory(s, new[] {s.CreateInstance(type)}),
type.Lifetime));
}
return services;
}
Usage:
services
.AddDecorator(typeof(IAsyncCommandHandler<>), typeof(LoggingCommandDecorator<>), typeof(LoggingCommandDecorator<>).Assembly)
.AddDecorator(typeof(IAsyncCommandHandler<>), typeof(TracingCommandDecorator<>), typeof(TracingCommandDecorator<>).Assembly)
.AddDecorator(typeof(IAsyncQueryHandler<,>), typeof(TracingQueryDecorator<,>), typeof(TracingQueryDecorator<,>).Assembly);
If for whatever reason you can't use Scrutor, this might help:
public static class ServiceCollectionExtensions
{
public static void AddWithDecorators<TService, TImplementation>(
this ServiceCollection serviceCollection, IEnumerable<Type> decorators, ServiceLifetime serviceLifetime)
{
serviceCollection.Add(new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), serviceLifetime));
var inner = typeof(TImplementation);
foreach (var decoratorType in decorators)
{
var innerCopy = inner;
var sd = new ServiceDescriptor(decoratorType,
sp => ActivatorUtilities.CreateInstance(sp, decoratorType, sp.GetRequiredService(innerCopy)),
serviceLifetime);
serviceCollection.Add(sd);
inner = decoratorType;
}
serviceCollection.Add(new ServiceDescriptor(typeof(TService), sp => sp.GetRequiredService(inner), serviceLifetime));
}
public static void AddWithDecorator<TService, TImplementation, TDecorator>(this ServiceCollection serviceCollection,
ServiceLifetime serviceLifetime)
=> AddWithDecorators<TService, TImplementation>(
serviceCollection,
new[] { typeof(TDecorator) },
serviceLifetime);
public static void AddWithDecorators<TService, TImplementation, TDecorator1, TDecorator2>(
this ServiceCollection serviceCollection, ServiceLifetime serviceLifetime)
=> AddWithDecorators<TService, TImplementation>(
serviceCollection,
new[] { typeof(TDecorator1), typeof(TDecorator2) },
serviceLifetime);
public static void AddWithDecorators<TService, TImplementation, TDecorator1, TDecorator2, TDecorator3>(
this ServiceCollection serviceCollection, ServiceLifetime serviceLifetime)
=> AddWithDecorators<TService, TImplementation>(
serviceCollection,
new[] { typeof(TDecorator1), typeof(TDecorator2), typeof(TDecorator3) },
serviceLifetime);
}
usage:
var sc = new ServiceCollection();
sc.AddWithDecorators<IStore<NamedEntity>, SimpleStore<NamedEntity>, CachedStore<NamedEntity>, WrapperStore<NamedEntity>>(ServiceLifetime.Singleton);
or
sc.AddWithDecorators<IStore<NamedEntity>, SimpleStore<NamedEntity>>(new[] { typeof(CachedStore<NamedEntity>), typeof(WrapperStore<NamedEntity>) },
ServiceLifetime.Singleton);