We have this custom Authorization scheme which I'm trying to solve with the ability to unit test and use dependency injection in .NET core. Let me explain the setup:
I created an interface IStsHttpClient and class StsHttpClient. This class connects to a internal web service that creates & decodes tokens. This has exactly 1 method "DecodeToken(string token)" and the constructor is very simple - it takes in an option object that is loaded from DI.
Then my AuthorizationHandler would in theory just use the IStsHttpClient to call and decode the token. My question is, based on the examples online I don't know how to properly specify/build the Authorization Handler (see code below).
Auth Code here:
public class MyAuthorizationRequirement : AuthorizationHandler<MyAuthorizationRequirement >, IAuthorizationRequirement
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyStsAuthorizationRequirement requirement)
{
/* remaining code omitted - but this will call IStsHttpClient.Decode() */
My Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
/* initialize this differently?? */
policy.AddRequirements(new MyStsAuthorizationRequirement( /* somethign is needed here?? */));
});
});
Nicholas,
You have to separate your handler and requirements here. In addition to that keep your DI stuff in the handler. Requirement itself is going to be either a DTO or an empty class with marker interface IAuthorizationRequirement.
Requirement:
public class MyAuthorizationRequirement : IAuthorizationRequirement
{
}
Handler:
public class MyAuthorizationHandler : AuthorizationHandler<MyAuthorizationRequirement>
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyAuthorizationRequirement requirement)
{
...
}
}
Configuration:
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
policy.AddRequirements(new MyAuthorizationRequirement());
});
});
For other people looking to wrap authorization around an existing permission handler in C#9 NetCore5, the I found the following solution which allowed me to make use of the stock dependency injection container to inject a service into an AuthorizationHandler.
For me this required 5 new classes and some changes to Startup.cs
The following is my PermissionPolicyProvider.cs, this will represent a generic permission, and not a policy (I filter for permissions later)
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
Next is my PermissionPolicyProvider.cs, this code allows us to filter out policies and to dynamically build a permission when received.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
internal class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) =>
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
// Dynamically creates a policy with a requirement that contains the permission.
// The policy name must match the permission that is needed.
/// <summary>
///
/// </summary>
/// <param name="policyName"></param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (! policyName.StartsWith("Permission", StringComparison.OrdinalIgnoreCase))
{
// If it doesn't start with permission, then it's a policy.
// pass policies onward to default provider
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return Task.FromResult(policy.Build());
}
}
}
Next up is the PermissionAuthorizationHandler.cs, this is where microsoft wants you to custom db checks, so if you don't want to separate your service layer you can stop after this. Note that you can handle one permission at a time or all at once (note the commented out code).
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
If you don't want the service layer separation, this is the last step for you. You just need to properly register all the services. Add the following to your Startup.cs
services.AddDbContext<PokeflexContext>
(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<UserManager<IdentityUser>>()
.AddDefaultTokenProviders();
To separate out the service layer, we need to extend the UserManager. UserManager actually gets access to the entire service layer injected into your app, but it hides it under a private modifier. Our solution is simple: extend the UserManager and override the constructor to pass on our service to a public variable instead. Here is my custom version as AppUserManager
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
public class AppUserManager<TUser> : UserManager<TUser> where TUser : class
{
public IServiceProvider Services;
public AppUserManager(IUserStore<TUser> store,
IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors,
IServiceProvider services, ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
Services = services;
}
}
}
Last step here, we need to update Startup.cs again to reference our custom type. We also add another line here to ensure that if someone requests our service within an endpoint and not as an attribute they will get our custom AppUserManager. My final resulting ConfigureServices contents is as follows
services.AddDbContext<PokeflexContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<PermissionService>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<AppUserManager<AppUser>>()
.AddDefaultTokenProviders();
services.AddScoped(s => s.GetService<AppUserManager<AppUser>>());
If you are already comfortable with service configuration then you probably don't need the following, but here is a simple service I created that the authorization handler can access via DI.
using System.Threading.Tasks;
using App.Data;
using Microsoft.EntityFrameworkCore;
namespace App.Services.Permissions
{
public class PermissionService
{
private PokeflexContext _dbContext;
public PermissionService(PokeflexContext dbContext)
{
_dbContext = dbContext;
}
public virtual async Task<bool> Permitted(string permission)
{
return await _dbContext.AppUsers.AnyAsync();
}
}
}
For some more information on permissioning, visit: https://github.com/iammukeshm/PermissionManagement.MVC
Related
I have an Azure SQL DB that initially had the following columns:
user name
password hash
password salt
This DB serves a .NET Core C# API that checks username and password to return a JWT token.
The API had a User object that comprised all three columns with the correct types, a DbContext with a DbSet<User>, and an IServiceCollection that used said DbContext.
The API worked fine, returning a JWT token as needed.
I have since needed to add an extra parameter to check and pass to the JWT creation - the relevant column has been created in the DB, the User object in the API has been updated to include the extra parameter and that extra parameter is observed in the Intellisense throughout the API code.
The issue is that when the API is deployed to Azure, the extra parameter isn't being recognised and populated; how do I make the API correctly update to use the new DbContext and retrieve the User with the extra parameter?
(I've omitted the interfaces for brevity, as they're essentially the corresponding classes)
User, UserRequest and MyApiDbContext Classes:
using Microsoft.EntityFrameworkCore;
namespace MyApi.Models
{
// Basic user model used for authentication
public class User
{
public string UserId { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] PasswordSalt { get; set; }
public string ExtraParam { get; set; } // newly added parameter
}
public class UserRequest
{
public string UserId { get; set; }
public string password { get; set; }
}
public class MyApiDbContext : DbContext
{
public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
: base(options)
{
}
public DbSet<User> Users { get; set; }
}
}
The AuthRepository that retrieves the user:
using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthRepository : IAuthRepository
{
private readonly MyApiDbContext _context;
public AuthRepository(MyApiDbContext context)
{
_context = context;
}
public async Task<User> Login(string username, string password)
{
// my test user gets returned
User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);
if (returnedUser == null)
{
return null;
}
// the password get verified
if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
{
return null;
}
// this does not get changed, but the value set in the DB is definitely a string
if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
{
returnedUser.ExtraParam = "placeholder"
}
return returnedUser;
}
}
}
The AuthService that calls the AuthRepository for the user then "creates the JWT token" (just returning a string for this example), currently set up to return the user details:
using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;
namespace MyApi.Services
{
public class AuthService : IAuthService
{
private readonly IOptions<MyApiBlobStorageOptions> _settings;
private readonly IAuthRepository _repository;
public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
{
_repository = repository;
_settings = settings;
}
public async Task<string> Login(string username, string password)
{
User returnedUser = await _repository.Login(username, password);
if (returnedUser != null)
{
// currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
}
return null;
}
}
}
The controller that calls the AuthService:
using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MyApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly MyApiDbContext _context;
private readonly IAuthService _authService;
public AuthController(MyApiDbContext context, IAuthService authService)
{
_context = context;
_authService = authService;
}
[HttpPost("login")]
public async Task<IActionResult> Login(UserRequest loginUser)
{
string token = await _authService.Login(loginUser.UserId, loginUser.Password);
if (token != null)
{
return Ok(token);
}
return Unauthorized("Access Denied!!");
}
}
}
The startup class that registers everything:
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;
namespace MyApi
{
public class Startup
{
public IConfiguration Configuration { get; }
private readonly ILogger<Startup> _logger;
private readonly IConfiguration _config;
public Startup(ILogger<Startup> logger, IConfiguration config)
{
_logger = logger;
_config = config;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add dBContext for DB
services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
// Add DI Reference for Repository
services.AddScoped<IAuthRepository, AuthRepository>();
// Add DI Reference for Azure Blob Storage Processes
services.AddScoped<IBlobService, AzureBlobService>();
// DI Reference for AuthService
services.AddScoped<IAuthService, AuthService>();
// Add configuration section for Constructor Injection
services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));
services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
.GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
ValidateIssuer = false,
ValidateAudience = false
};
options.Events = new JwtBearerEvents()
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning("Token authentication failed whilst attempting to upload file");
return Task.CompletedTask;
}
};
});
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
app.UseAuthentication();
app.UseMvc();
}
}
internal static class StartupExtensions
{
public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddBlobServiceClient(serviceUri);
}
else
{
return builder.AddBlobServiceClient(serviceUriOrConnectionString);
}
}
public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
{
if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
{
return builder.AddQueueServiceClient(serviceUri);
}
else
{
return builder.AddQueueServiceClient(serviceUriOrConnectionString);
}
}
}
}
Let me know if there is anything else required for understanding: the only difference between before and now is the addition of ExtraParam and the corresponding references throughout for the API, and the DB getting the identically named column.
I tried adding the parameter and deploying it to Azure and making the POST request as normal, starting and stopping the app service, deploying the API while the app service was stopped and starting it again, and restarting the app service. I don't know how much I could try changing up what I'm doing, I'm trying to do exactly the same as before, but with an extra parameter getting requested from the DB.
I can also confirm that the DB contains the ExtraParam column, and that it contains values against the existing data rows, as viewed using the Azure Portal's DB Query Editor.
I've resolved the issue, partially because of posting this question and sanitising the code for public discussion.
In the Login Controller, in my development code the request for the user to be returned was subsequently ignored, passing through the user request details which had a null ExtraParam, not the returned user which had the ExtraParam populated.
The moral of the story is to confirm which objects are being used at which points in the code, or have one object that is passed into, updated by, then returned from functions to maintain consistency.
I have a Windows Service application written using .NET 5 platform. Here's my Main method:
public static void Main(string[] args) =>
Host
.CreateDefaultBuilder(args)
.ConfigureServices((_, services) =>
{
var configuration = services
.BuildServiceProvider()
.GetRequiredService<IConfiguration>();
services
.AddApplicationServices()
.AddInfrastructureServices(configuration);
})
.UseWindowsService()
.Build()
.Run();
I have few services in the application registered with the following method:
public static IServiceCollection AddCronJob<TJob>(this IServiceCollection services,
IConfiguration configuration,
string cronExpressionSectionKey,
string triggerOnStartupSectionKey = default) where TJob : CronJobService
{
var cronExpression = configuration.GetSection(cronExpressionSectionKey).Value;
var missingCronExpression = string.IsNullOrEmpty(cronExpressionSectionKey) || string.IsNullOrWhiteSpace(cronExpression);
if (missingCronExpression)
{
throw new ArgumentException(EmptyCronExpressionMessage);
}
var triggerOnStartup = bool.TryParse(configuration.GetSection(triggerOnStartupSectionKey).Value,
out var parsingResult) && parsingResult;
var config = new ScheduleConfig<TJob>
{
CronExpression = cronExpression,
TimeZoneLocal = TimeZoneInfo.Local,
TimeZoneUtc = TimeZoneInfo.Utc,
TriggerOnStartup = triggerOnStartup
};
return services
.AddSingleton<IScheduleConfig<TJob>>(config)
.AddHostedService<TJob>();
}
The registration looks like this:
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services,
IConfiguration configuration)
{
services
.AddDatabaseContext<ExchangeContext>(
configuration,
DatabaseProvider.SqlServer,
false,
ExchangeConnectionStringSectionKey,
Assembly.GetExecutingAssembly().FullName)
.AddDomesticMessaging(GetHandlersAssembly())
.AddScoped<ICompanyRepository, CompanyRepository>()
.AddScoped<IImportMetadataRepository, ImportMetadataRepository>()
.AddScoped<IFetchedStockPriceRepository, FetchedStockPriceRepository>()
.AddScoped<IPredictionModelRepository, PredictionModelRepository>()
.AddScoped<ITimeSeriesPort, TimeSeriesAdaptor>()
.AddScoped<IPredictionModelPort, PredictionModelAdaptor>()
.AddScoped<IPredictorPort, PredictorAdaptor>()
.AddCronJob<ExchangeDataImportService>(
configuration,
ExchangeDataImportServiceCronExpressionSectionKey,
ExchangeDataImportServiceTriggerOnStartupSectionKey)
.AddCronJob<PredictionModelService>(
configuration,
PredictionModelServiceCronExpressionSectionKey,
PredictionModelServiceTriggerOnStartupSectionKey)
.AddCronJob<PredictionService>(
configuration,
PredictionServiceCronExpressionSectionKey,
PredictionServiceTriggerOnStartupSectionKey)
.Configure<TimeSeriesSourceSettings>(configuration.GetSection(TimeSeriesSourceSettingsSectionKey))
.Configure<PredictionModelSettings>(configuration.GetSection(PredictionModelSettingsSectionKey));
var migrator = new Migrator(services);
migrator.UseMigrationsOfContext<ExchangeContext>();
return services;
}
The AddCronJob mechanism was tested many many times in my projects - it works perfectly and my problem is for sure not connected with the working principle of given method. Please notice that it encapsulates AddHostedService method. Let's move to the main obstacle. This is the first time i've tried to have more than one cron job in one Windows Service. Everything works correctly, until i register the third service. Services derive from BackgroundService class (they also need to override ExecuteAsync method). When random services are registered, ExecuteAsync method is called properly, but when i add the third one - his method is never called (don't know why).
The last registered service is invalid - when i change the order of the registration, the other service stops working. What am i doing wrong? Are there any limitations with the AddHostedService method? Thanks for any answer.
PredictionService definition:
using System.Threading;
using System.Threading.Tasks;
using AsCore.Application.Abstractions.Messaging.Commands;
using AsCore.Infrastructure.Workers;
using Microsoft.Extensions.DependencyInjection;
using SharePricePredictor.Application.Contracts.Commands;
namespace SharePricePredictor.Infrastructure.Workers
{
public sealed class PredictionService : CronJobService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public PredictionService(IScheduleConfig<PredictionService> scheduleConfig,
IServiceScopeFactory serviceScopeFactory)
: base(
scheduleConfig.CronExpression,
scheduleConfig.TimeZoneLocal,
scheduleConfig.TimeZoneUtc,
scheduleConfig.TriggerOnStartup) =>
_serviceScopeFactory = serviceScopeFactory;
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
using var scope = _serviceScopeFactory.CreateScope();
var commandBus = scope.ServiceProvider.GetRequiredService<ICommandBus>();
var command = new PredictCommand();
await commandBus.SendAsync(command, cancellationToken);
}
}
}
PredictionModelService definition:
using System.Threading;
using System.Threading.Tasks;
using AsCore.Application.Abstractions.Messaging.Commands;
using AsCore.Infrastructure.Workers;
using Microsoft.Extensions.DependencyInjection;
using SharePricePredictor.Application.Contracts.Commands;
namespace SharePricePredictor.Infrastructure.Workers
{
public sealed class PredictionModelService : CronJobService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public PredictionModelService(
IScheduleConfig<PredictionModelService> scheduleConfig,
IServiceScopeFactory serviceScopeFactory)
: base(scheduleConfig.CronExpression,
scheduleConfig.TimeZoneLocal,
scheduleConfig.TimeZoneUtc,
scheduleConfig.TriggerOnStartup)
{
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
using var scope = _serviceScopeFactory.CreateScope();
var commandBus = scope.ServiceProvider.GetRequiredService<ICommandBus>();
var command = new CreatePredictionModelCommand();
await commandBus.SendAsync(command, cancellationToken);
}
}
}
If we want access to HttpContext in a class library we can simple pass it like this:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebAppLib;
namespace WebApplication
{
public class WebAppMiddleware
{
private readonly RequestDelegate _next;
public WebAppMiddleware(RequestDelegate next)
{
_next = next;
}
public Task Invoke(HttpContext httpContext)
{
Test test = new test();
test.TestMethod(httpContext) <--- passing the current httpcontext to the method.
// Return httpcontext
return _next(httpContext);
}
}
// Extension method used to add the middleware to the HTTP request pipeline.
public static class WebAppMiddlewareExtensions
{
public static IApplicationBuilder UseWebAppMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<WebAppMiddleware>();
}
}
}
My Class library dll file(with FrameworkReference Include="Microsoft.AspNetCore.App" in csproj file)
using Microsoft.AspNetCore.Http;
namespace WebAppLib
{
public class Test
{
public void TestMethod(HttpContext httpContext)
{
httpContext.Response.WriteAsync("hello from haldner");
// continue with context instance
}
}
}
I'm wondering if there's other ways this can be done? Basically I want to avoid passing "httpContext" to my method that i run in my custom middlewear.
Could you please tell me how you want to use this WebAppLib? Will you inject this class into the startup.cs?
If you will inject, then you could use other service inside the asp.net application. Like httpcontextaccessor or else to achieve your requirement. If you don't inject it and you don't want to pass httpcontext into it, you couldn't get it.
Details about how to use it, like this:
inject:
services.AddScoped<IMyDependency, MyDependency>();
services.AddHttpContextAccessor();
MyDependency class:
public class MyDependency : IMyDependency
{
private IHttpContextAccessor _context;
public MyDependency(IHttpContextAccessor context) {
_context = context;
}
public void WriteMessage(string message)
{
var path= _context.HttpContext.Request.Path;
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
Inspired by an article on custom claims, I've added a tenant id custom claim to my Identity server sign in process as follows:
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using MyNamespace.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using MyNamespace.Data;
using MyNamespace.Constants;
namespace MyNamespace.Factories
{
public class TenantClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser>
{
public TenantClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor) {
}
// TODO: Remove hard binding to application db context
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user) {
var identity = await base.GenerateClaimsAsync(user);
var tenantId = ApplicationDbContext.DefaultTenantId;
if (user.TenantId != Guid.Empty) {
tenantId = user.TenantId;
}
identity.AddClaim(new Claim(CustomClaimTypes.TenantId, tenantId.ToString()));
return identity;
}
}
}
The claims generating method is executed at login and claims are added to the identity, so this part seems ok. Later I try to read out the claim later in my tenant provider service as follows
using System;
using MyNamespace.Data;
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using System.Linq;
using MyNamespace.Constants;
namespace MyNamespace.Services
{
public interface ITenantProvider
{
Guid GetTenantId();
}
public class TenantProvider : ITenantProvider
{
private IHttpContextAccessor _httpContextAccessor;
public TenantProvider(IHttpContextAccessor httpContextAccessor
{
_httpContextAccessor = httpContextAccessor;
}
// TODO: Remove hard binding to application db context
public Guid GetTenantId()
{
var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
var user = _httpContextAccessor.HttpContext.User;
var tenantId = _httpContextAccessor.HttpContext.User.FindFirst(CustomClaimTypes.TenantId).Value;
Guid tenantGuid = ApplicationDbContext.DefaultTenantId;
Guid.TryParse(tenantId, out tenantGuid);
return tenantGuid;
}
}
}
As far as I understand, however, the claim identified by CustomClaimTypes.TenantId is not automatically mapped by the Identity server. My question is this: how can I map
options.ClaimActions.MapUniqueJsonKey(CustomClaimTypes.TenantId, CustomClaimTypes.TenantId);
from Startup.cs where I add the Identity server the my dependencies:
services.AddAuthentication()
.AddIdentityServerJwt();
So, in the end I ended up with a different solution than what I sought originally. Instead of mapping the claims as created by the factory, I came across another post here at StackOverflow. Basically, what I did was the following. I implemented the following ProfileService
namespace MyNamespace.Services
{
public class ProfileService : IProfileService
{
protected UserManager<ApplicationUser> _userManager;
public ProfileService(UserManager<ApplicationUser> userManager)
{
_userManager = userManager;
}
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
var claims = new List<Claim>
{
new Claim(CustomClaimTypes.TenantId, user.TenantId.ToString()),
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = (user != null) && user.IsActive;
}
}
}
Then, I added the service in the DI Container at Configure:
services.AddIdentityServer()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>()
.AddProfileService<ProfileService>();
services.AddAuthentication()
.AddIdentityServerJwt();
So, I still have a good time letting AddIdentityServerJwt setting up IdentityServer4, while getting my claims at the same time.
I need to render a view to a string (to send as email). I'm using this implementation.
I want to unit test it, without needing a full ASP.NET Core environment. So I must create an instance of IRazorViewEngine.
The default implementation is RazorViewEngine. I has a mega constructor because each argument needs to be created and each one has a mega constructor, etc., etc. (I can't mock it, I need a live instance.)
Surely there is a simpler way to get an instance?
(Before Core, I could use ViewEngines.Engines. Maybe Core has something similar?)
I tried to do this as well with a unit test similar to this and ran into various issues:
var services = new ServiceCollection();
services.AddMvc();
... ended up needing to add random things into the service collection ...
var serviceProvider = services.BuildServiceProvider();
var razorViewEngine = serviceProvider.GetRequiredService<IRazorViewEngine>();
I ended up going with more of a component test approach using Microsoft.AspNetCore.Mvc.Testing:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
public class ComponentTestStartup : IStartup
{
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
return services.BuildServiceProvider();
}
public void Configure(IApplicationBuilder app)
{
}
}
public class ComponentTestServerFixture : WebApplicationFactory<ComponentTestStartup>
{
public TService GetRequiredService<TService>()
{
if (Server == null)
{
CreateDefaultClient();
}
return Server.Host.Services.GetRequiredService<TService>();
}
protected override IWebHostBuilder CreateWebHostBuilder()
{
var hostBuilder = new WebHostBuilder();
return hostBuilder.UseStartup<ComponentTestStartup>();
}
// uncomment if your test project isn't in a child folder of the .sln file
// protected override void ConfigureWebHost(IWebHostBuilder builder)
// {
// builder.UseSolutionRelativeContentRoot("relative/path/to/test/project");
// }
}
public class RazorViewToStringRendererTests
{
private readonly RazorViewToStringRenderer viewRenderer;
public RazorViewToStringRendererTests()
{
var server = new ComponentTestServerFixture();
var serviceProvider = server.GetRequiredService<IServiceProvider>();
var viewEngine = server.GetRequiredService<IRazorViewEngine>();
var tempDataProvider = server.GetRequiredService<ITempDataProvider>();
viewRenderer = new RazorViewToStringRenderer(viewEngine, tempDataProvider, serviceProvider);
}
[Fact]
public async Task CanRenderViewToString()
{
// arrange
var model = "test model";
// act
var renderedView = await viewRenderer.RenderViewToStringAsync("/Path/To/TestView.cshtml", model);
// assert
Assert.NotNull(renderedView);
Assert.Contains(model, renderedView, StringComparison.OrdinalIgnoreCase);
}
}
TestView.cshtml:
#model string
<h1>#Model</h1>