Closed einari closed 5 years ago
pretty pictures and ugly code please, @einari
Working on it.. :) I'll update with more in a few minutes..
How about now? @woksin
A little better @eianri 👍
The resource system should be tapping into the dependency inversion and registering bindings for the resource types we offer. This is done through implementing the ICanProvideBindings
interface. The challenge with this is that at startup it does not really have enough information to be able to actually do this since this is all dynamic and it does not know which resource types thats out there. Another aspect is that the bounded-context.json
file will have the information about which actual concrete resource type implementation should be used.
Suggestion The resource system needs a couple of hooks for it to be able to do this. First off it needs to be able to discover which bindings - or resource types are in the system. Example of resource types are typically as ReadModelRepository and EventStore. These are represented as one or more interfaces that represent their services. We need something that describes these in something like the following:
public interface IAmAResourceType
{
ResourceType Name { get; }
IEnumerable<Type> Services { get; }
}
To be able to discover these, it needs the ITypeFinder
. I suggest therefor we do something similar to what we have already for other parts that needs information during startup. Looking at ServicesExtension.cs
in our ASP.NET Core - Core startup, and the Host.cs
in Hosting
in our .NET SDK, one will see typically the following calls:
var assemblies = Dolittle.Assemblies.Bootstrap.EntryPoint.Initialize(logger);
var typeFinder = Dolittle.Types.Bootstrap.EntryPoint.Initialize(assemblies);
Right after these we could have a call to bootstrap the resource system, for instance making it something like:
var assemblies = Dolittle.Assemblies.Bootstrap.EntryPoint.Initialize(logger);
var typeFinder = Dolittle.Types.Bootstrap.EntryPoint.Initialize(assemblies);
Dolittle.Resources.Bootstrap.EntryPoint.Initialize(typeFinder);
The TypeFinder could then be kept as an internal static readonly property that we can use from the ICanProvideBindings
implementation. The DependencyInversion bootstrapper will then discover the binding provider and call its Provide()
method.
In this method, we can use the ITypeFinder
to find the resource types and provide bindings.
The bindings can then be towards a callback that will be able to have more context when its called and provide the correct implementation.
In order for it to be able to provide the correct implementation it needs to be told what was configured in the bounded-context.json
file.
I suggest we do a minor change of the original design in the description where we encapsulate the resource configuration in the following way:
{
"id": "090c68ef-c307-48bb-b02d-833af2f04df8",
"name": "My bounded context",
"application": "eb7e5e64-94e4-4a27-b2cb-d3ab71a8c35d",
"resources": {
"readModels": {
"production": "MongoDB",
"development": "MongoDB"
},
"eventStore": {
"production": "Azure",
"development": "MongoDB"
}
},
"interaction": {
"type": "web",
"system": "reactjs"
}
}
The resources
key can then represent a specific resource configuration object, where the key is the actual resource type name - and we all of a sudden have now something much easier to link towards the IAmResourceType
implementations.
The BoundedContextLoader
needs to be hooked up during boot and actually call into the resource system with this configuration object. Introducing a boot procedure - implementing the ICanPerformBootProcedure
in the Applications.Configuration
that takes the loader as a dependency and then calls into the resource system would then make it work. The Resource system obviously needs a configuration service to be able to do take this - a [Singleton]
scoped service. Once handed the configuration, it can expose it through a internal static readonly property that the ICanProvideBindings
implmentation in the resource system can then take and use.
This implementation would then be pretty straight forward:
public class ResourceSystemBindings : ICanProvideBindings
{
internal static ITypeFinder _typeFinder;
internal static IResourceConfiguration _resourceConfiguration;
public void Provide(IBindingBuilder builder)
{
var resourceTypes = _typeFinder.FindMultiple<IAmResourceType>();
resourceTypes.ForEach(_ => builder.Bind(_).To(() => resourceConfiguration.GetImplementationFor(_));
}
}
Since the ResourceConfiguration
is marked as [Singleton]
it can then safely set the _resourceConfiguration
field on the ResourceSystemBindings
to point to itself.
The boot procedure will use the IResourceConfiguration
system itself to give it the configuration. During this it can flatten out all the services and do the discovery of implementations of them, so that the GetImplementationFor(Type type)
method just does a dictionary look up for the type.
On the implementation side, we just need the following:
public interface IRepresentAResourceType
{
ResourceType Type { get; }
ResourceName Name { get; }
Type ConfigurationType { get; }
IDictionary<Type, Type> Bindings { get; }
}
The Bindings
here will contain the service to implementation binding, for instance IReadModelRepositoryFor<>
to MongoDB.ReadModelRepositoryFor<>
.
IMPORTANT
Notice the change of ResourceType
and its meaning, that would hold values such as ReadModels
, EventStore
- while the ResourceName
is then MongoDB
, Azure
and so forth.
We can have multiple implementations of a IRepresentAResourceType
targetting the same, for instance for the Azure
scenario and EventStore
as resource type, we have multiple interfaces that needs to be implemented to fulfil its dependencies. They might be defined in different assemblies. So the ResourceConfiguration
should be smart enough to just add to its internal dictionary. However, if there are multiple implementations of the same interface with the same ResourceType
and ResourceName
- we do not allow that.
Goal We need to be able to provide resources transparently in the context of the current tenant based on the
ExecutionContext
. That means that resources themselves will have to have a specific configuration per tenant, typically in case of databases a connection string - but we don't want to limit it to only this, but rather be able to have a resource type implementation specific configuration object.Design discussion Lets take for instance a specific implementation of
IReadModelRepositoryFor<TReadModel>
for MongoDB, it could have something like:The
ReadModelRepositoryFor<>
implementation would then need a mechanism to get an instance scoped correctly. So something like an interface:The configuration would then be two parts; one for the bounded context stating which type of resource to use and the second part holding the tenant specific configuration of it.
So for instance the
bounded-context.json
file could hold the following:Notice the environments - production and development. During local development, one typically wants something else than when its running in the cloud for instance.
The tenant specific configuration should be generated and put into a file that gets mounted for the container when running. While for developers, you'd have a
resources.json
file in the.dolittle
folder holding the development specific configuration:At runtime in Kubernetes for instance, we'd have a file like the following:
The concrete implementation of a resource type, such as read model repository would take a dependency to the configuration and use it as follows:
The implementation of the
ConfigurationFor<>
could be something like this:It would be typically marked with
[Singleton]
on its implementation and be responsible for loading the.dolittle/resources.json
file on creation and hold this in memory - for instance a dictionary - and then provide the appropriate configuration object for the type asked.This whole mechanism would need to rely on the concept of a
ResourceType
- which can be aConceptAs<string>
. We'd need an interface that can be discovered to identity the implementations of resource types with the information about which configuration type to expect. This would then make it possible to do the deserialization to the correct type at startup and put into the dictionary as mentioned above with the correct instance.An interface for this could be something like: