Asp.Net MVC 5 Custom Action Filter With StructureM

2019-05-06 20:07发布

问题:

i am facing issue in asp.net mvc custom acitonfilte using structuremap in my "LogAttribute" class i have setter dependency injection which is coming null when executing the "OnActionExecuted" Method of my customfilterclass which is "LogAttribute"

my LogAttribute Class code is

    public class LogAttribute : ActionFilterAttribute
{
    public ApplicationDbContext Context { get; set; }
    public string Description { get; set; }
    public LogAttribute(string description)
    {
        Description = description;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var userId = filterContext.HttpContext.User.Identity.GetUserId();
        var user = Context.Users.Find(userId); **i am getting error here the Context is coming null here** 
        Context.Logs.Add(new Log(user, filterContext.ActionDescriptor.ActionName,
                                filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                                Description
                                )
                        );
        Context.SaveChanges();
    }
}

i creat another class from wheren i am passing value to the setter dependency property

    public class StructureMapFilterProvider : FilterAttributeFilterProvider 
{
    private readonly Func<IContainer> _container;
    public StructureMapFilterProvider(Func<IContainer> container)
    {
        _container = container;
    }

    public override IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        var filters = base.GetFilters(controllerContext, actionDescriptor);
        var container = _container();
        foreach (var filter in filters)
        {
            container.BuildUp(filter.Instance);
            yield return filter;
        }
    }
}

my dependency resolver class code is

public class StructureMapDependencyResolver : IDependencyResolver
{
    private readonly Func<IContainer> _containerFactory;
    public StructureMapDependencyResolver(Func<IContainer> containerFactory)
    {
        _containerFactory = containerFactory;
    }
    public object GetService(Type serviceType)
    {
        if (serviceType == null)
        {
            return null;
        }
        var container = _containerFactory();

        return serviceType.IsAbstract || serviceType.IsInterface
            ? container.TryGetInstance(serviceType)
            : container.GetInstance(serviceType);
    }

    public IEnumerable<object> GetServices(Type serviceType)
    {
        return _containerFactory().GetAllInstances(serviceType).Cast<object>();
    }
}

and my global.ascx code is

public class MvcApplication : System.Web.HttpApplication
{

    public IContainer Container
    {
        get
        {
            return (IContainer)HttpContext.Current.Items["_Container"];
        }
        set
        {
            HttpContext.Current.Items["_Container"] = value;
        }
    }

    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);
        DependencyResolver.SetResolver(new StructureMapDependencyResolver(() => Container ?? ObjectFactory.Container));

        ObjectFactory.Configure(cfg =>
           {
               cfg.Scan(Scan =>
               {
                   Scan.TheCallingAssembly();
                   Scan.WithDefaultConventions();
                   Scan.With(new ControllerConfiguration());
               });
               cfg.For<IFilterProvider>().Use(new StructureMapFilterProvider(() => Container ?? ObjectFactory.Container));
               cfg.For<IUserStore<ApplicationUser>>()
                  .Use<UserStore<ApplicationUser>>();
               cfg.For<DbContext>()
                  .Use(() => new ApplicationDbContext());
               cfg.SetAllProperties(x =>
                x.Matching(p =>
                     p.DeclaringType.CanBeCastTo(typeof(ActionFilterAttribute)) &&
                     p.DeclaringType.Namespace.StartsWith("TestingSturctureMap") &&
                     p.PropertyType.IsPrimitive &&
                     p.PropertyType != typeof(string)));

           });
    }

    public void Application_BeginRequest()
    {
        Container = ObjectFactory.Container.GetNestedContainer();
    }

    public void Application_EndRequest()
    {
        Container.Dispose();
        Container = null;
    }
}

回答1:

I installed structuremap and StructureMap.MVC5 NuGet packages.
This added DependencyResolution folder and a StructuremapMvc.cs to App_Start folder.
I then created Filters folder and added an attribute, an action filter and a filter provider classes to it:

My attribute class - LogActionsAttribute - is the easiest. It is just an attribute without any references to any actions:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class LogActionsAttribute : System.Attribute
{
}

Usage on a Controller:

[LogActions]
public class HomeController : System.Web.Mvc.Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Now I needed to add an action filter that would look for this attribute and do something. In my case, log the incoming parameters and execution result. ILog is a custom interface with those 2 methods that are used by this filter.

ControllerLoggerFilter class:

public class ControllerLoggerFilter : IActionFilter
{
    private readonly ILog _log;
    private string _request;

    public ControllerLoggerFilter(ILog log)
    {
        _log = log;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (ApplyBehavior(filterContext))
        {
            ActionExecuting(filterContext);
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (ApplyBehavior(filterContext))
        {
            ActionExecuted(filterContext);
        }
    }

    private void ActionExecuting(ControllerContext c)
    {
        if (c == null || c.HttpContext == null)
            return;

        _log.LogInput(c.HttpContext.Request);
        _request = c.HttpContext.Request;
    }

    private void ActionExecuted(ControllerContext c)
    {
        if (c.HttpContext.Response == null)
            return;

        _log.LogOutput(_request, c.HttpContext.Response);
    }

    private static bool ApplyBehavior(ActionExecutingContext filterContext)
    {
        return
            filterContext.ActionDescriptor.ControllerDescriptor
.GetCustomAttributes(typeof (LogActionsAttribute),
                false).Any();
    }

    private static bool ApplyBehavior(ActionExecutedContext filterContext)
    {
        return
            filterContext.ActionDescriptor.ControllerDescriptor
.GetCustomAttributes(typeof (LogActionsAttribute),
                false).Any();
    }
}

I now had to tell MVC that this filter needed to be in the pipeline. Since it is built by structuremap container, I needed a custom IFilterProvider:

public class MvcInjectableFilterProvider : IFilterProvider
{
    private readonly IEnumerable<Filter> _list;

    public MvcInjectableFilterProvider(IContainer container)
    {
        _list = GetContainerFilters(container);
    }

    public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
    {
        return _list;
    }

    private static IEnumerable<Filter> GetContainerFilters(IContainer container)
    {
        return
            container.GetAllInstances<IActionFilter>()
                .Select(instance => new Filter(instance, FilterScope.Action, null));
    }
}

Last thing to do: register my classes in my IoC.cs.
This is the only place I had to modify in the files that were created when I installed NuGet packages.

public static class IoC {
    public static IContainer Initialize() {
        return new Container(c =>
        {
            c.AddRegistry<DefaultRegistry>();
            // added these lines:
            c.For<ILog>().Use<Log>();
            c.For<IActionFilter>().Use<ControllerLoggerFilter>();
            c.For<IFilterProvider>().Use<MvcInjectableFilterProvider>();
        });
    }
}

Sources:
Mark Seemann (and whole bunch other places)
Javier G. Lozano
eric.sowell
K. Scott Allen



回答2:

Dear friends i solve my question it was just [SetterProperty] missing from the setter property in my LogAttribute Class

    public class LogAttribute : ActionFilterAttribute
{
    [SetterProperty]
    public ApplicationDbContext Context { get; set; }
    public string Description { get; set; }
    public LogAttribute(string description)
    {
        Description = description;
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        var userId = filterContext.HttpContext.User.Identity.GetUserId();
        var user = Context.Users.Find(userId);
        Context.Logs.Add(new Log(user, filterContext.ActionDescriptor.ActionName,
                                filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                                Description
                                )
                        );
        Context.SaveChanges();
    }
}

now its working :)



回答3:

Very bad idea to have setter properties in ActionFilters! The same actionFilter instance could be shared among multiple requests. As result different requests (threads) will obtain the same reference to ApplicationDbContext.

Are ActionFilterAttributes reused across threads? How does that work?