From 9e67d03f32f98e3e2ff202c4c975041c9c56c8fd Mon Sep 17 00:00:00 2001 From: Darrell Tunnell Date: Sat, 8 Jun 2019 22:22:07 +0100 Subject: [PATCH] Tenant Restart implemented Closes #1 --- README.md | 76 ++++- .../TenantShellPipelineExtensions.cs | 4 +- .../TenantShellPipelineExtensions.cs | 10 +- .../TenantShellFileSystemExtensions.cs | 9 +- src/Dotnettency/CompositeDisposable.cs | 282 ++++++++++++++++++ .../TenantShellContainerExtensions.cs | 12 +- src/Dotnettency/DelegateDisposable.cs | 19 ++ src/Dotnettency/MultitenancyOptionsBuilder.cs | 3 +- .../ConcurrentDictionaryTenantShellCache.cs | 5 + .../{ => TenantShell}/ITenantShellAccessor.cs | 18 +- .../TenantShell/ITenantShellCache.cs | 2 + .../TenantShell/ITenantShellResolver.cs | 7 +- .../TenantShell/ITenantShellRestarter.cs | 10 + src/Dotnettency/TenantShell/TenantShell.cs | 175 ++++++++--- .../{ => TenantShell}/TenantShellAccessor.cs | 3 +- .../TenantShell/TenantShellResolver.cs | 194 +++++++----- .../TenantShell/TenantShellRestarter.cs | 37 +++ .../Pages/Gicrosoft/Index.cshtml | 17 +- .../Pages/Gicrosoft/Index.cshtml.cs | 11 + 19 files changed, 753 insertions(+), 141 deletions(-) create mode 100644 src/Dotnettency/CompositeDisposable.cs create mode 100644 src/Dotnettency/DelegateDisposable.cs rename src/Dotnettency/{ => TenantShell}/ITenantShellAccessor.cs (96%) create mode 100644 src/Dotnettency/TenantShell/ITenantShellRestarter.cs rename src/Dotnettency/{ => TenantShell}/TenantShellAccessor.cs (92%) create mode 100644 src/Dotnettency/TenantShell/TenantShellRestarter.cs diff --git a/README.md b/README.md index 9454f87..12b73ff 100644 --- a/README.md +++ b/README.md @@ -61,11 +61,77 @@ Once configured in `startup.cs` you can resolve the current tenant in any one of - Inject `Task` - Allows you to `await` the current `Tenant` (so non blocking). `Task` is convenient. - Inject `ITenantAccessor`. This is similar to injecting `Task` in that it provides lazy access the current tenant in a non blocking way. For convenience it's now easier to just inject `Task` instead, unless you want a more descriptive API. +## Tenant Restart (New in v2.0.0) + +You can `Restart` a tenant. This does not stop the web application, or interfere with other tenants. +It will mean that the net request for the tenant, will result in the tenant starting up from scratch again - + - Tenant Container will be re-built (if you are usijng tenant services the method you use to register services for the current tenant will be re-rexecuted.) + - Tenant Middleware Pipeline will be re-built (if you are using tenant middleware pipeline, it will be rebuilt - you'll have a chance to include additional middlewares etc.) + +For sample usage, see the Sample.AspNetCore30.RazorPages sample in this solution, in partcular the Pages/Gicrosoft/Index.cshtml page. + +Injext `ITenantShellRestarter` and invoke the `Restart()` method: + +``` + public class IndexModel : PageModel + { + + public bool IsRestarted { get; set; } + + public void OnGet() + { + + } + + public async Task OnPost([FromServices]ITenantShellRestarter restarter) + { + await restarter.Restart(); + IsRestarted = true; + this.Redirect("/"); + } + } +``` + +and corresponding razor page: + +``` + +@page +@using Sample.Pages.T1 +@model IndexModel +@{ + ViewData["Title"] = "Home page"; +} + +
+

Tenant Gicrosoft Razor Pages!

+ +
+ @{ + if (!@Model.IsRestarted) + { + + } + else + { + +

Tenant has been restarted, the next request will result in Tenant Container being rebuilt, and tenant middleware pipeline being re-initialised.

+ } + } + +
+
+ +``` ## Tenant Shell Injection -The `TenantShell` stores additional context for a Tenant, such as it's `Container` and it's `MiddlewarePipeline`. +The `TenantShell` stores the context for a Tenant, such as it's `Container` and it's `MiddlewarePipeline`. +It's stored in a cache, and is evicted if the tenant is Restarted. +You probably won't need to use it directly, but if you want you can do so. + + - Inject `ITenantShellAccessor` to access the TenantShell for the current tenant. + - Extensions (such as Middleware, or Container) - store things for the tenant in it's concurrent property bag. You can get at these properties if you know the keys. + - You can also register callbacks that will be invoked when the TenantShell is disposed of - this happens when the tenant is restarted for example. -- Inject `ITenantShellAccessor` in order to access context for the currnet tenant, which is primarily used by: - - Extensions (such as Middleware, or Container) - which store things for the tenant in the `ITenantShellAccessor`'s concurrent property bag. - - Tenant Admin screens - if you need to "Restart" a tenant, then the idea is, you can resolve the `ITenantShellAccessor` and then use extension methods (provided by the dotnettency extensions such as Middleware pipeline, or Container) to allow you to control the state of the running tenant - for example to trigger rebuild of the tenant's container, or pipeline on the next request. - \ No newline at end of file + Another way to register code that will run when the tenant is restarted, is to use TenantServices - add a disposable singleton service the tenant's container. + When the tenant is disposed of, it's container will be disposed of, and your disposable service will be disposed of - depending upon your needs this hook might suffice. \ No newline at end of file diff --git a/src/Dotnettency.AspNetCore/MiddlewarePipeline/TenantShellPipelineExtensions.cs b/src/Dotnettency.AspNetCore/MiddlewarePipeline/TenantShellPipelineExtensions.cs index f56a896..01c99fa 100644 --- a/src/Dotnettency.AspNetCore/MiddlewarePipeline/TenantShellPipelineExtensions.cs +++ b/src/Dotnettency.AspNetCore/MiddlewarePipeline/TenantShellPipelineExtensions.cs @@ -8,8 +8,8 @@ public static class TenantShellPipelineExtensions { public static Lazy> GetOrAddMiddlewarePipeline(this TenantShell tenantShell, Lazy> requestDelegateFactory) where TTenant : class - { - return tenantShell.Properties.GetOrAdd(nameof(TenantShellPipelineExtensions), requestDelegateFactory) as Lazy>; + { + return tenantShell.GetOrAddProperty>>(nameof(TenantShellPipelineExtensions), requestDelegateFactory); } } } diff --git a/src/Dotnettency.Owin/MiddlewarePipeline/TenantShellPipelineExtensions.cs b/src/Dotnettency.Owin/MiddlewarePipeline/TenantShellPipelineExtensions.cs index b4f5f72..714be83 100644 --- a/src/Dotnettency.Owin/MiddlewarePipeline/TenantShellPipelineExtensions.cs +++ b/src/Dotnettency.Owin/MiddlewarePipeline/TenantShellPipelineExtensions.cs @@ -9,7 +9,15 @@ public static class TenantShellPipelineExtensions public static Lazy> GetOrAddMiddlewarePipeline(this TenantShell tenantShell, Lazy> requestDelegateFactory) where TTenant : class { - return tenantShell.Properties.GetOrAdd(nameof(TenantShellPipelineExtensions), requestDelegateFactory) as Lazy>; + var property = tenantShell.GetOrAddProperty(nameof(TenantShellPipelineExtensions), requestDelegateFactory); + tenantShell.RegisterCallbackOnDispose(() => + { + if (requestDelegateFactory.IsValueCreated) + { + requestDelegateFactory.Value?.Dispose(); + } + }); + return property; } } } diff --git a/src/Dotnettency.TenantFileSystem/TenantShellFileSystemExtensions.cs b/src/Dotnettency.TenantFileSystem/TenantShellFileSystemExtensions.cs index 050bd3e..fe5fd13 100644 --- a/src/Dotnettency.TenantFileSystem/TenantShellFileSystemExtensions.cs +++ b/src/Dotnettency.TenantFileSystem/TenantShellFileSystemExtensions.cs @@ -7,16 +7,15 @@ public static class TenantShellFileSystemExtensions { public static Lazy GetOrAddTenantFileSystem(this TenantShell tenantShell, string key, Lazy factory) where TTenant : class - { - return tenantShell.Properties.GetOrAdd(key, factory) as Lazy; + { + return tenantShell.GetOrAddProperty(key, factory) as Lazy; } public static Lazy TryGetTenantFileSystem(this TenantShell tenantShell, string key) where TTenant : class { - object result; - var success = tenantShell.Properties.TryGetValue(key, out result); - return result as Lazy; + tenantShell.TryGetProperty(key, out Lazy result); + return result; } } diff --git a/src/Dotnettency/CompositeDisposable.cs b/src/Dotnettency/CompositeDisposable.cs new file mode 100644 index 0000000..b5b54e0 --- /dev/null +++ b/src/Dotnettency/CompositeDisposable.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; + +namespace Dotnettency +{ + + // Note: This class was taken from the UniRx project - which is under a permissive MIT licence found here: https://github.com/neuecc/UniRx/blob/master/LICENSE + // Highly recommend you check out this project. + public sealed class CompositeDisposable : ICollection, IDisposable + { + private readonly object _gate = new object(); + + private bool _disposed; + private List _disposables; + private int _count; + private const int SHRINK_THRESHOLD = 64; + + /// + /// Initializes a new instance of the class with no disposables contained by it initially. + /// + public CompositeDisposable() + { + _disposables = new List(); + } + + /// + /// Initializes a new instance of the class with the specified number of disposables. + /// + /// The number of disposables that the new CompositeDisposable can initially store. + /// is less than zero. + public CompositeDisposable(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + + _disposables = new List(capacity); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + public CompositeDisposable(params IDisposable[] disposables) + { + if (disposables == null) + throw new ArgumentNullException("disposables"); + + _disposables = new List(disposables); + _count = _disposables.Count; + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + public CompositeDisposable(IEnumerable disposables) + { + if (disposables == null) + throw new ArgumentNullException("disposables"); + + _disposables = new List(disposables); + _count = _disposables.Count; + } + + /// + /// Gets the number of disposables contained in the CompositeDisposable. + /// + public int Count + { + get + { + return _count; + } + } + + /// + /// Adds a disposable to the CompositeDisposable or disposes the disposable if the CompositeDisposable is disposed. + /// + /// Disposable to add. + /// is null. + public void Add(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + var shouldDispose = false; + lock (_gate) + { + shouldDispose = _disposed; + if (!_disposed) + { + _disposables.Add(item); + _count++; + } + } + if (shouldDispose) + item.Dispose(); + } + + /// + /// Removes and disposes the first occurrence of a disposable from the CompositeDisposable. + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + public bool Remove(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + var shouldDispose = false; + + lock (_gate) + { + if (!_disposed) + { + // + // List doesn't shrink the size of the underlying array but does collapse the array + // by copying the tail one position to the left of the removal index. We don't need + // index-based lookup but only ordering for sequential disposal. So, instead of spending + // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also + // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. + // + var i = _disposables.IndexOf(item); + if (i >= 0) + { + shouldDispose = true; + _disposables[i] = null; + _count--; + + if (_disposables.Capacity > SHRINK_THRESHOLD && _count < _disposables.Capacity / 2) + { + var old = _disposables; + _disposables = new List(_disposables.Capacity / 2); + + foreach (var d in old) + if (d != null) + _disposables.Add(d); + } + } + } + } + + if (shouldDispose) + item.Dispose(); + + return shouldDispose; + } + + /// + /// Disposes all disposables in the group and removes them from the group. + /// + public void Dispose() + { + var currentDisposables = default(IDisposable[]); + lock (_gate) + { + if (!_disposed) + { + _disposed = true; + currentDisposables = _disposables.ToArray(); + _disposables.Clear(); + _count = 0; + } + } + + if (currentDisposables != null) + { + foreach (var d in currentDisposables) + if (d != null) + d.Dispose(); + } + } + + /// + /// Removes and disposes all disposables from the CompositeDisposable, but does not dispose the CompositeDisposable. + /// + public void Clear() + { + var currentDisposables = default(IDisposable[]); + lock (_gate) + { + currentDisposables = _disposables.ToArray(); + _disposables.Clear(); + _count = 0; + } + + foreach (var d in currentDisposables) + if (d != null) + d.Dispose(); + } + + /// + /// Determines whether the CompositeDisposable contains a specific disposable. + /// + /// Disposable to search for. + /// true if the disposable was found; otherwise, false. + /// is null. + public bool Contains(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + lock (_gate) + { + return _disposables.Contains(item); + } + } + + /// + /// Copies the disposables contained in the CompositeDisposable to an array, starting at a particular array index. + /// + /// Array to copy the contained disposables to. + /// Target index at which to copy the first disposable of the group. + /// is null. + /// is less than zero. -or - is larger than or equal to the array length. + public void CopyTo(IDisposable[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException("array"); + if (arrayIndex < 0 || arrayIndex >= array.Length) + throw new ArgumentOutOfRangeException("arrayIndex"); + + lock (_gate) + { + var disArray = new List(); + foreach (var item in _disposables) + { + if (item != null) disArray.Add(item); + } + + Array.Copy(disArray.ToArray(), 0, array, arrayIndex, array.Length - arrayIndex); + } + } + + /// + /// Always returns false. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Returns an enumerator that iterates through the CompositeDisposable. + /// + /// An enumerator to iterate over the disposables. + public IEnumerator GetEnumerator() + { + var res = new List(); + + lock (_gate) + { + foreach (var d in _disposables) + { + if (d != null) res.Add(d); + } + } + + return res.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the CompositeDisposable. + /// + /// An enumerator to iterate over the disposables. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed + { + get { return _disposed; } + } + } +} diff --git a/src/Dotnettency/Container/TenantShellContainerExtensions.cs b/src/Dotnettency/Container/TenantShellContainerExtensions.cs index 6a98cc0..0d73da5 100644 --- a/src/Dotnettency/Container/TenantShellContainerExtensions.cs +++ b/src/Dotnettency/Container/TenantShellContainerExtensions.cs @@ -9,9 +9,17 @@ public static class TenantShellContainerExtensions public static Lazy> GetOrAddContainer(this TenantShell tenantShell, Func> containerAdaptorFactory) where TTenant : class { - return tenantShell.Properties.GetOrAdd(nameof(TenantShellContainerExtensions), (a) => + return tenantShell.GetOrAddProperty(nameof(TenantShellContainerExtensions), (a) => { - return new Lazy>(containerAdaptorFactory); + var newItem = new Lazy>(containerAdaptorFactory); + tenantShell.RegisterCallbackOnDispose(() => { + if(newItem.IsValueCreated) + { + var result = newItem.Value.Result; + result?.Dispose(); + } + }); + return newItem; }) as Lazy>; } } diff --git a/src/Dotnettency/DelegateDisposable.cs b/src/Dotnettency/DelegateDisposable.cs new file mode 100644 index 0000000..16ff137 --- /dev/null +++ b/src/Dotnettency/DelegateDisposable.cs @@ -0,0 +1,19 @@ +using System; + +namespace Dotnettency +{ + internal class DelegateDisposable : IDisposable + { + private readonly Action _dispose; + + public DelegateDisposable(Action dispose) + { + _dispose = dispose; + } + + public void Dispose() + { + _dispose(); + } + } +} diff --git a/src/Dotnettency/MultitenancyOptionsBuilder.cs b/src/Dotnettency/MultitenancyOptionsBuilder.cs index 9348ed5..3ea12f5 100644 --- a/src/Dotnettency/MultitenancyOptionsBuilder.cs +++ b/src/Dotnettency/MultitenancyOptionsBuilder.cs @@ -22,11 +22,12 @@ protected virtual void AddDefaultServices(IServiceCollection serviceCollection) // Tenant shell cache is special in that it houses the tenant shell for each tenant, and each // tenant shell has state that needs to be kept local to the application (i.e the tenant's container or middleware pipeline.) - // Therefore it should always be a local / in-memory based cache as will have will have fundamentally non-serialisable state. + // Therefore it should always be a local / in-memory based cache as will have fundamentally non-serialisable state. Services.AddSingleton, ConcurrentDictionaryTenantShellCache>(); Services.AddSingleton, TenantShellResolver>(); Services.AddScoped>(); Services.AddScoped, TenantShellAccessor>(); + Services.AddScoped, TenantShellRestarter>(); diff --git a/src/Dotnettency/TenantShell/ConcurrentDictionaryTenantShellCache.cs b/src/Dotnettency/TenantShell/ConcurrentDictionaryTenantShellCache.cs index ceee609..ae908ca 100644 --- a/src/Dotnettency/TenantShell/ConcurrentDictionaryTenantShellCache.cs +++ b/src/Dotnettency/TenantShell/ConcurrentDictionaryTenantShellCache.cs @@ -30,6 +30,11 @@ public TenantShell AddOrUpdate(TenantIdentifier key, public bool TryGetValue(TenantIdentifier key, out TenantShell value) { return _mappings.TryGetValue(key, out value); + } + + public bool TryRemove(TenantIdentifier key, out TenantShell value) + { + return _mappings.TryRemove(key, out value); } } } diff --git a/src/Dotnettency/ITenantShellAccessor.cs b/src/Dotnettency/TenantShell/ITenantShellAccessor.cs similarity index 96% rename from src/Dotnettency/ITenantShellAccessor.cs rename to src/Dotnettency/TenantShell/ITenantShellAccessor.cs index 1674d99..c8ccc6e 100644 --- a/src/Dotnettency/ITenantShellAccessor.cs +++ b/src/Dotnettency/TenantShell/ITenantShellAccessor.cs @@ -1,10 +1,10 @@ -using System; -using System.Threading.Tasks; - -namespace Dotnettency -{ - public interface ITenantShellAccessor where TTenant : class - { - Lazy>> CurrentTenantShell { get; } - } +using System; +using System.Threading.Tasks; + +namespace Dotnettency +{ + public interface ITenantShellAccessor where TTenant : class + { + Lazy>> CurrentTenantShell { get; } + } } \ No newline at end of file diff --git a/src/Dotnettency/TenantShell/ITenantShellCache.cs b/src/Dotnettency/TenantShell/ITenantShellCache.cs index 7bf8586..32505e7 100644 --- a/src/Dotnettency/TenantShell/ITenantShellCache.cs +++ b/src/Dotnettency/TenantShell/ITenantShellCache.cs @@ -10,5 +10,7 @@ public interface ITenantShellCache TenantShell AddOrUpdate(TenantIdentifier key, TenantShell addValue, Func, TenantShell> updateValueFactory); bool TryGetValue(TenantIdentifier key, out TenantShell value); + + bool TryRemove(TenantIdentifier key, out TenantShell value); } } diff --git a/src/Dotnettency/TenantShell/ITenantShellResolver.cs b/src/Dotnettency/TenantShell/ITenantShellResolver.cs index fbbb17a..69c82a5 100644 --- a/src/Dotnettency/TenantShell/ITenantShellResolver.cs +++ b/src/Dotnettency/TenantShell/ITenantShellResolver.cs @@ -1,10 +1,13 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Dotnettency { public interface ITenantShellResolver where TTenant : class { - Task> ResolveTenant(TenantIdentifier identifier, ITenantShellFactory tenantFactory); + Task> ResolveTenantShell(TenantIdentifier identifier, ITenantShellFactory tenantFactory); + + Task RemoveTenantShell(TenantIdentifier identifier); } } diff --git a/src/Dotnettency/TenantShell/ITenantShellRestarter.cs b/src/Dotnettency/TenantShell/ITenantShellRestarter.cs new file mode 100644 index 0000000..78fd8d3 --- /dev/null +++ b/src/Dotnettency/TenantShell/ITenantShellRestarter.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Dotnettency +{ + public interface ITenantShellRestarter + where TTenant : class + { + Task Restart(); + } +} \ No newline at end of file diff --git a/src/Dotnettency/TenantShell/TenantShell.cs b/src/Dotnettency/TenantShell/TenantShell.cs index 3d9fc9d..e1cd239 100644 --- a/src/Dotnettency/TenantShell/TenantShell.cs +++ b/src/Dotnettency/TenantShell/TenantShell.cs @@ -1,40 +1,135 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Dotnettency -{ - public class TenantShell - where TTenant : class - { - public TenantShell(TTenant tenant, params TenantIdentifier[] distinguishers) - { - Id = Guid.NewGuid(); - Tenant = tenant; - Properties = new ConcurrentDictionary(); - Distinguishers = new HashSet(); - - if (distinguishers != null) - { - foreach (var item in distinguishers) - { - Distinguishers.Add(item); - } - } - } - - public ConcurrentDictionary Properties { get; private set; } - - /// - /// Uniquely identifies this tenant. - /// - public Guid Id { get; set; } - - public TTenant Tenant { get; set; } - - /// - /// Represents context distinguihers for this same tenant. Allows future request with any of these distinguishers to be mapped to this same tenant. - /// - internal HashSet Distinguishers { get; set; } - } -} +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Dotnettency +{ + public class TenantShell : IDisposable + where TTenant : class + { + + private CompositeDisposable _disposables = new CompositeDisposable(); + + public TenantShell(TTenant tenant, params TenantIdentifier[] distinguishers) + { + Id = Guid.NewGuid(); + Tenant = tenant; + Properties = new ConcurrentDictionary(); + Identifiers = new HashSet(); + + if (distinguishers != null) + { + foreach (var item in distinguishers) + { + Identifiers.Add(item); + } + } + } + + protected ConcurrentDictionary Properties { get; private set; } + + /// + /// Gets or adds the item with the specified key from tenant properties. If implements it will automatically be disposed of if the is disposed. + /// + /// + /// + /// + /// + public T GetOrAddProperty(string key, T item, bool disposeIfDisposable = true) + { + if (disposeIfDisposable) + { + EnsureDisposableRegistered(item); + } + + var getOrAddItem = this.Properties.GetOrAdd(key, item); + return (T)getOrAddItem; + } + + public bool TryGetProperty(string key, out T value) + { + var success = Properties.TryGetValue(key, out object item); + value = (T)item; + return success; + } + + /// + /// Gets or adds the item with the specified key from tenant properties. If implements it will automatically be disposed of if the is disposed. + /// + /// + /// + /// + /// + public T GetOrAddProperty(string key, Func factory, bool disposeIfDisposable = true) + { + var getOrAddItem = this.Properties.GetOrAdd(key, (a) => + { + T newItem = factory.Invoke(a); + if (disposeIfDisposable) + { + EnsureDisposableRegistered(newItem); + } + return newItem; + }); + return (T)getOrAddItem; + } + + private void EnsureDisposableRegistered(T item) + { + var disposable = item as IDisposable; + if (disposable != null) + { + _disposables.Add(disposable); + } + } + + public void RegisterCallbackOnDispose(Action action) + { + this._disposables.Add(new DelegateDisposable(action)); + } + + + /// + /// Uniquely identifies this tenant. + /// + public Guid Id { get; set; } + + public TTenant Tenant { get; set; } + + /// + /// Represents identifiers for this tenant. + /// A tenant can have multiple identifiers associated with it. + /// An identifier is returned from usally based on information available from HttpContext. + /// + internal HashSet Identifiers { get; set; } + + #region IDisposable Support + private bool disposedValue = false; // To detect redundant calls + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + _disposables.Dispose(); + // TODO: dispose managed state (managed objects). + Properties.Clear(); + } + + // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below. + // TODO: set large fields to null. + + disposedValue = true; + } + } + + public void Dispose() + { + Dispose(true); + } + #endregion + + + } +} diff --git a/src/Dotnettency/TenantShellAccessor.cs b/src/Dotnettency/TenantShell/TenantShellAccessor.cs similarity index 92% rename from src/Dotnettency/TenantShellAccessor.cs rename to src/Dotnettency/TenantShell/TenantShellAccessor.cs index db3e39d..9cc4d29 100644 --- a/src/Dotnettency/TenantShellAccessor.cs +++ b/src/Dotnettency/TenantShell/TenantShellAccessor.cs @@ -26,10 +26,11 @@ public TenantShellAccessor(ITenantShellFactory tenantFactory, return null; } - return await _tenantResolver.ResolveTenant(identifier, _tenantFactory); + return await _tenantResolver.ResolveTenantShell(identifier, _tenantFactory); }); } public Lazy>> CurrentTenantShell { get; private set; } + } } diff --git a/src/Dotnettency/TenantShell/TenantShellResolver.cs b/src/Dotnettency/TenantShell/TenantShellResolver.cs index 1c7d1f2..f0603ca 100644 --- a/src/Dotnettency/TenantShell/TenantShellResolver.cs +++ b/src/Dotnettency/TenantShell/TenantShellResolver.cs @@ -1,72 +1,122 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Dotnettency -{ - public class TenantShellResolver : ITenantShellResolver - where TTenant : class - { - private readonly ITenantShellCache _cache; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - - public TenantShellResolver(ITenantShellCache tenantShellCache) - { - _cache = tenantShellCache; - } - - public async Task> ResolveTenant(TenantIdentifier identifier, ITenantShellFactory tenantFactory) - { - if (_cache.TryGetValue(identifier, out TenantShell result)) - { - return result; - } - - try - { - await _semaphore.WaitAsync(); - - // Double locking - if (_cache.TryGetValue(identifier, out result)) - { - return result; - } - - var tenantResult = await tenantFactory.Get(identifier); - if (tenantResult == null) - { - // don't create null shell tenants - future requests with this identifier will still not find a shell for the tenant, - // so they will keep flowing through to ask the factory for the tenant. This is better if you are creating new tenants dynamically and a request is - // recieved with the tenants identifier, before the tenant has been set up in the source. It means the request will resolve to a null - // tenant for a while (factory may keep returning null), and eventually when a tenant is actually returned from the factory, a new tenant shell will then be created and added to - // the cache, at which point the factory will stop being used for subsequent requests. - return null; - } - - // We got a new shell tenant, so add it to the cache under its identifier, and any additional identifiers. - _cache.AddOrUpdate(identifier, tenantResult, (a, b) => { return tenantResult; }); - - bool distinguisherFound = false; - foreach (var item in tenantResult.Distinguishers) - { - if (item.Equals(identifier)) - { - distinguisherFound = true; - continue; - } - _cache.AddOrUpdate(item, tenantResult, (a, b) => { return tenantResult; }); - } - - if (!distinguisherFound) - { - tenantResult.Distinguishers.Add(identifier); - } - - return tenantResult; - } - finally - { - _semaphore.Release(); - } - } - } -} +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Dotnettency +{ + public class TenantShellResolver : ITenantShellResolver + where TTenant : class + { + private readonly ITenantShellCache _cache; + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); + + public TenantShellResolver(ITenantShellCache tenantShellCache) + { + _cache = tenantShellCache; + } + + public async Task> ResolveTenantShell(TenantIdentifier identifier, ITenantShellFactory tenantFactory) + { + if (_cache.TryGetValue(identifier, out TenantShell result)) + { + return result; + } + + try + { + await _semaphore.WaitAsync(); + + // Double locking - check cache again incase another thread just put it there. + if (_cache.TryGetValue(identifier, out result)) + { + return result; + } + + // initialise tenant shell for tenant identifier. + var tenantResult = await tenantFactory.Get(identifier); + if (tenantResult == null) + { + // don't cache / associate null tenant shells with tenant identifiers. This means future requests for a tenant who currenlty has a null shell will contiue to flow throw to the factory + // until it returns a tenant shell result. + + // This is better flow if you are creating new tenants dynamically and a request is recieved on the new tenants URL before the tenants has been set up in the source. + // It means the request will resolve to a null tenant shell for a while, (factory may keep returning null), and eventually when a tenant is actually returned from the factory, a new tenant shell will then be created and added to + // the cache, at which point the factory will stop being used for subsequent requests. + return null; + } + + // We got a new shell, so add it to the cache under its identifier, and any additional identifiers. + _cache.AddOrUpdate(identifier, tenantResult, (a, b) => { return tenantResult; }); + + bool distinguisherFound = false; + foreach (var item in tenantResult.Identifiers) + { + if (item.Equals(identifier)) + { + distinguisherFound = true; + continue; + } + _cache.AddOrUpdate(item, tenantResult, (a, b) => { return tenantResult; }); + } + + if (!distinguisherFound) + { + tenantResult.Identifiers.Add(identifier); + } + + return tenantResult; + } + finally + { + _semaphore.Release(); + } + } + + public async Task RemoveTenantShell(TenantIdentifier identifier) + { + + TenantShell removed = null; + + if (!_cache.TryGetValue(identifier, out TenantShell result)) + { + // tenant hasn't yet been initialised.. so nothing to do.. + return null; + } + + try + { + await _semaphore.WaitAsync(); // block restarting if a tenant intiialise in process or another restart in prcess. + + // tenant may have just been restarted on another thread and removed from cache, so check again. + //Todo: make this more robust if TryRemove returns false for example.. + var isRemoved = _cache.TryRemove(identifier, out removed); + if (isRemoved) + { + foreach (var item in removed.Identifiers) + { + if (item.Equals(identifier)) + { + continue; + } + isRemoved = _cache.TryRemove(item, out TenantShell itemRemoved); + } + } + + // dispose after small delay to allow respoinse to be returned flowing back through the same pipeline. +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + return removed; + //Task.Delay(new TimeSpan(0, 0, 2)).ContinueWith((t) => + //{ + // removed?.Dispose(); + //}); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + + } + finally + { + _semaphore.Release(); + } + } + } +} diff --git a/src/Dotnettency/TenantShell/TenantShellRestarter.cs b/src/Dotnettency/TenantShell/TenantShellRestarter.cs new file mode 100644 index 0000000..51e5579 --- /dev/null +++ b/src/Dotnettency/TenantShell/TenantShellRestarter.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; + +namespace Dotnettency +{ + public class TenantShellRestarter : ITenantShellRestarter + where TTenant : class + { + private readonly IHttpContextProvider _httpContextProvider; + private readonly TenantIdentifierAccessor _tenantDistinguisherAccessor; + private readonly ITenantShellResolver _tenantResolver; + + public TenantShellRestarter( + IHttpContextProvider httpContextProvider, + TenantIdentifierAccessor tenantDistinguisherAccessor, + ITenantShellResolver tenantResolver) + { + _httpContextProvider = httpContextProvider; + _tenantDistinguisherAccessor = tenantDistinguisherAccessor; + _tenantResolver = tenantResolver; + } + + public async Task Restart() + { + var identifier = await _tenantDistinguisherAccessor.TenantDistinguisher.Value; + if (identifier == null) + { + // current tenant cannot be identified. + return; + } + + var disposable = await _tenantResolver.RemoveTenantShell(identifier); + _httpContextProvider.GetCurrent().SetItem(Guid.NewGuid().ToString(), disposable, true); + + } + } +} diff --git a/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml b/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml index 6f5e0ac..6f28b54 100644 --- a/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml +++ b/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml @@ -6,5 +6,20 @@ }
-

Tenant Gicrosoft Razor Pages!

+

Tenant Gicrosoft Razor Pages!

+ +
+ @{ + if (!@Model.IsRestarted) + { + + } + else + { + +

Tenant has been restarted, the next request will result in Tenant Container being rebuilt, and tenant middleware pipeline being re-initialised.

+ } + } + +
diff --git a/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml.cs b/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml.cs index d63d53f..c90c316 100644 --- a/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml.cs +++ b/src/Sample.AspNetCore30.RazorPages/Pages/Gicrosoft/Index.cshtml.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Dotnettency; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; @@ -9,9 +10,19 @@ namespace Sample.Pages.T1 { public class IndexModel : PageModel { + + public bool IsRestarted { get; set; } + public void OnGet() { } + + public async Task OnPost([FromServices]ITenantShellRestarter restarter) + { + await restarter.Restart(); + IsRestarted = true; + this.Redirect("/"); + } } }