Skip to content

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

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

Open
wants to merge 6 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
102 changes: 102 additions & 0 deletions docs/ConfigurationAndUsage.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,108 @@ The `TenantResolver` options are configured in the `AddMultiTenant<TTenantInfo>`
contains the resolved multi-tenant context and can be changed by the event handler to override the resolver's
result.

## ASP.NET Core Features

Some additional features are avaiable to tailor the middleware to your specific needs.

### Exclude Endpoints From Tenant Resolution

If you have scenarios where you do not want Tenant resolution to be performed, you can exclude specific endpoints.

Using the `IEndpointConventionBuilder` extension `ExcludeFromMultiTenantResolution`:

```csharp
var app = builder.Build();

// Exclude OpenApi endpoints.
app.MapOpenApi()
.ExcludeFromMultiTenantResolution();

// Exclude a specific endpoint.
app.MapGet("/oops", () => "Oops! An error happened.")
.ExcludeFromMultiTenantResolution();

// Exclude a group of endpoints.
app.MapGroup("api/v{version:apiVersion}/dashboard")
.ExcludeFromMultiTenantResolution();

// Exclude static asset endpoints.
app.MapStaticAssets()
.ExcludeFromMultiTenantResolution();

app.Run();
```

Using the `ExcludeFromMultiTenantResolutionAttribute` attribute to configure the behavior of controllers and action methods.

```csharp
// Here --> [ExcludeFromMultiTenantResolution]
public class DashboardController : Controller
{
// Or here --> [ExcludeFromMultiTenantResolution]
public ActionResult Index()
{
return View();
}
}
```

### Short Curcuiting

If you would like to short circuit for any reason, ending the request handling process early from within the `MultiTenantMiddleware`.
The `ShortCircuitWhenTenantNotResolved<TTenantInfo()` or `ShortCircuitWhen<TTenantInfo>()` methods can be called after
`AddMultiTenant<TTenantInfo>()`.

DISCLAIMER: If you short circuit when tenant not resolved, and you have endpoints that do not require a tenant,
then `ExcludeFromMultiTenantResolution` becomes a necessity, otherwise, these endpoints would never be reached.

## Short Circuit When Tenant Not resolved

`MultiTenantOptions` provides convenient events, such as `OnTenantResolveCompleted`, giving you the flexibility to take action during resolution
when no tenant was found. However, these events have no inherit way to direct the `MultiTenantMiddleware` to take any specific actions.
In the case you want to short circuit when a tenant is not found, use the `MultiTenantBuilder<TTenantInfo>` extension
`ShortCircuitWhenTenantNotResolved()`.

```csharp
// Simply short circuit the request, ending request handling.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHostStrategy()
.WithConfigurationStore()
.ShortCircuitWhenTenantNotResolved();

// Short circuit and redirect to a specific Uri.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHostStrategy()
.WithConfigurationStore()
.ShortCircuitWhenTenantNotResolved(new Uri("/tenant/notfound", UriKind.Relative));
```

## Short Circuit When

If you find that you need to short circuit for other, more advanced reasons,
use the `MultiTenantBuilder<TTenantInfo>` extension `ShortCircuitWhen()`.

```csharp
// Advanced short circuiting, if an obsolete strategy was used or when tenant not resolved.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHostStrategy()
.WithConfigurationStore()
.ShortCircuitWhen(config =>
{
config.Predicate = context => context.StrategyInfo is IMyCustomObsoleteStrategy || !context.IsResolved;
});

// Including a redirect.
builder.Services.AddMultiTenant<TenantInfo>()
.WithHostStrategy()
.WithConfigurationStore()
.ShortCircuitWhen(config =>
{
config.Predicate = context => context.StrategyInfo is IMyCustomObsoleteStrategy || !context.IsResolved;
config.RedirectTo = new Uri("/tenant/notfound", UriKind.Relative)
});
```

## Getting the Current Tenant

There are several ways an app can see the current tenant:
Expand Down
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 @@ -23,6 +23,59 @@ 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="configureOptions">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> configureOptions)
where TTenantInfo : class, ITenantInfo, new()
{
builder.Services.Configure(configureOptions);

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()
{
return builder.ShortCircuitWhen(config =>
{
config.Predicate = context => !context.IsResolved;
});
}

/// <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>
/// <param name="redirectTo">A <see cref="Uri"/> to redirect the request to, if short circuited.</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, Uri redirectTo)
where TTenantInfo : class, ITenantInfo, new()
{
return builder.ShortCircuitWhen(config =>
{
config.Predicate = context => !context.IsResolved;
config.RedirectTo = redirectTo;
});
}

/// <summary>
/// Configures authentication options to enable per-tenant behavior.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
// Refer to the solution LICENSE file for more information.

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 @@ -13,14 +16,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 @@ -29,7 +45,10 @@ public async Task Invoke(HttpContext context)
var multiTenantContext = await resolver.ResolveAsync(context).ConfigureAwait(false);
mtcSetter.MultiTenantContext = multiTenantContext;
context.Items[typeof(IMultiTenantContext)] = multiTenantContext;

await next(context).ConfigureAwait(false);
}

if (options?.Predicate is null || !options.Predicate(multiTenantContext))
await next(context);
else if (options.RedirectTo is not null)
context.Response.Redirect(options.RedirectTo.ToString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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;
}
}

/// <summary>
/// A <see cref="Uri"/> to redirect the request to, if short circuited.
/// </summary>
public Uri? RedirectTo { get; set; }
}
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; }
}
Loading