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

feat: Add ExcludeFromMultiTenantResolution() and ExcludeFromMultiTenantResolutionAttribute for endpoints. #935

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
@@ -0,0 +1,27 @@
using Finbuckle.MultiTenant.AspNetCore.Routing;
using Microsoft.AspNetCore.Builder;

namespace Finbuckle.MultiTenant.AspNetCore.Extensions;

public static class EndpointConventionBuilderExtensions
{
private static readonly ExcludeFromMultiTenantResolutionAttribute s_excludeFromMultiTenantResolutionAttribute = new();

/// <summary>
/// Adds the <see cref="IExcludeFromMultiTenantResolutionMetadata"/> to <see cref="EndpointBuilder.Metadata"/>
/// for all endpoints produced by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <returns>A <see cref="IEndpointConventionBuilder"/> that can be used to further customize the endpoint.</returns>
public static TBuilder ExcludeFromMultiTenantResolution<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder
=> builder.WithMetadata(s_excludeFromMultiTenantResolutionAttribute);

/// <summary>
/// Adds the <see cref="IExcludeFromMultiTenantResolutionMetadata"/> to <see cref="EndpointBuilder.Metadata"/>
/// for all endpoints produced by the <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="RouteHandlerBuilder"/>.</param>
/// <returns>A <see cref="RouteHandlerBuilder"/> that can be used to further customize the endpoint.</returns>
public static RouteHandlerBuilder ExcludeFromMultiTenantResolution(this RouteHandlerBuilder builder)
=> ExcludeFromMultiTenantResolution<RouteHandlerBuilder>(builder);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,39 @@ namespace Finbuckle.MultiTenant;
/// </summary>
public static class MultiTenantBuilderExtensions
{
/// <summary>
/// Configures a callback that determines when endpoints should be short circuited
/// during MultiTenant resolution.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> instance.</param>
/// <param name="options">The short circuit options to configure.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> ShortCircuitWhen<TTenantInfo>(
this MultiTenantBuilder<TTenantInfo> builder, Action<ShortCircuitWhenOptions> options)
where TTenantInfo : class, ITenantInfo, new()
{
builder.Services.Configure(options);

return builder;
}

/// <summary>
/// Configures endpoints to be short circuited during MultiTenant resolution when
/// no Tenant was resolved.
/// </summary>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <param name="builder">The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> instance.</param>
/// <returns>The <see cref="MultiTenantBuilder&lt;TTenantInfo&gt;"/> so that additional calls can be chained.</returns>
public static MultiTenantBuilder<TTenantInfo> ShortCircuitWhenTenantNotResolved<TTenantInfo>(
this MultiTenantBuilder<TTenantInfo> builder)
where TTenantInfo : class, ITenantInfo, new()
{
builder.Services.Configure<ShortCircuitWhenOptions>(config => config.Predicate = context => !context.IsResolved);

return builder;
}

/// <summary>
/// Configures authentication options to enable per-tenant behavior.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@

using System.Threading.Tasks;
using Finbuckle.MultiTenant.Abstractions;
using Finbuckle.MultiTenant.AspNetCore.Options;
using Finbuckle.MultiTenant.AspNetCore.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Finbuckle.MultiTenant.AspNetCore.Internal;

Expand All @@ -14,14 +17,27 @@ namespace Finbuckle.MultiTenant.AspNetCore.Internal;
public class MultiTenantMiddleware
{
private readonly RequestDelegate next;
private readonly ShortCircuitWhenOptions? options;

public MultiTenantMiddleware(RequestDelegate next)
{
this.next = next;
}

public MultiTenantMiddleware(RequestDelegate next, IOptions<ShortCircuitWhenOptions> options)
{
this.next = next;
this.options = options.Value;
}

public async Task Invoke(HttpContext context)
{
if (context.GetEndpoint()?.Metadata.GetMetadata<IExcludeFromMultiTenantResolutionMetadata>() is { ExcludeFromResolution: true })
{
await next(context);
return;
}

context.RequestServices.GetRequiredService<IMultiTenantContextAccessor>();
var mtcSetter = context.RequestServices.GetRequiredService<IMultiTenantContextSetter>();

Expand All @@ -30,7 +46,8 @@ public async Task Invoke(HttpContext context)
var multiTenantContext = await resolver.ResolveAsync(context);
mtcSetter.MultiTenantContext = multiTenantContext;
context.Items[typeof(IMultiTenantContext)] = multiTenantContext;

await next(context);

if (options?.Predicate is null || !options.Predicate(multiTenantContext))
await next(context);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Finbuckle.MultiTenant.Abstractions;

namespace Finbuckle.MultiTenant.AspNetCore.Options;

public class ShortCircuitWhenOptions
{
private Func<IMultiTenantContext, bool>? _predicate;

/// <summary>
/// The callback that determines if the endpoint should be short circuited.
/// </summary>
public Func<IMultiTenantContext, bool>? Predicate
{
get
{
return _predicate;
}
set
{
ArgumentNullException.ThrowIfNull(value);

_predicate = value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http;

namespace Finbuckle.MultiTenant.AspNetCore.Routing;

/// <summary>
/// Indicates that this <see cref="Endpoint"/> should be excluded from MultiTenant resolution.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)]
public class ExcludeFromMultiTenantResolutionAttribute : Attribute, IExcludeFromMultiTenantResolutionMetadata
{
/// <inheritdoc />
public bool ExcludeFromResolution => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Http;

namespace Finbuckle.MultiTenant.AspNetCore.Routing;

/// <summary>
/// Indicates whether MultiTenant resolution should occur for this <see cref="Endpoint"/>.
/// </summary>
public interface IExcludeFromMultiTenantResolutionMetadata
{
/// <summary>
/// Gets a value indicating whether MultiTenant resolution should
/// occur for this <see cref="Endpoint"/>. If <see langword="true"/>,
/// tenant resolution will not be executed.
/// </summary>
bool ExcludeFromResolution { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
using System.Threading.Tasks;
using Finbuckle.MultiTenant.Abstractions;
using Finbuckle.MultiTenant.AspNetCore.Internal;
using Finbuckle.MultiTenant.AspNetCore.Options;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;

Expand All @@ -15,7 +18,7 @@
public class MultiTenantMiddlewareShould
{
[Fact]
public async void SetHttpContextItemIfTenantFound()

Check warning on line 21 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 21 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0, ubuntu-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 21 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 21 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0, windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var services = new ServiceCollection();
services.AddMultiTenant<TenantInfo>().
Expand All @@ -27,6 +30,7 @@

var context = new Mock<HttpContext>();
context.Setup(c => c.RequestServices).Returns(sp);
context.Setup(c => c.Features).Returns(new FeatureCollection());

var itemsDict = new Dictionary<object, object?>();
context.Setup(c => c.Items).Returns(itemsDict);
Expand All @@ -41,8 +45,47 @@
Assert.Equal("initech", mtc.TenantInfo.Id);
}

[Fact]
public async void NotShortCircuitIfTenantFound()

Check warning on line 49 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 49 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0, ubuntu-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 49 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 49 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0, windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var services = new ServiceCollection();
services.AddMultiTenant<TenantInfo>().
WithStaticStrategy("initech").
WithInMemoryStore();
var sp = services.BuildServiceProvider();
var store = sp.GetRequiredService<IMultiTenantStore<TenantInfo>>();
await store.TryAddAsync(new TenantInfo { Id = "initech", Identifier = "initech" });

var context = new Mock<HttpContext>();
context.Setup(c => c.RequestServices).Returns(sp);
context.Setup(c => c.Features).Returns(new FeatureCollection());

var itemsDict = new Dictionary<object, object?>();
context.Setup(c => c.Items).Returns(itemsDict);

var options = new ShortCircuitWhenOptions { Predicate = context => !context.IsResolved };
var optionsMock = new Mock<IOptions<ShortCircuitWhenOptions>>();
optionsMock.Setup(c => c.Value).Returns(options);

var calledNext = false;
var mw = new MultiTenantMiddleware(_ =>
{
calledNext = true;

return Task.CompletedTask;
}, optionsMock.Object);

await mw.Invoke(context.Object);

var mtc = (IMultiTenantContext<TenantInfo>?)context.Object.Items[typeof(IMultiTenantContext)];

Assert.NotNull(mtc?.TenantInfo);
Assert.Equal("initech", mtc.TenantInfo.Id);
Assert.True(calledNext);
}

[Fact]
public async void SetTenantAccessor()

Check warning on line 88 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)

Check warning on line 88 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (8.0, windows-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var services = new ServiceCollection();
services.AddMultiTenant<TenantInfo>().
Expand All @@ -54,7 +97,8 @@

var context = new Mock<HttpContext>();
context.Setup(c => c.RequestServices).Returns(sp);

context.Setup(c => c.Features).Returns(new FeatureCollection());

var itemsDict = new Dictionary<object, object?>();
context.Setup(c => c.Items).Returns(itemsDict);

Expand All @@ -77,7 +121,7 @@
}

[Fact]
public async void NotSetTenantAccessorIfNoTenant()

Check warning on line 124 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var services = new ServiceCollection();
services.AddMultiTenant<TenantInfo>().
Expand All @@ -89,7 +133,8 @@

var context = new Mock<HttpContext>();
context.Setup(c => c.RequestServices).Returns(sp);

context.Setup(c => c.Features).Returns(new FeatureCollection());

var itemsDict = new Dictionary<object, object?>();
context.Setup(c => c.Items).Returns(itemsDict);

Expand All @@ -110,4 +155,44 @@
Assert.False(mtc.IsResolved);
Assert.Null(mtc.TenantInfo);
}

[Fact]
public async void ShortCircuitIfNoTenant()

Check warning on line 160 in test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantMiddlewareShould.cs

View workflow job for this annotation

GitHub Actions / build-and-test (9.0, macos-latest)

Support for 'async void' unit tests is being removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead. (https://xunit.net/xunit.analyzers/rules/xUnit1048)
{
var services = new ServiceCollection();
services.AddMultiTenant<TenantInfo>().
WithStaticStrategy("not_initech").
WithInMemoryStore();
var sp = services.BuildServiceProvider();
var store = sp.GetRequiredService<IMultiTenantStore<TenantInfo>>();
await store.TryAddAsync(new TenantInfo { Id = "initech", Identifier = "initech" });

var context = new Mock<HttpContext>();
context.Setup(c => c.RequestServices).Returns(sp);
context.Setup(c => c.Features).Returns(new FeatureCollection());

var itemsDict = new Dictionary<object, object?>();
context.Setup(c => c.Items).Returns(itemsDict);

var options = new ShortCircuitWhenOptions { Predicate = context => !context.IsResolved };
var optionsMock = new Mock<IOptions<ShortCircuitWhenOptions>>();
optionsMock.Setup(c => c.Value).Returns(options);

var calledNext = false;
var mw = new MultiTenantMiddleware(_ =>
{
calledNext = true;

return Task.CompletedTask;
}, optionsMock.Object);

await mw.Invoke(context.Object);

var mtc = (IMultiTenantContext<TenantInfo>?)context.Object.Items[typeof(IMultiTenantContext)];

Assert.NotNull(mtc);
Assert.False(mtc.IsResolved);
Assert.Null(mtc.TenantInfo);
Assert.False(calledNext);
}
}
Loading
Loading