Question

I have a project with several layers - among them the web front end (ASP.NET MVC3) and the service back end (mainly business logic). The project is a few months old, so everything is working as expected. Now I am trying to add a logging aspect to some of the MVC3 controller methods using custom [Log] attributes.

I am using Castle Windsor for dependency injection. To get a logging aspect I leverage Castle DynamicProxy through SNAP. Controllers are being resolved using WindsorControllerFactory from Krzysztof Koźmic's helpful tutorial - but I modified it to look for the default interface for the controller (see below).

In my service layer:

[Log(LoggingLevel.Info)]
public void Save(MyBusinessDto dto)
{
    // business logic and other checks

    this.repository.Save(mbo);
}

In my web front end's IWindsorInstaller for controllers:

private static BasedOnDescriptor FindControllers()
{
    return AllTypes
            .FromThisAssembly()
            .BasedOn<IController>()
            .WithService.DefaultInterface();
}

In my (slightly customized) WindsorControllerFactory that looks for the default interface for the controller:

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
    if (controllerType == null)
    {
        throw new HttpException(404, string.Format(Error404, requestContext.HttpContext.Request.Path));
    }

    string controllerName = controllerType.Name;
    string defaultInterfaceName = 'I' + controllerName;
    Type defaultInterface = controllerType.GetInterface(defaultInterfaceName);

    object controller = this.kernel.Resolve(defaultInterface);

    return (IController)controller;
}

In my controllers:

public class MyBusinessController : MyBusinessControllerBase, IMyBusinessController
{
    [Log(LoggingLevel.Debug)]
    public ActionResult CreateOrUpdate(MyBusinessFormModel fm)
    {
        // Convert form model to data transfer object,
        // perform validation and other checks

        this.service.Save(dto);

        return View(fm);
    }
}

This all works fine in the service project, but in the controllers the methods are not being intercepted.

  • I have confirmed that the WindsorControllerFactory returns proxied controllers.
  • I have confirmed that the controllers have the interceptor registered.
  • I have confirmed that the MasterProxy in SNAP intercepts the controller - but it only intercepts IController.Execute(RequestContext requestContext).

How can I intercept all controller methods that have my [Log] attribute?

Update 1: I have considered using DynamicProxy directly instead of SNAP, but this is secondary to getting it to work for controllers as well.

Update 2+4: It seems that SNAP is missing from github back on github.

Update 3: This is what I see in the Visual Studio debugger when breaking in the WindsorControllerFactory (see above). The inspected controller variable is what is returned to MVC, and it is indeed proxied.

  • controller {Castle.Proxies.IMyBusinessControllerProxy}
    • __interceptors {Castle.DynamicProxy.IInterceptor[1]}
      • [0] {Snap.MasterProxy}
    • __target {My.Business.Web.Controllers.MyBusinessController}
      • service {Castle.Proxies.IMyBusinessServiceProxy}
      • (other contructor injections)
    • MyInjectedProperty {My.Business.Useful.MyOtherType}
Was it helpful?

Solution

In IController GetControllerInstance(...), don't serve interface proxies, serve class proxies with virtual methods.

The user-implemented methods in the controller returned from IController GetControllerInstance(...) will not be accessed through the proxied IMyBusinessController interface, but cast from IController to to the actual class of the controller; for example MyBusinessController. Use a class proxy instead, to make MVC3's cast return the proxy. Also, mark methods as virtual, otherwise the intercepting proxy won't be able to intercept the method calls and check for custom attributes.

In the controllers, add virtual to your methods with attributes:

public class MyBusinessController : MyBusinessControllerBase, IMyBusinessController
{
    [Log(LoggingLevel.Debug)]
    public virtual ActionResult CreateOrUpdate(MyBusinessFormModel fm)
    {
        // Convert form model to data transfer object,
        // perform validation and other checks

        this.service.Save(dto);

        return View(fm);
    }
}

Why is only Execute(...) intercepted? The IController interface only contains Execute(...). Execute is called on the returned controller interface proxy, thus it can be intercepted. But once MVC3's internal ControllerBase.Execute(...) gets the call, it performs the cast to the class it expected from the ControllerFactory.

The problem is similar to this leaking, in that both bypass the interface proxy. I guess it could be solved in a number of ways; perhaps by creating a custom type converter, creating a class proxy from the interface proxy's target in the factory, a clever Windsor configurations etcetera.

Krzysztof Koźmic's IController installer and WindsorControllerFactory should work out of the box. Interface proxies may be recommended in the bigger picture (and they work well until using interceptors in the controllers) but in this case there might be a reason not to go that far, to avoid further side effects.

Thanks to Marius for pointing me in the right direction!

OTHER TIPS

Since DynamicProxy (SNAP uses dynamicproxy) can't intercept non-virtual methods I am guessing that the returned proxy is a derived class of your controller and thus, the non virtual methods are ignored. You either need to make SNAP (don't know how this works though) return an interface proxy with target (your implementation) or simply try to make your controller methods virtual.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top