Skip to content

Commit

Permalink
Merge pull request #62 from marcwittke/develop
Browse files Browse the repository at this point in the history
5.2
  • Loading branch information
marcwittke committed Mar 1, 2019
2 parents d87eda4 + bdaa910 commit 8a0862b
Show file tree
Hide file tree
Showing 39 changed files with 844 additions and 783 deletions.
Original file line number Diff line number Diff line change
@@ -1,95 +1,102 @@
namespace Backend.Fx.Environment.MultiTenancy
{
using System;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using Logging;
using System;
using System.Globalization;
using System.Linq;
using Backend.Fx.Logging;
using JetBrains.Annotations;

namespace Backend.Fx.Environment.MultiTenancy
{
/// <summary>
/// Encapsulates the management of tenants
/// Note that this should not use repositories and other building blocks, but access the persistence layer directly
/// </summary>
public interface ITenantManager : IDisposable
public interface ITenantManager : IDisposable
{
event EventHandler<TenantId> TenantCreated;
event EventHandler<TenantId> TenantActivated;

TenantId[] GetTenantIds();
Tenant[] GetTenants();
Tenant GetTenant(TenantId id);
Tenant FindTenant(TenantId tenantId);
TenantId FindMatchingTenantId(string requestUri);
TenantId CreateDemonstrationTenant(string name, string description, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression = null);
TenantId CreateProductionTenant(string name, string description, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression = null);
TenantId GetDefaultTenantId();
void SaveTenant(Tenant tenant);
void ActivateTenant(Tenant tenant);
}

public abstract class TenantManager : ITenantManager
{
private static readonly ILogger Logger = LogManager.Create<TenantManager>();
private readonly object _syncLock = new object();

private readonly object _padlock = new object();

public event EventHandler<TenantId> TenantCreated;
public event EventHandler<TenantId> TenantActivated;

public TenantId CreateDemonstrationTenant(string name, string description, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression = null)
{
lock (_syncLock)
{
Logger.Info($"Creating demonstration tenant: {name}");
return CreateTenant(name, description, true, isDefault, defaultCultureInfo, uriMatchingExpression);
}
Logger.Info($"Creating demonstration tenant: {name}");
return CreateTenant(name, description, true, isDefault, defaultCultureInfo, uriMatchingExpression);
}

public TenantId CreateProductionTenant(string name, string description, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression = null)
{
lock (_syncLock)
{
Logger.Info($"Creating production tenant: {name}");
return CreateTenant(name, description, false, isDefault, defaultCultureInfo, uriMatchingExpression);
}
Logger.Info($"Creating production tenant: {name}");
return CreateTenant(name, description, false, isDefault, defaultCultureInfo, uriMatchingExpression);
}

public TenantId GetDefaultTenantId()
public abstract TenantId FindMatchingTenantId(string requestUri);

public abstract TenantId GetDefaultTenantId();
public void ActivateTenant(Tenant tenant)
{
var defaultTenant = GetTenants().SingleOrDefault(t => t.IsDefault);
return defaultTenant == null
? null
: new TenantId(defaultTenant.Id);
tenant.State = TenantState.Active;
SaveTenant(tenant);
RaiseTenantActivated(new TenantId(tenant.Id));
}

public abstract void SaveTenant(Tenant tenant);
protected abstract void SaveTenant(Tenant tenant);

public event EventHandler<TenantId> TenantCreated;
protected virtual void RaiseTenantCreated(TenantId tenantId)
{
TenantCreated?.Invoke(this, tenantId);
}

protected virtual void RaiseTenantActivated(TenantId tenantId)
{
TenantActivated?.Invoke(this, tenantId);
}

public abstract TenantId[] GetTenantIds();

public abstract Tenant[] GetTenants();

public Tenant GetTenant(TenantId tenantId)
{
Tenant tenant = FindTenant(tenantId);

if (tenant == null)
{
throw new ArgumentException($"Invalid tenant Id [{tenantId.Value}]", nameof(tenantId));
}

return tenant;
}
public abstract Tenant GetTenant(TenantId tenantId);

public abstract Tenant FindTenant(TenantId tenantId);

private TenantId CreateTenant([NotNull] string name, string description, bool isDemo, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression)
protected virtual TenantId CreateTenant([NotNull] string name, string description, bool isDemo, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
}

if (GetTenants().Any(t => t.Name != null && t.Name.ToLowerInvariant() == name.ToLowerInvariant()))
{
throw new ArgumentException($"There is already a tenant named {name}");
}

Tenant tenant = new Tenant(name, description, isDemo, defaultCultureInfo) { State = TenantState.Created, IsDefault = isDefault, UriMatchingExpression = uriMatchingExpression};
Tenant tenant = new Tenant(name, description, isDemo, defaultCultureInfo)
{
State = TenantState.Created,
IsDefault = isDefault,
UriMatchingExpression = uriMatchingExpression
};
SaveTenant(tenant);
var tenantId = new TenantId(tenant.Id);
TenantCreated?.Invoke(this, tenantId);
RaiseTenantCreated(tenantId);
return tenantId;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using JetBrains.Annotations;

namespace Backend.Fx.Environment.MultiTenancy
{
/// <summary>
/// A tenant manager that keeps all tenants in a lookup held in memory. Refresh is done
/// automatically when changes occur. However, in a multi process environment, manual
/// triggering of Refresh is required, when another process is updating the tenants.
/// </summary>
public abstract class LookupTenantManager : TenantManager
{
private LookupItem[] _lookupItems;
private TenantId _defaultTenantId = new TenantId(null);

public TenantId DefaultTenantId => _defaultTenantId;

public void EnsureLoaded()
{
if (_lookupItems == null) RefreshLookupItems();
}

public void RefreshLookupItems()
{
var newLookupItems = LoadTenantLookupItems();
Interlocked.Exchange(ref _lookupItems, newLookupItems);
}

public override TenantId[] GetTenantIds()
{
EnsureLoaded();
return _lookupItems.Select(itm => itm.TenantId).ToArray();
}

public override Tenant[] GetTenants()
{
EnsureLoaded();
return _lookupItems.Select(itm => itm.Tenant).ToArray();
}

public override Tenant GetTenant(TenantId tenantId)
{
EnsureLoaded();
return _lookupItems.First(itm => itm.TenantId.Value == tenantId.Value).Tenant;
}

[CanBeNull]
public override Tenant FindTenant(TenantId tenantId)
{
EnsureLoaded();
return _lookupItems.FirstOrDefault(itm => itm.Tenant.Id == tenantId.Value)?.Tenant;
}

public override TenantId FindMatchingTenantId(string requestUri)
{
EnsureLoaded();
return _lookupItems.FirstOrDefault(t => t.Regex != null && t.Regex.IsMatch(requestUri))?.TenantId ?? DefaultTenantId;
}

public override TenantId GetDefaultTenantId()
{
EnsureLoaded();
return DefaultTenantId;
}

protected override TenantId CreateTenant(string name, string description, bool isDemo, bool isDefault, CultureInfo defaultCultureInfo, string uriMatchingExpression)
{
var tenantId = base.CreateTenant(name, description, isDemo, isDefault, defaultCultureInfo, uriMatchingExpression);
return tenantId;
}

protected abstract Tenant[] LoadTenants();

protected override void Dispose(bool disposing)
{ }

protected override void RaiseTenantCreated(TenantId tenantId)
{
RefreshLookupItems();
base.RaiseTenantCreated(tenantId);
}

protected override void RaiseTenantActivated(TenantId tenantId)
{
RefreshLookupItems();
base.RaiseTenantActivated(tenantId);
}

private LookupItem[] LoadTenantLookupItems()
{
List<LookupItem> items = new List<LookupItem>();
foreach (var tenant in LoadTenants())
{
var tenantId = new TenantId(tenant.Id);
if (tenant.IsDefault)
{
Interlocked.Exchange(ref _defaultTenantId, tenantId);
}

items.Add(new LookupItem(tenant));
}

var newLookupItems = items.ToArray();
return newLookupItems;
}

private class LookupItem
{
public LookupItem(Tenant tenant)
{
Regex = tenant.UriMatchingExpression == null ? null : new Regex(tenant.UriMatchingExpression, RegexOptions.Compiled | RegexOptions.IgnoreCase);
Tenant = tenant;
TenantId = new TenantId(Tenant.Id);
}

public Regex Regex { get; }
public Tenant Tenant { get; }
public TenantId TenantId { get; }
}
}
}
22 changes: 22 additions & 0 deletions src/abstractions/Backend.Fx/Extensions/MultipleDisposable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;

namespace Backend.Fx.Extensions
{
public class MultipleDisposable : IDisposable
{
private readonly IDisposable[] _disposables;

public MultipleDisposable(params IDisposable[] disposables)
{
_disposables = disposables;
}

public void Dispose()
{
foreach (var disposable in _disposables)
{
disposable?.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using System;
using System.Threading;

namespace Backend.Fx.Extensions
{
public static class ReaderWriterLockSlimExtensions
{
private sealed class ReadLockToken : IDisposable
{
private ReaderWriterLockSlim _sync;
public ReadLockToken(ReaderWriterLockSlim sync)
{
_sync = sync;
sync.EnterReadLock();
}
public void Dispose()
{
if (_sync != null)
{
_sync.ExitReadLock();
_sync = null;
}
}
}
private sealed class WriteLockToken : IDisposable
{
private ReaderWriterLockSlim _sync;
public WriteLockToken(ReaderWriterLockSlim sync)
{
_sync = sync;
sync.EnterWriteLock();
}
public void Dispose()
{
if (_sync != null)
{
_sync.ExitWriteLock();
_sync = null;
}
}
}

public static IDisposable Read(this ReaderWriterLockSlim obj)
{
return new ReadLockToken(obj);
}
public static IDisposable Write(this ReaderWriterLockSlim obj)
{
return new WriteLockToken(obj);
}
}
}

0 comments on commit 8a0862b

Please sign in to comment.