ninjectaopninject-interceptionninject-conventions

Ninject Conventions and Interception


I want to decorate my services with attributes for interception, and then have conventions based binding set the interceptors up for me. I don't want my attributes to inherit from the interception attributes... if I can avoid it.

For example, I have the following class:

[Log]
public class SomeClassToLog
{
    public void DoSomething() { ... }
}

I understand I can bind this as follows:

var kernel = new StandardKernel();
            kernel.Bind(x => x.FromAssembliesMatching("SomeProject.*")
                              .SelectAllClasses()
                              .WithAttribute(typeof(LogAttribute))
                              .BindToSelf().Configure(syntax => syntax.Intercept().With(LogInterceptor)));

How can I do this with different combinations of attributes and interceptors? For example:

If I have Log and Authorize attributes I would have to configure 3 sets of bindings? (1 for log without authorize, 1 for authorize without log and one for both log and authorize).

Updated: While I couldn't find a solution based on my original question parameters, I did stumble upon a similar question which lead me to the solution I ended up going with. Here is the source code:

Notes: Common.Interception.Interceptors namespace is in an assembly which has a reference to Ninject.Extensions.Interception (and all of its required dependencies). My attributes are defined in a separate assembly with no dependencies of their own.

MiscExtensions.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Common.Interception.Interceptors
{
    // TODO: Remove the dependence on these eventually

    /// <summary>
    /// A bundle of extension methods which I needed to borrow from ninject source since they were internal:
    /// Ninject.Extensions.Interception.Infrastructure.Language
    /// ExtensionsForIEnumerable
    /// ExtensionsForMethodInfo
    /// ExtensionsForICustomAttributeProvider  
    /// </summary> 
    internal static class MiscExtensions
    {
        /// <summary>
        /// Converts all of the items in the specified series using the specified converter.
        /// </summary>
        /// <typeparam name="TInput">The type of items contained in the input list.</typeparam>
        /// <typeparam name="TOutput">The type of items to return.</typeparam>
        /// <param name="items">The series of items to convert.</param>
        /// <param name="converter">The converter to use to convert the items.</param>
        /// <returns>A list of the converted items.</returns>
        public static IEnumerable<TOutput> Convert<TInput, TOutput>(this IEnumerable<TInput> items,
                                                                     Func<TInput, TOutput> converter)
        {
            return items.Select(converter);
        }

        /// <summary>
        /// Skips the last items where the count of skipped items is given by count.
        /// </summary>
        /// <typeparam name="T">The type of the enumerable.</typeparam>
        /// <param name="source">The source.</param>
        /// <param name="count">The count of skipped items.</param>
        /// <returns>An enumerable that skippes the last items from the source enumerable.</returns>
        public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
        {
            var enumerator = source.GetEnumerator();
            var items = new Queue<T>();

            while (enumerator.MoveNext())
            {
                if (count-- <= 0)
                {
                    yield return items.Dequeue();
                }

                items.Enqueue(enumerator.Current);
            }
        }

        private const BindingFlags DefaultBindingFlags =
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Instance;

        public static PropertyInfo GetPropertyFromMethod(this MethodInfo method, Type implementingType)
        {
            if (!method.IsSpecialName)
            {
                return null;
            }

            var isGetMethod = method.Name.Substring(0, 3) == "get";
            var returnType = isGetMethod ? method.ReturnType : method.GetParameterTypes().Last();
            var indexerTypes = isGetMethod ? method.GetParameterTypes() : method.GetParameterTypes().SkipLast(1);

            return implementingType.GetProperty(method.Name.Substring(4), DefaultBindingFlags, null, returnType, indexerTypes.ToArray(), null);
        }

        public static PropertyInfo GetPropertyFromMethod(this MethodInfo method)
        {
            if (!method.IsSpecialName)
            {
                return null;
            }
            return method.DeclaringType.GetProperty(method.Name.Substring(4), DefaultBindingFlags);
        }

        /// <summary>
        /// Gets the types of the parameters of the method.
        /// </summary>
        /// <param name="method">The method in question.</param>
        /// <returns>An array containing the types of the method's parameters.</returns>
        public static IEnumerable<Type> GetParameterTypes(this MethodBase method)
        {
            return method.GetParameters().Convert(p => p.ParameterType);
        }

        /// <summary>
        /// Gets the method handle of either the method or its generic type definition, if it is
        /// a generic method.
        /// </summary>
        /// <param name="method">The method in question.</param>
        /// <returns>The runtime method handle for the method or its generic type definition.</returns>
        public static RuntimeMethodHandle GetMethodHandle(this MethodBase method)
        {
            var mi = method as MethodInfo;

            if (mi != null &&
                 mi.IsGenericMethod)
            {
                return mi.GetGenericMethodDefinition().MethodHandle;
            }
            return method.MethodHandle;
        }

        /// <summary>
        /// Gets the first attribute of a specified type that decorates the member.
        /// </summary>
        /// <typeparam name="T">The type of attribute to search for.</typeparam>
        /// <param name="member">The member to examine.</param>
        /// <returns>The first attribute matching the specified type.</returns>
        public static T GetOneAttribute<T>(this ICustomAttributeProvider member)
            where T : Attribute
        {
            var attributes = member.GetCustomAttributes(typeof(T), true) as T[];

            return (attributes == null) ||
                   (attributes.Length == 0)
                       ? null
                       : attributes[0];
        }

        /// <summary>
        /// Gets the first attribute of a specified type that decorates the member.
        /// </summary>
        /// <param name="member">The member to examine.</param>
        /// <param name="type">The type of attribute to search for.</param>
        /// <returns>The first attribute matching the specified type.</returns>
        public static object GetOneAttribute(this ICustomAttributeProvider member, Type type)
        {
            object[] attributes = member.GetCustomAttributes(type, true);

            return (attributes == null) ||
                   (attributes.Length == 0)
                       ? null
                       : attributes[0];
        }

        /// <summary>
        /// Gets an array of attributes matching the specified type that decorate the member.
        /// </summary>
        /// <typeparam name="T">The type of attribute to search for.</typeparam>
        /// <param name="member">The member to examine.</param>
        /// <returns>An array of attributes matching the specified type.</returns>
        public static T[] GetAllAttributes<T>(this ICustomAttributeProvider member)
            where T : Attribute
        {
            return member.GetCustomAttributes(typeof(T), true) as T[];
        }

        /// <summary>
        /// Gets an array of attributes matching the specified type that decorate the member.
        /// </summary>
        /// <param name="member">The member to examine.</param>
        /// <param name="type">The type of attribute to search for.</param>
        /// <returns>An array of attributes matching the specified type.</returns>
        public static object[] GetAllAttributes(this ICustomAttributeProvider member, Type type)
        {
            return member.GetCustomAttributes(type, true);
        }

        /// <summary>
        /// Determines whether the member is decorated with one or more attributes of the specified type.
        /// </summary>
        /// <typeparam name="T">The type of attribute to search for.</typeparam>
        /// <param name="member">The member to examine.</param>
        /// <returns><see langword="True"/> if the member is decorated with one or more attributes of the type, otherwise <see langword="false"/>.</returns>
        public static bool HasAttribute<T>(this ICustomAttributeProvider member)
            where T : Attribute
        {
            return member.IsDefined(typeof(T), true);
        }

        /// <summary>
        /// Determines whether the member is decorated with one or more attributes of the specified type.
        /// </summary>
        /// <param name="member">The member to examine.</param>
        /// <param name="type">The type of attribute to search for.</param>
        /// <returns><see langword="True"/> if the member is decorated with one or more attributes of the type, otherwise <see langword="false"/>.</returns>
        public static bool HasAttribute(this ICustomAttributeProvider member, Type type)
        {
            return member.IsDefined(type, true);
        }

        /// <summary>
        /// Determines whether the member is decorated with an attribute that matches the one provided.
        /// </summary>
        /// <typeparam name="T">The type of attribute to search for.</typeparam>
        /// <param name="member">The member to examine.</param>
        /// <param name="attributeToMatch">The attribute to match against.</param>
        /// <returns><see langword="True"/> if the member is decorated with a matching attribute, otherwise <see langword="false"/>.</returns>
        public static bool HasMatchingAttribute<T>(this ICustomAttributeProvider member, T attributeToMatch)
            where T : Attribute
        {
            T[] attributes = member.GetAllAttributes<T>();

            if ((attributes == null) ||
                 (attributes.Length == 0))
            {
                return false;
            }

            return attributes.Any(attribute => attribute.Match(attributeToMatch));
        }
    }
}

AlternateInterceptorRegistrationStrategy.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Ninject;
using Ninject.Components;
using Ninject.Extensions.Interception;
using Ninject.Extensions.Interception.Advice;
using Ninject.Extensions.Interception.Planning.Directives;
using Ninject.Extensions.Interception.Registry;
using Ninject.Planning;
using Ninject.Planning.Strategies;

namespace Common.Interception.Interceptors
{
    /// <summary>
    /// This is a derivation of InterceptorRegistrationStrategy from Ninject.Extensions.Interception.Planning.Strategies, merged with 
    /// http://stackoverflow.com/questions/6386461/ninject-intercept-any-method-with-certain-attribute
    /// </summary>
    public class AlternateInterceptorRegistrationStrategy<TAttribute, TInterceptor> : NinjectComponent, IPlanningStrategy
        where TAttribute : Attribute
        where TInterceptor : IInterceptor
    {
        protected const BindingFlags DefaultBindingFlags =
            BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Instance;

        public AlternateInterceptorRegistrationStrategy(IAdviceFactory adviceFactory, IAdviceRegistry adviceRegistry, IKernel kernel)
        {
            AdviceFactory = adviceFactory;
            AdviceRegistry = adviceRegistry;
            Kernel = kernel;
        }

        public IKernel Kernel { get; set; }
        public IAdviceFactory AdviceFactory { get; set; }
        public IAdviceRegistry AdviceRegistry { get; set; }

        public virtual void Execute(IPlan plan)
        {
            IEnumerable<MethodInfo> candidates = GetCandidateMethods(plan.Type);

            RegisterClassInterceptors(plan.Type, plan, candidates);

            foreach (MethodInfo method in candidates)
            {
                PropertyInfo property = method.GetPropertyFromMethod(plan.Type);
                ICustomAttributeProvider provider = (ICustomAttributeProvider)property ?? method;
                TAttribute[] attributes = provider.GetAllAttributes<TAttribute>();

                if (attributes.Length == 0)
                {
                    continue;
                }

                RegisterMethodInterceptor(plan.Type, method);

                // Indicate that instances of the type should be proxied.
                if (!plan.Has<ProxyDirective>())
                {
                    plan.Add(new ProxyDirective());
                }
            }
        }

        protected virtual void RegisterClassInterceptors(Type type, IPlan plan, IEnumerable<MethodInfo> candidates)
        {
            var attributes = type.GetAllAttributes<TAttribute>();

            if (attributes.Length == 0)
            {
                return;
            }

            foreach (MethodInfo method in candidates)
            {
                PropertyInfo property = method.GetPropertyFromMethod(type);
                ICustomAttributeProvider provider = (ICustomAttributeProvider) property ?? method;

                var config = Kernel.Get<IInterceptorConfig>();

                if (config.DoNotInterceptAttribute == null)
                {
                    // A "do not intercept" attribute wasn't defined in the config, so go ahead and register
                    RegisterMethodInterceptor(type, method);
                }
                else if (!provider.IsDefined(config.DoNotInterceptAttribute, true))
                {
                    // The method wasn't decorated with the "do not intercept" attribute, so go ahead and register
                    RegisterMethodInterceptor(type, method);
                }
            }

            if (!plan.Has<ProxyDirective>())
            {
                plan.Add(new ProxyDirective());
            }
        }

        protected virtual void RegisterMethodInterceptor(Type type, MethodInfo method)
        {
            IAdvice advice = AdviceFactory.Create(method);

            advice.Callback = request => request.Context.Kernel.Get<TInterceptor>();

            var config = Kernel.TryGet<IInterceptorConfig>();
            if (config != null)
            {
                advice.Order = config.GetOrder<TInterceptor>();
            }

            AdviceRegistry.Register(advice);
        }

        protected virtual IEnumerable<MethodInfo> GetCandidateMethods(Type type)
        {
            MethodInfo[] methods = type.GetMethods(DefaultBindingFlags);

            return methods.Where(ShouldIntercept);
        }

        protected virtual bool ShouldIntercept(MethodInfo methodInfo)
        {
            return methodInfo.DeclaringType != typeof(object) &&
                   !methodInfo.IsPrivate;// &&
            //!methodInfo.IsFinal;
        }
    }
}

IInterceptorConfig.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ninject.Extensions.Interception;

namespace Common.Interception.Interceptors
{
    public interface IInterceptorConfig
    {
        IInterceptorConfig SpecifyOrder<TInterceptor>(int order) where TInterceptor : IInterceptor;

        IInterceptorConfig SpecifyDoNotInterceptAttribute<TAttribute>() where TAttribute : Attribute;

        int GetOrder<TInterceptor>() where TInterceptor : IInterceptor;

        Type DoNotInterceptAttribute { get; }
    }
}

InterceptorConfig.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ninject.Extensions.Interception;

namespace Common.Interception.Interceptors
{
    public class InterceptorConfig : IInterceptorConfig
    {
        private readonly Dictionary<Type, int> _orderDictionary = new Dictionary<Type, int>();

        public IInterceptorConfig SpecifyOrder<TInterceptor>(int order) where TInterceptor : IInterceptor
        {
            _orderDictionary.Add(typeof(TInterceptor), order);
            return this;
        }

        public IInterceptorConfig SpecifyDoNotInterceptAttribute<TAttribute>() where TAttribute : Attribute
        {
            DoNotInterceptAttribute = typeof(TAttribute);
            return this;
        }

        public int GetOrder<TInterceptor>() where TInterceptor : IInterceptor
        {
            return _orderDictionary[typeof(TInterceptor)];
        }

        public Type DoNotInterceptAttribute { get; private set; }
    }
}

TraceInterceptor.cs - just a sample interceptor

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Ninject.Extensions.Interception;

namespace Common.Interception.Interceptors
{
    public class TraceInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Console.WriteLine("Enter Method");

            invocation.Proceed();

            Console.WriteLine("Exit Method");
        }
    }
}

Notes: Here is a simple console app that shows how to wire up the attributes/interceptors. This has dependencies on both Ninject.Extensions.Interception.DynamicProxy and Ninject.Extensions.Conventions (and all their required dependencies)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Common.Interception.Attributes;
using Common.Interception.Interceptors;
using Ninject;
using Ninject.Extensions.Conventions;
using Ninject.Planning.Strategies;
using SomeProject.Infrastructure;

namespace SomeProject.ConsoleApp
{
    class Program
    {
        static void Main(string[] args)
        {
            var kernel = new StandardKernel();

            kernel.Components.Add<IPlanningStrategy, AlternateInterceptorRegistrationStrategy<TraceAttribute, TraceInterceptor>>();
            kernel.Components.Add<IPlanningStrategy, AlternateInterceptorRegistrationStrategy<AuthorizeAttribute, AuthorizeInterceptor>>();

            // I needed a way to specify execution order and "DoNotIntercept" without deriving from attributes that would force ninject references all over my domain
            // not 100% confident this is the best way - but it works
            kernel.Bind<IInterceptorConfig>().ToConstant(new InterceptorConfig()
                                                             .SpecifyOrder<TraceInterceptor>(1)
                                                             .SpecifyOrder<AuthorizeInterceptor>(0)
                                                             .SpecifyDoNotInterceptAttribute<DoNotInterceptAttribute>());

            // SomeProject.Infrastructure contains my service classes decorated with my custom attributes
            kernel.Bind(x => x.FromAssembliesMatching("SomeProject.Infrastructure")
                              .SelectAllClasses()
                              .BindToSelf());

            var a = kernel.Get<SomeServiceA>();
            var b = kernel.Get<SomeServiceB>();

            Console.WriteLine("Calling a.DoSomeStuff()...");
            a.DoSomeStuff();

            Console.WriteLine("Calling b.DoMyThing()...");
            b.DoMyThing();

            Console.WriteLine("Calling b.NowTraceThis()...");
            b.NowTraceThis();

            Console.ReadLine();
        }
    }
}

Solution

  • After scouring and scouring, I found that a similar question had been asked and answered here: Ninject Intercept any method with certain attribute?

    I used this in conjunction with the source code of the InterceptorRegistrationStrategy class within Ninject.Extensions.Interception.Planning.Strategies to create a derivation which I am now using.

    crickets