Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Options builder support #672

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public FinbuckleMultiTenantBuilder<T> WithPerTenantOptions<TOptions>(
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<T> WithPerTenantNamedOptions<TOptions>(string
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 bool DecorateService<TService, TImpl>(this IServiceCollection serv

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