Why is my custom `AuthentictionStateProvider` not null in AddSingleton but null in AddScoped - asp.net-core

I had previously asked a question that was answered properly, but the problem is that when my custom AuthenticationStateProvider is registered as a scoped
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
I get the following error:
System.InvalidOperationException: GetAuthenticationStateAsync was called before SetAuthenticationState
But, when it is registered as a singleton, it works correctly, However, the single instance creates for the lifetime of the application domain by AddSingelton, and so this is not good.(Why? Because of :))
What should I do to register my custom AuthenticationStateProvider as a scoped, but its value is not null?
Edit:
According to #MrC aka Shaun Curtis Comment:
It's my CustomAuthenticationStateProvider:
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory _scopeFactory;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory)
: base(loggerFactory) =>
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user from a new scope to ensure it fetches fresh data
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
And it's a program configuration and service registration:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
#region Authentication
//Authentication
services.AddDbContextFactory<ApplicationDbContext>(options =>
{
options.UseSqlServer(
Configuration.GetConnectionString("LocalDBConnection"),
serverDbContextOptionsBuilder =>
{
var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
serverDbContextOptionsBuilder.CommandTimeout(minutes);
serverDbContextOptionsBuilder.EnableRetryOnFailure();
})
.AddInterceptors(new CorrectCommandInterceptor()); ;
});
//add policy
services.AddAuthorization(options =>
{
options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
});
// Needed for cookie auth.
services
.AddAuthentication(options =>
{
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.SlidingExpiration = false;
options.LoginPath = "/";
options.LogoutPath = "/login";
//options.AccessDeniedPath = new PathString("/Home/Forbidden/");
options.Cookie.Name = ".my.app1.cookie";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = context =>
{
var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
return cookieValidatorService.ValidateAsync(context);
}
};
});
#endregion
//AutoMapper
services.AddAutoMapper(typeof(MappingProfile).Assembly);
//CustomAuthenticationStateProvider
services.AddScoped<AuthenticationStateProvider, CustomAuthenticationStateProvider>();
.
.
}

Don't worry about the AddSingelton in the Blazor apps. Scoped dependencies act the same as Singleton registered dependencies in Blazor apps (^).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
The Blazor Server hosting model supports the Scoped lifetime across HTTP requests (Just for the Razor Pages or MVC portion of the app) but not across SignalR connection/circuit messages among components that are loaded on the client.
That's why there's a scope.ServiceProvider.GetRequiredService here to ensure the retrived user is fetched from a new scope and has a fresh data.
Actually this solution is taken from the Microsoft's sample.

Your problem is probably here:
var scope = _scopeFactory.CreateScope();
/...
var userManager = scope.ServiceProvider.GetRequiredService<IUsersService>();
You create a new IOC container and request the instance of IUsersService from that container.
If IUsersService is Scoped, it creates a new instance.
IUsersService requires various other services which the new container must provide.
public UsersService(IUnitOfWork uow, ISecurityService securityService, ApplicationDbContext dbContext, IMapper mapper)
Here's the definition of those services in Startup:
services.AddScoped<IUnitOfWork, ApplicationDbContext>();
services.AddScoped<IUsersService, UsersService>();
services.AddScoped<IRolesService, RolesService>();
services.AddScoped<ISecurityService, SecurityService>();
services.AddScoped<ICookieValidatorService, CookieValidatorService>();
services.AddScoped<IDbInitializerService, DbInitializerService>();
IUnitOfWork and ISecurityService are both Scoped, so it creates new instances of these in the the new Container. You almost certainly don't want that: you want to use the ones from the Hub SPA session container.
You have a bit of a tangled web so without a full view of everything I can't be sure how to restructure things to make it work.
One thing you can try is to just get a standalone instance of IUsersService from the IOC container using ActivatorUtilities. This instance gets instantiated with all the Scoped services from the main container. Make sure you Dispose it if it implements IDisposable.
public class CustomAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceProvider _serviceProvider;
public CustomAuthenticationStateProvider(ILoggerFactory loggerFactory, IServiceProvider serviceProvider)
: base(loggerFactory) =>
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(scopeFactory));
protected override TimeSpan RevalidationInterval { get; } = TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get an instance of IUsersService from the IOC Container Service to ensure it fetches fresh data
IUsersService userManager = null;
try
{
userManager = ActivatorUtilities.CreateInstance<IUsersService>(_serviceProvider);
return await ValidateUserAsync(userManager, authenticationState?.User);
}
finally
{
userManager?.Dispose();
}
}
private async Task<bool> ValidateUserAsync(IUsersService userManager, ClaimsPrincipal? principal)
{
if (principal is null)
{
return false;
}
var userIdString = principal.FindFirst(ClaimTypes.UserData)?.Value;
if (!int.TryParse(userIdString, out var userId))
{
return false;
}
var user = await userManager.FindUserAsync(userId);
return user is not null;
}
}
For reference this is my test code using the standard ServerAuthenticationStateProvider in a Blazor Server Windows Auth project.
public class MyAuthenticationProvider : ServerAuthenticationStateProvider
{
IServiceProvider _serviceProvider;
public MyAuthenticationProvider(IServiceProvider serviceProvider, MyService myService)
{
_serviceProvider = serviceProvider;
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
MyService? service = null;
try
{
service = ActivatorUtilities.CreateInstance<MyService>(_serviceProvider);
// Do something with service
}
finally
{
service?.Dispose();
}
return base.GetAuthenticationStateAsync();
}
}

Related

Configuring asp.net core identity oauth2 authentication with user provided oauth2 credentials

I build an application that extends the gitlab user experience and admins of an organization (Organizations are the tenants in the system) can configure their gitlab installation (register an OAuth2 application in their gitlab instance) and normal users in an organization can just authenticate themselves with their gitlab account via OAuth2.
My problem at the moment is, the credentials (oauth2 client id and client secret, as well as the base url) are provided by the organization admin and are stored in the database. I want to give every organization its own subdomain and the Sign In with Gitlab button should redirect the user to their gitlab instance and follow the usual oauth2 flow for authentication, but I can't figure out how to configure the asp.net core identity framework to decide on the fly (based on the subdomain) which credentials to use for the oauth2 flow. All tutorials and microsoft provided documentations assume that you only have one "hard coded" oauth2 provided (usually configured in the ConfigureServices method of the Startup class).
My current implementation follows the microsoft provided documentation and looks like this:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Gitlab";
}).AddCookie()
.AddOAuth("Gitlab", options =>
{
options.ClientId = Configuration["Gitlab:ClientId"];
options.ClientSecret = Configuration["Gitlab:ClientSecret"];
options.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-gitlab");
options.AuthorizationEndpoint = Configuration["Gitlab:BaseUrl"] + "/oauth/authorize";
options.TokenEndpoint = Configuration["Gitlab:BaseUrl"] + "/oauth/token";
options.UserInformationEndpoint = Configuration["Gitlab:BaseUrl"] + "/api/v4/user";
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("gitlab:avatar_url", "avatar_url");
options.ClaimActions.MapJsonKey("gitlab:profile_url", "web_url");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JsonSerializer.Deserialize<JsonElement>(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
});
}
How can I implement such a system?
The OAuth handler uses the options pattern for configuration, which means you can utilize it to set properties such as ClientId, ClientSecret, etc, dynamically, on per-request basis, based on request properties.
You need to do the following (please bear with any compile problems, I used it with different options so writing this mostly from my head):
Modify the ConfigureServices body as follows:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Gitlab";
}).AddCookie()
.AddOAuth("Gitlab", delegate { }); // Don't specify hard coded OAuth options. Instead, you will return them from an options provider.
services.AddTransient<TenantResolver>();
services.AddSingleton<OAuthOptionsCacheAccessor>();
services.AddTransient<IConfigureNamedOptions<OAuthOptions>, OAuthOptionsInitializer>();
services.AddTransient<IOptionsMonitor<OAuthOptions>, OAuthOptionsProvider>();
}
Implement your tenant resolution logic based on the incoming request and register it to DI. For example:
public class TenantResolver // don't forget to register this to DI in ConfigureServices
{
private readonly IHttpContextAccessor httpContextAccessor;
public TenantAuthorityResolver(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetCurrentTenant()
{
// TODO: Read the current request from httpContextAccessor.HttpContext.Request
// and parse it to resolve the current tenant Id based on your own logic
}
}
Use the cache to store your instances of OAuthOptions and register it to DI as singleton. I used the ConcurrentDictionary like this:
public class OAuthOptionsCacheAccessor // register to DI as singleton
{
public ConcurrentDictionary<(string name, string tenant), Lazy<OAuthOptions>> Cache =>
new ConcurrentDictionary<(string, string), Lazy<OAuthOptions>>();
}
Implement the options initializer which will return the correct OAuthOptions instance based on the resolved tenant, and register this class to DI as a transient dependency.
public class OAuthOptionsInitializer : IConfigureNamedOptions<OAuthOptions> // register as transient
{
private readonly IDataProtectionProvider dataProtectionProvider;
private readonly TenantResolver tenantResolver;
public OAuthOptionsInitializer(
IDataProtectionProvider dataProtectionProvider,
TenantResolver tenantResolver)
{
this.dataProtectionProvider = dataProtectionProvider;
this.tenantResolver = tenantResolver;
}
public void Configure(string name, OAuthOptions options)
{
if (!string.Equals(name, OpenIdConnectDefaults.AuthenticationScheme, StringComparison.Ordinal))
{
return;
}
var tenant = tenantResolver.GetCurrentTenant();
// TODO: You will probably want to save your per-tenant OAuth options
// in the database or somewhere, so now is the time to obtain those.
// I also recommend using Nito.AsyncEx to be able to safely call async methods from here
var savedOptions = Nito.AsyncEx.AsyncContext.Run(async () => await GetSavedOptions(tenant));
options.ClientId = savedOptions.ClientId;
options.ClientSecret = savedOptions.ClientSecret;
options.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-gitlab");
options.AuthorizationEndpoint = savedOptions.BaseUrl + "/oauth/authorize";
options.TokenEndpoint = savedOptions.BaseUrl + "/oauth/token";
options.UserInformationEndpoint = savedOptions.BaseUrl + "/api/v4/user";
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("gitlab:avatar_url", "avatar_url");
options.ClaimActions.MapJsonKey("gitlab:profile_url", "web_url");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JsonSerializer.Deserialize<JsonElement>(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
}
public void Configure(OpenIdConnectOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called.");
}
And finally, implement the OAuth options provider and register it to DI as transient:
public class OAuthOptionsProvider : IOptionsMonitor<OAuthOptions>
{
private readonly OAuthOptionsCacheAccessor cacheAccessor;
private readonly IOptionsFactory<OAuthOptions> optionsFactory;
private readonly TenantResolver tenantResolver;
public OAuthOptionsProvider(
IOptionsFactory<OAuthOptions> optionsFactory,
TenantResolver tenantResolver,
OAuthOptionsCacheAccessor cacheAccessor)
{
this.cacheAccessor = cacheAccessor;
this.optionsFactory = optionsFactory;
this.tenantAuthorityResolver = tenantAuthorityResolver;
}
public OAuthOptions CurrentValue => Get(Options.DefaultName);
public OAuthOptions Get(string name)
{
var tenant = tenantResolver.GetCurrentTenant();
Lazy<OAuthOptions> Create() => new Lazy<OAuthOptions>(() => optionsFactory.Create(name));
return cacheAccessor.Cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OAuthOptions, string> listener) => null;
}
And not to forget, I want to attribute the original answer for this idea: https://stackoverflow.com/a/52977687/828023

Hangfire aspnetcore2 default authentication challenge not working

Using hangfire version: 1.6.17
I have successfully setup hangifire on aspnetcore 2.0
I added authorization by using:
app.UseHangfireDashboard("/jobs", new DashboardOptions
{
Authorization = new[] { new HangfireAuthorizationFilter() }
});
and
public class HangfireAuthorizationFilter :IDashboardAuthorizationFilter
{
private const string PERMISSION = "read:jobs";
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// allow only users with correct permission
if (httpContext.User.Identity.IsAuthenticated)
{
var permissions = httpContext.User.Claims.FirstOrDefault(x => x.Type.Equals(CustomClaims.Permissions))?.Value?.Split(' ');
return permissions?.Contains(PERMISSION) ?? false;
}
return false;
}
}
The only problem i cannot resolve is that a blank screen with 401 is returned to the user instead of the default challenge /account/login.
If you access my controllers with the [Authorize] attribute, they are automatically redirected to /account/login, so the loginpath is working.
Even if i specify it specifically, the user is not redirected while accessing Hangfire unauthorised:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Account/Login/";
})
Somebody an idea or should i mark it as a bug at Hangfire github.
First you should add HangFireAuthorizationFilter
public sealed class HangFireAuthorizationFilter : IDashboardAuthorizationFilter
{
private readonly IAuthorizationService _authorizationService;
private readonly IHttpContextAccessor _httpContextAccessor;
public HangFireAuthorizationFilter(IAuthorizationService authorizationService,
IHttpContextAccessor httpContextAccessor)
{
_authorizationService = authorizationService;
_httpContextAccessor = httpContextAccessor;
}
public bool Authorize([NotNull] DashboardContext context)
{
var httpContext = context.GetHttpContext();
return httpContext.User.Identity.IsAuthenticated;
}
}
Then use the below in the startup:
var hangFireAuth = new DashboardOptions
{
Authorization = new[]
{
new HangFireAuthorizationFilter(app.ApplicationServices.GetService<IAuthorizationService>(),
app.ApplicationServices.GetService<IHttpContextAccessor>())
},
AppPath = "/login"
};
app.UseHangfireDashboard("/hangfire", options: hangFireAuth);
You can refer to the following https://medium.com/ricos-note/hangfire-dashboard-of-authorization-b41d7135b044 for more details

SignalR core not working with cookie Authentication

I cant seem to get SignalR core to work with cookie authentication. I have set up a test project that can successfully authenticate and make subsequent calls to a controller that requires authorization. So the regular authentication seems to be working.
But afterwards, when I try and connect to a hub and then trigger methods on the hub marked with Authorize the call will fail with this message: Authorization failed for user: (null)
I inserted a dummy middleware to inspect the requests as they come in. When calling connection.StartAsync() from my client (xamarin mobile app), I receive an OPTIONS request with context.User.Identity.IsAuthenticated being equal to true. Directly after that OnConnectedAsync on my hub gets called. At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated is false. What is responsible to de-authenticating my request. From the time it leaves my middleware, to the time OnConnectedAsync is called, something removes the authentication.
Any Ideas?
Sample Code:
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await this._next(context);
//At this point context.User.Identity.IsAuthenticated == true
}
}
public class TestHub: Hub
{
private readonly IHttpContextAccessor _contextAccessor;
public TestHub(IHttpContextAccessor contextAccessor)
{
_contextAccessor = contextAccessor;
}
public override async Task OnConnectedAsync()
{
//At this point _contextAccessor.HttpContext.User.Identity.IsAuthenticated is false
await Task.FromResult(1);
}
public Task Send(string message)
{
return Clients.All.InvokeAsync("Send", message);
}
[Authorize]
public Task SendAuth(string message)
{
return Clients.All.InvokeAsync("SendAuth", message + " Authed");
}
}
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyContext>(options => options.UseInMemoryDatabase(databaseName: "MyDataBase1"));
services.AddIdentity<Auth, MyRole>().AddEntityFrameworkStores<MyContext>().AddDefaultTokenProviders();
services.Configure<IdentityOptions>(options => {
options.Password.RequireDigit = false;
options.Password.RequiredLength = 3;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
options.Lockout.MaxFailedAccessAttempts = 10;
options.User.RequireUniqueEmail = true;
});
services.AddSignalR();
services.AddTransient<TestHub>();
services.AddTransient<MyMiddleware>();
services.AddAuthentication();
services.AddAuthorization();
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseMiddleware<MyMiddleware>();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseSignalR(routes =>
{
routes.MapHub<TestHub>("TestHub");
});
app.UseMvc(routes =>
{
routes.MapRoute(name: "default", template: "{controller=App}/{action=Index}/{id?}");
});
}
}
And this is the client code:
public async Task Test()
{
var cookieJar = new CookieContainer();
var handler = new HttpClientHandler
{
CookieContainer = cookieJar,
UseCookies = true,
UseDefaultCredentials = false
};
var client = new HttpClient(handler);
var json = JsonConvert.SerializeObject((new Auth { Name = "craig", Password = "12345" }));
var content = new StringContent(json, Encoding.UTF8, "application/json");
var result1 = await client.PostAsync("http://localhost:5000/api/My", content); //cookie created
var result2 = await client.PostAsync("http://localhost:5000/api/My/authtest", content); //cookie tested and works
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:5000/TestHub")
.WithConsoleLogger()
.WithMessageHandler(handler)
.Build();
connection.On<string>("Send", data =>
{
Console.WriteLine($"Received: {data}");
});
connection.On<string>("SendAuth", data =>
{
Console.WriteLine($"Received: {data}");
});
await connection.StartAsync();
await connection.InvokeAsync("Send", "Hello"); //Succeeds, no auth required
await connection.InvokeAsync("SendAuth", "Hello NEEDSAUTH"); //Fails, auth required
}
If you are using Core 2 try changing the order of UseAuthentication, place it before the UseSignalR method.
app.UseAuthentication();
app.UseSignalR...
Then inside the hub the Identity property shouldn't be null.
Context.User.Identity.Name
It looks like this is an issue in the WebSocketsTransport where we don't copy Cookies into the websocket options. We currently copy headers only. I'll file an issue to get it looked at.

Add user to local database after registration in Stormpath

I want to add new user to my local database after register in Stormpath. In doc https://docs.stormpath.com/dotnet/aspnetcore/latest/registration.html#registration is section about post-registration handler. I have problem becouse i can't use UserRepository in StartUp file.
I have error:
Unable to resolve service for type
'AppProject.Repositories.IUserRepository' while attempting to
activate 'AppProject.Startup'
.
public void ConfigureServices(IServiceCollection services, IUserRepository userRepository)
{
services.AddStormpath(new StormpathOptions()
{
Configuration = new StormpathConfiguration()
{
Client = new ClientConfiguration()
{
ApiKey = new ClientApiKeyConfiguration()
{
Id = "xxxxxxxxxxx",
Secret = "xxxxxxxxx"
}
}
},
PostRegistrationHandler = (context, ct) =>
{
return MyPostRegistrationHandler(context, ct, userRepository);
}
});
}
private Task MyPostRegistrationHandler(PostRegistrationContext context, CancellationToken ct, IUserRepository userRepository)
{
userRepository.Add(new User(context.Account.Email, context.Account.FullName, context.Account.GivenName, context.Account.Surname, context.Account.Username));
userRepository.SaveChangesAsync();
return Task.FromResult(0);
}
In this scenario, I don't think it can resolve dependency of IUserRepository in StartUp. You can try something like this.
1) Add an extension method.
public static IServiceProvider AddServices(this IServiceCollection services)
{
services.AddTransient<IUserRepository, UserRepository>();
// rest of the things.
return services.BuildServiceProvider();
}
2) Get the userRepository instance like like this.
IServiceCollection services = new ServiceCollection();
services.AddServices();
var provider = services.BuildServiceProvider();
var userRepository = provider.GetRequiredService<IUserRepository>();
ConfigurationServices will not have IUserRepository input parameter.

CQRS ValidatorHandler not recognizing FluentValidation validators?

I'm using Web Api 2, Autofac, and MediatR (CQRS). I have a mediator pipeline in place that has pre/post request handlers. That all works fine. I'm trying to hook up Validation now and decorate the pipeline with it.
Here is my Autofac DI code:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
FluentValidationModelValidatorProvider.Configure(config);
ConfigureDependencyInjection(app, config);
WebApiConfig.Register(config);
app.UseWebApi(config);
}
private static void ConfigureDependencyInjection(IAppBuilder app, HttpConfiguration config)
{
var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());
builder.RegisterAssemblyTypes(typeof(IMediator).Assembly).AsImplementedInterfaces();
builder.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
});
//register all pre handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPreRequestHandler<>))))
.InstancePerLifetimeScope();
//register all post handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPostRequestHandler<,>))))
.InstancePerLifetimeScope();
//register all async handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfaceType => interfaceType.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>)))
.Select(interfaceType => new KeyedService("asyncRequestHandler", interfaceType)))
.InstancePerLifetimeScope();
//register pipeline decorator
builder.RegisterGenericDecorator(
typeof(AsyncMediatorPipeline<,>),
typeof(IAsyncRequestHandler<,>),
"asyncRequestHandler")
.Keyed("asyncMediatorPipeline", typeof(IAsyncRequestHandler<,>))
.InstancePerLifetimeScope();
//register validator decorator
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IAsyncRequestHandler<,>),
"asyncMediatorPipeline")
.InstancePerLifetimeScope();
// Register Web API controller in executing assembly.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
//register RedStripeDbContext
builder.RegisterType<RedStripeDbContext>().As<IRedStripeDbContext>().InstancePerRequest();
builder.RegisterType<AutofacServiceLocator>().AsImplementedInterfaces();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// This should be the first middleware added to the IAppBuilder.
app.UseAutofacMiddleware(container);
// Make sure the Autofac lifetime scope is passed to Web API.
app.UseAutofacWebApi(config);
}
Here is the ValidatorHandler:
public class ValidatorHandler<TRequest, TResponse> : IAsyncRequestHandler<TRequest, TResponse> where TRequest : IAsyncRequest<TResponse>
{
private readonly IAsyncRequestHandler<TRequest, TResponse> _inner;
private readonly IValidator<TRequest>[] _validators;
public ValidatorHandler(
IAsyncRequestHandler<TRequest, TResponse> inner,
IValidator<TRequest>[] validators)
{
_inner = inner;
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request)
{
var context = new ValidationContext(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return await _inner.Handle(request);
}
}
Here is a sample query:
[Validator(typeof(GetAccountRequestValidationHandler))]
public class GetAccountRequest : IAsyncRequest<GetAccountResponse>
{
public int Id { get; set; }
}
Here is the fluent validation handler:
public class GetAccountRequestValidationHandler : AbstractValidator<GetAccountRequest>
{
public GetAccountRequestValidationHandler()
{
RuleFor(m => m.Id).GreaterThan(0).WithMessage("Please specify an id.");
}
public Task Handle(GetAccountRequest request)
{
Debug.WriteLine("GetAccountPreProcessor Handler");
return Task.FromResult(true);
}
}
Here is the request handler:
public class GetAccountRequestHandler : IAsyncRequestHandler<GetAccountRequest, GetAccountResponse>
{
private readonly IRedStripeDbContext _dbContext;
public GetAccountRequestHandler(IRedStripeDbContext redStripeDbContext)
{
_dbContext = redStripeDbContext;
}
public async Task<GetAccountResponse> Handle(GetAccountRequest message)
{
return await _dbContext.Accounts.Where(a => a.AccountId == message.Id)
.ProjectToSingleOrDefaultAsync<GetAccountResponse>();
}
}
Finally here is the Web Api 2 HttpGet method:
[Route("{id:int}")]
[HttpGet]
public async Task<IHttpActionResult> GetById([FromUri] GetAccountRequest request)
{
var model = await _mediator.SendAsync<GetAccountResponse>(request);
return Ok(model);
}
I put breakpoints all over the place and when I hit this endpoint, the first thing I get into is the GetAccountRequestValidationHandler. Then I get into the ValidatorHandler's constructor. The problem is, the IValidator[] validators parameter to the constructor is always null.
I must be missing something with fluent validation and its registration via Autofac? Any help is much appreciated.
The validator types must be registered in the IoC. Adding the below to your ConfigureDependencyInjection method should do it.
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t => t.Name.EndsWith("ValidationHandler"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();