SNAP - Keeping aspects in a container

Hi, this is my first blog post in English, as I want to share it with an English-speaking audience too. Today I want to speak about aspect oriented framework named SNAP, build simple logging aspect, then describe why it is bad to let SNAP manage aspects instances on its own, and how it restricts us as a developers in building complex aspects. Then I show how I've solved the problem described.

SNAP - Simple .NET Aspect-Oriented Programming

Recently I've been using SNAP, a yet another Aspect Oriented Programming framework for .NET. I've had a positive experience, and here are some key points to describe it.

  1. Does not use code weaving, does not rewrite IL code to get interception mechanism to work. Instead uses Castle.DynamicProxy library features like subclassing, interface proxying.
  2. Ability to intercept only selected methods, instead of intercepting all methods of a given class.
  3. Integrates with a favorite DI/IoC container. Currently five implementations are supported: Autofac, Castle.Windsor, StructureMap, NInject, LinFu.
  4. Provides unified model to program aspects regardless selected DI/IoC container implementation.
  5. Lightweight and simple.

Sounds good, isn't it? If you are new to SNAP, I'd recommend to take a look at it. Here is a home page, and here is a source code, and nuget packages are also there.

Snap (Simple .NET Aspect-Oriented Programming) was conceived out of a recurring need for flexible AOP in the wake of PostSharp's metamorphosis into a commercial product.

Not to be mistaken with a code weaver (i.e. a compile-time tool), Snap is a fully run-time executed library. Snap aims to pair with your Inversion of Control (IoC) container of choice to deliver a powerful aspect-oriented programming model.

Logging aspect implementation

To show SNAP in a work, lets consider a simple scenario. Assume, we have a class CalculateRouteCommandHandler, which calculates a route between given two points A and B.
public class CalculateRouteCommandHandler
{
    public virtual void HandleCommand(CalculateRouteCommand command)
    {
        try
        {
            var intermediateroutes = CalculateIntermediateRoutes();
            foreach (var intermediateroute in intermediateroutes)
            {
                CalculateDistance(intermediateroute);
                CalculateTransport(intermediateroute);
                FindAlternatives(intermediateroute);
            }
        }
    }
}

Next, we want to log command started and completed events. Logging is a cross-cutting concern, so being aspect-oriented we need to encapsulate logging functionality in the form of aspect. From a SNAP implementation viewpoint, aspect is a class which inherits from the MethodInterceptor base class, and decorates target class with a custom behavior.

public class TraceCommand : MethodInterceptor
{
    public override void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute)
    {
        var command = (Command) invocation.Arguments.First();
        var targetAttribute = (TraceCommandAttribute) attribute;

        // trace command start
        if(targetAttribute.TraceStart)
        {
           Logger.Info("Command started: {0}".FormatString(command));
        }

        // call target method
        invocation.Proceed();

        // trace command completion
        if(targetAttribute.TraceComplete)
        {
           Logger.Info("Command completed: {0}".FormatString(command));
        }
    }
}

Next we need to apply the aspect to the business code. We need an attribute class, and then apply it to the target method.

public class TraceCommandAttribute : MethodInterceptAttribute
{
    // these are settings which influence aspect behavior
    public bool TraceStart { get; set; }
    public bool TraceComplete { get; set; }
}
// do not trace command start event, just command completion one
[TraceCommand(TraceStart = false)]
public virtual void HandleCommand(CalculateRouteCommand command)
{
    // business logic goes here
}

Next step is to configure SNAP framework to let it know about our aspects and attributes, and what components do we want to intercept.

// using Autofac as a container implementation
var builder = new ContainerBuilder();

// configure aspects
SnapConfiguration.For(new AutofacAspectContainer(builder)).Configure(c =>
{
    c.IncludeNamespace("MyCommands.*");
    c.Bind<TraceCommand>().To<TraceCommandAttribute>();
});

// configure application components in container
builder.RegisterType<CalculateRouteCommandHandler>().As<ICommandHandler<CalculateRouteCommand>>();

And that's all you need to get SNAP to work.

Aspect management problem

So far so good. But you say: "Hey, this is a very simple scenario. Show us something more practical.". Ok. Let's try to identify some SNAP pitfalls with a bit more difficult scenario.

LoggingAspect uses static Logger class to get an access to logging functionality. I don't like logging to be exposed via static Logger class. I think, you are too. Instead I redesign it into ILoggingService service.
public interface ILoggingService
{
    void Debug(string message);
    void Info(string message);
    void Warn(string message);
    void Error(string message, Exception failure);
    void Fatal(string message, Exception failure);
}
Furthermore, I register ILoggingService in a my favorite container.
builder.RegisterType<LoggingService>().As<ILoggingService>();

In fact, aspect could be considered as a regular component, and rare component lives in an isolation. Usually, it has external dependencies. So now I'm going to make some refactoring in the LoggerAspect class, and switch using static Logger class to ILoggingService instead.

public class TraceCommand : MethodInterceptor
{
    private ILoggingService _loggingService;

    // ILoggingService is constructor injected in TraceCommand aspect class
    public TraceCommand(ILoggingService loggingService)
    {
        _loggingService = loggingService;
    }
        
    public override void InterceptMethod(IInvocation invocation, MethodBase method, Attribute attribute)
    {
        var command = (Command) invocation.Arguments.First();
        _loggingService.Info("Command started: {0}".FormatString(command));
        // other aspect code goes here
    }
}

The last step is to configure our new aspect with an external dependency in a SNAP. But here we run into a problem. SNAP allows to specify aspect type only, not the instance. There is no way to supply pre-built LoggerAspect instance to a SNAP.

SnapConfiguration.For(new AutofacAspectContainer(builder)).Configure(c =>
{
    c.IncludeNamespace("MyCommands.*");
    // no way to do it
    c.Bind(new TraceCommand(new LoggingService)).To<TraceCommandAttribute>();
});

To summarize the problem cause, SNAP manages aspect instances on his own. It creates a single instance for a given aspect type, and uses that instance for all interceptions. In other words, aspect is a singleton, and it is created by SNAP internally, without even letting developer to supply user-defined aspect instance. This is a significant problem, which impose several restrictions on how aspects are designed.

  • Aspect should have parameterless constructor.
  • External dependencies could not be injected in the aspect class. Aspect class can only refer to globally visible services (like singletons or static classes).
  • Another option is to make aspect class to be autonomous and let encapsulate all stuff required to get it to work. This way, we could end up with an enormously large aspect class with many responsibilities.

Keep and resolve aspects from container

So, now I want to share my solution to the problem described above. Recently I've implemented new feature, which allows developers to keep aspects in a container too, and let SNAP know whether aspect is managed by a container or should be instantiated by a SNAP (which is the default behavior). So, the idea is to let container manage components (aspects), and SNAP - to bring aspect-orientation and intercept method calls.

All aspects are managed by container

So, assume you have several aspects, which are managed by a container. Here is how you could tell SNAP about it.

[Test]
public void Autofac_Supports_Resolving_All_Aspects_From_Container()
{
    var builder = new ContainerBuilder();

    SnapConfiguration.For(new AutofacAspectContainer(builder)).Configure(c =>
    {
        c.IncludeNamespace("SnapTests.*");
        c.Bind<FirstInterceptor>().To<FirstAttribute>();
        c.Bind<SecondInterceptor>().To<SecondAttribute>();
        c.AllAspects().KeepInContainer();
    });

    builder.Register(r => new OrderedCode()).As<IOrderedCode>();
    builder.Register(r => new FirstInterceptor("first_kept_in_container"));
    builder.Register(r => new SecondInterceptor("second_kept_in_container"));

    using (var container = builder.Build())
    {
        var orderedCode = container.Resolve<IOrderedCode>();
        orderedCode.RunInOrder();

        // both interceptors are resolved from container
        CollectionAssert.AreEquivalent(
            OrderedCode.Actions,
            new[] { "first_kept_in_container", "second_kept_in_container" });
    }
}
This way SNAP uses a container to resolve aspect instances, whenever interception occurs.

Only subset of aspects are managed by container

In this scenario, some of your aspects are managed by container, while other are not. Once again, we need to tell SNAP about it.

[Test]
public void Autofac_Supports_Resolving_Only_Selected_Aspects_From_Container()
{
    var builder = new ContainerBuilder();

    SnapConfiguration.For(new AutofacAspectContainer(builder)).Configure(c =>
    {
        c.IncludeNamespace("SnapTests.*");
        c.Bind<FirstInterceptor>().To<FirstAttribute>();
        c.Bind<SecondInterceptor>().To<SecondAttribute>();
        c.Aspects(typeof(FirstInterceptor)).KeepInContainer();
    });

    builder.Register(r => new OrderedCode()).As<IOrderedCode>();
    builder.Register(r => new FirstInterceptor("first_kept_in_container"));
    builder.Register(r => new SecondInterceptor("second_kept_in_container"));

    using (var container = builder.Build())
    {
        var orderedCode = container.Resolve<IOrderedCode>();
        orderedCode.RunInOrder();

        // first interceptor is resolved from container, while second one is via new() 
        CollectionAssert.AreEquivalent(
            OrderedCode.Actions,
            new[] { "first_kept_in_container", "Second" });
    }
}
When nothing is configured explicitly, SNAP uses its default behavior and instantiates aspect instances on its own.

Conclusion

As you can see, it becomes possible to keep aspects in a container too, not only application components. These changes are already pulled into the main SNAP repository. So you can already use this IMO valuable feature.

Stay tuned. Happy aspecting.

1 comments:

Anonymous said...

Shooting Star Casino Review | $500 Match Bonus + Free Spins
First Look. There are a few bonus rounds on the online casino floor. Here you'll find a list of all available games. First Look. There are a few bonus rounds on the online casino floor. Here you'll find a 제왕 카지노 list of all available games. First Look. There are a few bonus rounds on the online casino floor. Here you'll find 메리트 카지노 a list of septcasino all available games. First Look. There are a few bonus rounds on the online casino floor.

Post a Comment