0

I'm trying to fully understand Dependency Injections. I'm defining a Filter and would like to read from a configuration file. Is it a better practice to instantiate Configuration inside of the filter or can this be done so globally, such as in the startup? If So, any pointers for how to do so?

public class CompanyFilter : ActionFilterAttribute
{
   string _ERPUrl;
   public CompanyFilter(IConfiguration iconfiguration)
   {
       ERPUrl = iconfiguration.GetSection("AppSettings").GetSection("ERPUrl").Value;  

    }
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.Controller is Controller controller)
            controller.ViewBag.ERPUrl = _ERPUrl;      
            //filterContext.Controller.ViewBag.Company = "Test";
    }
}

Startup Class

public class Startup
{


    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
....

Controllers

namespace Projects.Controllers
{
    [CompanyFilter]
    public class HomeController : Controller
    {
....

The following error is produced.

Controllers\HomeController.cs(14,6): error CS7036: There is no argument given that corresponds to the required formal parameter 'iconfiguration' of 'CompanyFilter.CompanyFilter(IConfiguration)'

Jasen
  • 14,030
  • 3
  • 51
  • 68
ffejrekaburb
  • 656
  • 1
  • 10
  • 35
  • 1
    I would write the filter so that what gets injected is not `IConfiguration` but the actual value, like a url, that the class needs. It's better for the class just to get the dependency it needs, not something that it has to inspect. If you like I can provide a more detailed example. – Scott Hannen Jan 16 '19 at 17:25
  • I would be interested in seeing that. – ffejrekaburb Jan 16 '19 at 18:09
  • What IoC container are you using? Just Microsoft.Extensions.DependencyInjection? – mason Jan 16 '19 at 18:37
  • standard asp.net core mvc? – ffejrekaburb Jan 16 '19 at 18:37
  • 1
    Okay, by default that is Microsoft.Extensions.DependencyInjection. Unrelated: since you mentioned you're trying to learn about DI, I highly recommend [this video](https://channel9.msdn.com/Events/TechEd/NorthAmerica/2014/DEV-B412). It's what made DI finally "click" for me, and I would attribute a vast improvement in my code quality to that video, eventually leading to several developer jobs (and raises!). I don't think it will answer your current question, but hopefully you'll find it as useful as I did. – mason Jan 16 '19 at 18:43
  • You shouldn't inject `IConfiguration` anywhere. You should instead be following the convention and injecting `IOptions` into your `CompanyFilter`. Use `services.Configure(configuration.GetSection("SectionName"));` in your Startup.cs where `T` is a class that is a physical representation of your JSON object. – ColinM Jan 16 '19 at 18:51
  • 1
    @ColinM I'm not sure that constructor injection is directly compatible with ASP.NET Core MVC Action Filters. See [this question](https://stackoverflow.com/questions/39256341/how-to-use-action-filters-with-dependency-injection-in-asp-net-core). Seems you have to either do service locator or TypeFilter. Not sure if that situation has improved or not. I do agree however that IOptions is much better than injecting IConfiguration everywhere. – mason Jan 16 '19 at 18:52
  • @mason You are correct, the filter should be registered in the `services` collection and resolved using `ServiceFilterAttribute(Type)` – ColinM Jan 16 '19 at 18:54
  • That's only the case with filters used as attributes, as attributes cannot be dependency injected, because they're essentially instantiated in place. If you don't need the filter to be an attribute, you may use dependency injection. – Chris Pratt Jan 16 '19 at 18:56
  • I'm thinking it has to be an attribute as I want to globally set ViewBag. – ffejrekaburb Jan 16 '19 at 18:59
  • If you want this to apply to every single action method that gets called (that's what "globally" means) then it doesn't need to be an attribute. See [the documentation](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#filter-scopes-and-order-of-execution). – mason Jan 16 '19 at 19:03
  • @ffejrekaburb that doesn't make sense. Doesn't matter what style you use, `Attribute` or not, you'll have access to the `ActionExecutingContext` regardless because that's a detail attached to the `ActionFilterAttribute` base class. If it's global then use the `AddMvc` overload to configure the `Filters` collection. – ColinM Jan 16 '19 at 19:03
  • If you need an attribute you can use a passive attribute. The filter is global, but it checks to see if the attribute is on the controller or method and only executes if the attribute is present. http://scotthannen.org/blog/2016/03/13/constructor-injection-webapi-actionfilters.html – Scott Hannen Jan 16 '19 at 19:11
  • @ScottHannen Nice example. Have you considered turning that into a Q&A here on SO? Then we can upvote it, and we can also use it to close duplicates in the future, when applicable. – mason Jan 16 '19 at 19:18
  • @mason - great idea, i will do that tonight. – Scott Hannen Jan 16 '19 at 19:19
  • Actually, it looks like ASP.NET Core eliminates the need for the passive attribute approach described in my blog post. It lets you do this: `[ServiceFilter(typeof(YourFilterType))]`. Then, as long as `YourFilterType` is registered with the container it gets executed. – Scott Hannen Jan 16 '19 at 20:16

3 Answers3

0

I would suggest you to use IOptions<T> to retrieve configuration from a file with all of the advantages supported by .Net Core. You can see how to do it here.

Also, to inject it to dependecy injection resolver add services.AddTransient(p => new MyService(mySettings)); to your ConfigureServices() function as transient or scoped or singleton (decide which one suits you better).

If you insist on using IConfiguration to retrieve configuration and solve the problem that you got, you should inject your IConfiguration instance like this services.AddSingleton(Configuration);. Hope this solves your problem.

Hasan
  • 1,243
  • 12
  • 27
  • This doesn't answer, or highlight the need to use `ServiceFilterAttribute(Type)` for applying the filter to a controller or action so it can inject dependencies. – ColinM Jan 17 '19 at 09:09
  • Question is about how to inject IConfiguration or any other way to inject settings from a file (in this case appsettings.json I suppose). So it doesn’t matter so much it’s a filter or any other class that we try to inject configuration. Read the question properly. – Hasan Jan 17 '19 at 09:18
  • I have read the question property, read my comment properly - to inject anything into a filter you must apply it to an action or controller using the `ServiceTypeAttribute` after registering the filter in the services collection, doesn't matter what is being injected into that class. https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#dependency-injection – ColinM Jan 17 '19 at 09:21
0

Based upon some of the feedback here the following is workable by adding to Startup.cs.

services.AddMvc(options => {
   options.Filters.Add(new ERPFilter(Configuration));
}

The url can be factored per the point above to improve performance.

url = ...
services.AddMvc(options => {
   options.Filters.Add(new ERPFilter(url));
}
ffejrekaburb
  • 656
  • 1
  • 10
  • 35
0

To provide an answer based on the comments provided yesterday by others & myself, it is recommended to inject IOptions<T> into your filters, or any other objects which require configuration data to be injected.

You can add your ERP settings to your appSettings.json file like so

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Erp": {
    "Url": "https://localhost"
  }
}

To inject your settings into dependencies you must register it via ConfigureServices, you'll also notice that CompanyFilter is added to the IServiceCollection via AddTransient, this is to allow the ServiceFilterAttribute to resolve it at a later stage and inject any dependencies it the filter has.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.Configure<ErpSettings>(Configuration.GetSection("Erp"));
    services.AddTransient<CompanyFilter>();
}

To apply your filter on your controller action, use ServiceFilterAttribute(Type)`

[HttpGet]
[ServiceFilter(typeof(CompanyFilter))]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { ViewBag.ERPUrl };
}

In the above code you'll see that I am returning ViewBag.ERPUrl, this is because your ComapnyFilter has overrided OnActionExecuting which is executed before the action is invoked, whereas OnActionExecuted is invoked after your action has finished and before the response is returned to the caller.

This is how theCompanyFilter now looks, you'll notice that the constructor now accepts IOptions<ErpSettings>

public class CompanyFilter : ActionFilterAttribute
{
    private readonly ErpSettings erpSettings;
    public CompanyFilter(IOptions<ErpSettings> erpSettings)
    {
        this.erpSettings= erpSettings.Value;

    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (context.Controller is Controller controller)
            controller.ViewBag.ERPUrl = erpSettings.Url;
    }
}

With all of this done, this is the response

API Response

ColinM
  • 2,622
  • 17
  • 29