dazinator / Dotnettency

Mutlitenancy for dotnet applications
MIT License
111 stars 23 forks source link

Idea: Consider re-design based on routing #30

Closed dazinator closed 6 years ago

dazinator commented 6 years ago

Having read more about Routing I am considering some changes to make multitenancy features a part of routing.

Custom IRouters could potentially be used to:

  1. Restore a container appropriate for the current Request
  2. Invoke a middleware pipeline appropriate for the current Request.

Idea being;

  1. Request comes in.
  2. RouterMiddleware fires
  3. RouterMiddleware middleware loops through it's RouteCollection calling RouteAsync() on each IRouter in the collection, until one of them sets a Handler on the RouteContext. If no handler is set, then the next middleware in the chain is invoked as normal.

I am considering having the following custom IRouter implementations that you could add to a RouteCollection.

- ContainerRouter
- MiddlewarePipelineRouter

ContainerRouter

When this Router has RouteAsync invoked, it will:

  1. Lazy initialise a configurable container and cache it for future use.
  2. ~~Swap out IApplicationBuilder.ApplicationServices for the current container. ~~
  3. Swap out the HttpContext.RequestServices` for a scoped / nested instance of the current container.
  4. Register the scoped / nested instance of the current container for dispose at the end of the request / response.

MiddlewareRouter

When this Router has RouteAsync invoked, it will lazy initialise a RequestDelegate representing a MiddlewarePipeline. When it creates the RequestDelegate it will fork the root IApplicationBuilder instance, and it will swap out the ApplicationServices on the forked instance for the current containers IServiceProvider. This ensures that middleware in the pipeline has access only to services from the current resolved container.

For this to work, the ContainerRouter should be ahead of the MiddlewareRouter. This is so that the appropriate container is resolved, prior to the middleware pipeline being lazy initialised - ensuring that when the middleware is created, it gets access to the services from the currently resolved container.

Order of events are roughly:

  1. Request --> RouterMiddleware (with 2 routers, ContainerRouter and MiddlewarePipelineRouter)
  2. RouterMiddleware --> ContainerRouter.RouteAsync() (Resolves the current container)
  3. RouterMiddleware --> MiddlewareRouter.RouteAsync() (Resolves the current middleware pipeline)
  4. MiddlewareRouter --> RouteContext.Handler = requestDelegate; (Handles the route by setting the handler to the RequestDelegate for the middleware pipeline to be invoked)
  5. RouterMiddleware --> RouteContext.Handler.Invoke(request) (middleware pipeline is invoked)

Note: When the middleware pipeline is invoked at the last step - the whole process could be nested.

Benefits?

  1. I can potentially remove custom dotnettency middleware, and rely on single Routing middleware with custom routers instead. i.e routes.MapContainer() and routes.MapMiddlewarePipeline() extension methods could take the pace of custom middleware.

  2. The ContainerRouter, could be optionally configured to only restore some container if a route template matched. i.e

routes.MapContainer("/foo/bar", someContainerProvider())
  1. The MiddewarePipelineRouter, could be optionally configured to only handle the request if a route template matched. i.e
routes.MapMiddlewarePipeline("/foo/bar", someRequestDelegateProvider())
  1. You can nest to form a hierarchy of environments and subsystems if required e.g:

Request --> RouterMiddleware (Application A) --> ContainerRouter, MiddlewareRouter --> Middleware Pipeline --> |NESTING begins| RouterMiddleware (Subsystem A) --> ContainerRouter, MiddlewareRouter --> Middleware Pipeline

In the above scenario, a request is received by router middleware, and the dotnettency container router restores a container for the matching route / httprequest, before allowing the dotnettency middleware router to resolve a RequestDelegate (middleware pipeline) for the request. The router RouterMiddleware then invokes the RequestDelegate for the middleware pipeline - allowing the request to flow through Application A's middleware. In the middleware pipeline for Application A, we nest the whole process again for "Subsystem A". Subsystem A for example, may have a ContainerRouter and MiddlewareRouter that only matches on specific URL's / endpoints related to subsystem A. For example, an MVC Controller that should be isolated from other services could have its own ContainerRouter. Also perhaps it requires middleware only for this endpoint i.e the url is /SubsystemA/Foo.

Note: If there is more than one ContainerRouter added to a RouteCollection there is a "last one wins" scenario.

A ContainerRouter will never handle a Request as it will never set RouteContext.Handler. However it may add itself to RouteContext.Routers or add DataTokens to the RouteContext when it successfully restores a container.

molaie commented 6 years ago

Hope it will be available asap.

dazinator commented 6 years ago

There is a working sample in my feature/routing branch. The tenant container and middleware pipeline are still set up in configureservices, but its now the asp.net router that is configured in Configure() method rather than adding dotnettency middleware. Im still exploring it as an idea but it seems to work fine.

dazinator commented 6 years ago

Closing this. Leaving routing in branch.