Adding Asp .NET Core Controller dynamically when analyzing route - c#

Summary:
Imagine we are calling the url http://localhost:5600/Student/All/
My goal is that my Asp .NET Core Web Api compiles a Student controller on the fly when that route is called and then uses that controller for answering that request.
Detailed:
I want to compile an Asp .NET Core Controller when analyzing the route. So I'm using a DynamicRouteValueTransfer to analyze the route and modify it when needed.
app.UseEndpoints(endpoints =>
{
//...
endpoints.MapDynamicControllerRoute<SearchValueTransformer>("{controller}/{action}/{**params}");
//...
In the SearchValue Transfer (inherited from DynamicRouteValueTransfer) I can analyze and rewrite the route. There I'm compiling an Controller with a Get method which returns a list of Students (with an OData - [EnableQuery] - attribute because in the end I want to provide any data for OData access).
Then I try to add this newly compiled StudentController to the Controllers to be used in that request. But that is not working.
I tried saving the IMvcBuilder which I get at startup at the ConfigureServices:
//inside ConfigureServices - MvcManager is my static class
MvcManager.Builder = services.AddMvc();
//in my static class
public static IMvcBuilder Builder { get; set; }
After compiling the Controller I tried to add that Assembly by calling
MvcManager.Builder.AddApplicationPart(assembly).AddControllersAsServices();
The problem is that I can't go into that controller. I found some articles about dynamically adding but none is doing it while the request is already started.

After some search I finally found the optimal solution to create controllers at runtime based on the request path coming in.
Here is the solution - you have to use a middleware. In the middleware you can compile according to the request route and add it as AssemblyPart to your current assembly. Example:
The request
http://localhost:56002/Account/getdata
comes in. You can get the "/Account/getdata" in your middleware and compile an account controller with a get method with name getdata inside. The middleware compiles the controller, add it as AssemblyPart to the currently running assembly and after the middleware has finished the controller can directly be used.
First you have to add a middleware. There you can
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
//...
app.UseMiddleware<MyMiddleware>();
The middleware can look like that:
public class MyMiddleware
{
public MyMiddleware(RequestDelegate requestDel, ControllerGenerator generator)
{
RequestDel = requestDel;
Generator = generator;
}
public RequestDelegate RequestDel { get; }
public ControllerGenerator Generator { get; }
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.HasValue)
{
var queryParams = context.Request.Path.Value;
var entityName = queryParams.Split("/").Where(s => !string.IsNullOrEmpty(s)).FirstOrDefault();
var result = Generator.AppendController(entityName);
Console.WriteLine(result + ", " + queryParams);
}
await RequestDel.Invoke(context);
}
}
The controller generator looks like that in my case. The important thing here is the ApplicationPartManager which can be used to add the assembly (part) to the current assembly.
public class ControllerGenerator
{
private readonly ApplicationPartManager _partManager;
private readonly IHostingEnvironment _hostingEnvironment;
public ControllerGenerator(
ApplicationPartManager partManager,
IHostingEnvironment env)
{
_partManager = partManager;
_hostingEnvironment = env;
}
public bool AppendController(string className)
{
var generator = new WebApiGenerator();
Assembly assembly = generator.Exists(className) ?
generator.GetAssembly(className) : generator.CreateDll(className);
if (assembly != null)
{
_partManager.ApplicationParts.Add(new AssemblyPart(assembly));
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
return true;
}
return false;
}
}
The ActionDescriptorChangeProvider is used to inform the framework that something has changed and has to be reloaded.
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
Don't forget to register that Action Descriptor at startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
My code generator just creates a controller and an entity class. The controller returns a OData enabled list of entities with three properties on it. Nothing special, just here to have a working example for someone who want to try this out.
public class WebApiGenerator
{
public WebApiGenerator()
{
}
private static CSharpCompilation GenerateCode(string sourceCode, string className)
{
var codeString = SourceText.From(sourceCode);
var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp9);
var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options);
var references = new List<MetadataReference>
{
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Console).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.Runtime.AssemblyTargetedPatchBandAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly.Location),
};
var referencedAssemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var referenced in referencedAssemblies)
{
string location = null;
try
{
location = referenced.Location;
}
catch
{
}
if (location != null)
{
references.Add(MetadataReference.CreateFromFile(location));
}
}
string outputDll = className + ".dll";
return CSharpCompilation.Create(outputDll,
new[] { parsedSyntaxTree },
references: references,
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary,
optimizationLevel: OptimizationLevel.Release,
assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));
}
public bool Exists(string className)
{
string outputDll = className + ".dll";
return File.Exists(outputDll);
}
public Assembly GetAssembly(string className)
{
string outputDll = className + ".dll";
return Assembly.LoadFrom(outputDll);
}
public Assembly CreateDll(string className)
{
className = className.Replace(" ", string.Empty);
string outputDll = className + ".dll";
if (File.Exists(outputDll)) return Assembly.LoadFrom(outputDll);
var code = new StringBuilder()
.AppendLine("using Microsoft.AspNetCore.Mvc;")
.AppendLine("using Microsoft.Extensions.Logging;")
.AppendLine("using System;")
.AppendLine("using System.Collections.Generic;")
.AppendLine("using System.Linq;")
.AppendLine("using Microsoft.AspNet.OData;")
.AppendLine("")
.AppendLine("namespace ControllerLibrary")
.AppendLine("{")
.AppendLine($"public class {className}")
.AppendLine("{")
.AppendLine(" public string FirstProperty { get; set; }")
.AppendLine(" public string SecondProperty { get; set; }")
.AppendLine(" public int IntValue { get; set; }")
.AppendLine("}")
.AppendLine($"[ApiController]")
.AppendLine($"[Route(\"[controller]\")]")
.AppendLine($"public class {className}Controller : ControllerBase")
.AppendLine(" {")
.AppendLine(" [HttpGet(\"GetData\")]")
.AppendLine(" [EnableQuery]")
.AppendLine($" public IList<{className}> Get()")
.AppendLine(" {")
.AppendLine($" var list = new List<{className}>();");
for (int i = 0; i < 3; i++)
{
code.AppendLine($"list.Add(new {className} {{ FirstProperty = \"First prop of {className}\", SecondProperty = \"asd\", IntValue = {i} }});");
}
code
.AppendLine(" return list;")
.AppendLine(" }")
.AppendLine(" }")
.AppendLine("}");
File.WriteAllText("code.txt", code.ToString());
var result = GenerateCode(code.ToString(), className).Emit(outputDll);
//CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code.ToString());
if (!result.Success)
{
Console.WriteLine("Compilation done with error.");
var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
foreach (var diagnostic in failures)
{
Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
}
}
else
{
Console.WriteLine("Build Succeeded");
return Assembly.LoadFrom(outputDll);
}
return null;
}
}

Related

Unittesting a ServiceStack service that uses AutoMapper

We are using ServiceStack for our .NET backend and I am trying to work on getting unit testing into the project. However there are some automated tools within ServiceStack that makes it a bit complicated to isolate the units so I could really use some advice. In the example below I would like to unit test a simple service that basically does the following:
Takes a request DTO
Passes the DTO to the repository
Gets back a domain model
If the model exists, it maps it to a responseDTO using Automapper and returns it as a part of an IHTTPResult
So the problem I have is that it seems like Automapper is automatically added to the ServiceStack application and in the application the mapper are registered by just calling:
AutoMapping.RegisterConverter().
So how could I inject this into the service to be able to do the unittest?
Example test:
using AutoMapper;
using FluentAssertions;
using NSubstitute;
namespace Api.Services.Tests.Unit;
public class OrderApiServiceTests
{
private readonly OrderApiService _sut;
private readonly IOrderApiRepository accountApiRepository = Substitute.For<IOrderApiRepository>();
public OrderApiServiceTests()
{
_sut = new OrderApiRepository(orderApiRepository);
var config = new MapperConfiguration(cfg => ApiDtoMapping.Register());
var mapper = config.CreateMapper();
}
[Fact]
public async Task Get_ShouldReturnAccount_WhenAccountExistsAsync()
{
// Arrange
var order = new Order
{
Name = "MyOrder",
Value = 1000,
};
var expectedResponse = new OrderApiDto
{
Name = "MyOrder",
Value = 1000,
};
orderApiRepository.GetAsync(Arg.Any<GetOrder>()).Returns(order);
// Act
var result = await _sut.Get(new GetOrder());
// Assert
result.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
result.Response.Should().BeEquivalentTo(expectedResponse);
}
}
Added a full example including all files:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// 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.UseServiceStack(new AppHost());
app.Run();
// Configure.AppHost.cs
using Funq;
using ssUnitTests.ServiceInterface;
[assembly: HostingStartup(typeof(ssUnitTests.AppHost))]
namespace ssUnitTests;
public class AppHost : AppHostBase, IHostingStartup
{
public void Configure(IWebHostBuilder builder) => builder
.ConfigureServices(services =>
{
});
public AppHost() : base("ssUnitTests", typeof(MyServices).Assembly) { }
public override void Configure(Container container)
{
container.RegisterAutoWiredAs<OrderRepository, IOrderRepository>().ReusedWithin(ReuseScope.None);
// Configure ServiceStack only IOC, Config & Plugins
SetConfig(new HostConfig
{
UseSameSiteCookies = true,
});
Mappings.RegisterConverters();
}
}
// Mappings.cs
using ssUnitTests.ServiceModel;
namespace ssUnitTests;
public static class Mappings
{
public static void RegisterConverters()
{
AutoMapping.RegisterConverter((Order from) =>
{
var to = from.ConvertTo<OrderDto>();
to.DtoProperty = from.BaseProperty + "Dto";
return to;
});
}
}
// IOrderRepository.cs
using ssUnitTests.ServiceModel;
namespace ssUnitTests.ServiceInterface;
public interface IOrderRepository
{
Order GetOrder();
}
// Order.cs
namespace ssUnitTests.ServiceModel;
public class Order
{
public string Name { get; set; }
public string BaseProperty { get; set; }
}
// OrderDto.cs
namespace ssUnitTests.ServiceModel;
public class OrderDto
{
public string Name { get; set; }
public string DtoProperty { get; set; }
}
// OrderRequest.cs
using ServiceStack;
namespace ssUnitTests.ServiceModel;
[Route("/order")]
public class OrderRequest : IReturn<OrderDto>
{
public int Id { get; set; }
}
// UnitTest.cs
using NSubstitute;
using NUnit.Framework;
using ssUnitTests.ServiceInterface;
using ssUnitTests.ServiceModel;
namespace ssUnitTests.Tests;
public class UnitTest
{
private readonly MyServices _sut;
private readonly IOrderRepository _repository = Substitute.For<IOrderRepository>();
public UnitTest()
{
_sut = new MyServices(_repository);
}
[Test]
public void Get_ShouldReturn_OrderDto()
{
var order = new Order
{
Name = "MyName",
BaseProperty = "MyBaseProperty"
};
_repository.GetOrder().Returns(order);
var response = (OrderDto)_sut.Any(new OrderRequest { Id = 1 });
Assert.That(response.Name.Equals(order.Name));
Assert.That(response.DtoProperty.Equals(order.BaseProperty + "Dto"));
}
}
ServiceStack.dll does not have any dependencies to any 3rd Party Libraries, e.g. it's built-in AutoMapping is a completely different stand-alone implementation to AutoMapper.
If you're using AutoMapper you can ignore ServiceStack's AutoMapping which is completely unrelated.

How to call class with dependency injection (DI)

Description
I want to create an object of a class with dependency injection. If I set the parameter manually I got the exception Cannot access a disposed of the object..
This Application is a Blazor wasm with Dotnet core 3.1. I´ve created a Middleware that should connect to a query console. So I have a static list that contains all query clients. If a client is missing it will be created.
Invoke Async in the middleware:
public async Task InvokeAsync(HttpContext context,
IConfiguration configuration,
IInstanceControlRepository instanceControlRepository,
IServiceProvider serviceProvider)
{
_configuration = configuration;
_instanceControlRepository = instanceControlRepository;
long timestamp = new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();
var instances = _instanceControlRepository.GetAllInstances();
if (_time + 3 <= timestamp)
{
_time = timestamp;
// Remove
foreach(var client in TeamspeakInstanceQueryClients.ToList())
{
var cl = instances.ToList().Find(el => el.Id == client.Instance.Id);
if(cl == null)
{
client.Dispose();
TeamspeakInstanceQueryClients.RemoveAll(el => el.Instance.Equals(client.Instance));
}
}
// Create & Update
foreach (var instance in instances)
{
var queryClient = TeamspeakInstanceQueryClients.Find(el => el.Instance.Id == instance.Id);
if(queryClient == null)
{
//var test = ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider);
//var dbContext = serviceProvider.GetService<ApplicationDbContext>();
//queryClient = new TeamspeakInstanceQueryClient(new InstancesControlRepository(ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider)));
queryClient = new TeamspeakInstanceQueryClient(serviceProvider);
_ = queryClient.Connect(instance);
TeamspeakInstanceQueryClients.Add(queryClient);
}
else
{
_ = queryClient.CheckInstanceData(instance);
}
}
}
await _next(context);
}
TeamspeakInstanceQueryClient.cs
public partial class TeamspeakInstanceQueryClient : ITeamspeakInstanceQueryClient
{
private IInstanceControlRepository _instanceControlRepository;
private const short MAX_RETRYS = 3;
private const short TIME_TO_RETRY = 10;
private EventHandler OnConnected;
public Instance Instance { get; internal set; }
public TeamSpeakClient Client { get; internal set; }
public bool IsSelected { get; internal set; }
private short _connectionTrys = 0;
public TeamspeakInstanceQueryClient(IServiceProvider serviceProvider)
{
_instanceControlRepository = new InstancesControlRepository(ActivatorUtilities.CreateInstance<ApplicationDbContext>(serviceProvider));
Init();
}
}
InstancesControlRepository.cs
public class InstancesControlRepository : IInstanceControlRepository
{
private readonly ApplicationDbContext _applicationDbContext;
public InstancesControlRepository(ApplicationDbContext applicationDbContext)
{
_applicationDbContext = applicationDbContext;
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(option =>
option.UseMySql(
Configuration.GetConnectionString("DefaultConnection"),
mySqlOptions => mySqlOptions.ServerVersion(new System.Version(10, 4, 13), ServerType.MariaDb)
)
);
services.AddScoped<IInstanceControlRepository, InstancesControlRepository>();
services.AddScoped<IServerQueryRepository, ServerQueryRepository>();
What I´ve tried
I´ve tried to create the class with the service provider but it comes to the same error
I´ve tried to create the missing parameters with the service provider in the created class. But I need to inject the service provider which also creates the exception Cannot access a disposed of the object. Object name: 'IServiceProvider'.
I´ve tried to make the service provider static so it can´t be disposed but it is disposed.
It seems that instance of IServiceProvider is a scoped one and it is disposed when the scope ends (in the end of request I assume). You can try define singleton factory for your TeamspeakInstanceQueryClient and use it:
class ClientFactory
{
private IServiceProvider _sp { get; set; }
private IServiceScope _scope { get; set; }
public MyClass(IServiceProvider sp)
{
_sp = sp;
_scope = sp.CreateScope();
}
public TeamspeakInstanceQueryClient Create() => new TeamspeakInstanceQueryClient(_scope.ServiceProvider);
}
// register it as singleton
services.AddSingleton<ClientFactory>();
and use it in InvokeAsync:
var factory = serviceProvider.GetRequiredService<ClientFactory>();
queryClient = factory.Create();
P.S. this code can be improved vastly and is used only for demonstration purposes.

how do I cache an output in ASP.NET Core

I have a API controller,and the scenario is:
I need to consume third party datasource(let's say the third party is provided as a dll file for simplicity, and the dll contain Student model and StudentDataSource that contain a lot of method to retrieve student ), and calling the third party data source is costly and data only gets updated every 6 hours.
so somehow I need to cache the output, below is some action method from my api controller:
// api controller that contain action methods below
[HttpGet]
public JsonResult GetAllStudentRecords()
{
var dataSource = new StudentDataSource();
return Json(dataSource.GetAllStudents());
}
[HttpGet("{id}")]
public JsonResult GetStudent(int id)
{
var dataSource = new StudentDataSource();
return Json(dataSource.getStudent(id));
}
then how should I cache the result especially for the second action method, it is dumb to cache every student result with different id
My team is implementing a similar caching strategy on an API controller using a custom Action filter attribute to handle the caching logic. See here for more info on Action filters.
The Action filter's OnActionExecuting method runs prior to your controller method, so you can check whether the data you're looking for is already cached and return it directly from here, bypassing the call to your third party datasource when cached data exists. We also use this method to check the type of request and reset the cache on updates and deletes, but it sounds like you won't be modifying data.
The Action filter's OnActionExecuted method runs immediately AFTER your controller method logic, giving you an opportunity to cache the response object before returning it to the client.
The specifics of how you implement the actual caching are harder to provide an answer for, but Microsoft provides some options for in-memory caching in .NET Core (see MemoryCache.Default not available in .NET Core?)
I used the solution with the cache strategy through the controller API as #chris-brenberg pointed out, it turned out like this
on controller class
[ServerResponseCache(false)]
[HttpGet]
[Route("cache")]
public ActionResult GetCache(string? dateFormat) {
Logger.LogInformation("Getting current datetime");
return Ok(new { date = DateTime.Now.ToString() });
}
on ServerResponseCacheAttribute.cs
namespace Site.Api.Filters {
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System.Globalization;
using System.Threading.Tasks;
public class ServerResponseCacheAttribute : TypeFilterAttribute {
public ServerResponseCacheAttribute(bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
Arguments = new object[] { new ServerResponseCacheProps { ByUserContext = byUserContext } };
public ServerResponseCacheAttribute(int secondsTimeout, bool byUserContext = true) : base(typeof(ServerResponseCacheAttributeImplementation)) =>
Arguments = new object[] { new ServerResponseCacheProps { SecondsTimeout = secondsTimeout, ByUserContext = byUserContext } };
public class ServerResponseCacheProps {
public int? SecondsTimeout { get; set; }
public bool ByUserContext { get; set; }
}
public class ServerResponseCacheConfig {
public bool Disabled { get; set; }
public int SecondsTimeout { get; set; } = 60;
public string[] HeadersOnCache { get; set; } = { "Accept-Language" };
}
private class ServerResponseCacheAttributeImplementation : IAsyncActionFilter {
private string _cacheKey = default;
readonly ILogger<ServerResponseCacheAttributeImplementation> _logger;
readonly IMemoryCache _memoryCache;
readonly ServerResponseCacheConfig _config;
readonly bool _byUserContext;
public ServerResponseCacheAttributeImplementation(ILogger<ServerResponseCacheAttributeImplementation> logger,
IMemoryCache memoryCache, ServerResponseCacheProps props) {
_logger = logger;
_memoryCache = memoryCache;
_byUserContext = props.ByUserContext;
_config = new ServerResponseCacheConfig {
SecondsTimeout = props.SecondsTimeout ?? 60,
HeadersOnCache = new[] { "Accept-Language" }
};
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
if (context == null) {
throw new ArgumentNullException(nameof(context));
}
if (next == null) {
throw new ArgumentNullException(nameof(next));
}
if (_config.Disabled) {
await next();
return;
}
OnActionExecutingAsync(context);
if (context.Result == null) {
OnActionExecuted(await next());
}
}
void OnActionExecutingAsync(ActionExecutingContext context) {
SetCacheKey(context.HttpContext.Request);
// Not use a stored response to satisfy the request. Will regenerates the response for the client, and updates the stored response in its cache.
bool noCache = context.HttpContext.Request.Headers.CacheControl.Contains("no-cache");
if (noCache) {
return;
}
TryLoadResultFromCache(context);
}
void SetCacheKey(HttpRequest request) {
if (request == null) {
throw new ArgumentException(nameof(request));
}
if (!string.Equals(request.Method, "GET", StringComparison.InvariantCultureIgnoreCase)) {
return;
}
List<string> cacheKeys = new List<string>();
if (_byUserContext && request.HttpContext.User.Identity.IsAuthenticated) {
cacheKeys.Add($"{request.HttpContext.User.Identity.Name}");
}
string uri = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase, request.Path, request.QueryString);
cacheKeys.Add(uri);
foreach (string headerKey in _config.HeadersOnCache) {
StringValues headerValue;
if (request.Headers.TryGetValue(headerKey, out headerValue)) {
cacheKeys.Add($"{headerKey}:{headerValue}");
}
}
_cacheKey = string.Join('_', cacheKeys).ToLower();
}
void TryLoadResultFromCache(ActionExecutingContext context) {
ResultCache resultCache;
if (_cacheKey != null && _memoryCache.TryGetValue(_cacheKey, out resultCache)) {
_logger.LogInformation("ServerResponseCache: Response loaded from cache, cacheKey: {cacheKey}, expires at: {expiration}.", _cacheKey, resultCache.Expiration);
context.Result = resultCache.Result;
SetExpiresHeader(context.HttpContext.Response, resultCache.Expiration);
}
}
/// <summary>Add expires header (the time after which the response is considered stale).</summary>
void SetExpiresHeader(HttpResponse response, DateTimeOffset expiration) {
string expireHttpDate = expiration.UtcDateTime.ToString("ddd, dd MMM yyyy HH:mm:ss", CultureInfo.InvariantCulture);
response.Headers.Add("Expires", $"{expireHttpDate} GMT");
}
void OnActionExecuted(ActionExecutedContext context) {
if (_cacheKey == null) {
return;
}
if (context.Result != null) {
DateTimeOffset expiration = SetCache(context.Result);
SetExpiresHeader(context.HttpContext.Response, expiration);
} else {
RemoveCache();
}
}
DateTimeOffset SetCache(IActionResult result) {
DateTimeOffset absoluteExpiration = DateTimeOffset.Now.AddSeconds(_config.SecondsTimeout);
ResultCache resultCache = new ResultCache {
Result = result,
Expiration = absoluteExpiration
};
_memoryCache.Set(_cacheKey, resultCache, absoluteExpiration);
_logger.LogInformation("ServerResponseCache: Response set on cache, cacheKey: {cacheKey}, until: {expiration}.", _cacheKey, absoluteExpiration);
return absoluteExpiration;
}
void RemoveCache() {
_memoryCache.Remove(_cacheKey);
_logger.LogInformation("ServerResponseCache: Response removed from cache, cacheKey: {cacheKey}.", _cacheKey);
}
}
private class ResultCache {
public IActionResult Result { get; set; }
public DateTimeOffset Expiration { get; set; }
}
}}
I hope it helps someone, best regards

Custom Authorize attribute - ASP .NET Core 2.2

I want to create a custom Authorize attribute to be able to send a personalized response when it fails. There are many examples, but I could not find what I'm looking for.
When registering a policy, I add a "claim". Is it possible to access that registered claim within the custom attribute without having to pass the claim by parameter? or is it possible to know if the check of the claim happened and if not, return a personalized response? Thx!
public static void AddCustomAuthorization(this IServiceCollection serviceCollection)
{
serviceCollection.AddAuthorization(x =>
{
x.AddPolicy(UserPolicy.Read,
currentPolicy => currentPolicy.RequireClaim(UserClaims.Read));
});
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext.User.HasClaim(x => x.Value == "CLAIM_NAME")) // ACCESS TO REGISTER CLAIM => currentPolicy => currentPolicy.RequireClaim(UserClaims.Read)
{
authorizationFilterContext.Result = new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
[HttpGet]
[CustomAuthorizeAttribute(Policy = UserPolicy.Read)]
public async Task<IEnumerable<UserDTO>> Get()
{
return ...
}
You can use IAuthorizationPolicyProvider to get the policy and then use ClaimsAuthorizationRequirement.ClaimType to get a claim name. And since it has async API, it is better to use IAsyncAuthorizationFilter instead of IAuthorizationFilter. Try this:
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext authorizationFilterContext)
{
var policyProvider = authorizationFilterContext.HttpContext
.RequestServices.GetService<IAuthorizationPolicyProvider>();
var policy = await policyProvider.GetPolicyAsync(UserPolicy.Read);
var requirement = (ClaimsAuthorizationRequirement)policy.Requirements
.First(r => r.GetType() == typeof(ClaimsAuthorizationRequirement));
if (authorizationFilterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (!authorizationFilterContext.HttpContext
.User.HasClaim(x => x.Value == requirement.ClaimType))
{
authorizationFilterContext.Result =
new ObjectResult(new ApiResponse(HttpStatusCode.Unauthorized));
}
}
}
}
This attribute takes an array of strings, which was needed in my case. I needed to pass different users roles to this attribute and return result based on some custom logic.
public class CustomAuthFilter : AuthorizeAttribute, IAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
//Custom code ...
//Resolving a custom Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
string name = service.GetName(); // returns "anish"
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
You can decorate your controller with this attribute as shown below
[CustomAuthFilter("Anish","jiya","sample")]
public async Task<IActionResult> Index()
Sample is a class that returns a hard coded string
public class Sample : ISample
{
public string GetName() => "anish";
}
services.AddScoped(); //Register ISample, Sample as scoped.
FOR ASYNCHRONOUS SUPPORT use IAsyncAuthorizationFilter
public class CustomAuthFilter : AuthorizeAttribute, IAsyncAuthorizationFilter
{
public CustomAuthFilter(params string[] args)
{
Args = args;
}
public string[] Args { get; }
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
//DO Whatever...
//Resolve Services from the container
var service = context.HttpContext.RequestServices.GetRequiredService<ISample>();
var httpClientFactory = context.HttpContext.RequestServices.GetRequiredService<IHttpClientFactory>();
string name = service.GetName();
using var httpClient = httpClientFactory.CreateClient();
var resp = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/todos/1");
var data = await resp.Content.ReadAsStringAsync();
//Return based on logic
context.Result = new UnauthorizedResult();
}
}
Hope that helps..

Custom Localized Routes using IApplicationModelConvention are not respected in IUrlHelper.GetVirtualPathData

I'm currently porting an application from ASP.NET 4 to ASP.NET Core. I want to use attribute based routing while having the ability to localize the URLs.
The legacy application was using an approach using a custom IDirectRouteProvider. Since I didn't find the corresponding type in ASP.NET Core, I went with a solution inspired by https://www.strathweb.com/2015/11/localized-routes-with-asp-net-5-and-mvc-6/. Here's the implementation using an IApplicationModelConvention
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o =>
{
o.Conventions.Insert(0, new LocalizedRouteConvention());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseRequestLocalization(new RequestLocalizationOptions { ... });
app.UseMvc();
}
}
public class LocalizedRouteConvention : IApplicationModelConvention
{
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
foreach (var action in controller.Actions)
{
var attributes = action.Attributes.OfType<RouteAttribute>().ToArray();
if (!attributes.Any()) return;
foreach (var attribute in attributes)
{
SelectorModel defaultSelector = action.Selectors.First();
foreach (var localizedVersion in GetLocalized(attribute.Template))
{
if (!action.Selectors.Any(s => s.AttributeRouteModel.Template == localizedVersion.Template))
{
action.Selectors.Insert(0, new SelectorModel(defaultSelector)
{
AttributeRouteModel = localizedVersion,
ActionConstraints =
{
new CultureActionConstraint { Culture = ((LocalizedRouteAttribute) localizedVersion.Attribute).Culture }
}
});
}
}
}
}
}
}
}
public class LocalizedRouteAttribute : RouteAttribute
{
public LocalizedRouteAttribute(string template) : base(template)
{
}
public string Culture { get; set; }
}
public class CultureActionConstraint : IActionConstraint
{
public string Culture { get; set; }
public int Order => 0;
public bool Accept(ActionConstraintContext context)
{
return CultureInfo.CurrentCulture.TwoLetterISOLanguageName == Culture;
}
}
Now, this approach works and the localized routes are only available when the correct request culture is set. However, when I use Html.ActionLink(...) or any other function that uses IUrlHelper.GetVirtualPathData(), the default route is returned instead of the localized one.
As far as I understand, the IUrlHelper will check the IRouteConstraints of a Route but it doesn't seem to respect the IActionConstraint. Unfortunately, I haven't found a way to set custom IRouteConstraints in my IApplicationModelConvention.

Categories