Last active
May 15, 2023 07:30
-
-
Save maartenba/77ca6f9cfef50efa96ec to your computer and use it in GitHub Desktop.
ASP.NET MVC 6 / ASP.NET 5 Domain Routing + Tenant Middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public string GetVirtualPath(VirtualPathContext context) | |
{ | |
foreach (var matcherParameter in _matcher.Template.Parameters) | |
{ | |
context.Values.Remove(matcherParameter.Name); // make sure none of the domain-placeholders are appended as query string parameters | |
} | |
return _innerRoute.GetVirtualPath(context); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public async Task RouteAsync(RouteContext context) | |
{ | |
EnsureLoggers(context.HttpContext); | |
using (_logger.BeginScope("DomainTemplateRoute.RouteAsync")) | |
{ | |
var requestHost = context.HttpContext.Request.Host.Value; | |
if (IgnorePort && requestHost.Contains(":")) // check if we want to match a port as well | |
{ | |
requestHost = requestHost.Substring(0, requestHost.IndexOf(":")); | |
} | |
var values = _matcher.Match(requestHost); | |
if (values == null) | |
{ | |
// if we got back a null value set, that means the URI did not match | |
return; | |
} | |
var oldRouteData = context.RouteData; | |
var newRouteData = new RouteData(oldRouteData); | |
MergeValues(newRouteData.DataTokens, DataTokens); | |
newRouteData.Routers.Add(_target); | |
MergeValues(newRouteData.Values, values.ToImmutableDictionary()); | |
try | |
{ | |
context.RouteData = newRouteData; | |
// delegate further processing to inner route | |
await _innerRoute.RouteAsync(context); | |
} | |
finally | |
{ | |
// Restore the original values to prevent polluting the route data. | |
if (!context.IsHandled) | |
{ | |
context.RouteData = oldRouteData; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Collections.Immutable; | |
using System.Threading.Tasks; | |
using Microsoft.AspNet.Http; | |
using Microsoft.AspNet.Routing; | |
using Microsoft.AspNet.Routing.Template; | |
using Microsoft.Framework.DependencyInjection; | |
using Microsoft.Framework.Logging; | |
namespace Multitenancy.Routing | |
{ | |
public class DomainTemplateRoute | |
: INamedRouter, IRouter | |
{ | |
private readonly TemplateRoute _innerRoute; | |
private readonly IRouter _target; | |
private readonly string _domainTemplate; | |
private readonly TemplateMatcher _matcher; | |
private ILogger _logger; | |
public DomainTemplateRoute(IRouter target, string domainTemplate, string routeTemplate, bool ignorePort, IInlineConstraintResolver inlineConstraintResolver) | |
: this(target, domainTemplate, routeTemplate, null, null, null, ignorePort, inlineConstraintResolver) | |
{ | |
} | |
public DomainTemplateRoute(IRouter target, string domainTemplate, string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, bool ignorePort, IInlineConstraintResolver inlineConstraintResolver) | |
: this(target, null, domainTemplate, routeTemplate, defaults, constraints, dataTokens, ignorePort, inlineConstraintResolver) | |
{ | |
} | |
public DomainTemplateRoute(IRouter target, string routeName, string domainTemplate, string routeTemplate, IDictionary<string, object> defaults, IDictionary<string, object> constraints, IDictionary<string, object> dataTokens, bool ignorePort, IInlineConstraintResolver inlineConstraintResolver) | |
{ | |
_innerRoute = new TemplateRoute(target, routeName, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver); | |
_target = target; | |
_domainTemplate = domainTemplate; | |
_matcher = new TemplateMatcher( | |
TemplateParser.Parse(DomainTemplate), Defaults); | |
Name = routeName; | |
IgnorePort = ignorePort; | |
} | |
public string Name { get; private set; } | |
public IReadOnlyDictionary<string, object> Defaults | |
{ | |
get | |
{ | |
return _innerRoute.Defaults; | |
} | |
} | |
public IReadOnlyDictionary<string, object> DataTokens | |
{ | |
get | |
{ | |
return _innerRoute.DataTokens; | |
} | |
} | |
public string RouteTemplate | |
{ | |
get | |
{ | |
return _innerRoute.RouteTemplate; | |
} | |
} | |
public IReadOnlyDictionary<string, IRouteConstraint> Constraints | |
{ | |
get | |
{ | |
return _innerRoute.Constraints; | |
} | |
} | |
public string DomainTemplate | |
{ | |
get | |
{ | |
return _domainTemplate; | |
} | |
} | |
public bool IgnorePort { get; set; } | |
public async Task RouteAsync(RouteContext context) | |
{ | |
EnsureLoggers(context.HttpContext); | |
using (_logger.BeginScope("DomainTemplateRoute.RouteAsync")) | |
{ | |
var requestHost = context.HttpContext.Request.Host.Value; | |
if (IgnorePort && requestHost.Contains(":")) | |
{ | |
requestHost = requestHost.Substring(0, requestHost.IndexOf(":")); | |
} | |
var values = _matcher.Match(requestHost); | |
if (values == null) | |
{ | |
if (_logger.IsEnabled(LogLevel.Verbose)) | |
{ | |
_logger.WriteVerbose("DomainTemplateRoute " + Name + " - Host \"" + context.HttpContext.Request.Host + "\" did not match."); | |
} | |
// If we got back a null value set, that means the URI did not match | |
return; | |
} | |
var oldRouteData = context.RouteData; | |
var newRouteData = new RouteData(oldRouteData); | |
MergeValues(newRouteData.DataTokens, DataTokens); | |
newRouteData.Routers.Add(_target); | |
MergeValues(newRouteData.Values, values.ToImmutableDictionary()); | |
try | |
{ | |
context.RouteData = newRouteData; | |
// delegate further processing to inner route | |
await _innerRoute.RouteAsync(context); | |
} | |
finally | |
{ | |
// Restore the original values to prevent polluting the route data. | |
if (!context.IsHandled) | |
{ | |
context.RouteData = oldRouteData; | |
} | |
} | |
} | |
} | |
public string GetVirtualPath(VirtualPathContext context) | |
{ | |
foreach (var matcherParameter in _matcher.Template.Parameters) | |
{ | |
context.Values.Remove(matcherParameter.Name); // make sure none of the domain-placeholders are appended as query string parameters | |
} | |
return _innerRoute.GetVirtualPath(context); | |
} | |
private static void MergeValues(IDictionary<string, object> destination, IReadOnlyDictionary<string, object> values) | |
{ | |
foreach (var kvp in values) | |
{ | |
// This will replace the original value for the specified key. | |
// Values from the matched route will take preference over previous | |
// data in the route context. | |
destination[kvp.Key] = kvp.Value; | |
} | |
} | |
private void EnsureLoggers(HttpContext context) | |
{ | |
if (_logger == null) | |
{ | |
var factory = context.RequestServices.GetRequiredService<ILoggerFactory>(); | |
_logger = factory.Create<TemplateRoute>(); | |
} | |
} | |
public override string ToString() | |
{ | |
return _domainTemplate + "/" + RouteTemplate; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using Microsoft.AspNet.Routing; | |
using Microsoft.Framework.DependencyInjection; | |
namespace Multitenancy.Routing | |
{ | |
public static class DomainTemplateRouteBuilderExtensions | |
{ | |
public static IRouteBuilder MapDomainRoute(this IRouteBuilder routeCollectionBuilder, string name, string domainTemplate, string routeTemplate) | |
{ | |
MapDomainRoute(routeCollectionBuilder, name, domainTemplate, routeTemplate, (object)null); | |
return routeCollectionBuilder; | |
} | |
public static IRouteBuilder MapDomainRoute(this IRouteBuilder routeCollectionBuilder, string name, string domainTemplate, string routeTemplate, object defaults, bool ignorePort = true) | |
{ | |
return MapDomainRoute(routeCollectionBuilder, name, domainTemplate, routeTemplate, defaults, null, ignorePort); | |
} | |
public static IRouteBuilder MapDomainRoute(this IRouteBuilder routeCollectionBuilder, string name, string domainTemplate, string routeTemplate, object defaults, object constraints, bool ignorePort = true) | |
{ | |
return MapDomainRoute(routeCollectionBuilder, name, domainTemplate, routeTemplate, defaults, constraints, null, ignorePort); | |
} | |
public static IRouteBuilder MapDomainRoute(this IRouteBuilder routeCollectionBuilder, string name, string domainTemplate, string routeTemplate, object defaults, object constraints, object dataTokens, bool ignorePort = true) | |
{ | |
if (routeCollectionBuilder.DefaultHandler == null) | |
throw new InvalidOperationException("Default handler must be set."); | |
var inlineConstraintResolver = routeCollectionBuilder.ServiceProvider.GetRequiredService<IInlineConstraintResolver>(); | |
routeCollectionBuilder.Routes.Add(new DomainTemplateRoute(routeCollectionBuilder.DefaultHandler, name, domainTemplate, routeTemplate, ObjectToDictionary(defaults), ObjectToDictionary(constraints), ObjectToDictionary(dataTokens), ignorePort, inlineConstraintResolver)); | |
return routeCollectionBuilder; | |
} | |
private static IDictionary<string, object> ObjectToDictionary(object value) | |
{ | |
return value as IDictionary<string, object> ?? new RouteValueDictionary(value); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Multitenancy.Features | |
{ | |
public interface ITenantFeature | |
{ | |
Tenant Tenant { get; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"authors": [ "Maarten Balliauw <[email protected]>" ], | |
"description": "Domain routing and tenant middleware.", | |
"version": "1.0.0-*", | |
"dependencies": { | |
"Microsoft.AspNet.Mvc": "6.0.0-beta4-12742", | |
"Microsoft.AspNet.Diagnostics": "1.0.0-beta4-12200", | |
"Microsoft.Framework.Logging": "1.0.0-beta4-10854" | |
}, | |
"frameworks": { | |
"aspnet50": { | |
"dependencies": { | |
} | |
}, | |
"aspnetcore50": { | |
"dependencies": { | |
"System.Runtime": "4.0.20-beta-22231" | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Multitenancy.Features | |
{ | |
public class Tenant | |
{ | |
public string Id { get; set; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace Multitenancy.Features | |
{ | |
public class TenantFeature | |
: ITenantFeature | |
{ | |
public TenantFeature(Tenant tenant) | |
{ | |
Tenant = tenant; | |
} | |
public Tenant Tenant { get; private set; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Threading.Tasks; | |
using Microsoft.AspNet.Builder; | |
using Microsoft.AspNet.Http; | |
using Microsoft.Framework.Logging; | |
namespace Multitenancy.Features | |
{ | |
public class TenantResolverMiddleware | |
{ | |
private readonly RequestDelegate _next; | |
private readonly ILogger _logger; | |
public TenantResolverMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) | |
{ | |
_next = next; | |
_logger = loggerFactory.Create<TenantResolverMiddleware>(); | |
} | |
public async Task Invoke(HttpContext context) | |
{ | |
using (_logger.BeginScope("TenantResolverMiddleware")) | |
{ | |
var tenant = new Tenant | |
{ | |
Id = "Sample" // todo: determine this based on HttpContext etc. | |
}; | |
_logger.WriteInformation(string.Format("Resolved tenant. Current tenant: {0}", tenant.Id)); | |
var tenantFeature = new TenantFeature(tenant); | |
context.SetFeature<ITenantFeature>(tenantFeature); | |
await _next(context); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.AspNet.Builder; | |
namespace Multitenancy.Features | |
{ | |
public static class TenantResolverMiddlewareAppBuilderExtensions | |
{ | |
public static void UseTenantResolver(this IApplicationBuilder builder) | |
{ | |
builder.UseMiddleware<TenantResolverMiddleware>(); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
app.UseMvc(routes => | |
{ | |
routes.MapDomainRoute( | |
name: "SampleDomainRoute", | |
domainTemplate: "{tenant}.localtest.me", | |
routeTemplate: "{controller}/{action}/{id?}", | |
defaults: new { controller = "Home", action = "Index" }); | |
// ... more routes here ... | |
}); | |
// ... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
app.UseTenantResolver(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
single unit that validates, authenticates and redirects the subdomain to their matching tenants (or routes if not a tenant), and does that the least time possible,,