WebAPI: Retrieve GET parameter from Controller Constructor - c#

Every call to my WebAPI may (or may not) contain the GET parameter
/api/SomeControllerFunction?loglevel=(someint)
From the function inside the controller I can initialize a LogCollector:
[HttpGet]
SomeControllerFunction(int loglevel = 0)
{
LogCollector logger = new LogCollector(loglevel)
}
To not repeat myself too often, I want to hide this in the class hierarchy by adding it into the constructor of a BaseController, from which all my controllers shall inherit:
public class BaseController: ApiController
{
internal LogCollector Logger
BaseController()
{
Logger = new LogCollector(loglevel);
}
But how can I access a GET parameter from the constructor?

Instead of using the constructor you could inject the LogCollector directly into the method. If you did want to use the constructor you should use a Di / IoC framework as that would be more appropriate.
In the example below you can use a custom ActionFilterAttribute instance which injects the Logger based the incoming (optional) log level. The log level is then defined in the route using a RouteAttribute on the action. The RouteAttribute also defines a default value for the log level so it is not required when calling that action.
LogInjectorFilterAttribute.cs
public class LogInjectorFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
const string key = "loglevel";
if(actionContext.ControllerContext.RouteData.Values.ContainsKey(key))
{
var loglevel = int.Parse(actionContext.ControllerContext.RouteData.Values[key].ToString());
LogCollector logger = new LogCollector(loglevel);
actionContext.ActionArguments["logger"] = logger;
}
base.OnActionExecuting(actionContext);
}
}
HomeController.cs
[HttpGet]
[Route("api/Home/Get/{loglevel:int=1}")]
[LogInjectorFilter]
public IHttpActionResult Get(LogCollector logger)
{
}

The constructor is invoked too early, you can't access the parameters from there. However, you can override the Initialize method and retrieve the GET parameters from the context:
protected override void Initialize(HttpControllerContext controllerContext)
{
foreach (var parameter in controllerContext.Request.GetQueryNameValuePairs())
{
Debug.WriteLine(string.Format("{0} = {1}", parameter.Key, parameter.Value));
}
base.Initialize(controllerContext);
}

Related

C# - Custom ActionFilter pass in configuration variables

I have a custom action filter that takes in a property but I need the property to come from my appsettings.json file. I pass my configuration into the controller, but when I try to pass in the "_config.GetValue< string >("myString")" the "_config" is red underlined with the message:
An object reference is required for the non-static field, method, or property 'MyController._config'
Action Filter
public class MyActionFilter : ActionFilterAttribute
{
public string Property1 { get; set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
...
}
}
Controller
[ApiController]
[Route("[controller]")]
public class MyController : ControllerBase
{
private readonly IConfiguration _config;
public MyController(IConfiguration config) {
_config = config;
}
[Authorize]
[HttpPost(Constants.ActionName.MyMethod, Name = nameof(MyMethod))]
[MyActionFilter(Property1 = _config.GetValue<string>("myString"))] // Breaks here!
public ActionResult<string> MyMethod()
{
...
}
}
How can I do this? Or at least, how can I avoid hardcoding a value for my action filter properties?
Your current approach does not work because constructor parameters and properties of attributes are evaluated at compile time.
You could use the service locator pattern in your filter like so:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var config = filterContext.HttpContext.RequestServices.GetService<IConfiguration>();
string Property1 = config.GetValue<string>("myString");
}
However, this approach is debatable because the service locator pattern is considered an anti-pattern.
Another approach is to use a ServiceFilter. First, create a constructor for the action filter:
public MyActionFilter(string property1)
{
Property1 = property1;
}
Second, change the action filter to a service filter in the controller:
[ServiceFilter(typeof(MyActionFilter))]
public ActionResult<string> MyMethod()
{
...
}
Third, register the action filter:
builder.Services.AddScoped(p => new MyActionFilter(p.GetService<IConfiguration>().GetValue<string>("myString")));
Here is a great blogpost about the topic: Dependency Injection in action filters in ASP.NET Core.

Use DbContext in ActionFilter Middleware

I want to use a DbContext in my ActionFilter Middleware. Is it possible?
public class VerifyProfile : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
using (var context = new SamuraiDbContext())
{
var user = filterContext.HttpContext.User.Identity.Name;
if (context.Profiles.SingleOrDefaultAsync(p => p.IdentityName == user).Result == null)
{
filterContext.Result = new RedirectResult("~/admin/setup");
}
}
}
}
But this code using (var context = new SamuraiDbContext()) requires to pass options. Should I again pass DbContextOptionsBuilder() here or is there any other way?
I want to have [VerifyProfile] attribute in my controllers method. Is it possible or not?
Instead of trying to create a new instance of SamuraiDbContext yourself, use Dependency Injection in your Filter. To achieve this, you need to do three things:
Add a constructor to VerifyProfile with a parameter of type SamuraiDbContext and store it as a field:
private readonly SamuraiDbContext dbContext;
public VerifyProfile(SamuraiDbContext dbContext)
{
this.dbContext = dbContext;
}
Add VerifyProfile to the DI container:
services.AddScoped<VerifyProfile>();
Use ServiceFilter to take care of connecting your filter to the DI container:
[ServiceFilter(typeof(VerifyProfile))]
public IActionResult YourAction()
...
You can apply the ServiceFilter attribute at the action level, as shown, or at the controller level. You can also apply it globally. To do that, replace step 3 above with the following:
services.AddMvc(options =>
{
options.Filters.Add<VerifyProfile>();
});
As an additional resource, this blog post has a good write up of some of the other options.

Inject a specific implementation of an interface in a controller in ASP.NET Core

I am working on a web application which is built on ASP.NET Core and I am using Autofac for dependency injection.
I have an interface as ICacheProvider, and there are 3 concrete implementations of this interface - OrderCacheProvider, ProductsCacheProvider and CustomerCacheProvider. The infrastructure and logic are different for the different cache providers. They are registered as below:
builder.RegisterType<CustomerCacheProvider>()
.Keyed<ICacheProvider>(CacheType.Customer);
builder.RegisterType<OrderCacheProvider>()
.Keyed<ICacheProvider>(CacheType.Order);
builder.RegisterType<ProductCacheProvider>()
.Keyed<ICacheProvider>(CacheType.Product);
Now I have 3 controllers - OrdersController, ProductsController and CustomerController. Each controller expects an ICacheProvider in the below fashion
public class OrdersController: BaseController {
private readonly ICacheProvider _cacheProvider;
public OrdersController(ICacheProvider cacheProvider) {
_cacheProvider = cacheProvider;
}
}
Now my problem is how do I inject OrderCacheProvider is injected in OrdersController? The same goes for the CustomerCacheProvder to CustomersController and ProductsCacheProvider?
You can use the WithParameter method when you register your controller to specify which ICacheProvider it should use
builder.RegisterType<OrdersController>()
.WithParameter(ResolvedParameter.ForKeyed<ICacheProvider>(CacheType.Order));
Another option would be to use the KeyFilter attribute
public class OrdersController
{
public OrdersController([KeyFilter(CacheType.Order)]ICacheProvider cacheProvider)
{ }
}
I prefer the first solution than this one which looks more pure, your component just has to request ICacheProvider nothing more.
Another solution would be to create a custom module that will add the parameter for each controller based on conventions.
protected override void AttachToComponentRegistration(
IComponentRegistry componentRegistry, IComponentRegistration registration)
{
base.AttachToComponentRegistration(componentRegistry, registration);
if (registration.Activator.LimitType.IsSubclassOf(typeof(BaseController)))
{
String controllerName = registration.Activator.LimitType.Name;
controllerName = controllerName.Substring(0, controllerName.Length - 10);
if (Enum.TryParse<CacheType>(controllerName, out CacheType cacheType))
{
registration.Preparing += (sender, e) =>
{
e.Parameters = new Parameter[]
{
ResolvedParameter.ForKeyed<ICacheProvider>(cacheType)
}
.Concat(e.Parameters);
};
}
else
{
// throw, use default cache, do nothing, etc.
throw new Exception($"No cache found for controller {controllerName}");
}
}
}
}
It is more code but you don't have to use the WithParameter for every controller registration, it can be great if you have lot of controller. You still have to register the module :
builder.RegisterModule<CacheProviderModule>();

Inject service into Action Filter

I am trying to inject a service into my action filter but I am not getting the required service injected in the constructor. Here is what I have:
public class EnsureUserLoggedIn : ActionFilterAttribute
{
private readonly ISessionService _sessionService;
public EnsureUserLoggedIn()
{
// I was unable able to remove the default ctor
// because of compilation error while using the
// attribute in my controller
}
public EnsureUserLoggedIn(ISessionService sessionService)
{
_sessionService = sessionService;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
// Problem: _sessionService is null here
if (_sessionService.LoggedInUser == null)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Result = new JsonResult("Unauthorized");
}
}
}
And I am decorating my controller like so:
[Route("api/issues"), EnsureUserLoggedIn]
public class IssueController : Controller
{
}
Startup.cs
services.AddScoped<ISessionService, SessionService>();
Using these articles as reference:
ASP.NET Core Action Filters
Action filters, service filters and type filters in ASP.NET 5 and MVC 6
Using the filter as a ServiceFilter
Because the filter will be used as a ServiceType, it needs to be registered with the framework IoC. If the action filters were used directly, this would not be required.
Startup.cs
public void ConfigureServices(IServiceCollection services) {
services.AddMvc();
services.AddScoped<ISessionService, SessionService>();
services.AddScoped<EnsureUserLoggedIn>();
...
}
Custom filters are added to the MVC controller method and the controller class using the ServiceFilter attribute like so:
[ServiceFilter(typeof(EnsureUserLoggedIn))]
[Route("api/issues")]
public class IssueController : Controller {
// GET: api/issues
[HttpGet]
[ServiceFilter(typeof(EnsureUserLoggedIn))]
public IEnumerable<string> Get(){...}
}
There were other examples of
Using the filter as a global filter
Using the filter with base controllers
Using the filter with an order
Take a look, give them a try and see if that resolves your issue.
Hope this helps.
Global filters
You need to implement IFilterFactory:
public class AuthorizationFilterFactory : IFilterFactory
{
public bool IsReusable => false;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
// manually find and inject necessary dependencies.
var context = (IMyContext)serviceProvider.GetService(typeof(IMyContext));
return new AuthorizationFilter(context);
}
}
In Startup class instead of registering an actual filter you register your filter factory:
services.AddMvc(options =>
{
options.Filters.Add(new AuthorizationFilterFactory());
});
One more way for resolving this problem. You can get your service via Context as in the following code:
public override void OnActionExecuting(ActionExecutingContext context)
{
_sessionService = context.HttpContext.RequestServices.GetService<ISessionService>();
if (_sessionService.LoggedInUser == null)
{
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Result = new JsonResult("Unauthorized");
}
}
Please note that you have to register this service in Startup.cs
services.AddTransient<ISessionService, SessionService>();
Example
private ILoginService _loginService;
public override void OnActionExecuting(ActionExecutingContext context)
{
_loginService = (ILoginService)context.HttpContext.RequestServices.GetService(typeof(ILoginService));
}
Hope it helps.
After reading this article ASP.NET Core - Real-World ASP.NET Core MVC Filters (Aug 2016) I implemented it like this:
In Starup.cs / ConfigureServices:
services.AddScoped<MyService>();
In MyFilterAttribute.cs:
public class MyFilterAttribute : TypeFilterAttribute
{
public MyFilterAttribute() : base(typeof (MyFilterAttributeImpl))
{
}
private class MyFilterAttributeImpl : IActionFilter
{
private readonly MyService _sv;
public MyFilterAttributeImpl(MyService sv)
{
_sv = sv;
}
public void OnActionExecuting(ActionExecutingContext context)
{
_sv.MyServiceMethod1();
}
public void OnActionExecuted(ActionExecutedContext context)
{
_sv.MyServiceMethod2();
}
}
}
In MyFooController.cs :
[MyFilter]
public IActionResult MyAction()
{
}
Edit: Passing arguments like [MyFilter("Something")] can be done using the Arguments property of the TypeFilterAttribute class: How do I add a parameter to an action filter in asp.net? (rboe's code also shows how to inject things (the same way))
While the question implicitly refers to "filters via attributes", it is still worth highlighting that adding filters "globally by type" supports DI out-of-the-box:
[For global filters added by type] any constructor dependencies will be populated by dependency injection (DI). Adding a filter by type is equivalent to filters.Add(new TypeFilterAttribute(typeof(MyFilter))).
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#dependency-injection
With regards to attribute-based filters:
Filters that are implemented as attributes and added directly to controller classes or action methods cannot have constructor dependencies provided by dependency injection (DI). This is because attributes must have their constructor parameters supplied where they're applied. This is a limitation of how attributes work.
https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#dependency-injection
However, as mentioned in the previous answers to the OP, there are ways of indirection that can be used to achieve DI. For the sake of completeness, here are the links to the official docs:
ServiceFilterAttribute
TypeFilterAttribute
IFilterFactory implemented on your attribute

Custom filter attributes inject dependency

I'musing ASP.NET Web API and I need to have authorization so I've created custom authorization attribute
public class CustomAuthorizationAttribute : AuthorizeAttribute
In order to inject dependency inside constructor I have following :
public CustomAuthorizationAttribute(IAccountBL accountBl)
{
_accountBL = accountBl;
}
In IAccountBL I have method which interacts with database checking if user is authorized to make request.
Inside Member API controller I've register that attribute
[CustomAuthorization]
public class MemberController : ApiController
But I get following error
Project.Account.AccountBL' does not contain a constructor that takes 0 arguments
And if I register it like
[CustomAuthorization(IAccountBL)]
Thank you
Action filters are just attributes. You do not have control over when those attributes are instantiated by the CLR. One possibility is to write a marker attribute:
public class CustomAuthorizationAttribute : Attribute { }
and then the actual action filter:
public class CustomAuthorizationFilter : ActionFilterAttribute
{
private readonly IAccountBL accountBL;
public CustomAuthorizationFilter(IAccountBL accountBL)
{
this.accountBL = accountBL;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ControllerContext.ControllerDescriptor.GetCustomAttributes<CustomAuthorizationAttribute>().Any() ||
actionContext.ActionDescriptor.GetCustomAttributes<CustomAuthorizationAttribute>().Any())
{
// here you know that the controller or action is decorated
// with the marker attribute so that you could put your code
}
}
}
and finally register it as a global action filter:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
IAccountBL accountBL = ...
config.Filters.Add(new CustomAuthorizationFilter(accountBL));
}
}
and finally you could use the marker attribute:
[CustomAuthorization]
public class MemberController : ApiController
{
...
}
You can get dependency in your filter by using extension method GetDependencyScope for class HttpRequestMessage. It's not a canonical way for dependency injection, but can be used as workaround. A basic example may look like this:
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var dependencyScope = context.Request.GetDependencyScope();
var dependency = dependencyScope.GetService(typeof (MyDependencyType));
//use your dependency here
}
This method may be used with constructor injection to simplify unit testing:
public class MyAuthenticationFilter : Attribute, IAuthenticationFilter
{
private Func<HttpRequestMessage, MyDependencyType> _dependencyFactory;
public MyAuthenticationFilter() :
this(request => (MyDependencyType)request.GetDependencyScope().GetService(typeof(MyDependencyType)))
{
}
public MyAuthenticationFilter(Func<HttpRequestMessage, MyDependencyType> dependencyFactory)
{
_dependencyFactory = dependencyFactory;
}
public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
var dependencyScope = context.Request.GetDependencyScope();
var dependency = dependencyFactory.Invoke(context.Request);
//use your dependency here
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
throw new NotImplementedException();
}
public bool AllowMultiple { get; private set; }
}
If anyone finds similar issue here's how I manage to solve it.
My custom filter inherits IAutofacAuthorizationFilter. Besides this one you can also inherit IAutofacExceptionFilter and IAutofacActionFilter.
And inside my DI container I've register this filter for each controller I want to use like this
builder.Register(c => new CustomAuthorizationAttribute(c.Resolve<IAccountBL>()))
.AsWebApiAuthorizationFilterFor<MemberController>()
.InstancePerApiRequest();
If you registered your service on the application using any container, it's very easy to get the instance of your service from anywhere in the scope. Just follow the below code to get your service.
var myService = DependencyResolver.Current.GetService(typeof(IMyService)) as IMyService;
Please make sure you have included System.Web.Mvc in the file.
Happy coding!!!

Categories

Resources