jezzsantos / saastack

A comprehensive codebase template for starting your real-world, fully featured SaaS web products. On the .NET platform
The Unlicense
15 stars 5 forks source link

Dependency Injection patterns need improving #14

Closed jezzsantos closed 4 months ago

jezzsantos commented 7 months ago

We have some complex issues to resolve with DI.

We need to support physical data partitioning (out of the box), which means that it is possible that each tenant (each HTTP request) to be using separate credentials/connectionstrings/etc (or other secret config) on any of their store adapters (IDataStore, IQueueStore, IEventStore or IBlogStore) for any of the tenanted domains.

It also means that the "platform" subdomains (e.g. Organization, EndUser, Ancillary, etc) will require different instances of the IDataStore, IQueueStore, IEventStore or IBlogStore adapters since those will have a fixed set of credentials/connectionstrings/etc (separate from each tenants) int he adapter that will point to the same set of external systems.

Thus, we need certain implementations (e.g. the IDataStore adapter) to be in the IoC container twice: once for usage by Platform components, and once for tenanted (per HTTP request) components.

.NET 7.0 (and prior) does not support named instances, however, .NET 8.0 does support "keyed instances", which is exactly what we need to achieve this using keys. (one known "key" for the platform stuff, and no key for everything else). But it means that when dependencies are resolved, the component resolving them must use different API's to do it, and be explicit about it.

While we can use whatever workaround to achieve this same thing, developers need to understand the implications of these scopes and why they are different.

We need a clear abstraction that speaks to this and make it first-class.

Current Solution

We have started on creating extension methods on IServiceCollection and on IServiceProvider such as: services.RegisterPlatform and container.ResolveForPlatform() but the current abstraction is not well thought out and descriptive enough.

The implementation is also a little janky, since it wraps the desired dependency in other type, so it can co-exist in the container. For example, the platform version of the IDataStore adapter would be in the container as a singleton IPlatformDependency<IDataStore> whereas the tenanted version is in the container as just IDataStore (either as a singleton or scoped).

Future Solutions

We could create an abstraction over the top of dotnet abstractions to make this clear, but the disadvantage is having to re-educate developers familiar with dotnet APIs already.

We need to find a balance between usability and descriptiveness so that developers are not using the wrong lifetime and scopes for their dependencies in each subdomain.