A beginning
The other day I was cleaning my keyboard. I don't know exactly what I did but BAM! a Djinn popped up on my screen!
Well, after establishing that meta-wishes were out of the question, the conversation went something like:
Me: I really wish I didn't need to include special attributes in my entities; just so I can serialize properly.
Mr. J: No prob.. what else?
Me: Hmm.. I wish I could use logging in my classes.. maybe even take some other services as dependencies, so my logic was in an entity class, not a service that took a DTO.
Mr. J: Yep, I can do that... anything else.. you know it doesn't have to be about code right?
Me: sure.. sure.. let me think... oh yeh I wish I could have a straight-forward async-int, you know not all instances are created equally.. if you get what I mean hehe.
Mr. J: Ok.. well, last time I had to start a world-war, so this time it should be a piece of cake!
Me: Now.. lets talk about C. Theron. I wish...
Mr J: Soz, you're done
Me: Aww.. F$*@.. I need a do-over.. cmon!
...15 minutes later...
EntityProvider
An approach for enabling richer entity classes in C#.
IInternalState<T>
Ever try to write a class with some calculated properties; only to back-track because you don't want to serialize the property?
I know, just use STJ: JsonIgnore, or Newtonsoft: JsonIgnore or other, but that get's messy. And it doesn't solve
- Needing a public property for what should be a private field.
- Wanting to expose a read-only collection, but needing to use a List for serialzation serialize.
In the end I always fall-back to needing some DTO to describe the 'storable' vs public properties, a rich class is just not viable as a serializable class itself.
So; use a simple interface:
public interfact IInternalState<TState>{
TSTate CurrentState();
}
This allows the entity to be itself, while still providing a serializer friendly 'internal-state'.
public class MyEntity : IInternalState<MyEntity.State>{
private State _state;
public record State(int A, int B);
public MyEntity(State state)
{_State = state;}
State IInternalState<State>.CurrentState() => _state;
//Don't store this, store the state
public double Distance => Math.Sqrt(_state.X^2+_state.Y^2);
}
An entity and its internal-state are tightly-coupled (by design). So I use a nested class for the state.
Use Explicit interface implementation because I want the interface, but I DON'T want to express it is part of the entity public interface.
I tend to find using a object-centric base interface gives me more flexibility:
public interface IInternalState{
public object GetCurrentState();
}
public interface IInternalState<TState> : IInternalState
{
object IInternalState.GetCurrentState() => CurrentState();
}
Now we have a ready-to-serialize internal-state we can store/load a MyEntity:
MyEntity entity;
var dto = ((IInternalState)entity).GetCurrentState();
var json = JsonSerializer.Serialize(dto);
var dtoLoaded = JsonSerializer.Deserialize<MyEntity.State>(json);
var entityLoaded = new MyEntity(dtoLoaded);
:+1: wish-1 - Serialize without attributes.
IEntityFactory<TEntity, TState>
Use an IInternalState<TState> to convert an entity into a 'dumb' serializer-friendly type. Then follow up with a corresponding IEntityFactory to get from internal state to entity.
//Make a TEntity from its state
public interface IEntityFactory<TEntity, TState>{
//Use a ValueTask so we're async friendly.
public ValueTask<TEntity> Create(Tstate state);
}
We can now implement the factory and use it via a container
//An entity factory, with aService
public class MyEntityFactory : IEntityFactory<MyEntity, MyEntity.State>{
private IService _aService;
public MyEntityFactory(IService aService)
{_aService = aService;}
public ValueTask<MyEntity> Create(MyEntity.State state)
=> new MyEntity(state);
}
We'll add the MyEntityFactory to a IoC container, that way we can get it when needed, and it can take other services as dependencies.
EntityProvider
And now; an over-arching service to act as the entry point for entity creation. This service takes the IoC container as a dependency, and uses the entity-factory above to create a entity from its state.
You (defiantly) do not normally want to take the IServiceProvider as a dependency;
But the EntityProvider's responsibility is to use a ServiceProvider to create an entity
from its factory. So I feel its ok here.
public class EntityProvider{
private IServiceProvider _container;
public EntityProvider(IServiceProvider container)
{_container = container;}
//Use the IInternalState interface to work-out the state type.
public static EntityStateType(Type entityType) =>
entityType.GetInterface(
typeof(IInternalState<>).Name)
.GetTypeArguments[0];
//The factory MUST implement IEntityFactory.
public static EntityFactoryType(Type e, Type s) =>
typeof(IEntityFactory<,>).MakeGenericType(e,s);
public ValueTask<TEntity> Crate<TEntity>(object state){
var entityStateType = EntityStateType(typeof(TEntity));
var factoryType = EntityFactoryType(typeof(TEntity), entityStateType);
//Can't do it, need object-centric base.
//var factory = (IEntityFactory)container.GetRequiredService(factoryType);
//var entity = await factory.Create(state);
//return (TEntity)entity;
}
}
Woops; lets add a base interface, with object-centric method.
public interface IEntityFactory{
public ValueTask<object> Create(object state);
}
public interface IEntityFactory<TEntity, TState> : IEntityFactory{
async ValueTask<object> Create(object state) =>
await Create((TState)state);
}
Done. Putting it all together:
var cont = new ServiceCollection()
//some random service.
.AddSingleton<IService, AService>()
//Factory for MyEntity
.AddScoped<
IEntityFactory<MyEntity, MyEntity.State>,
MyEntityFactory>()
//Entry point for entity creation.
.AddScoped<EntityProvider>()
.BuildServiceProvider();
var p = cont.GetRequiredService<EntityProvider>();
var entity = await p.Create<MyEntity>(new MyEntity.State(2,2));
Since we're injecting services:
public class MyEntity{
//entity depends on IService.
public MyEntity(State state, IService aService)
{...}
}
public class MyEntityFactory{
//entity-factory passes service to entity on create.
public ValueTask<MyEntity> Create(State state)
=> new MyEntity(state, _aService);
}
:+1: wish-2 - Entity with services.
Now we have entity state, and a factory... small step to async-initialize, or maybe call it 2-stage construction.
So, our entity now wants to initialize during construction; lets wrap it in a static method to make it obvious.
public MyEntity{
private MyEntity(State state, IService aService){}
public static async ValueTask<MyEntity> New(State state, IService aService)
{
var result = new MyEntity(state, aService);
//...other init stuff
await OtherAsyncStuff();
return result;
}
}
Then the the entity factory uses this async create:
public class MyEntityFactory{
public async ValueTask<MyEntity> Create(State state)
=> await MyEntity.New(state, _aService);
}
:+1: wish-3 - Entity create with async initialization.
EntityMethodFactory
Well, that's the back bone of the approach. In its current form is a little verbose for my taste; the Factory type(s) are little more than wrappers around a static method.
We can implement a general approach for the entity factory, using some reflection.
Lets call the method factory EntityMethodFactory<,>
public class EntityMethodFactory<T, S> : IEntityFactory<T, S>{}
Since we're using reflection, better to maintain its results in a cached set of strategies. That way we incur the cost only during start-up. A service to maintain the discovered strategies.
//A method and some args to create a Entity.
public record Strategy(MethodInfo op, Type[] Args);
//Dictionary mapping an entity type to a create method (strategy).
public class FactoryStrategies{
private Dictionary<Type,Strategy> _data = new();
public Strategy this[Type t] {
get => _data[t];
set => _date[t] = value;}
}
Now a config class to setup:
public class EntityProviderConfig {
private List<Type> _entityTypes = new();
//try find a method to create the entity
public static Strategy GetStrategy(Type e, Type s){
var returnType = typeof(ValueTask<>).MakeGenericType(e);
var op = e.GetMethods(BindingFlags.Static)
//return a ValueTask<entity>.
.Where(x => x.ReturnType == returnType)
//has a parameter that takes the state.
.Where(x => x.GetParameters().Any(p => p.ParameterType == s))
.First();
var args = op.GetParameters()
.Select(p => p.ParameterType)
.ToArray();
return new Strategy(op, args);
}
private static Type GetMethodFactoryType(Type e, Type s)
=> typeof(EntityMethodFactory<,>).MakeGenericType(s, s);
//register an entity to be created.
public AddEntity<T>(){
_entityTypes.Add(typeof(T));
}
//register strategy and MethodFactory for entity types.
public Register(IServiceCollection services){
var strategies = new FactoryStrategies();
//keep the discovered method strategies.
services.AddSingleton(strategies);
foreach(var e in _entityTypes){
var s = EntityProvider.EntityStateType(x);
strategies[e] = GetStrategy(x, s);
services.AddScoped(
EntityProvider.EntityFactoryType(e,s),
GetMethodFactoryType(e,s)
);
}
}
}
Now our MethodFactory can use this collection of strategies to do its job.
public class EntityMethodFactory<T, S> : IEntityFactory<T, S>{
private readonly IServiceProvider _container;
private readonly FactoryStrategies _strategies;
//I need the container, and the strategy to create a Entity.
public EntityMethodFactory<T, S>(IServiceProvider container, FactoryStrategies strategies)
{
_container = container;
_strategies = strategies;
}
public async ValueTask<T> Create(S state){
var strategy = _strategies[typeof(T)];
//resolve all except state with container.
var args = strategy.Args.Select(x =>
x is typeof(s) ?
state :
_container.GetREquiredService(x);
);
var opResult = (ValueTask<T>)strategy.Op.Invoke(null, args);
return await opResult;
}
}
Putting it all together
We register all the parts in container setup, then use the EntityProvider as the entry point to create an entity.
var services = new ServiceCollection()
.AddSingleton<IService, AService>()
.AddScoped<EntityProvider>();
var cfg = new EntityProviderConfig()
.AddEntity<MyEntity>();
cfg.Register(services);
var cont = services.BuildServiceProvider();
var p = cont.GetRequiredService<EntityProvider>();
var entity = await p.Create<MyEntity>(new MyEntity.State(2,2));
Where to next?
Currently I use this style to help drive toward richer domain models rather than anemic models
We can add some other capabilities, such as more flexibility for EntityMethodFactory strategies; but I think that's enough for now.
If you'd like some runnable code; I've packaged this into a little library kwd.CoreDomain / nuget.