The handler pattern
Often the extensions you are registering are handlers designed to act upon other objects.
There are 2 interfaces provided to help with identifying the handlers appropriate for the target objects.
- IHandlesType: targets all objects of a certain type
- IHandlesObject: targets objects based on the object instance (state)
IHandlesType Example
Extend IHandlesType and implement the HandlesType property. In this case the decision about whether an item will be handled is based on its type matching or deriving from the HandlesType property.
public interface IKeyGenerator : IHandlesType { }
public class OrderKeyGenerator : IKeyGenerator
{
public Type HandlesType => typeof(Order);
}
IHandlesObject Example
Extend IHandlesObject and implement the CanHandle method. In this case you need to provide the logic to determine whether or not to handle the object.
public interface IKeyGenerator : IHandlesObject { }
public class OrderKeyGenerator : IKeyGenerator
{
public bool CanHandle(object item)
{
// ...
}
}
A base class is a great idea
Not always required, but sometimes it helps to just simplify the implementation of your handlers and allow your extensions to leverage generic types.
public interface IKeyGenerator : IHandlesType {
string GenerateKey(object item);
}
public abstract class KeyGeneratorBase<T> : IKeyGenerator
{
public Type HandlesType => typeof(T);
public string GenerateKey(object item)
{
if (item is T t)
return GenerateKey(t);
else
throw new Exception("Incorrect type for item.");
}
public abstract string GenerateKey(T item);
}
public class OrderKeyGenerator : KeyGeneratorBase<Order>
{
public override string GenerateKey(Order item)
{
/// ...
}
}
Use the Handles() extension method in your service or provider
The Handles extension methods handle both IHandlesType and IHandlesObject as a transparent way to identify handlers for your objects.
public class KeyProvider : IKeyProvider
{
private readonly IEnumerable<IKeyGenerator> _keyGenerators;
public KeyProvider(IEnumerable<IKeyGenerator> keyGenerators)
{
_keyGenerators = keyGenerators;
}
public string GenerateKey(object item)
{
return _keyGenerators.FirstOrDefault(p => p.Handles(item))?.GenerateKey(item);
}
}
Improve performance by caching the handler lookup
This is not always possible, but when your handler is based on type, it's an essential performance improvement.
public class KeyProvider : IKeyProvider
{
private readonly IEnumerable<IKeyGenerator> _keyGenerators;
private readonly ConcurrentDictionary<Type, IKeyGenerator> _keyGeneratorLookupCache = new ConcurrentDictionary<Type, IKeyGenerator>();
public KeyProvider(IEnumerable<IKeyGenerator> keyGenerators)
{
_keyGenerators = keyGenerators;
}
public string GenerateKey(object item)
{
if (item == null)
return null;
return _keyGeneratorLookupCache.GetOrAdd(item.GetType(),
t => _keyGenerators.FirstOrDefault(p => p.Handles(t))
)?.GenerateKey(item);
}
}