Paul Fryer
Paul Fryer

Reputation: 9527

How to inject property dependencies on a .net attribute?

I'm trying to apply some behavior using a home grown type of "aspect", really a .net Attribute. I have a base class (BankingServiceBase) that reflects on itself at startup to see what "aspects" are applied to it. It then can execute custom behavior before or after operations. I'm using Autofac as my IOC container. I'm trying to apply the PropertiesAutowired method to the aspect's registration. In the below sample code I want Autofac to inject an ILog instance to my aspect/attribute. It isn't doing that however. My guess is that when I call GetCustomAttributes, it's creating a new instance instead of getting the registered instance from Autofac. Thoughts? Here is some usable sample code to display the problem:

internal class Program
{
    private static void Main()
    {
        var builder = new ContainerBuilder();

        builder
            .RegisterType<ConsoleLog>()
            .As<ILog>();

        builder
            .RegisterType<BankingService>()
            .As<IBankingService>();

        builder
            .RegisterType<LogTransfer>()
            .As<LogTransfer>()
            .PropertiesAutowired();

        var container = builder.Build();

        var bankingService = container.Resolve<IBankingService>();

        bankingService.Transfer("ACT 1", "ACT 2", 180);

        System.Console.ReadKey();
    }

    public interface IBankingService
    {
        void Transfer(string from, string to, decimal amount);
    }

    public interface ILog
    {
        void LogMessage(string message);
    }

    public class ConsoleLog : ILog
    {
        public void LogMessage(string message)
        {
            System.Console.WriteLine(message);
        }
    }

    [AttributeUsage(AttributeTargets.Class)]
    public abstract class BankingServiceAspect : Attribute
    {
        public virtual void PreTransfer(string from, string to, decimal amount)
        {
        }

        public virtual void PostTransfer(bool success)
        {
        }
    }

    public class LogTransfer : BankingServiceAspect
    {
        // Note: this is never getting set from Autofac!
        public ILog Log { get; set; }

        public override void PreTransfer(string from, string to, decimal amount)
        {
            Log.LogMessage(string.Format("About to transfer from {0}, to {1}, for amount {2}", from, to, amount));
        }

        public override void PostTransfer(bool success)
        {
            Log.LogMessage(success ? "Transfer completed!" : "Transfer failed!");
        }
    }

    public abstract class BankingServiceBase : IBankingService
    {
        private readonly List<BankingServiceAspect> aspects;

        protected BankingServiceBase()
        {
            // Note: My guess is that this "GetCustomAttributes" is happening before the IOC dependency map is built.
            aspects =
                GetType().GetCustomAttributes(typeof (BankingServiceAspect), true).Cast<BankingServiceAspect>().
                    ToList();
        }

        void IBankingService.Transfer(string from, string to, decimal amount)
        {
            aspects.ForEach(a => a.PreTransfer(from, to, amount));

            try
            {
                Transfer(from, to, amount);
                aspects.ForEach(a => a.PostTransfer(true));
            }
            catch (Exception)
            {
                aspects.ForEach(a => a.PostTransfer(false));
            }
        }

        public abstract void Transfer(string from, string to, decimal amount);
    }

    [LogTransfer]
    public class BankingService : BankingServiceBase
    {
        public override void Transfer(string from, string to, decimal amount)
        {
            // Simulate some latency..
            Thread.Sleep(1000);
        }
    }
}

Upvotes: 4

Views: 4903

Answers (2)

Rich Tebb
Rich Tebb

Reputation: 7126

You're correct that GetCustomAttributes doesn't resolve the custom attributes via Autofac - if you think about it, how could FCL code such as GetCustomAttributes know about Autofac? The custom attributes are actually retrieved from assembly metadata, so they never go through Autofac's resolution process and therefore your registration code is never used.

What you can do is to inject the services into the attribute instance yourself. Begin with the code in Oliver's answer to generate the list of aspect attributes. However, before returning the list, you can process each attribute and inject services into any dependent fields and properties. I have a class called AttributedDependencyInjector, which I use via an extension method. It uses reflection to scan for fields and properties that are decorated with the InjectDependencyAttribute and then set the value of those properties. There's rather a lot of code to cope with various scenarios, but here it is.

The attribute class:

/// <summary>
///     Attribute that signals that a dependency should be injected.
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class InjectDependencyAttribute : Attribute
{
    /// <summary>
    ///     Initializes a new instance of the <see cref = "InjectDependencyAttribute" /> class.
    /// </summary>
    public InjectDependencyAttribute()
    {
        this.PreserveExistingValue = false;
    }

    /// <summary>
    /// Gets or sets a value indicating whether to preserve an existing non-null value.
    /// </summary>
    /// <value>
    /// <c>true</c> if the injector should preserve an existing value; otherwise, <c>false</c>.
    /// </value>
    public bool PreserveExistingValue { get; set; }
}

The injector class:

public class AttributedDependencyInjector
{
    /// <summary>
    /// The component context.
    /// </summary>
    private readonly IComponentContext context;

    /// <summary>
    /// Initializes a new instance of the <see cref="AttributedDependencyInjector"/> class.
    /// </summary>
    /// <param name="context">The context.</param>
    public AttributedDependencyInjector(IComponentContext context)
    {
        this.context = context;
    }

    /// <summary>
    /// Injects dependencies into an instance.
    /// </summary>
    /// <param name="instance">The instance.</param>
    public void InjectDependencies(object instance)
    {
        this.InjectAttributedFields(instance);
        this.InjectAttributedProperties(instance);
    }

    /// <summary>
    /// Gets the injectable fields.
    /// </summary>
    /// <param name="instanceType">
    /// Type of the instance.
    /// </param>
    /// <param name="injectableFields">
    /// The injectable fields.
    /// </param>
    private static void GetInjectableFields(
        Type instanceType, ICollection<Tuple<FieldInfo, InjectDependencyAttribute>> injectableFields)
    {
        const BindingFlags BindingsFlag =
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly;
        IEnumerable<FieldInfo> fields = instanceType.GetFields(BindingsFlag);

        // fields
        foreach (FieldInfo field in fields)
        {
            Type fieldType = field.FieldType;

            if (fieldType.IsValueType)
            {
                continue;
            }

            // Check if it has an InjectDependencyAttribute
            var attribute = field.GetAttribute<InjectDependencyAttribute>(false);
            if (attribute == null)
            {
                continue;
            }

            var info = new Tuple<FieldInfo, InjectDependencyAttribute>(field, attribute);
            injectableFields.Add(info);
        }
    }

    /// <summary>
    /// Gets the injectable properties.
    /// </summary>
    /// <param name="instanceType">
    /// Type of the instance.
    /// </param>
    /// <param name="injectableProperties">
    /// A list into which are appended any injectable properties.
    /// </param>
    private static void GetInjectableProperties(
        Type instanceType, ICollection<Tuple<PropertyInfo, InjectDependencyAttribute>> injectableProperties)
    {
        // properties
        foreach (var property in instanceType.GetProperties(
            BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            Type propertyType = property.PropertyType;

            // Can't inject value types
            if (propertyType.IsValueType)
            {
                continue;
            }

            // Can't inject non-writeable properties 
            if (!property.CanWrite)
            {
                continue;
            }

            // Check if it has an InjectDependencyAttribute
            var attribute = property.GetAttribute<InjectDependencyAttribute>(false);
            if (attribute == null)
            {
                continue;
            }

            // If set to preserve existing value, we must be able to read it!
            if (attribute.PreserveExistingValue && !property.CanRead)
            {
                throw new BoneheadedException("Can't preserve an existing value if it is unreadable");
            }

            var info = new Tuple<PropertyInfo, InjectDependencyAttribute>(property, attribute);
            injectableProperties.Add(info);
        }
    }

    /// <summary>
    /// Determines whether the <paramref name="propertyType"/> can be resolved in the specified context.
    /// </summary>
    /// <param name="propertyType">
    /// Type of the property.
    /// </param>
    /// <returns>
    /// <c>true</c> if <see cref="context"/> can resolve the specified property type; otherwise, <c>false</c>.
    /// </returns>
    private bool CanResolve(Type propertyType)
    {
        return this.context.IsRegistered(propertyType) || propertyType.IsAssignableFrom(typeof(ILog));
    }

    /// <summary>
    /// Injects dependencies into the instance's fields.
    /// </summary>
    /// <param name="instance">
    /// The instance.
    /// </param>
    private void InjectAttributedFields(object instance)
    {
        Type instanceType = instance.GetType();

        // We can't get information about the private members of base classes through reflecting a subclass,
        // so we must walk up the inheritance hierarchy and reflect at each level
        var injectableFields = new List<Tuple<FieldInfo, InjectDependencyAttribute>>();
        var type = instanceType;
        while (type != null)
        {
            GetInjectableFields(type, injectableFields);
            type = type.BaseType;
        }

        // fields
        foreach (var fieldDetails in injectableFields)
        {
            var field = fieldDetails.Item1;
            var attribute = fieldDetails.Item2;

            if (!this.CanResolve(field.FieldType))
            {
                continue;
            }

            // Check to preserve existing value
            if (attribute.PreserveExistingValue && (field.GetValue(instance) != null))
            {
                continue;
            }

            object fieldValue = this.Resolve(field.FieldType, instanceType);
            field.SetValue(instance, fieldValue);
        }
    }

    /// <summary>
    /// Injects dependencies into the instance's properties.
    /// </summary>
    /// <param name="instance">
    /// The instance.
    /// </param>
    private void InjectAttributedProperties(object instance)
    {
        Type instanceType = instance.GetType();

        // We can't get information about the private members of base classes through reflecting a subclass,
        // so we must walk up the inheritance bierarchy and reflect at each level
        var injectableProperties = new List<Tuple<PropertyInfo, InjectDependencyAttribute>>();
        var type = instanceType;
        while (type != typeof(object))
        {
            Debug.Assert(type != null, "type != null");
            GetInjectableProperties(type, injectableProperties);
            type = type.BaseType;
        }

        // Process the list and inject properties as appropriate
        foreach (var details in injectableProperties)
        {
            var property = details.Item1;
            var attribute = details.Item2;

            // Check to preserve existing value
            if (attribute.PreserveExistingValue && (property.GetValue(instance, null) != null))
            {
                continue;
            }

            var propertyValue = this.Resolve(property.PropertyType, instanceType);
            property.SetValue(instance, propertyValue, null);
        }
    }

    /// <summary>
    /// Resolves the specified <paramref name="propertyType"/> within the context.
    /// </summary>
    /// <param name="propertyType">
    /// Type of the property that is being injected.
    /// </param>
    /// <param name="instanceType">
    /// Type of the object that is being injected.
    /// </param>
    /// <returns>
    /// The object instance to inject into the property value.
    /// </returns>
    private object Resolve(Type propertyType, Type instanceType)
    {
        if (propertyType.IsAssignableFrom(typeof(ILog)))
        {
            return LogManager.GetLogger(instanceType);
        }

        return this.context.Resolve(propertyType);
    }
}

The extension method:

public static class RegistrationExtensions
{
    /// <summary>
    /// Injects dependencies into the instance's properties and fields.
    /// </summary>
    /// <param name="context">
    /// The component context.
    /// </param>
    /// <param name="instance">
    /// The instance into which to inject dependencies.
    /// </param>
    public static void InjectDependencies(this IComponentContext context, object instance)
    {
        Enforce.ArgumentNotNull(context, "context");
        Enforce.ArgumentNotNull(instance, "instance");

        var injector = new AttributedDependencyInjector(context);
        injector.InjectDependencies(instance);
    }
}

Upvotes: 4

Olivier Jacot-Descombes
Olivier Jacot-Descombes

Reputation: 112402

Try to implement a lazy loading of the aspects

private readonly List<BankingServiceAspect> _aspects;
private List<BankingServiceAspect> Aspects
{
    get
    {
        if (_aspects == null) {
            _aspects = GetType()
                .GetCustomAttributes(typeof(BankingServiceAspect), true)
                .Cast<BankingServiceAspect>()
                .ToList();
        }
        return _aspects;
    }
}

Then use it like this

Aspects.ForEach(a => a.PreTransfer(from, to, amount));
...

Upvotes: 0

Related Questions