Как внедрить зависимости свойств в атрибут .net? - PullRequest
4 голосов
/ 22 февраля 2012

Я пытаюсь применить некоторое поведение, используя доморощенный тип «аспекта», действительно атрибут .net.У меня есть базовый класс (BankingServiceBase), который отражает себя при запуске, чтобы увидеть, какие «аспекты» применяются к нему.Затем он может выполнять пользовательское поведение до или после операций.Я использую Autofac в качестве контейнера IOC.Я пытаюсь применить метод PropertiesAutowired для регистрации аспекта.В приведенном ниже примере кода я хочу, чтобы Autofac внедрил экземпляр ILog в мой аспект / атрибут.Это не делает это однако.Я предполагаю, что когда я вызываю GetCustomAttributes , он создает новый экземпляр вместо получения зарегистрированного экземпляра из Autofac.Мысли?Вот пример кода, который можно использовать для отображения проблемы:

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);
        }
    }
}

Ответы [ 2 ]

4 голосов
/ 22 февраля 2012

Вы правы, что GetCustomAttributes не разрешает пользовательские атрибуты с помощью Autofac - если вы об этом думаете, как может FCL-код, такой как GetCustomAttributes, знать об Autofac? Пользовательские атрибуты фактически извлекаются из метаданных сборки, поэтому они никогда не проходят процесс разрешения Autofac, и поэтому ваш регистрационный код никогда не используется.

Что вы можете сделать, это добавить службы в экземпляр атрибута самостоятельно. Начните с кода в Ответ Оливера , чтобы сгенерировать список атрибутов аспекта. Однако перед возвратом списка вы можете обработать каждый атрибут и внедрить службы в любые зависимые поля и свойства. У меня есть класс с именем AttributedDependencyInjector, который я использую через метод расширения. Он использует отражение для сканирования полей и свойств, которые отмечены InjectDependencyAttribute, а затем устанавливает значение этих свойств. Там довольно много кода, чтобы справиться с различными сценариями, но вот он.

Атрибут класса:

/// <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; }
}

Класс инжектора:

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);
    }
}

Метод расширения:

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);
    }
}
0 голосов
/ 22 февраля 2012

Попробуйте реализовать ленивую загрузку аспектов

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

Затем используйте это как

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