Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save weitzhandler/b9033e97339c6814f287 to your computer and use it in GitHub Desktop.
Save weitzhandler/b9033e97339c6814f287 to your computer and use it in GitHub Desktop.
ASP.NET MVC 6 / ASP.NET 5 Domain Routing + Tenant Middleware
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.Services
{
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; }
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.LogVerbose($"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 VirtualPathData GetVirtualPath(VirtualPathContext context)
{
foreach (var matcherParameter in _matcher.Template.Parameters)
// make sure none of the domain-placeholders are appended as query string parameters
context.Values.Remove(matcherParameter.Name);
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.CreateLogger<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; }
}
}
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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment