Using Dependency Injection for Implementation Enumeration
...There is probably a better name for this
Scenario
I'm building a system that will look at a lot of transaction data and determine if each transaction qualifies for one of many criteria. At a high level, I'm asking "Does this transaction qualify for any or all of sales criteria A, B, C,...,N?"
This entry will examine a poor approach without dependency injection, then an approach that will leverage Autofac to register all criteria with one entry.
Bad Approach
I could have a method that simply iterates though a series of functions:
IEnumerable<Sale> GetSales(Transaction transaction)
{
var sales = new IEnumerable<Sale>();
sales.Add(CriteriaA(transaction));
sales.Add(CriteriaB(transaction));
sales.Add(CriteriaC(transaction));
...
sales.Add(CriteriaN(transaction));
return sales;
}
Sale CriteriaA(Transaction transaction) {...}
...
Sale CriteriaN(Transaction transaction) {...}
This approach sucks. It's easy to see what we're doing, but every time a transaction type is added, we have to add it to the list. Additionally, all of the logic lives in one place, so it quickly becomes a mess when we've got numerous criteria and forget about unit testing a specific requirement.
Better Solution
Hey, what about Dependency Injection? I've been using Autofac for awhile, certainly there must be something for this!
Yes, yes there is. In digging around the documentation, I ran across a gem that describes enumerable relationship types which states:
Dependencies of an enumerable type provide multiple implementations of the same service (interface). This is helpful in cases like message handlers, where a message comes in and more than one handler is registered to process the message.
Hey! That's exactly what I want to do. I want to allow for multiple implementations of the same interface to describe what a sale is. So I create an interface for checking the transaction and returning a sale where appropriate.
interface ISaleHandler
{
void QualifySale(Transaction transaction);
}
... and the implementations ...
class CriteriaASaleHandler : ISaleHandler
{
public void QualifySale(Transaction transaction);
{
if (!MeetRequirements(transaction)) return null;
Write(transaction);
}
}
Making the Magic Happen
Now we need to get Autofac to allow us our laziness. Within the Autofac configuration, we want to add the following registration:
builder.RegisterAssemblyTypes(typeof (CriteriaASaleHandler).Assembly)
.Where(x => x.Name.EndsWith("SaleHandler"))
.AsImplementedInterfaces();
This entry may look daunting at first, but it's pretty straightforward. The first line uses reflection to find all of the types in the same assembly as 'CriteriaASaleHandler'. The second line limits these types to only those ending with the name 'SaleHandler'. There are several ways to specify the correct types rather than name, but as a person preference, I stick with the naming convention. Finally, the last line tells Autofac to inject that type as whatever interface it inherits from.
Here is the best part. Autofac will return an Enumerable of objects for injection when the types were injected with 'RegisterAssemblyTypes' which makes for ridiculously easy injection from here on out.
Stay on Target!
Almost there. The final key to this is to properly inject the enumerated classes into your implementation. That means my final class that processes the multiple implementations will look something like this:
public class ProcessingService : IProcessingService
{
private readonly IEnumerable<ISaleHandler> _handlers;
public ProcessingService(IEnumerable<ISaleHandler> handlers)
{
_handlers = handlers;
}
public void Process(IEnumerable<Transactions> transactions)
{
foreach(var transaction in transactions)
{
foreach(var handler in _handlers)
{
handler.QualifySale(transaction);
}
}
}
Autofac will then enumerate all of the implementations and return all of the Sales objects returned therein.
Conclusion
I've shown how to implement a dependency injection solution for testing an object against multiple criteria, giving better adherence to separation of concerns and test driven development strategies, while also giving the added benefit of adding handlers without the need to register each implementation with Autofac or changing the class that calls the enumeration of implementations.
Edit
Originally, I had each handler return a Sale object. I later realized it would be better to just have the handler do whatever it needs to do (in this case write any results to the database). Loren Paulsen arrived at the same conclusion independantly.
Software Development Nerd