简体   繁体   中英

Does ASP.NET MVC 3 cache Filters?

I have an UnitOfWork attribute, something like this:

public class UnitOfWorkAttribute : ActionFilterAttribute
{
    public IDataContext DataContext { get; set; }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {            
        if (filterContext.Controller.ViewData.ModelState.IsValid)
        {
            DataContext.SubmitChanges();
        }

        base.OnActionExecuted(filterContext);
    }
}

As you can see, it has DataContext property, which is injected by Castle.Windsor. DataContext has lifestyle of PerWebRequest - meaning single instance reused for each request.

Thing is, that from time to time I get DataContext is Disposed exception in this attribute and I remember that ASP.NET MVC 3 tries to cache action filters somehow, so may that causes the problem?

If it is so, how to solve the issue - by not using any properties and trying to use ServiceLocator inside method?

Is it possible to tell ASP.NET MVC to not cache filter if it does cache it?

I would strongly advice against using such a construct. For a couple of reasons:

  1. It is not the responsibility of the controller (or an on the controller decorated attribute) to commit the data context.
  2. This would lead to lots of duplicated code (you'll have to decorate lots of methods with this attribute).
  3. At that point in the execution (in the OnActionExecuted method) whether it is actually safe to commit the data.

Especially the third point should have drawn your attention. The mere fact that the model is valid, doesn't mean that it is okay to submit the changes of the data context. Look at this example:

[UnitOfWorkAttribute]
public View MoveCustomer(int customerId, Address address)
{
    try
    {
        this.customerService.MoveCustomer(customerId, address);
    }
    catch { }

    return View();
}

Of course this example is a bit naive. You would hardly ever swallow each and every exception, that would just be plain wrong. But what it does show is that it is very well possible for the action method to finish successfully, when the data should not be saved.

But besides this, is committing the transaction really a problem of MVC and if you decide it is, should you still want to decorate all action methods with this attribute. Wouldn't it be nicer if you just implement this without having to do anything on the Controller level? Because, which attributes are you going to add after this? Authorization attributes? Logging attributes? Tracing attributes? Where does it stop?

What you can try instead is to model all business operations that need to run in a transaction, in a way that allows you to dynamically add this behavior, without needing to change any existing code, or adding new attributes all over the place. A way to do this is to define an interface for these business operations. For instance:

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

Using this interface, your controller would look like this:

private readonly ICommandHandler<MoveCustomerCommand> handler;

// constructor
public CustomerController(
    ICommandHandler<MoveCustomerCommand> handler)
{
    this.handler = handler;
}

public View MoveCustomer(int customerId, Address address)
{
    var command = new MoveCustomerCommand
    {
        CustomerId = customerId,
        Address = address,
    };

    this.handler.Handle(command);

    return View();
}

For each business operation in the system you define a class (a DTO and Parameter Object ). In the example the MoveCustomerCommand class. This class contains merely the data. The implementation is defined in a class that implementation of the ICommandHandler<MoveCustomerCommand> . For instance:

public class MoveCustomerCommandHandler
    : ICommandHandler<MoveCustomerCommand>
{
    private readonly IDataContext context;

    public MoveCustomerCommandHandler(IDataContext context)
    {
        this.context = context;
    }

    public void Handle(MoveCustomerCommand command)
    {
        // TODO: Put logic here.
    }
}

This looks like an awful lot of extra useless code, but this is actually really useful (and if you look closely, it isn't really that much extra code anyway).

Interesting about this is that you can now define one single decorator that handles the transactions for all command handlers in the system:

public class TransactionalCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly IDataContext context;
    private readonly ICommandHandler<TCommand> decoratedHandler;

    public TransactionalCommandHandlerDecorator(IDataContext context,
        ICommandHandler<TCommand> decoratedHandler)
    {
        this.context = context;
        this.decoratedHandler = decoratedHandler;
    }

    public void Handle(TCommand command)
    {
        this.decoratedHandler.Handle(command);

        this.context.SubmitChanges();
    }
}

This is not much more code than your UnitOfWorkAttribute , but the difference is that this handler can be wrapped around any implementation and injected into any controller, without the controller to know about this. And directly after executing a command is really the only safe place where you actually know whether you can save the changes or not.

You can find more information about this way of designing your application in this article: Meanwhile... on the command side of my architecture

Today I've half accidently found the original issue of the problem.
As it is seen from the question, filter has property, that is injected by Castle.Windsor , so those, who use ASP.NET MVC know, that for that to work you need to have IFilterProvider implementation, which would be able to use IoC container for dependency injection.

So I've started to look at it's implementation, and noticed, that it is derrived from FilterAttributeFilterProvider and FilterAttributeFilterProvider has constructor:

public FilterAttributeFilterProvider(bool cacheAttributeInstances)

So you can control to cache or not your attribute instances.

After disabling this cache, site was blown with NullReferenceExceptions , so I was able to find one more thing, that was overlooked and caused undesired side effects.

Thing was, that original filter was not removed, after we added Castle.Windsor filter provider. So when caching was enabled, IoC filter provider was creating instances and default filter provider was reusing them and all dependency properties were filled with values - that was not clearly noticeable, except the fact, that filters were running twice, after caching was disabled, default provider was needed to create instances by it self, so dependency properties were unfilled, that's why NullRefereceExceptions occurred.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM