Constructor Dependency Injection WebApi Attributes

后端 未结 2 745
日久生厌
日久生厌 2020-11-27 21:56

I have been looking around for a non Parameter injection option for the WebApi attributes.

My question is simply whether this is actually possible using Structuremap

相关标签:
2条回答
  • 2020-11-27 22:29

    I struggled with custom action filter providers, without getting it to work for my auth attributes. I also trying out various approaches with constructor and property injection, but did not find a solution that felt nice.

    I finally ended up injecting functions into my attributes. That way, unit tests can inject a function that returns a fake or mock, while the application can inject a function that resolves the dependency with the IoC container.

    I just wrote about this approach here: http://danielsaidi.com/blog/2015/09/11/asp-net-and-webapi-attributes-with-structuremap

    It works really well in my project and solves all problems I had with the other approaches.

    0 讨论(0)
  • 2020-11-27 22:38

    Yes, it is possible. You (like most people) are being thrown by Microsoft's marketing of Action Filter Attributes, which are conveniently put into a single class, but not at all DI-friendly.

    The solution is to break the Action Filter Attribute into 2 parts as demonstrated in this post:

    1. An attribute that contains no behavior to flag your controllers and action methods with.
    2. A DI-friendly class that implements IActionFilter and contains the desired behavior.

    The approach is to use the IActionFilter to test for the presence of the attribute, and then execute the desired behavior. The action filter can be supplied with all dependencies (through the constructor) and then injected when the application is composed.

    IConfigProvider provider = new WebConfigProvider();
    IActionFilter filter = new MaxLengthActionFilter(provider);
    config.Filters.Add(filter);
    

    NOTE: If you need any of the filter's dependencies to have a lifetime shorter than singleton, you will need to use a GlobalFilterProvider as in this answer.

    To wire this up with StructureMap, you will need to return an instance of the container from your DI configuration module. The Application_Start method is still part of the composition root, so you can use the container anywhere within this method and it is still not considered a service locator pattern. Note that I don't show a complete WebApi setup here, because I am assuming you already have a working DI configuration with WebApi. If you need one, that is another question.

    public class DIConfig()
    {
        public static IContainer Register()
        {
            // Create the DI container
            var container = new Container();
    
            // Setup configuration of DI
            container.Configure(r => r.AddRegistry<SomeRegistry>());
            // Add additional registries here...
    
            #if DEBUG
                container.AssertConfigurationIsValid();
            #endif
    
            // Return our DI container instance to the composition root
            return container;
        }
    }
    
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            // Hang on to the container instance so you can resolve
            // instances while still in the composition root
            IContainer container = DIConfig.Register();
    
            AreaRegistration.RegisterAllAreas();
    
            // Pass the container so we can resolve our IActionFilter
            WebApiConfig.Register(GlobalConfiguration.Configuration, container);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
            AuthConfig.RegisterAuth();
        }
    }
    
    public static class WebApiConfig
    {
        // Add a parameter for IContainer
        public static void Register(HttpConfiguration config, IContainer container)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
    
            // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
            // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
            // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
            //config.EnableQuerySupport();
    
            // Add our action filter
            config.Filters.Add(container.GetInstance<IMaxLengthActionFilter>());
            // Add additional filters here that look for other attributes...
        }
    }
    

    The implementation of MaxLengthActionFilter would look something like this:

    // Used to uniquely identify the filter in StructureMap
    public interface IMaxLengthActionFilter : System.Web.Http.Filters.IActionFilter
    {
    }
    
    public class MaxLengthActionFitler : IMaxLengthActionFilter
    {
        public readonly IConfigProvider configProvider;
    
        public MaxLengthActionFilter(IConfigProvider configProvider)
        {
            if (configProvider == null)
                throw new ArgumentNullException("configProvider");
            this.configProvider = configProvider;
        }
    
        public Task<HttpResponseMessage> ExecuteActionFilterAsync(
            HttpActionContext actionContext,
            CancellationToken cancellationToken,
            Func<Task<HttpResponseMessage>> continuation)
        {
            var attribute = this.GetMaxLengthAttribute(filterContext.ActionDescriptor);
            if (attribute != null)
            {
                var maxLength = attribute.MaxLength;
                // Execute your behavior here (before the continuation), 
                // and use the configProvider as needed
    
                return continuation().ContinueWith(t =>
                {
                    // Execute your behavior here (after the continuation), 
                    // and use the configProvider as needed
    
                    return t.Result;
                });
            }
            return continuation();
        }
    
        public bool AllowMultiple
        {
            get { return true; }
        }
    
        public MaxLengthAttribute GetMaxLengthAttribute(ActionDescriptor actionDescriptor)
        {
            MaxLengthAttribute result = null;
    
            // Check if the attribute exists on the action method
            result = (MaxLengthAttribute)actionDescriptor
                .GetCustomAttributes(typeof(MaxLengthAttribute), false)
                .SingleOrDefault();
    
            if (result != null)
            {
                return result;
            }
    
            // Check if the attribute exists on the controller
            result = (MaxLengthAttribute)actionDescriptor
                .ControllerDescriptor
                .GetCustomAttributes(typeof(MaxLengthAttribute), false)
                .SingleOrDefault();
    
            return result;
        }
    }
    

    And, your attribute which should not contain any behavior should look something like this:

    // This attribute should contain no behavior. No behavior, nothing needs to be injected.
    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class MaxLengthAttribute : Attribute
    {
        public MaxLengthAttribute(int maxLength)
        {
            this.MaxLength = maxLength;
        }
    
        public int MaxLength { get; private set; }
    }
    
    0 讨论(0)
提交回复
热议问题