Reputation: 6363
I would like to modify my current LetterFactory implementation and remove the call to Activator.CreateInstance with a call to the container to resolve the current Letter fully initialized with constructor injection. I have read the docs here and here, and even this SO Post while penning this post, but nothing seems to click.
Notes:
1) IDocumentServicesCore is an Aggregate.
2) All Letters are decorated with the LetterTypeAttribute (hundreds of them)
3) This LetterFactory itself is registered in the container.
public class LetterFactory : ILetterFactory
{
private readonly IDocumentServicesCore _documentServicesCore;
public LetterFactory(IDocumentServicesCore documentServicesCore)
{
_documentServicesCore = documentServicesCore;
}
public LetterBase Create(int letterId)
{
if (letterId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(letterId));
}
List<Type> types = typeof(LetterBase).Assembly.GetTypes()
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(LetterBase)))
.ToList();
LetterBase letter = null;
foreach(Type type in types)
{
LetterTypeAttribute attribute = type.GetCustomAttributes<LetterTypeAttribute>().First();
if (!attribute.LetterId.Contains(letterId))
{
continue;
}
letter = Activator.CreateInstance(type, _documentServicesCore) as LetterBase;
break;
}
if (letter != null)
{
return letter;
}
string message = $"Could not find a LetterBase to create for id {letterId}.";
throw new NotSupportedException(message);
}
}
Update1
The problems seems to start with the fact that the letters themselves aren't registered, how to I take the LINQ code that collects the Letters from the assembly and register those enmass?
Thank you, Stephen
Upvotes: 4
Views: 1095
Reputation: 54628
You made me do real work, good job :) The following is my solution.
Autofac - Named and Keyed Services - Resolving with Index
using System;
using System.Collections.Generic;
using System.Linq;
using Autofac;
using Autofac.Features.Indexed;
public class Program
{
private static IContainer _Container;
public static void Main()
{
InitDependencyInjection();
var rd1 = _Container.Resolve<RequiresDependency>(new NamedParameter("letterId", 1));
rd1.PrintType();
var rd2 = _Container.Resolve<RequiresDependency>(new NamedParameter("letterId", 2));
rd2.PrintType();
}
private static void InitDependencyInjection()
{
var builder = new ContainerBuilder();
var letterTypes = typeof(LetterBase).Assembly.GetTypes()
// Find all types that derice from LetterBase
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(LetterBase)))
// Make sure they are decorated by attribute
.Where(t =>
t.GetCustomAttributes(typeof(LetterTypeAttribute), false).Length == 1)
.ToList();
//Register with Autofac, Keyed by LetterId
//This should throw an exception if any are duplicated
//You may want to consider using an enum instead
//It's not hard to convert an Int to Enum
foreach(Type letterType in letterTypes)
{
// we already tested the type has the attribute above
var attribute = letterType
.GetCustomAttributes(typeof(LetterTypeAttribute)
, false)[0] as LetterTypeAttribute;
builder.RegisterType(letterType)
.Keyed<LetterBase>(attribute.LetterId);
}
builder.RegisterType<RequiresDependency>();
_Container = builder.Build();
}
}
public class RequiresDependency
{
private readonly LetterBase _letter;
//Autofac automagically provides a factory that returns type
//type you need via indexer
public RequiresDependency(int letterId, IIndex<int, LetterBase> letterFactory)
{
//resolve the needed type based on the index value passed in
_letter = letterFactory[letterId];
}
public void PrintType()
{
Console.WriteLine(_letter.GetType().Name);
}
}
public abstract class LetterBase
{
}
[LetterType(1)]
public class LetterA : LetterBase
{}
[LetterType(2)]
public class LetterB : LetterBase
{}
// make sure the classes using this attribute has only a single attribute
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class LetterTypeAttribute : Attribute
{
public LetterTypeAttribute(int letterId)
{
LetterId = letterId;
}
public int LetterId { get; private set; }
}
Result:
LetterA
LetterB
Upvotes: 2
Reputation: 16192
You are looking for IIndex<TKey, TValue>
which is a kind of dictionary and it can be composed so IIndex<Int32, Func<LetterBase>>
is the type you want.
With such a type your LetterFactory
will look like this :
public class LetterFactory : ILetterFactory
{
private readonly IIndex<Int32, Func<LetterBase>> _lettersFactory;
public LetterFactory(IIndex<Int32, Func<LetterBase>> lettersFactory)
{
_lettersFactory = lettersFactory;
}
public LetterBase Create(int letterId)
{
if (letterId <= 0)
{
throw new ArgumentOutOfRangeException(nameof(letterId));
}
Func<LetterBase> letterFactory = null;
if(!this._lettersFactory.tryGetValue(letterId, out letterFactory))
{
string message = $"Could not find a LetterBase to create for id {letterId}.";
throw new NotSupportedException(message);
}
Letter letter = letterFactory();
return letter;
}
}
And then you have to register your types like this :
List<Type> letterTypes = typeof(LetterBase).Assembly.GetTypes()
.Where(t => !t.IsAbstract && t.IsSubclassOf(typeof(LetterBase)))
.ToList();
foreach(Type letterType in letterTypes)
{
LetterTypeAttribute attribute = type.GetCustomAttributes<LetterTypeAttribute>()
.First();
builder.RegisterType(letterType)
.Keyed<LetterBase>(attribute.LetterId);
}
You will also improve performance with this code : the heavy assembly scanning will only happen once at startup and not for each call.
By the way, be aware of assembly scanning limitation in IIS hosted application : http://autofaccn.readthedocs.io/en/latest/register/scanning.html#iis-hosted-web-applications
You can also rely directly on IIndex<Int32, LetterBase>
instead of IIndex<Int32, Func<LetterBase>>
it depends on your scope strategy.
Upvotes: 4