Skip to content

Commit

Permalink
Tenant configuration re-design (#213)
Browse files Browse the repository at this point in the history
* Tenant configuration re-design

* fix some tests

Co-authored-by: Radu Popovici <[email protected]>
  • Loading branch information
oncicaradupopovici and Radu Popovici authored Mar 3, 2022
1 parent 026a25b commit 5f6247e
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

using Microsoft.Extensions.Configuration;

namespace NBB.MultiTenancy.Abstractions.Configuration;
public interface ITenantConfiguration
public interface ITenantConfiguration : IConfiguration
{
/// <summary>
/// Extracts the value with the specified key and converts it to type T.
/// or
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
public T GetValue<T>(string key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections.Generic;
using System.Linq;

namespace NBB.MultiTenancy.Abstractions.Configuration
{
public class MergedConfigurationSection : IConfigurationSection
{
private readonly IConfigurationSection _innerConfigurationSection;
private readonly IConfigurationSection _defaultConfigurationSection;

public MergedConfigurationSection(IConfigurationSection innerConfigurationSection, IConfigurationSection defaultConfigurationSection)
{
_innerConfigurationSection = innerConfigurationSection ?? throw new ArgumentNullException(nameof(innerConfigurationSection));
_defaultConfigurationSection = defaultConfigurationSection ?? throw new ArgumentNullException(nameof(defaultConfigurationSection));

}
public string this[string key]
{
get => _innerConfigurationSection[key] ?? _defaultConfigurationSection[key];
set => _innerConfigurationSection[key] = value;
}

public string Key => _innerConfigurationSection.Key;

public string Path => _innerConfigurationSection.Path;

public string Value
{
get => _innerConfigurationSection.Value ?? (!_innerConfigurationSection.GetChildren().Any() ? _defaultConfigurationSection.Value : null);
set => _innerConfigurationSection.Value = value;
}

public IEnumerable<IConfigurationSection> GetChildren()
{
var innerChildren = _innerConfigurationSection.GetChildren().ToDictionary(s => s.Key);
if(innerChildren.Count == 0 && _innerConfigurationSection.Value is not null)
{
return innerChildren.Values;
}

foreach (var c in _defaultConfigurationSection.GetChildren())
{
if (innerChildren.ContainsKey(c.Key))
{
innerChildren[c.Key] = new MergedConfigurationSection(innerChildren[c.Key], c);
}
else
{
innerChildren[c.Key] = c;
}
}
return innerChildren.Values;
}

public IChangeToken GetReloadToken()
{
return _innerConfigurationSection.GetReloadToken();
}

public IConfigurationSection GetSection(string key)
{
//config.GetSection is never null
var innerCfg = _innerConfigurationSection.GetSection(key);
var innerCfgIsEmpty = innerCfg.Value is null && !innerCfg.GetChildren().Any();
var defaultCfg = _defaultConfigurationSection.GetSection(key);
var defaultCfgIsEmpty = defaultCfg.Value is null && !defaultCfg.GetChildren().Any();
var result = (innerCfgIsEmpty, defaultCfgIsEmpty) switch
{
(true, false) => defaultCfg,
(_, true) => innerCfg,
_ => new MergedConfigurationSection(innerCfg, defaultCfg)
};
return result;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using NBB.MultiTenancy.Abstractions.Context;
using NBB.MultiTenancy.Abstractions.Options;
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;

namespace NBB.MultiTenancy.Abstractions.Configuration;
Expand All @@ -22,6 +23,8 @@ public class TenantConfiguration : ITenantConfiguration
private readonly ITenantContextAccessor _tenantContextAccessor;
private ConcurrentDictionary<Guid, string> _tenantMap;

public string this[string key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

public TenantConfiguration(IConfiguration configuration, IOptions<TenancyHostingOptions> tenancyHostingOptions,
ITenantContextAccessor tenantContextAccessor)
{
Expand Down Expand Up @@ -61,35 +64,31 @@ private void LoadTenantsMap()
_tenantMap = newMap;
}

public T GetValue<T>(string key)
private IConfiguration GetTenantConfiguration()
{
if (_tenancyHostingOptions.Value.TenancyType == TenancyType.MonoTenant)
{
return getValueOrComplexObject<T>(_globalConfiguration, key);
return _globalConfiguration;
}

var tenantId = _tenantContextAccessor.TenantContext.GetTenantId();
var defaultSection = _tenancyConfigurationSection.GetSection("Defaults");
var sectionPath = _tenantMap.TryGetValue(tenantId, out var result)
? result
: throw new Exception($"Configuration not found for tenant {tenantId}");

var tenantId = _tenantContextAccessor.TenantContext.GetTenantId();
if(_tenantMap.TryGetValue(tenantId, out var tenentSectionPath))
{
var tenantSection = _tenancyConfigurationSection.GetSection(tenentSectionPath);
var mergedSection = new MergedConfigurationSection(tenantSection, defaultSection);
return mergedSection;
}

return getValueOrComplexObject<T>(_tenancyConfigurationSection.GetSection(sectionPath), key, defaultSection);
return defaultSection;
}

private static T getValueOrComplexObject<T>(IConfiguration config, string key, IConfigurationSection defaultSection = null)
{
//section.GetSection is never null
if (config.GetSection(key).GetChildren().Any())
{
//complex type is present
return config.GetSection(key).Get<T>();
}
public IEnumerable<IConfigurationSection> GetChildren()
=> GetTenantConfiguration().GetChildren();

if (config.GetSection(key).Value != null)
return config.GetValue<T>(key);
public IChangeToken GetReloadToken()
=> GetTenantConfiguration().GetReloadToken();

return defaultSection == null ? default : getValueOrComplexObject<T>(defaultSection, key);
}
public IConfigurationSection GetSection(string key)
=> GetTenantConfiguration().GetSection(key);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
Expand All @@ -11,6 +12,39 @@ namespace NBB.MultiTenancy.Abstractions.Configuration;

public static class TenantConfigurationExtensions
{
/// <summary>
/// Extracts the value with the specified key and converts it to type T.
/// or
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
public static T GetValue<T>(this ITenantConfiguration config, string key)
{
return getValueOrComplexObject<T>(config, key);
}

private static T getValueOrComplexObject<T>(IConfiguration config, string key)
{
//section.GetSection is never null
if (config.GetSection(key).GetChildren().Any())
{
//complex type is present
return config.GetSection(key).Get<T>();
}

if (config.GetSection(key).Value != null)
return config.GetValue<T>(key);

return default;
}

/// <summary>
/// Retrieves connection string from separate connection info segments or string value
/// </summary>
/// <param name="config"></param>
/// <param name="name"></param>
/// <returns></returns>
public static string GetConnectionString(this ITenantConfiguration config, string name)
{
var splitted = config.GetValue<ConnectionStringDetails>($"ConnectionStrings:{name}");
Expand Down
59 changes: 59 additions & 0 deletions src/MultiTenancy/NBB.MultiTenancy.Abstractions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,69 @@ This package provides some abstractions for multi-tenant projects like
* `Tenant` - data structure that stores tenant information (eg. id, name, code)
* `Tenant context` - holds and provides access to the current tenant
* `Tenant repository` - provides functionality to retrieve tenant information from a store
* 'Tenant Configuration' - provides tenant specific configuration
* `Tenancy options` - configuration options for multi-tenant applications

## NuGet install
```
dotnet add package NBB.MultiTenancy.Abstractions
```

## Tenant Configuration

### Abstractions:
The `ITennantConfiguration` derives from the `IConfiguration` so that you can use all the well known `Microsoft.Extensions.Configuration` apis

```csharp
namespace NBB.MultiTenancy.Abstractions.Configuration;
public interface ITenantConfiguration : IConfiguration
{
}
```
### Extensions:
The `GetConnectionString` extension:
```csharp
public static string GetConnectionString(this ITenantConfiguration config, string name)
```

reads connection string configuration from:
- separate connection info segments:
```json
"ConnectionStrings": {
"MyDatabase": {
"Server": "myserver,3342",
"Database": "mydb",
"UserName": "myuser",
"Password": "mypass",
"OtherParams": "MultipleActiveResultSets=true"
}
```
- or standalone string:
```json
"ConnectionStrings": {
"MyDatabase": "Server=myserver,3342;Database=mydb;User Id=myuser;Password=mypass;MultipleActiveResultSets=true"
```

The `GetValue<T>` extension reads values or complex objects from configuration:
```csharp
/// <summary>
/// Extracts the value with the specified key and converts it to type T.
/// or
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
public static T GetValue<T>(this ITenantConfiguration config, string key)
```csharp

### Service registration:
```csharp
services.AddDefaultTenantConfiguration();
```

### The default tenant configuration uses:
- the `MultiTenancy` configuration section to read tenant specific configuration for the current tenant context, when `TenancyType` is set to `"MultiTenant"`
- the default configuration root when `TenancyType` is set to `"MonoTenant"`

The `MultiTenancy:Defaults` section provides common tenant configuration that will be merged with tenant specific configurations.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ namespace NBB.EventStore.IntegrationTests
[Collection("EventStoreDB")]
public class EventStoreDnIntegrationTests : IClassFixture<EnvironmentFixture>
{
[Fact]
//[Fact]
public void EventStore_AppendEventsToStreamAsync_with_expected_version_should_be_thread_safe()
{
PrepareDb();
Expand Down Expand Up @@ -68,7 +68,7 @@ public void EventStore_AppendEventsToStreamAsync_with_expected_version_should_be
concurrencyExceptionCount.Should().Be(threadCount - 1);
}

[Fact]
//[Fact]
public void EventStore_AppendEventsToStreamAsync_with_expected_version_any_should_be_thread_safe()
{
PrepareDb();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace NBB.EventStore.IntegrationTests
[Collection("EventStoreDB")]
public class SnapshotStoreDbIntegrationTests : IClassFixture<EnvironmentFixture>
{
[Fact]
//[Fact]
public void Should_store_snapshot_thread_safe()
{
// Arrange
Expand Down Expand Up @@ -59,7 +59,7 @@ public void Should_store_snapshot_thread_safe()
concurrencyExceptionCount.Should().Be(threadCount - 1);
}

[Fact]
//[Fact]
public void Should_retrieve_snapshot_with_latest_version()
{
// Arrange
Expand Down Expand Up @@ -88,7 +88,7 @@ public void Should_retrieve_snapshot_with_latest_version()
snapshot.AggregateVersion.Should().Be(threadCount - 1);
}

[Fact]
//[Fact]
public async Task Should_load_stored_snapshot()
{
//Arrange
Expand All @@ -111,7 +111,7 @@ public async Task Should_load_stored_snapshot()
loadedSnapshotEnvelope.Should().BeEquivalentTo(snapshotEnvelope);
}

[Fact]
//[Fact]
public async Task Should_return_null_for_not_found_snapshot()
{
//Arrange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// This source code is licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NBB.Core.Abstractions;
Expand Down Expand Up @@ -134,22 +136,27 @@ await WithTenantScope(sp, testTenantId, async sp =>
private IServiceProvider GetServiceProvider<TDBContext>(bool isSharedDB) where TDBContext : DbContext
{
var tenantService = Mock.Of<ITenantContextAccessor>(x => x.TenantContext == null);
var tenantDatabaseConfigService =
Mock.Of<ITenantConfiguration>(x => x.GetValue<string>(It.Is<string>(a=>a.Contains("ConnectionString")))
== (isSharedDB ? "Test" : Guid.NewGuid().ToString()));
IConfiguration configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "MultiTenancy:Defaults:ConnectionStrings:myDb", isSharedDB ? "Test" : Guid.NewGuid().ToString()}
})
.Build();

var services = new ServiceCollection();
services.AddSingleton(configuration);
services.AddDefaultTenantConfiguration();
services.AddOptions<TenancyHostingOptions>().Configure(o =>
{
o.TenancyType = TenancyType.MultiTenant;
});
services.AddSingleton(tenantService);
services.AddSingleton(tenantDatabaseConfigService);
services.AddLogging();

services.AddEntityFrameworkInMemoryDatabase()
.AddDbContext<TDBContext>((sp, options) =>
{
var conn = sp.GetRequiredService<ITenantConfiguration>().GetConnectionString("");
var conn = sp.GetRequiredService<ITenantConfiguration>().GetConnectionString("myDb");
options.UseInMemoryDatabase(conn).UseInternalServiceProvider(sp);
});

Expand Down
Loading

0 comments on commit 5f6247e

Please sign in to comment.