Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autofac Interception with custom attributes

I've been looking for a specific solution for AOP logging. I need a interception that makes possible do something like this:

[MyCustomLogging("someParameter")]

The thing is, I saw examples in others DI frameworks that makes this possible. But my project is already using Autofac for DI and I don't know if is a good idea mix up with Unity (for example). In Autofac.extras.dynamiclibrary2 the class InterceptAttribute is sealed.

Anyone has an idea for this problem?

Ps.: I would be satisfied with this:

[Intercept(typeof(MyLoggingClass), "anotherParameter"]
like image 693
Rafael P. Miranda Avatar asked Dec 14 '22 14:12

Rafael P. Miranda


1 Answers

Although the use of attributes to enrich types with metadata to feed cross-cutting concerns with data to use isn't bad, the use of attributes to mark classes or methods to run some cross-cutting concern usually is.

Marking code with the attribute like you shown has some serious downsides:

  1. It makes your code dependent on the used interception library, making code harder to change and makes it harder to replace external libraries. The number of dependencies the core of your application has with external libraries should be kept to an absolute minimum. It would be ironic if your code was littered with dependencies upon the Dependency Injection library; the tool that is used to allow minimizing external dependencies and increasing loose coupling.
  2. To apply cross-cutting concerns to a wide range of classes (which is what you usually want to do), you will have to go through the complete code base to add or remove attributes from methods. This is time consuming and error prone. But even worse, making sure that aspects are run in a particular order is hard with attributes. Some frameworks allow you to specify an order to the attribute (using some sort of Order property), but changing the order means making sweeping changes through the code to change the Order of the attributes. Forgetting one will cause bugs. This is a violation of the Open/closed principle.
  3. Since the attribute references the aspect class (in your example typeof(MyLoggingClass)), this makes your code still statically dependent on the cross-cutting concern. Replacing the class with another will again cause you to do sweeping changes to your code base, and keeping the hard dependency makes it much harder to reuse code or decide at runtime or time of deployment whether the aspect should be applied or not. In many cases, you can't have this dependency from your code to the aspect, because the code lives in a base library, while the aspect is specific to the application framework. For instance, you might have the same business logic that runs both in a web application and a Windows service. When ran in a web application, you want to log in a different way. In other words, you are violating the Dependency inversion principle.

I therefore consider applying attributes this way bad practice. Instead of using attributes like this, apply cross-cutting concerns transparently, either using interceptions or decorators. Decorators are my preferred approach, because their use is much cleaner, simpler, and therefore more maintainable. Decorators can be written without having to take a dependency at any external library and they can therefore be placed in any suitable place in your application. Downside of decorators however is that it's very cumbersome to write and apply them in case your design isn't SOLID, DRY and you're not following the Reused abstraction principle.

But if you use the right application design using SOLID and message based patterns, you'll find out that applying cross-cutting concerns such as logging is just a matter of writing a very simple decorator such as:

public class LoggingCommandHandlerDecorator<T> : ICommandHandler<T>
{
    private readonly ILogger logger;
    private readonly ICommandHandler<T> decoratee;
    public LoggingCommandHandlerDecorator(ILogger logger, ICommandHandler<T> decoratee) {
        this.logger = logger;
        this.decoratee = decoratee;
    }

    public void Handle(T command) {
        this.logger.Log("Handling {0}. Data: {1}", typeof(T).Name,
            JsonConvert.SerializeObject(command));
        this.decoratee.Handle(command);
    }
}

Without a proper design, you can still use interception (without attributes), because interception allows you to 'decorate' any types that seem to have no relationship in code (share no common interface). Defining which types to intercept and which not can be cumbersome, but you will usually still be able to define this in one place of the application, thus without having to make sweeping changes throughout the code base.

Side node. As I said, using attributes to describe pure metadata is fine and preferable. For instance, take some code that is only allowed to run for users with certain permissions. You can mark that code as follows:

[Permission(Permissions.Crm.ManageCompanies)]
public class BlockCompany : ICommand {
    public Guid CompanyId;
}

This attribute does not describe what aspects are run, nor does it reference any types from an external library (the PermissionAttribute is something you can (and should) define yourself), or any AOP-specific types. It solely enriches the code with metadata.

In the end, you obviously want to apply some cross-cutting concern that checks whether the current user has the right permissions, but the attribute doesn't force you into a specific direction. With the attribute above, I could imagine the decorator to look as follows:

public class PermissionCommandHandlerDecorator<T> : ICommandHandler<T>
{
    private static readonly Guid requiredPermissionId =
        typeof(T).GetCustomAttribute<PermissionAttribute>().PermissionId;

    private readonly IUserPermissionChecker checker;
    private readonly ICommandHandler<T> decoratee;

    public PermissionCommandHandlerDecorator(IUserPermissionChecker checker,
        ICommandHandler<T> decoratee) {
        this.checker = checker;
        this.decoratee = decoratee;
    }

    public void Handle(T command) {
        this.checker.CheckPermission(requiredPermissionId);
        this.decoratee.Handle(command);
    }
}
like image 173
Steven Avatar answered Dec 29 '22 13:12

Steven