Skip to content

Commit

Permalink
Options builder support (#672)
Browse files Browse the repository at this point in the history
* Implement support for OptionsBuilder

* Reverted changes to TenantConfigureNamedOptions

* Restored commented test

* Removed dependencies

---------

Co-authored-by: Sam Goldmann <[email protected]>
  • Loading branch information
2 people authored and AndrewTriesToCode committed Nov 14, 2023
1 parent be83d01 commit 7e8c0a0
Show file tree
Hide file tree
Showing 8 changed files with 357 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services)
Action<TOptions, T> tenantConfigureOptions) where TOptions : class, new()
{
// TODO maybe change this to string empty so null an be used for all options, note remarks.
return WithPerTenantNamedOptions(null, tenantConfigureOptions);
return WithPerTenantNamedOptions<TOptions>(null, tenantConfigureOptions);
}

/// <summary>
Expand All @@ -58,32 +58,16 @@ public FinbuckleMultiTenantBuilder(IServiceCollection services)
throw new ArgumentNullException(nameof(tenantConfigureNamedOptions));
}

// Handles multiplexing cached options.
Services.TryAddSingleton<IOptionsMonitorCache<TOptions>, MultiTenantOptionsCache<TOptions, T>>();

// Necessary to apply tenant named options in between configuration and post configuration
Services.AddSingleton<ITenantConfigureNamedOptions<TOptions, T>,
TenantConfigureNamedOptions<TOptions, T>>(_ => new TenantConfigureNamedOptions<TOptions,
T>(name, tenantConfigureNamedOptions));
Services.TryAddTransient<IOptionsFactory<TOptions>, MultiTenantOptionsFactory<TOptions, T>>();
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(BuildOptionsManager<TOptions>);
Services.TryAddSingleton<IOptions<TOptions>>(BuildOptionsManager<TOptions>);
Services.AddPerTenantOptionsCore<TOptions>();
Services.TryAddEnumerable(ServiceDescriptor.Scoped<IConfigureOptions<TOptions>, TenantConfigureNamedOptionsWrapper<TOptions, T>>());
Services.AddScoped<ITenantConfigureNamedOptions<TOptions, T>>(sp => new TenantConfigureNamedOptions<TOptions, T>(name, tenantConfigureNamedOptions));

return this;
}

// TODO consider per tenant AllOptions variation
// TODO consider per-tenant post options
// TODO consider OptionsBuilder api

private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp)
where TOptions : class, new()
{
var cache = (IOptionsMonitorCache<TOptions>)ActivatorUtilities.CreateInstance(sp,
typeof(MultiTenantOptionsCache<TOptions, T>));
return (MultiTenantOptionsManager<TOptions>)
ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager<TOptions>), cache);
}


/// <summary>
/// Adds and configures an IMultiTenantStore to the application using default dependency injection.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
using System.Linq;
using Finbuckle.MultiTenant;
using Finbuckle.MultiTenant.Internal;
using Finbuckle.MultiTenant.Options;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;
Expand Down Expand Up @@ -55,6 +58,29 @@ public static FinbuckleMultiTenantBuilder<T> AddMultiTenant<T>(this IServiceColl
return services.AddMultiTenant<T>(_ => { });
}

/// <summary>
/// Gets an options builder that forwards Configure calls for the same named per-tenant <typeparamref name="TOptions"/> to the underlying service collection.
/// </summary>
/// <typeparam name="TOptions">The options type to be configured.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="name">The name of the options instance.</param>
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that configure calls can be chained in it.</returns>
public static OptionsBuilder<TOptions> AddPerTenantOptions<TOptions>(this IServiceCollection services, string? name) where TOptions : class, new()
{

services.AddPerTenantOptionsCore<TOptions>();
return new OptionsBuilder<TOptions>(services, name);
}

/// <summary>
/// Gets an options builder that forwards Configure calls for the same per-tenant <typeparamref name="TOptions"/> to the underlying service collection.
/// </summary>
/// <typeparam name="TOptions">The options type to be configured.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that configure calls can be chained in it.</returns>
public static OptionsBuilder<TOptions> AddPerTenantOptions<TOptions>(this IServiceCollection services) where TOptions : class, new() =>
services.AddPerTenantOptions<TOptions>(Options.Options.DefaultName);

public static bool DecorateService<TService, TImpl>(this IServiceCollection services, params object[] parameters)
{
var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService));
Expand Down Expand Up @@ -118,4 +144,27 @@ public static FinbuckleMultiTenantBuilder<T> AddMultiTenant<T>(this IServiceColl

return true;
}

internal static void AddPerTenantOptionsCore<TOptions>(this IServiceCollection services) where TOptions : class, new()
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

// Handles multiplexing cached options.
services.TryAddSingleton<IOptionsMonitorCache<TOptions>, MultiTenantOptionsCache<TOptions>>();
services.TryAddTransient<IOptionsFactory<TOptions>, MultiTenantOptionsFactory<TOptions>>();
services.TryAddScoped<IOptionsSnapshot<TOptions>>(BuildOptionsManager<TOptions>);
services.TryAddSingleton<IOptions<TOptions>>(BuildOptionsManager<TOptions>);
}

private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp)
where TOptions : class, new()
{
var cache = (IOptionsMonitorCache<TOptions>)ActivatorUtilities.CreateInstance(sp,
typeof(MultiTenantOptionsCache<TOptions>));
return (MultiTenantOptionsManager<TOptions>)
ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager<TOptions>), cache);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright Finbuckle LLC, Andrew White, and Contributors.
// Refer to the solution LICENSE file for more information.

using Microsoft.Extensions.Options;

namespace Finbuckle.MultiTenant.Options;

interface ITenantConfigureNamedOptionsWrapper<TOptions> : IConfigureNamedOptions<TOptions>
where TOptions : class, new()
{
}
3 changes: 2 additions & 1 deletion src/Finbuckle.MultiTenant/Options/ITenantConfigureOptions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright Finbuckle LLC, Andrew White, and Contributors.
// Refer to the solution LICENSE file for more information.

using Microsoft.Extensions.Options;
using System;

namespace Finbuckle.MultiTenant.Options;
Expand All @@ -11,4 +12,4 @@ public interface ITenantConfigureOptions<TOptions, TTenantInfo>
where TTenantInfo : class, ITenantInfo, new()
{
void Configure(TOptions options, TTenantInfo tenantInfo);
}
}
107 changes: 106 additions & 1 deletion src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,111 @@

namespace Finbuckle.MultiTenant.Options;

/// <summary>
/// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext.
/// </summary>
public class MultiTenantOptionsCache<TOptions> : IOptionsMonitorCache<TOptions>
where TOptions : class
{
private readonly IMultiTenantContextAccessor multiTenantContextAccessor;

// The object is just a dummy because there is no ConcurrentSet<T> class.
//private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, object>> _adjustedOptionsNames =
// new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();

private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();

/// <summary>
/// Constructs a new instance of MultiTenantOptionsCache.
/// </summary>
/// <param name="multiTenantContextAccessor"></param>
/// <exception cref="ArgumentNullException"></exception>
public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor)
{
this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor));
}

/// <summary>
/// Clears all cached options for the current tenant.
/// </summary>
public void Clear()
{
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());

cache.Clear();
}

/// <summary>
/// Clears all cached options for the given tenant.
/// </summary>
/// <param name="tenantId">The Id of the tenant which will have its options cleared.</param>
public void Clear(string tenantId)
{
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());

cache.Clear();
}

/// <summary>
/// Clears all cached options for all tenants and no tenant.
/// </summary>
public void ClearAll()
{
foreach (var cache in map.Values)
cache.Clear();
}

/// <summary>
/// Gets a named options instance for the current tenant, or adds a new instance created with createOptions.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="createOptions">The factory function for creating the options instance.</param>
/// <returns>The existing or new options instance.</returns>
public TOptions GetOrAdd(string? name, Func<TOptions> createOptions)
{
if (createOptions == null)
{
throw new ArgumentNullException(nameof(createOptions));
}

name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());

return cache.GetOrAdd(name, createOptions);
}

/// <summary>
/// Tries to adds a new option to the cache for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="options">The options instance.</param>
/// <returns>True if the options was added to the cache for the current tenant.</returns>
public bool TryAdd(string? name, TOptions options)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());

return cache.TryAdd(name, options);
}

/// <summary>
/// Try to remove an options instance for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <returns>True if the options was removed from the cache for the current tenant.</returns>
public bool TryRemove(string? name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());

return cache.TryRemove(name);
}
}

/// <summary>
/// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext.
/// </summary>
Expand Down Expand Up @@ -59,7 +164,7 @@ public void Clear(string tenantId)
/// </summary>
public void ClearAll()
{
foreach(var cache in map.Values)
foreach (var cache in map.Values)
cache.Clear();
}

Expand Down
Loading

0 comments on commit 7e8c0a0

Please sign in to comment.