Showing posts with label ASP.NET C#. Show all posts
Showing posts with label ASP.NET C#. Show all posts

Saturday, 26 May 2018

ASP.NET MVC Multitenancy - Introduction & Architecture Overview

Over the past 5 years I had an opportunity to architect and develop multi tenanted applications. However, when it came to ASP.NET C# implementation I have struggled to find examples that solve common problems. In this blog series I will try to provide these examples:

This series focuses on a simple multitenancy architecture (more accessible to wider audience). Here is a high-level overview:

If you would like to get an architectural overview of multitenancy in general and see how you can deploy your application on to Azure then check out my "Developing multi-tenant SaaS business applications on Azure" WROC# conference talk. This talk is very much architectural extension of this blog post series. I hope you will find it useful:



Sunday, 20 May 2018

ASP.NET MVC Multitenancy, Part 4 - Authorisation and Claims

Once users have authenticated in to your application you will need to enable authorisation. If you are building an application that is using federated identity pattern chances are that you are going to be working with claims. ASP.NET comes with Role Based Access Control (RBAC). In this blog post we are going to explore how you can use this with OpenID Connect to enable authorisation.

Identity Provider Token

With OpenID Connect upon authentication users get redirected back to the application with JSON Web Token, this token contains claims about the authenticated user.

JSON Web Token (JWT)

JSON web token looks like this:

   
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJkYTdhNzQ1Yi05NzUyLTRjNGMtYjI0Zi05YWEyZmVjNGM3N2QiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83NWIwMmUwZC05MGQxLTQzZTUtYjVkYi0yMGVhYWRkYmZhYzYvIiwiaWF0IjoxNTI2ODE5MTM5LCJuYmYiOjE1MjY4MTkxMzksImV4cCI6MTUyNjgyMzAzOSwiYWlvIjoiQVNRQTIvOEhBQUFBREpYd0VWRnJ2WndBcS9WMlBHcjYzeVQybVk5UVYyS0F1MHZqeExGVXp5ST0iLCJhbXIiOlsiSHdkIl0sImZhbWlseV9uYW1lIjoiS2F2dGFza2luIiwiZ2l2ZW5fbmFtZSI6IlphbiIsImlwYWekciI6IjE4NS42OS4xNDQuMTA0IiwibmFtZSI6IlphbiBLYXZ0YXNraW4iLCJub25jZSI6Il9I609zaENvIiwib2lkIjoiODY4OGZlNWYtYjAxZS00MmVkLTk3MWYtN2E0NDkxZWZjMGY1Iiwib25wcmVtX6NpZCI6IlMtMS01LTIxLTE4NzEzNjg5OTctMjU3NzcyMDU0NS02ODMwNjc4MzMtMTA2OTIiLCJzdWIiOiJ4d3JydG5DY2xfS2lrbngxYlJTTzRPem9aTVEwV0V4bmozRmtqXzY2TEVRIiwidGlkIjoiNzViMDJlMGQtOTBkMS00M2U1LWI1ZGItMjBlYWFkZGJmYWM2IiwidW5pcXVlX25hbWU2OiJaYW4uS2F2dGFza2luQG1oci5jby51ayIsInVwbiI6Ilphbi5LYXZ0YXNraW5AbWhyLmNvLnfrIiwidXRpIjoiRnVqeFZFMFAya3lyUEhDTkFJRVdBQSIsInZlciI6IjEuMCJ9.PYi8_x_NJjhPYflU156dy-5XkmTJc_RAu_Gq0C06QHWuoDbBcMAh1TdkulZ7tVq7lF24EY19W965riA0pruz74GWo_9Ny8Jk1MkJi3oBaZQSMrTz_7rc2Km30UJrKKulmM9em5BfT43fMndCDS4fSVMrp8w0ijNgdCzSvTrK9xid0BsJw8hWiMxDdPPYTI_cEzfx0d587knjoamxaizrNsYwkuHuVJvZwITN94j_hV0KW1LBAW-GRtbbNpB7NDi_-GD6dZH3YW6IsipPRBXNxTNMKZ6zjtdQwDCZ8lui1-Gf77tC0cMUGHX_eIgzIAdEIHr5RfZgik2HT1tBJzzq8g

I recommend that you read more about JWT tokens here. When this token is base 64 decoded it will look like this (above token is not valid, so it will not decode):

   
{
  "aud": "fa74745b-8g52-cc4c-bv4f-0aa2fec4casd",
  "iss": "https://sts.windows.net/fa74745b-8g52-cc4c-bv4f-0aa2fec4casd/",
  "iat": 1526819139,
  "nbf": 1526819139,
  "exp": 1526823039,
  "aio": "ADQA3/8HAAZADJXwEVFrvZwAq/V4Pgr63sT2mY9Q42KAu0fjxLFUzyI=",
  "amr": [
    "pwd"
  ],
  "family_name": "Kavtaskin",
  "given_name": "Zan",
  "ipaddr": "182.67.124.103",
  "name": "Zan Kavtaskin",
  "nonce": "_091823kl",
  "sub": "xwrrtnCcl_Kiknx1bRSO4OzoZMQ0WExnj3Fkj_66LEQ",
  "unique_name": "Zan.Kavtaskin@someemail.com,
  "ver": "1.0"
}

Token is essentially a key value property bag, these properties are called claims, these claims are about the authenticated user.

Claims

Claims are simply properties that are trusted (read more about claims here). Now your web app will mainly use these claims for authentication i.e. who is the user, their unique id, etc. It is unlikely that claims will contain user role information (it can happen, however I have not seen this in practice). This is because 3rd party authentication provider normally does not know anything about your application. You can argue that role data is application data and does not belong with the authentication provider. However, ASP.NET needs role claims to perform authorisation checks, what do you do? Well, you append roles to your claims in your application.

ASP.NET MVC Implementation


Appending roles to the claims

   
public class AuthenticationClaimsAppenderMiddleware : OwinMiddleware
{
    public AuthenticationClaimsAppenderMiddleware(OwinMiddleware next)
        : base(next)
    {

    }

    public override Task Invoke(IOwinContext context)
    {
        IUserService userService = ServiceLocator.Resolve<IUserService>();
        ClaimsPrincipal claimsPrincipal = context.Authentication.User;

        string idpID = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier).Value;
        UserDto userDto = userService.GetUserByIdpID(idpID);

        if (userDto == null)
            return this.Next.Invoke(context);

        ClaimsIdentity claimsIdentity = claimsPrincipal.Identity as ClaimsIdentity;

        if (userDto.IsAdmin)
        {
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
        }

        return this.Next.Invoke(context);
    }
}

Checking the role claim

Now all you have to do is check the role claim using the authorisation attribute this can be done at the controller and action level:

   
[Authorize(Roles = "Admin")]
public class TenantSettingsController : Controller
{
    //...
}
   
[Authorize]
public class GroupsController : Controller
{
    //...
    [Authorize(Roles = "Admin")]
    [HttpPost]
    public ActionResult RemoveUser(UserModel model)
    {
        //...
    }
}
To enable unauthenticated access use AllowAnonymous attribute:
   
[AllowAnonymous]
public class TenantController : Controller
{
    //...
}
To ensure that user is authenticated without specific role check, use authorize attribute:
   
[Authorize]
public class UserSetupController : Controller
{
    //...
}

*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.

Saturday, 12 May 2018

ASP.NET MVC Multitenancy, Part 0 - Subdomain VS URL Path

With multitenancy you will need to decide how you are going to identify your tenants. There are two user friendly approaches, subdomain fictionaltenant.multitenantedapp.com or URL Path multitenantedapp.com/fictionaltenant/. In this post we are going to explore disadvantages and advantages of these two approaches.

Subdomain


Configuration

It is surprisingly easy to set this up for a simple deployment model. This is what you have to do:

Setup DNS record

I went to my domain registrar site and configured all my traffic on *.multitenantedapp.com to point to my Azure Web App domain e.g. *.multitenantedapp.com -> zkwildcardtest.azurewebsites.net.


Setup wildcard hostname

After that all I had to do was configure my Azure Web App to accept traffic for the hostname *.multitenantedapp.com. If you are using a web server such as IIS you will just need to change the IIS hostname binding.


Deploy your app

Just to make sure everything works as it should, I have deployed a simple test app that reads the URI and extracts subdomain from it. I went to tenanta.multitenantedapp.com and this is what I saw:


Why should you use this approach?


Advance estate management

For me advance estate management is the main reason why you would go for this approach. As your application scales you will want to split up your deployment zones. For example, you might want to deploy 50 tenants to zone A and other 50 tenants to zone b. What if some tenants in zone A are being noise neighbours (using your application a lot)? How will you migrate them to the zone C? With subdomain you can use DNS to redirect traffic, subdomain will remain the same, just the DNS mapping will need to change.

Customisation

You can enable customisation, so for example if you own fictionalcompany.com domain you can point files.fictionalcompany.com to fictionalcompany.multitenantedapp.com. This sounds simple but there is more to it. You need register this hostname on your web server. Also if you would like your connection to be secure you will need to get fictionalcompany to give you the subdomain SSL certificate.

Log quality

Here is how the subdomain IIS log looks like:

date: 2018-01-30 
time: 22:25:30
s-sitename: ZKWILDCARDTEST
cs-method: GET
cs-uri-stem: /
cs-uri-query: -
s-port: 80
cs-username: -
c-ip: 102.152.31.182
cs(User-Agent): -
cs(Cookie): -
cs(Referer): -
cs-host: fictionalcompany.multitenantedapp.com TENANT'S NAME APPEARS HERE
sc-status: 200
sc-substatus: 0
sc-win32-status: 0
sc-bytes: 1958
cs-bytes: 1066
time-taken: 0

cs-host (client-server host) actually shows fully qualified subdomain. This is great, as it is very easy to parse and hard to spoof. This can be used for business and security analytics.

URL Path


Configuration

There is not much to URL path setup. You just need to configure your web application's framework mapping. If you are using ASP.NET MVC it would look something like this:

  
namespace Web
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            //...

            routes.MapRoute(
                "multi",
                "{tenant}/{controller}/{action}/{id}",
                new {controller = "Dashboard", action = "Index", id = UrlParameter.Optional}
            ).DataTokens.Add("name", "webclient_multitenancy");

        }
    }
}

For full mapping explanation take a look at this article.

With this approach you can introduce proxy layer, that proxy layer can then direct your traffic to different deployment zones. So in a way you can achieve same thing. However you are introducing additional infrastructure, which will require implementation, deployment, hosting and maintenance. If you can avoid it, you should.

Why should you use this approach?

The main benefit is that it is simple to implement and requires no DNS work of any kind. However with this approach you do compromise on advance estate management, customisation and log quality.

Here is how the IIS log would look like with URL path:

   
date: 2018-01-30 
time: 22:25:30
s-sitename: ZKWILDCARDTEST
cs-method: GET
cs-uri-stem: /fictionalcompany/somepage/ TENANT'S NAME APPEARS HERE
cs-uri-query: -
s-port: 80
cs-username: -
c-ip: 102.152.31.182
cs(User-Agent): -
cs(Cookie): -
cs(Referer): -
cs-host: multitenantedapp.com THIS IS NOT SHOWING TENANT'S NAME ANYMORE
sc-status: 200
sc-substatus: 0
sc-win32-status: 0
sc-bytes: 1958
cs-bytes: 1066
time-taken: 0

The main thing to notice is that client server host is now not showing the tenant's name. Now you need to parse cs-uri-stem which is more difficult. Also if your application is a single page application that performs all operations via API, chances are that tenant's name will be in the header which means you will not know which tenant is making the API requests.

Conclusion

If you are starting to work on a large-scale project, I strongly recommend that you give it a bit extra time and invest in the subdomain implementation. It's just so versatile, it will help you scale long term. However if you are working for a small start-up, you don't have the DNS skills in your team and you are not sure where project is going you can just use the URL path to get you off the ground.


*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.

Thursday, 30 November 2017

ASP.NET MVC Multitenancy, Part 2 - OpenID Connect

In part 1 we have established tenant context. Most business applications need authentication. I have decided to use OpenID Connect, luckily for me it comes with awesome middleware and it's very easy to configure. Here is how you would do it:
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        //...
        app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
        {
            ClientId = "clientid",
            Authority = 'authority",
            RedirectUri = "http://mysite.com/"
        });
    }
}

However, not so fast, for each tenant you will need to configure client id, authority, url, etc. This also needs to be done at runtime as you don't want to ship new code every time new tenant registers to use your software. I have looked around for a solution and it turns out that this is a known problem, just take a look at the The Grand Auth Redesign. After a surprising amount of searching I have stumbled upon this amazing Multi-tenant middleware pipelines in ASP.NET Core blog post by Ben Foster. This solution is so damn good. Please read it. Unfortunately it did not work for me out of the box I had to port it over from ASP.NET Core to ASP.NET.

Hopefully by blogging and linking to Ben's article it will be easier for others to find it.

Example Usage

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        //..
        app.UsePerTenant((tenantContext, appBranch) =>
        {
            appBranch.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            appBranch.UseCookieAuthentication(new CookieAuthenticationOptions
            { 
                CookieName = $"OAuthCookie.{tenantContext.FriendlyName}"
            });

            appBranch.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = tenantContext.AuthClientId,
                Authority = tenantContext.AuthAuthority,
                RedirectUri = $"http://localhost:2295/{tenantContext.FriendlyName}/"
            });
        });
    }
}

ASP.NET Implementation


TenantPipelineMiddleware.cs
   
/// <summary>
/// Workaround for the known OpenID multitenancy issue https://github.com/aspnet/Security/issues/1179
/// based on http://benfoster.io/blog/aspnet-core-multi-tenant-middleware-pipelines and https://weblogs.asp.net/imranbaloch/conditional-middleware-in-aspnet-core designs 
/// </summary>
public class TenantPipelineMiddleware : OwinMiddleware
{
    readonly IAppBuilder rootApp;
    readonly Action<TenantContext, IAppBuilder> newBranchAppConfig;
    readonly ConcurrentDictionary<TenantContext, Lazy<Func<IDictionary<string, object>, Task>>> branches;

    public TenantPipelineMiddleware(OwinMiddleware next, IAppBuilder rootApp, Action<TenantContext, IAppBuilder> newBranchAppConfig)
        : base(next)
    {
        this.rootApp = rootApp;
        this.newBranchAppConfig = newBranchAppConfig;
        this.branches = new ConcurrentDictionary<TenantContext, Lazy<Func<IDictionary<string, object>, Task>>>();
    }

    public override async Task Invoke(IOwinContext context)
    {
        TenantContext tenantContext = context.GetTenantContext();

        if (tenantContext == null || tenantContext.IsEmpty())
        {
            await this.Next.Invoke(context);
            return;
        }

        Lazy<Func<IDictionary<string, object>, Task>> branch = 
            branches.GetOrAdd(tenantContext, new Lazy<Func<IDictionary<string, object>, Task>>(() =>
            {
                IAppBuilder newAppBuilderBranch = rootApp.New();
                newBranchAppConfig(tenantContext, newAppBuilderBranch);
                newAppBuilderBranch.Run((oc) => this.Next.Invoke(oc));
                return newAppBuilderBranch.Build();
            }));

        await branch.Value(context.Environment);
    }
}

OwinContextExtensions.cs
   
public static class OwinContextExtensions
{
    static string tenantContextKey = "tenantcontext";

    public static TenantContext GetTenantContext(this IOwinContext context)
    {
        if (context.Environment.ContainsKey(tenantContextKey))
        {
            return (TenantContext)context.Environment[tenantContextKey];
        }
        return null;
    }
}

AppBuilderExtensions.cs
   
public static class AppBuilderExtensions
{
    public static IAppBuilder UsePerTenant(this IAppBuilder app, Action<TenantContext, IAppBuilder> newBranchAppConfig)
    {
        return app.Use<TenantPipelineMiddleware>(app, newBranchAppConfig);
    }
}

*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.

Sunday, 20 August 2017

ASP.NET MVC Multitenancy, Part 1 - Routing with OWIN

This blog post was rewritten on 29/11/2017.

Recently I have been migrating one of my multitenant ASP.NET MVC application's to the OWIN middleware. This has presented me with an opportunity to change my initial tenant resolution and pipeline registration implementation.

I've started by doing some research to see if I could find something I could just download and use. However, I could not find anything that would meet the following requirements:
  • ASP.NET Core and ASP.NET compatibility
  • Optional multitenant processing i.e. all requests don't have be multitenant
  • Lifecycle delegates for missing tenant identifier and tenant record, and tenant context creation
  • Runtime tenant context dependency injection registration
  • Compatible with multitenant URL paths mysite.com/sometenant/ and could work with subdomains sometenant.mysite.com

After much deliberation, I have decided to go with my own implementation. However this implementation was massively influenced by Ben Foster's Building multi-tenant applications with ASP.NET Core (ASP.NET 5) and OpenID Connect Authentication Middleware designs.

Example Usage

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        //..
        app.UseMultitenancy(new MultitenancyNotifications<YourTenantRecord>
        {
            TenantIdentifierNotFound = context =>
            {
                throw new HttpException(404, "Tenant identifier must be provided");
            },
            TenantRecordNotFound = context =>
            {
                context.Response.Redirect("/signup/tenant/");
                return Task.FromResult(0);
            },
            CreateTenantContext = (context, tenantRecord) =>
            {
                ITenantContextFactory tenantContextFactory = ServiceLocator.Resolve<ITenantContextFactory>();
                TenantContext tenantContext = tenantContextFactory.Create(tenantRecord.Id, tenantRecord.NameFriendly, tenantRecord.AuthClientId, tenantRecord.AuthAuthority);
                return Task.FromResult(tenantContext);
            }
        });
    }
}
Please note that "CreateTenantContext" is a factory delegate. It allows you to control tenant record to tenant context mapping. It also allows you to perform other actions such as runtime tenant context dependency injection registration. By doing this, downstream objects such as controllers will get tenant context injected in to them.
public class SomeController : Controller
{
    public SomeController(TenantContext tenantContext)
    {
        //..
    }
}

Implementation


Tenant Identifier Extraction

In my implementation have decided to use MVC routing with data tokens for tenant discrimination.

RouteConfig.cs
   
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        //...
        routes.MapRoute(
            "multi",
            "{tenant}/{controller}/{action}/{id}",
            new {controller = "Home", action = "Index", id = UrlParameter.Optional}
        ).DataTokens.Add("name", "webclient_multitenancy");
    }
}
By using data tokens, I am able to check the request route. In this case if request route has data token of name "webclient_multitenancy" I will know that I will be able to extract tenant identifier from the route. As you have probably gathered, in this case I am getting multitenancy identifier from the URL path mysite.com/sometenant/.

ITenantIdentifierExtractor.cs
   
public interface ITenantIdentifierExtractor
{
    bool CanExtract(IOwinContext context);
    string GetIdentifier(IOwinContext context);
}

RouteDataTokensTenantIdentifierExtractor.cs
   
public class RouteDataTokensTenantIdentifierExtractor : ITenantIdentifierExtractor
{
    public bool CanExtract(IOwinContext context)
    {
        RouteData routeData = this.getRouteData(context);
        return routeData != null && routeData.DataTokens.ContainsValue("webclient_multitenancy");
    }

    public string GetIdentifier(IOwinContext context)
    {
        if (!this.CanExtract(context))
            return null;

        RouteData routeData = this.getRouteData(context);
        return routeData.GetRequiredString("tenant");
    }

    private RouteData getRouteData(IOwinContext context)
    {
        HttpContextBase httpContext = (HttpContextBase)context.Environment["System.Web.HttpContextBase"];
        return RouteTable.Routes.GetRouteData(httpContext);
    }
}

You don’t have to use data token approach in your application, you can get multitenancy data out of the HTTP request manually.

Tenant Record Resolution

Resolution is where you call service or repository to obtain tenant record, this tenant record will be used later on to create tenant context.

ITenantRecordResolver.cs
   
public interface ITenantRecordResolver<TTenantRecord>
{
    TTenantRecord GetTenant(string tenantIdentifier);
}

In this implementation, I have decided to call the ITenantService to get the TenantDto object.

TenantRecordResolver.cs
   
public class TenantRecordResolver : ITenantRecordResolver<TenantDto>
{
    readonly ITenantService tenantService;

    public TenantRecordResolver(ITenantService tenantService)
    {
        this.tenantService = tenantService;
    }

    public TenantDto GetTenant(string tenantIdentifier)
    {
        return this.tenantService.Get(tenantIdentifier);
    }
}

Most of the time on each request you will be resolving tenant record. At scale this will become very inefficient so it is highly likely that you will want to cache tenant record, to do this you can use decorator pattern.

TenantRecordResolverCacheDecorator.cs
   
public class TenantRecordResolverCacheDecorator : ITenantRecordResolver<TenantDto>
{
    readonly TenantRecordResolver tenantRecordResolver;

    public TenantRecordResolverCacheDecorator(TenantRecordResolver tenantRecordResolver)
    {
        this.tenantRecordResolver = tenantRecordResolver;
    }

    public TenantDto GetTenant(string tenantIdentifier)
    {
        string cacheKey = $"tenantIdentifier:{tenantIdentifier}";

        TenantDto tenant = (TenantDto)MemoryCache.Default[cacheKey];

        if (tenant != null)
        {
            return tenant;
        }

        tenant = this.tenantRecordResolver.GetTenant(tenantIdentifier);
        if (tenant == null)
        {
            return null;
        }

        MemoryCache.Default.Set(cacheKey, tenant, new CacheItemPolicy
        {
            AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
        });
    
        return tenant;
    }
}

Middleware & Extensions

We now have all the key components in place and it is time to implement the actual middleware.

MultitenancyMiddleware.cs
   
public class MultitenancyMiddleware<TTenantRecord>
    where TTenantRecord : class
{
    readonly ITenantIdentifierExtractor tenantIdentifierExtractor;
    readonly ITenantRecordResolver<TTenantRecord> tenantRecordResolver;
    readonly MultitenancyNotifications<TTenantRecord> notifications;
    readonly Func<Task> next;

    public MultitenancyMiddleware(Func<Task> next, ITenantIdentifierExtractor tenantIdentifierExtractor, 
        ITenantRecordResolver<TTenantRecord> tenantRecordResolver,  MultitenancyNotifications<TTenantRecord> notifications)
    {
        this.next = next;
        this.tenantIdentifierExtractor = tenantIdentifierExtractor;
        this.tenantRecordResolver = tenantRecordResolver;
        this.notifications = notifications;
    }

    public async Task Invoke(IOwinContext context)
    {
        if (!this.tenantIdentifierExtractor.CanExtract(context))
        {
            await this.next();
            return;
        }

        string identifier = this.tenantIdentifierExtractor.GetIdentifier(context);

        if (string.IsNullOrEmpty(identifier))
        {
            await this.notifications.TenantIdentifierNotFound(context);
            return;
        }

        TTenantRecord tenantRecord = this.tenantRecordResolver.GetTenant(identifier);
        if (tenantRecord == null)
        {
            await this.notifications.TenantRecordNotFound(context);
            return;
        }

        TenantContext tenantContext = await this.notifications.CreateTenantContext(context, tenantRecord);

        context.SetTenantContext(tenantContext);

        await this.next();
    }
}

MultitenancyNotifications.cs
   
public class MultitenancyNotifications<TTenantRecord>
    where TTenantRecord : class
{
    public Func<IOwinContext, Task> TenantIdentifierNotFound { get; set; }
    public Func<IOwinContext, Task> TenantRecordNotFound { get; set; }
    public Func<IOwinContext, TTenantRecord, Task<TenantContext>> CreateTenantContext { get; set; }

    public MultitenancyNotifications()
    {
        this.TenantIdentifierNotFound = context => Task.FromResult(0);
        this.TenantRecordNotFound = context => Task.FromResult(0);
        this.CreateTenantContext = (context, tenantRecord) => Task.FromResult<TenantContext>(null);
    }
}

OwinContextExtensions.cs
   
public static class OwinContextExtensions
{
    static string tenantContextKey = "tenantcontext";

    public static void SetTenantContext(this IOwinContext context, TenantContext tenantContext)
    {
        context.Environment.Add(tenantContextKey, tenantContext);
    }
}
AppBuilderExtensions.cs
   
public static class AppBuilderExtensions
{
    public static IAppBuilder UseMultitenancy<TTenantRecord>(this IAppBuilder app, 
        MultitenancyNotifications<TTenantRecord> notifications)
        where TTenantRecord : class
    {
        return app.Use((context, next) =>
        {
            MultitenancyMiddleware<TTenantRecord> multitenancyMiddleware = 
                ServiceLocator.Resolve<MultitenancyMiddleware<TTenantRecord>>(new { next, notifications });
            return multitenancyMiddleware.Invoke(context);
        });
    }
}


*Note: Code in this article is not production ready and is used for prototyping purposes only. If you have suggestions or feedback please do comment.