Unit tests that depend on a real instance of IRazorViewEngine - c#

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>

Related

How to register ServiceBusClient for dependency injection?

I’m trying to register ServiceBusClient from the new Azure.Messaging.ServiceBus package for dependency injection as recommended in this article using ServiceBusClientBuilderExtensions, but I can’t find any documentation or any help online on how exactly to go about this.
I'm trying to add as below
public override void Configure(IFunctionsHostBuilder builder)
{
ServiceBusClientBuilderExtensions.AddServiceBusClient(builder, Typsy.Domain.Configuration.Settings.Instance().Connections.ServiceBusPrimary);
}
but I'm getting the error
The type 'Microsoft.Azure.Functions.Extensions.DependencyInjection.IFunctionsHostBuilder' must be convertible to 'Azure.Core.Extensions.IAzureClientFactoryBuilder' in order to use it as parameter 'TBuilder' in the generic method 'IAzureClientBuilder<ServiceBusClient,ServiceBusClientOptions> Microsoft.Extensions.Azure.ServiceBusClientBuilderExtensions.AddServiceBusClient(this TBuilder, string)'
If anyone can help with this that'll be great!
ServiceBusClientBuilderExtensions.AddServiceBusClient is an extension method of IAzureClientFactoryBuilder:
public static IAzureClientBuilder<ServiceBusClient, ServiceBusClientOptions> AddServiceBusClient<TBuilder>(this TBuilder builder, string connectionString)
where TBuilder : IAzureClientFactoryBuilder
To get an instance of IAzureClientFactoryBuilder, you need to call AzureClientServiceCollectionExtensions.AddAzureClients(IServiceCollection, Action<AzureClientFactoryBuilder>) for a given IServiceCollection, which provides a delegate giving an instance of IAzureClientFactoryBuilder. (this method is in the Microsoft.Extensions.Azure NuGet package)
To call that method, you can use the IServiceCollection provided by IFunctionsHostBuilder. With all of that, what you have should look something like:
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddAzureClients(clientsBuilder =>
{
clientsBuilder.AddServiceBusClient(Typsy.Domain.Configuration.Settings.Instance().Connections.ServiceBusPrimary)
// (Optional) Provide name for instance to retrieve by with DI
.WithName("Client1Name")
// (Optional) Override ServiceBusClientOptions (e.g. change retry settings)
.ConfigureOptions(options =>
{
options.RetryOptions.Delay = TimeSpan.FromMilliseconds(50);
options.RetryOptions.MaxDelay = TimeSpan.FromSeconds(5);
options.RetryOptions.MaxRetries = 3;
});
});
}
To retrieve the named instance, instead of using ServiceBusClient as the injected type you use IAzureClientFactory<ServiceBusClient>. The ServiceBusClient is a Singleton regardless of whether you use a named instance or not.
public Constructor(IAzureClientFactory<ServiceBusClient> serviceBusClientFactory)
{
// Wherever you need the ServiceBusClient
ServiceBusClient singletonClient1 = serviceBusClientFactory.CreateClient("Client1Name")
}
For those only in need of single servicebus client a simple singleton would suffice
(Tested with .Net 6 Azure Functions v4):
using Azure.Messaging.ServiceBus;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using System;
[assembly: FunctionsStartup(typeof(YourProjName.Startup))]
namespace YourProjName
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
var serviceBusConnectionString = Environment.GetEnvironmentVariable("ServiceBusConnectionString");
if (string.IsNullOrEmpty(serviceBusConnectionString))
{
throw new InvalidOperationException(
"Please specify a valid ServiceBusConnectionString in the Azure Functions Settings or your local.settings.json file.");
}
//using AMQP as transport
builder.Services.AddSingleton((s) => {
return new ServiceBusClient(serviceBusConnectionString, new ServiceBusClientOptions() { TransportType = ServiceBusTransportType.AmqpWebSockets });
});
}
}
}
Then you could use it in the Azure Function constructor:
public Function1(ServiceBusClient client)
{
_client = client;
}
I was wondering the exact same thing, and while I like there is a specialized azure extension I did find another way I do prefer that seems less complicated and hopefully this can help others.
The code uses the function delegate provided by .AddSingleton Method. This function delegate will be called At the very end of the Build where you can make use of the service provider and retrieve your options (please correct me if I am wrong as documentation is dense and sparse at the same time :) ) .
Below is the key part:
serviceCollection.AddSingleton((serviceProvider) =>
{
ServiceBusOptions options = serviceProvider.GetService<IOptions<ServiceBusOptions>>().Value;
return new ServiceBusClient(options.ConnectionString);
});
**Full Code - speaks more :) **
using Azure.Messaging.ServiceBus;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example
{
class Startup
{
private static IHost DIHost;
static void Main(string[] args)
{
IHostBuilder hostbuilder = Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(GetAppConfigurationDefinition);
//Create Host (/build configuration)
ConfigureSettings(hostbuilder);
ConfigureServices(hostbuilder);
DIHost = hostbuilder.Build();
// Application code should start here.
DIHost.Run();
}
static void GetAppConfigurationDefinition(HostBuilderContext ctx, IConfigurationBuilder config)
{
config.SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // for simplicity appsettings.production.json / appsettings.development.json are not there. This is where settings would go.
.Build();
}
public static void ConfigureServices(IHostBuilder hostbuilder)
{
hostbuilder.ConfigureServices((hostContext, serviceCollection) =>
{
serviceCollection.AddSingleton<ServiceBusClient>((serviceProvider) =>
{
// options from appSettings.json
// leverages IOptions Pattern to get an options object from the DIHost Service provider
var myServiceBusOptions = serviceProvider.GetService<IOptions<ServiceBusOptions>>().Value;
var sbClientOptions=new ServiceBusClientOptions() {
TransportType=ServiceBusTransportType.AmqpTcp,
RetryOptions=new ServiceBusRetryOptions() { Mode = ServiceBusRetryMode.Exponential }
// ...
};
// returns the ServiceBusClient Object configured per options we wanted
return new ServiceBusClient(myServiceBusOptions.ConnectionString, sbClientOptions);
});
});
}
public static void ConfigureSettings(IHostBuilder hostbuilder)
{
hostbuilder.ConfigureServices((hostBuilderContext, serviceCollection) =>
{
IConfiguration configurationRoot = hostBuilderContext.Configuration;
serviceCollection.Configure<ServiceBusOptions>(configurationRoot.GetSection("ServiceBusOptions"));
});
}
public class ServiceBusOptions
{
public string ConnectionString { get; set; }
}
}
}
AppSettings.Json
{
"ServiceBusOptions": {
"ConnectionString": ""
}
}

Why can't i register more than 2 hosted services in a single Windows Service application in .NET 5?

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);
}
}
}

How to test all ASP.NET Core Controllers Dependency Injection is valid?

We occasionally have issues whereby someone adds some DI into a controller but forgets to add the relevant line into Startup.cs to set the scope of the object.
This does not prevent the application from starting, but rather, throws an exception when the relevent endpoint is hit.
Is there any way of programmatically checking all controllers are valid and preventing the application from starting otherwise?
Alternatively is there an easy way to write a catch-all automated test to check every controller can be instantiated using the specified DI in Startup.cs?
You can write it like this:
[TestFixture]
[Category(TestCategory.Integration)]
public class ControllersResolutionTest
{
[Test]
public void VerifyControllers()
{
var builder = new WebHostBuilder()
.UseStartup<IntegrationTestsStartup>();
var testServer = new TestServer(builder);
var controllersAssembly = typeof(UsersController).Assembly;
var controllers = controllersAssembly.ExportedTypes.Where(x => typeof(ControllerBase).IsAssignableFrom(x));
var activator = testServer.Host.Services.GetService<IControllerActivator>();
var serviceProvider = testServer.Host.Services.GetService<IServiceProvider>();
var errors = new Dictionary<Type, Exception>();
foreach (var controllerType in controllers)
{
try
{
var actionContext = new ActionContext(
new DefaultHttpContext
{
RequestServices = serviceProvider
},
new RouteData(),
new ControllerActionDescriptor
{
ControllerTypeInfo = controllerType.GetTypeInfo()
});
activator.Create(new ControllerContext(actionContext));
}
catch (Exception e)
{
errors.Add(controllerType, e);
}
}
if (errors.Any())
{
Assert.Fail(
string.Join(
Environment.NewLine,
errors.Select(x => $"Failed to resolve controller {x.Key.Name} due to {x.Value.ToString()}")));
}
}
}
This code actually goes through full process of setting up asp.net core application with database configuration and what not you have in you startup so you might want to derive from it and remove/mock some stuff. Also this code requires Microsoft.AspNetCore.TestHost nuget.
I changed original code that I posed as it was not working as expected.
Summarised from https://andrewlock.net/new-in-asp-net-core-3-service-provider-validation/, please see link for more details.
As of ASP.NET 3.0 there is now a way to validate controller dependencies on build:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddControllersAsServices(); // This part adds Controllers to DI
Program.cs:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
// Validate DI on build
options.ValidateOnBuild = true;
});
Notes:
Service provider validation is only enabled in the Development environment by default.
Will not work for run-time ServiceProvider look ups (service locator pattern) e.g. _service = provider.GetRequiredService<MyService>();
Will not work for [FromServices] parameters in methods (i.e. it only checks constructor dependencies)
Will not work for 'open generics' e.g. services.AddSingleton(typeof(MyServiceWithGeneric<>));
Will not work for services registered with factory functions e.g.
services.AddSingleton<MyService>(provider =>
{
var nestedService = provider.GetRequiredService<MyNestedService>();
return new MyService(nestedService);
});
Adapted #Rafal's answer to xUnit to avoid managing Exception iteration and skip dependency on TestHost:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace redacted.WebApi.Test {
using Core;
public class VerifyDependencies {
[Theory]
[MemberData(nameof(Controllers))]
public void VerifyController(Type controllerType) {
var services = new WebHostBuilder().UseStartup<Startup>().Build().Services;
ControllerUtilities.Create(
controllerType,
services.GetService<IControllerActivator>(),
services.GetService<IServiceProvider>()
);
}
public static IEnumerable<object[]> Controllers() {
return ControllerUtilities.GetControllers<ApiController>().Select(c => new object[] { c });
}
}
public class ControllerUtilities {
public static IEnumerable<Type> GetControllers<TProject>() {
return typeof(TProject)
.Assembly.ExportedTypes
.Where(x => typeof(Controller).IsAssignableFrom(x));
}
public static Controller Create(Type controllerType, IControllerActivator activator, IServiceProvider serviceProvider) {
return activator.Create(new ControllerContext(new ActionContext(
new DefaultHttpContext {
RequestServices = serviceProvider
},
new RouteData(),
new ControllerActionDescriptor {
ControllerTypeInfo = controllerType.GetTypeInfo()
})
)) as Controller;
}
public static TController Create<TController>(IControllerActivator activator, IServiceProvider serviceProvider) where TController : Controller {
return Create(typeof(TController), activator, serviceProvider) as TController;
}
}
}
The best method is to throw ArgumentNullExceptions. For example, in your controller's constructor:
_foo = foo ?? throw new ArgumentNullException(nameof(foo));
That will cause any action in the controller (any time the controller is constructed) to fail if the dependency is not satisified. Then, assuming you've got any sort of integration test suite around that controller, all your tests will instantly fail and you'll no exactly why: the constructor argument was not satisfied.
I'm sure Rafal's solution works, but I was unable to get it working. The IControllerActivator would not resolve, but I did end up figuring another way of doing this that does not require the Microsoft.AspNetCore.TestHost library. You will, however, need Microsoft.AspNetCore.Mvc.Core. Also, this is using MSTest.
[TestMethod]
public void ValidateDependencies()
{
// This is only necessary if you have reliance on the configuration.
// Make sure that your appsettings.json "Build Action" is "Content" and the "Copy to Output Directory" is "Copy if newer" or "Copy always"
var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
var svcCollection = new ServiceCollection();
// We've moved our dependencies to an extension method
svcCollection.RegisterServices(config);
var controllers = typeof(CustomerController).Assembly.ExportedTypes
.Where(x => !x.IsAbstract && typeof(ControllerBase).IsAssignableFrom(x)).ToList();
// By default, the controllers are not loaded so this is necessary
controllers.ForEach(c => svcCollection.AddTransient(c));
var serviceProvider = svcCollection.BuildServiceProvider();
var errors = new Dictionary<Type, Exception>();
foreach (Type controllerType in controllers)
{
try
{
serviceProvider.GetRequiredService(controllerType);
}
catch (Exception ex)
{
errors.Add(controllerType, ex);
}
}
if (errors.Any())
Assert.Fail(string.Join("\n",
errors.Select(x => $"Failed to resolve controller {x.Key.Name} due to {x.Value}")));
}

.NET Core Custom Authorization Dependency Injection

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

How can I protect MVC Hangfire Dashboard

I'm using Visual Studio 2013 MVC, and I installed "Hangfire" to perform scheduled tasks. (http://hangfire.io/)
How can I protect the Web Monitoring UI page (http://localhost/Hangfire) with a password?
Thanks
Please take a look to the documentation
In short.
You can use already created authorization filters or implement your own
using Hangfire.Dashboard;
public class MyRestrictiveAuthorizationFilter : IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
// In case you need an OWIN context, use the next line.
var context = new OwinContext(owinEnvironment);
return false;
}
}
Additional information:
Also you can take a look to the special package Hangfire.Dashboard.Authorization which contains the logic which you needed
Let me give the entire code for a RestrictiveAuthorizationFilter:
This way you can handle authorization however you desire.
Assuming you have the OWINStartup class added.
OWINStartup.cs
using Owin;
using Hangfire;
using Hangfire.Dashboard;
public class OWINStartup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configuration.UseSqlServerStorage("String");
DashboardOptions options = new DashboardOptions()
{
AuthorizationFilters = new IAuthorizationFilter[]
{
new MyRestrictiveAuthorizationFilter()
}
};
app.UseHangfireDashboard("/hangfire", options);
}
}
RestrictiveAuthorizationFilter.cs
using Hangfire.Dashboard;
using System.Collections.Generic;
using Microsoft.Owin;
public class MyRestrictiveAuthorizationFilter : IAuthorizationFilter
{
public bool Authorize(IDictionary<string, object> owinEnvironment)
{
var context = new OwinContext(owinEnvironment);
return context.Authentication.User.Identity.IsAuthenticated;
}
}
Notice: using System.Collections.Generic;
References:
https://github.com/HangfireIO/Hangfire/issues/202
https://media.readthedocs.org/pdf/hangfire/latest/hangfire.pdf (page 20)
Hangfire.Dashboard.Authorization version: 2.1.0
Set this up in your Startup.Cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
//TODO
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new MyAuthorizationFilter() }
});
app.UseHangfireDashboard();
var options = new BackgroundJobServerOptions { WorkerCount = 1 };
app.UseHangfireServer(options); }
Create this class, it allows authenticated users to see the Dashboard
public class MyAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
// Allow all authenticated users to see the Dashboard (potentially dangerous).
return httpContext.User.Identity.IsAuthenticated;
}
}

Categories

Resources