Every SaaS system is driven by configuration.
In order to run these systems is different environments (e.g., Local
, Staging
and Production
), different configuration is required, so that we can protect ourselves when developing a running system.
- We want all components in each runtime host (e.g.,
ApiHost1
) to have access to static configuration (read-only), that would be set at deployment time. (i.e., in static files likeappsettings.json
) - We want that configuration to be over-written into static packaged assets (like
appsettings.json
) just before deployment, in the CD pipeline. Rather then as design time by a developer, so we avoid storing these settings in source code, or expose them to unprivileged people designing the system. - We want that configuration to be specific to a specific environment (e.g.
Local
development,Staging
, orProduction
) - We do not want developers writing anything but
local
development environment settings (secrets or otherwise) into configuration files. With one exception, see note below. - We will need some "shared" configuration for the SaaS "platform" (used by all components), and some "private" configuration used by each "tenant" running on the platform. These two sets of configuration must be kept separate from each other, and may not be stored in the same repositories. (e.g. platform configuration is defined in
appsettings.json
, whilst tenancy configuration is stored in a data stores) - Configuration needs to be hierarchical (e.g. it can be grouped by namespace), and hierarchical in terms of layering.
- Settings are expected to be of only 3 kinds:
string
,number
andboolean
- Components are responsible for reading their own configuration, and shall not re-use other components configuration.
- Secrets may be stored separately from non-confidential configuration in other repositories (e.g. files, databases, 3rd party services).
- We want to be able to change storage location of configuration at any time, without breaking code (e.g. files, databases, 3rd party services).
- We want to use dependency injection to give components their configuration.
Point 4 above, with one exception: the configuration used to configure components for integration testing against real 3rd party systems (i.e. in tests of the category:
Integration.External
). These 3rd party accounts/environments, should never be related to production environments, and are designed only for testing-only. Configuration (and especially any secrets) used for these accounts/environments can NEVER lead those with access to them to compromise the system or its integrity.
The IConfigurationSettings
abstraction is used to give access to configuration for both Platform
settings and Tenancy
settings.
It is injected into any adapters that require access to any configuration settings.
In order to operate effectively at runtime, the selection mechanism of whether to use a Tenancy
setting or whether to use a Platform
setting must be dynamic at runtime and must be dependent on whether the actual inbound HTTP request is destined for a specific tenant or not.
For these reasons, the AspNetDynamicConfigurationSettings
adapter is to be used.
This adapter is injected as both a "singleton" and a "scoped" in the DI container, depending on what adapters need which version of it.
Platform settings are "shared" across all components running in the platform, regardless of the lifetime of the dependency.
For example:
- Connection strings to centralized repositories (for hosting data pertaining to all tenants on the platform)
- Account details for accessing shared 3rd party system accounts via adapters (e.g., an email provider)
- Keys and defaults for various application and domain services
Most of these settings will be stored in standard places that are supported by the .NET runtime, such as
appsettings.json
files for the specific environment.
Tenancy settings are "private" and are specific to a tenant running on the platform, they are only applicable to "scoped" dependencies.
For example:
- Connection strings to a tenant's physically partitioned repository (e.g., in a nearby data center of their choice)
- Account details for accessing a specific 3rd party system account via adapters (e.g., an accounting integration)
At runtime, in a multi-tenanted host, when the inbound HTTP request is destined for an API that is tenanted, the ITenantContext
will define the tenancy and it will define the ITenancyContext.Settings
for the current HTTP request.
These settings are read from the IOrganizationsRepository
(i.e., a data store), and can be updated by other APIs.
These settings are never to be accidentally accessed by or exposed to other tenants running on the platform.