dolittle-obsolete / DotNET.Fundamentals

Reusable, fundamental abstractions and building blocks
http://www.dolittle.io
MIT License
4 stars 8 forks source link

Resource system for dealing with resources per tenant #97

Closed einari closed 5 years ago

einari commented 6 years ago

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:

public class ReadModelRepositoryConfiguration
{
     public string Host { get; set; }
     public string Database { get; set; }
     public bool UseSSL { get; set; 
}

The ReadModelRepositoryFor<> implementation would then need a mechanism to get an instance scoped correctly. So something like an interface:

public interface IConfigurationFor<T> where T:class
{
    T Instance { get; } 
}

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:

{
    "id": "090c68ef-c307-48bb-b02d-833af2f04df8",
    "name": "My bounded context",
    "application": "eb7e5e64-94e4-4a27-b2cb-d3ab71a8c35d",
    "environments": {
        "production": {
            "readModels": "MongoDB",
            "eventStore" : "Azure",
        },
        "development": {
            "readModels": "MongoDB",
            "eventStore" : "MongoDB",
        }
    },
    "interaction": {
        "type": "web",
        "system": "reactjs"
    }
}

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:

{
    "<guid for development tenant>": {
        "readModels": {
            "connectionString": "mongodb://localhost:27017"
        },
        "eventStore": {
            "appendBlob": "a...a.sd.asd.",
            "table": ",adsasdas."
        }     
    }
}

At runtime in Kubernetes for instance, we'd have a file like the following:

{
    "<tenant id>": {
        "readModels": {
            "connectionString": "mongodb://localhost:27017"
        },
        "eventStore": {
            "appendBlob": "a...a.sd.asd.",
            "table": ",adsasdas."
        }     
    },
    "<tenant id>": {
        "readModels": {
            "connectionString": "mongodb://localhost:27017"
        },
        "eventStore": {
            "appendBlob": "a...a.sd.asd.",
            "table": ",adsasdas."
        }     
    },
    "<tenant id>": {
        "readModels": {
            "connectionString": "mongodb://localhost:27017"
        },
        "eventStore": {
            "appendBlob": "a...a.sd.asd.",
            "table": ",adsasdas."
        }     
    }
}

The concrete implementation of a resource type, such as read model repository would take a dependency to the configuration and use it as follows:

    public class ReadModelRepositoryFor
    {
        public ReadModelRepositoryFor(IConfigurationFor<MongoConnectionConfiguration> configuration)
        {
              this.Configuration = configuration.Instance;
        }
    }

The implementation of the ConfigurationFor<> could be something like this:

    [SingletonPerTenant]
    public class ConfigurationFor<T> where T:class
    {
        public ConfigurationFor(ITenantResourceManager tenantResourceManager, IExecutionContextManager executionContextManager)
        {
            // Mapping from config type - resource type 
            Instance  = tenantResourceManager.GetConfigurationFor<MongoConnectionConfiguration>(executionContextManager.Current.Tenant);
        }

        public T Instance { get; }
    }
``

The tenant resource manager interface:

```csharp
    public interface ITenantResourceManager
    {
        T GetConfigurationFor<T>(TenantId tenantId);
    }

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 a ConceptAs<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:

public interface IRepresentAResourceType
{
    ResourceType Type { get; }
    Type ConfigurationType { get; }
}
woksin commented 6 years ago

pretty pictures and ugly code please, @einari

einari commented 6 years ago

Working on it.. :) I'll update with more in a few minutes..

einari commented 6 years ago

How about now? @woksin

woksin commented 6 years ago

A little better @eianri 👍

einari commented 6 years ago

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 ResourceTypeand ResourceName - we do not allow that.