Reputation: 9527
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
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
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