Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
ASP.NET MVC 6 / ASP.NET 5 Domain Routing + Tenant Middleware
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);
}
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;
}
}
}
}
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;
}
}
}
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);
}
}
}
namespace Multitenancy.Features
{
public interface ITenantFeature
{
Tenant Tenant { get; }
}
}
{
"authors": [ "Maarten Balliauw <maarten@maartenballiauw.be>" ],
"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"
}
}
}
}
namespace Multitenancy.Features
{
public class Tenant
{
public string Id { get; set; }
}
}
namespace Multitenancy.Features
{
public class TenantFeature
: ITenantFeature
{
public TenantFeature(Tenant tenant)
{
Tenant = tenant;
}
public Tenant Tenant { get; private set; }
}
}
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);
}
}
}
}
using Microsoft.AspNet.Builder;
namespace Multitenancy.Features
{
public static class TenantResolverMiddlewareAppBuilderExtensions
{
public static void UseTenantResolver(this IApplicationBuilder builder)
{
builder.UseMiddleware<TenantResolverMiddleware>();
}
}
}
// ...
app.UseMvc(routes =>
{
routes.MapDomainRoute(
name: "SampleDomainRoute",
domainTemplate: "{tenant}.localtest.me",
routeTemplate: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
// ... more routes here ...
});
// ...
app.UseTenantResolver();
@weitzhandler

This comment has been minimized.

Copy link

weitzhandler commented Jul 25, 2015

Thanks for sharing!
Awesome.
I'm still looking for a way to use both the routing and the middleware while having a 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.

@SimonOzturk

This comment has been minimized.

Copy link

SimonOzturk commented Dec 10, 2015

Any update for RC1 ? I think most of the functions/methods not recognized by RC1

@ghost

This comment has been minimized.

Copy link

ghost commented Jan 24, 2016

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,,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.