I feel like I played buzzword bingo with the title. Here's a concise example of what I'm asking. Let's say I have some inheritance hierarchy for some entities.
class BaseEntity { ... }
class ChildAEntity : BaseEntity { ... }
class GrandChildAEntity : ChildAEntity { ... }
class ChildBEntity : BaseEntity { ... }
Now let's say I have a generic interface for a service with a method that uses the base class:
interface IEntityService<T> where T : BaseEntity { void DoSomething(BaseEntity entity)... }
I have some concrete implementations:
class BaseEntityService : IEntityService<BaseEntity> { ... }
class GrandChildAEntityService : IEntityService<GrandChildAEntity> { ... }
class ChildBEntityService : IEntityService<ChildBEntity> { ... }
Assume I've registered these all with the container. So now my question is if I'm iterating through a List
of BaseEntity
how do I get the registered service with the closest match?
var entities = List<BaseEntity>();
// ...
foreach(var entity in entities)
{
// Get the most specific service?
var service = GetService(entity.GetType()); // Maybe?
service.DoSomething(entity);
}
What I'd like to do is have a mechanism set up such that if an entity has a type of ClassA
the method would find no service for the specific class and so would return BaseEntityService
. Later if someone came along and added a registration for this service:
class ClassAEntityService : IEntityService<ChildAEntity> { ... }
The hypothetical GetService
method would start providing the ClassAEntityService
for the ClassA
types without requiring any further code changes. Conversely if someone came along and just removed all the services except BaseEntityService
then the GetService
method would return that for all classes inheriting from BaseEntity
.
I'm pretty sure I could roll something even if the DI container I'm using doesn't directly support it. Am I falling into a trap here? Is this an anti pattern?
EDIT:
Some discussion with @Funk (see below) and some additional Google searches those discussions made me think to look up has made me add some more buzzwords to this. It seems like I'm trying collect all the advantages of DI Containers, the Strategy Pattern and the Decorator Pattern in a type safe way and without using a Service Locator Pattern. I'm beginning wonder if the answer is "Use a Functional Language."
So I was able to roll something that did what I needed.
First I made an interface:
public interface IEntityPolicy<T>
{
string GetPolicyResult(BaseEntity entity);
}
Then I made a few implementations:
public class BaseEntityPolicy : IEntityPolicy<BaseEntity>
{
public string GetPolicyResult(BaseEntity entity) { return nameof(BaseEntityPolicy); }
}
public class GrandChildAEntityPolicy : IEntityPolicy<GrandChildAEntity>
{
public string GetPolicyResult(BaseEntity entity) { return nameof(GrandChildAEntityPolicy); }
}
public class ChildBEntityPolicy: IEntityPolicy<ChildBEntity>
{
public string GetPolicyResult(BaseEntity entity) { return nameof(ChildBEntityPolicy); }
}
I registered each of them.
// ...
.AddSingleton<IEntityPolicy<BaseEntity>, BaseEntityPolicy>()
.AddSingleton<IEntityPolicy<GrandChildAEntity>, GrandChildAEntityPolicy>()
.AddSingleton<IEntityPolicy<ChildBEntity>, ChildBEntityPolicy>()
// ...
As well as registering a policy provider class that looks something like this:
public class PolicyProvider : IPolicyProvider
{
// constructor and container injection...
public List<T> GetPolicies<T>(Type entityType)
{
var results = new List<T>();
var currentType = entityType;
var serviceInterfaceGeneric = typeof(T).GetGenericDefinition();
while(true)
{
var currentServiceInterface = serviceInterfaceGeneric.MakeGenericType(currentType);
var currentService = container.GetService(currentServiceInterface);
if(currentService != null)
{
results.Add(currentService)
}
currentType = currentType.BaseType;
if(currentType == null)
{
break;
}
}
return results;
}
}
This allows me to do the following:
var grandChild = new GrandChildAEntity();
var policyResults = policyProvider
.GetPolicies<IEntityPolicy<BaseEntity>>(grandChild.GetType())
.Select(x => x.GetPolicyResult(x));
// policyResults == { "GrandChildAEntityPolicy", "BaseEntityPolicy" }
More importantly I can do this without knowing the particular subclass.
var entities = new List<BaseEntity> {
new GrandChildAEntity(),
new BaseEntity(),
new ChildBEntity(),
new ChildAEntity() };
var policyResults = entities
.Select(entity => policyProvider
.GetPolicies<IEntityPolicy<BaseEntity>>(entity.GetType())
.Select(policy => policy.GetPolicyResult(entity)))
.ToList();
// policyResults = [
// { "GrandChildAEntityPolicy", "BaseEntityPolicy" },
// { "BaseEntityPolicy" },
// { "ChildBEntityPolicy", "BaseEntityPolicy" },
// { "BaseEntityPolicy" }
// ];
I expanded on this a bit to allow the policies to provide an ordinal value if necessary and added some caching inside GetPolicies
so it doesn't have to construct the collection every time. I've also added some logic which allows me to define interface policies IUnusualEntityPolicy : IEntityPolicy<IUnusualEntity>
and pick those up as well. (Hint: Subtract the interfaces of currentType.BaseType
from currentType
to avoid duplication.)
(It's worth mentioning that the order of List
is not guaranteed so I have used something else in my own solution. Consider doing the same before using this.)
Still not sure if this is something that already exists or if there's a term for it but it makes managing entity policies feel decoupled in a way that's manageable. For example if I registered a ChildAEntityPolicy : IEntityPolicy<ChildAEntity>
my results would automatically become:
// policyResults = [
// { "GrandChildAEntityPolicy", "ChildAEntityPolicy", "BaseEntityPolicy" },
// { "BaseEntityPolicy" },
// { "ChildBEntityPolicy", "BaseEntityPolicy" },
// { "ChildAEntityPolicy", "BaseEntityPolicy" }
// ];
EDIT: Though I haven't yet tried it, @xander's answer below seems to illustrate that Simple Injector can provide much of the behavior of the PolicyProvider
"out of the box". There's still a slight amount of Service Locator
to it but considerably less so. I'd highly recommend checking that out before using my half-baked approach. :)
EDIT 2: My understanding of the dangers around a service locator is that it makes your dependencies a mystery. However these policies are not dependencies, they're optional add-ons and the code should run whether or not they've been registered. With regard to testing, this design separates the logic to interpret the sum results of the policies and the logic of the policies themselves.