Skip to content

Commit

Permalink
feat: added MultiTenantDbContext.Create() and tests
Browse files Browse the repository at this point in the history
BREAKING CHANGE: MultiTenantDbContext constructors accepting ITenantInfo removed, use Create factory instead
  • Loading branch information
AndrewTriesToCode committed Nov 9, 2024
1 parent ebf6d86 commit 00dc5c4
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 145 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// Copyright Finbuckle LLC, Andrew White, and Contributors.
// Refer to the solution LICENSE file for more information.

using System.Threading;
using System.Threading.Tasks;
using Finbuckle.MultiTenant.Abstractions;
using Finbuckle.MultiTenant.Internal;
using Microsoft.EntityFrameworkCore;

namespace Finbuckle.MultiTenant.EntityFrameworkCore;
Expand All @@ -22,9 +21,33 @@ public abstract class MultiTenantDbContext : DbContext, IMultiTenantDbContext
/// <inheritdoc />
public TenantNotSetMode TenantNotSetMode { get; set; } = TenantNotSetMode.Throw;

protected MultiTenantDbContext(ITenantInfo? tenantInfo)
/// <summary>
/// Creates a new instance of a multitenant context that accepts a IMultiTenantContextAccessor instance and an optional DbContextOptions instance.
/// </summary>
/// <param name="tenantInfo">The tenant information to bind to the context.</param>
/// <param name="options">The database options instance.</param>
/// <typeparam name="TContext">The TContext implementation type.</typeparam>
/// <typeparam name="TTenantInfo">The ITenantInfo implementation type.</typeparam>
/// <returns></returns>
public static TContext Create<TContext, TTenantInfo>(TTenantInfo? tenantInfo, DbContextOptions? options = null)
where TContext : DbContext
where TTenantInfo : class, ITenantInfo, new()
{
TenantInfo = tenantInfo;
try
{
var mca = new StaticMultiTenantContextAccessor<TTenantInfo>(tenantInfo);
var context = options switch
{
null => (TContext)Activator.CreateInstance(typeof(TContext), mca)!,
not null => (TContext)Activator.CreateInstance(typeof(TContext), mca, options)!
};

return context;
}
catch (MissingMethodException)
{
throw new ArgumentException("The provided DbContext type does not have a constructor that accepts the required parameters.");
}
}

/// <summary>
Expand All @@ -36,17 +59,13 @@ protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAcc
TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
}

protected MultiTenantDbContext(ITenantInfo? tenantInfo, DbContextOptions options) : base(options)
{
TenantInfo = tenantInfo;
}

/// <summary>
/// Constructs the database context instance and binds to the current tenant.
/// </summary>
/// <param name="multiTenantContextAccessor">The MultiTenantContextAccessor instance used to bind the context instance to a tenant.</param>
/// <param name="options">The database options instance.</param>
protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(options)
protected MultiTenantDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) :
base(options)
{
TenantInfo = multiTenantContextAccessor.MultiTenantContext.TenantInfo;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Finbuckle.MultiTenant.Abstractions;

namespace Finbuckle.MultiTenant.Internal;

internal class StaticMultiTenantContextAccessor<TTenantInfo>(TTenantInfo? tenantInfo)
: IMultiTenantContextAccessor<TTenantInfo>
where TTenantInfo : class, ITenantInfo, new()
{
IMultiTenantContext IMultiTenantContextAccessor.MultiTenantContext => MultiTenantContext;

public IMultiTenantContext<TTenantInfo> MultiTenantContext { get; } =
new MultiTenantContext<TTenantInfo> { TenantInfo = tenantInfo };
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// Copyright Finbuckle LLC, Andrew White, and Contributors.
// Refer to the solution LICENSE file for more information.

using System;
using System.Linq;
using Finbuckle.MultiTenant.Abstractions;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
Expand All @@ -17,115 +14,115 @@ public class EntityTypeBuilderExtensionsShould : IDisposable

public EntityTypeBuilderExtensionsShould()
{
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
}
_connection = new SqliteConnection("DataSource=:memory:");
_connection.Open();
}

public void Dispose()
{
_connection?.Dispose();
}
_connection?.Dispose();
}

private TestDbContext GetDbContext(Action<ModelBuilder>? config = null, ITenantInfo? tenant = null)
private TestDbContext GetDbContext(Action<ModelBuilder>? config = null, TenantInfo? tenant = null)
{
var options = new DbContextOptionsBuilder()
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>() // needed for testing only
.UseSqlite(_connection)
.Options;
return new TestDbContext(config, tenant ?? new TenantInfo(), options);
}
var options = new DbContextOptionsBuilder()
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>() // needed for testing only
.UseSqlite(_connection)
.Options;
return new TestDbContext(config, tenant ?? new TenantInfo(), options);
}

[Fact]
public void SetMultiTenantAnnotation()
{
using var db = GetDbContext();
var annotation = db.Model.FindEntityType(typeof(MyMultiTenantThing))?
.FindAnnotation(Constants.MultiTenantAnnotationName);
using var db = GetDbContext();
var annotation = db.Model.FindEntityType(typeof(MyMultiTenantThing))?
.FindAnnotation(Constants.MultiTenantAnnotationName);

Assert.True((bool)annotation!.Value!);
}
Assert.True((bool)annotation!.Value!);
}

[Fact]
public void AddTenantIdStringShadowProperty()
{
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId");
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId");

Assert.Equal(typeof(string), prop?.ClrType);
Assert.True(prop?.IsShadowProperty());
Assert.Null(prop?.FieldInfo);
}
Assert.Equal(typeof(string), prop?.ClrType);
Assert.True(prop?.IsShadowProperty());
Assert.Null(prop?.FieldInfo);
}

[Fact]
public void RespectExistingTenantIdStringProperty()
{
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyThingWithTenantId))?.FindProperty("TenantId");
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyThingWithTenantId))?.FindProperty("TenantId");

Assert.Equal(typeof(string), prop!.ClrType);
Assert.False(prop.IsShadowProperty());
Assert.NotNull(prop.FieldInfo);
}
Assert.Equal(typeof(string), prop!.ClrType);
Assert.False(prop.IsShadowProperty());
Assert.NotNull(prop.FieldInfo);
}

[Fact]
public void ThrowOnNonStringExistingTenantIdProperty()
{
using var db = GetDbContext(b => b.Entity<MyThingWithIntTenantId>().IsMultiTenant());
Assert.Throws<MultiTenantException>(() => db.Model);
}
using var db = GetDbContext(b => b.Entity<MyThingWithIntTenantId>().IsMultiTenant());
Assert.Throws<MultiTenantException>(() => db.Model);
}

[Fact]
public void SetsTenantIdStringMaxLength()
{
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId");
using var db = GetDbContext();
var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId");

Assert.Equal(Internal.Constants.TenantIdMaxLength, prop!.GetMaxLength());
}
Assert.Equal(Internal.Constants.TenantIdMaxLength, prop!.GetMaxLength());
}

[Fact]
public void SetGlobalFilterQuery()
{
// Doesn't appear to be a way to test this except to try it out...
var tenant1 = new TenantInfo
{
Id = "abc"
};

var tenant2 = new TenantInfo
{
Id = "123"
};

using var db = GetDbContext(null, tenant1);
db.Database.EnsureCreated();
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 });
db.SaveChanges();

Assert.Equal(1, db.MyMultiTenantThings!.Count());
db.TenantInfo = tenant2;
Assert.Equal(0, db.MyMultiTenantThings!.Count());
}
// Doesn't appear to be a way to test this except to try it out...
var tenant1 = new TenantInfo
{
Id = "abc"
};

var tenant2 = new TenantInfo
{
Id = "123"
};

using var db = GetDbContext(null, tenant1);
db.Database.EnsureCreated();
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 });
db.SaveChanges();

Assert.Equal(1, db.MyMultiTenantThings!.Count());
db.TenantInfo = tenant2;
Assert.Equal(0, db.MyMultiTenantThings!.Count());
}

[Fact]
public void RespectExistingQueryFilter()
{
// Doesn't appear to be a way to test this except to try it out...
var tenant1 = new TenantInfo
{
Id = "abc"
};

using var db = GetDbContext(config =>
{
config.Entity<MyMultiTenantThing>().HasQueryFilter(e => e.Id == 1);
config.Entity<MyMultiTenantThing>().IsMultiTenant();
}, tenant1);
db.Database.EnsureCreated();
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 });
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 2 });
db.SaveChanges();

Assert.Equal(1, db.MyMultiTenantThings!.Count());
}
// Doesn't appear to be a way to test this except to try it out...
var tenant1 = new TenantInfo
{
Id = "abc"
};

using var db = GetDbContext(config =>
{
config.Entity<MyMultiTenantThing>().HasQueryFilter(e => e.Id == 1);
config.Entity<MyMultiTenantThing>().IsMultiTenant();
}, tenant1);
db.Database.EnsureCreated();
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 });
db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 2 });
db.SaveChanges();

Assert.Equal(1, db.MyMultiTenantThings!.Count());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Finbuckle.MultiTenant.Abstractions;
using Finbuckle.MultiTenant.Internal;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

Expand All @@ -14,10 +15,11 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext
{
private readonly Action<ModelBuilder>? _config;

public TestDbContext(Action<ModelBuilder>? config, ITenantInfo tenantInfo, DbContextOptions options) : base(tenantInfo, options)
public TestDbContext(Action<ModelBuilder>? config, TenantInfo tenantInfo, DbContextOptions options) :
base(new StaticMultiTenantContextAccessor<TenantInfo>(tenantInfo), options)
{
this._config = config;
}
this._config = config;
}

public DbSet<MyMultiTenantThing>? MyMultiTenantThings { get; set; }
public DbSet<MyThingWithTenantId>? MyThingsWithTenantIds { get; set; }
Expand All @@ -26,32 +28,32 @@ public TestDbContext(Action<ModelBuilder>? config, ITenantInfo tenantInfo, DbCon

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// If the test passed in a custom builder use it
if (_config != null)
_config(modelBuilder);
// Of use the standard builder configuration
else
{
modelBuilder.Entity<MyMultiTenantThing>().IsMultiTenant();
modelBuilder.Entity<MyThingWithTenantId>().IsMultiTenant();
}

base.OnModelCreating(modelBuilder);
// If the test passed in a custom builder use it
if (_config != null)
_config(modelBuilder);
// Of use the standard builder configuration
else
{
modelBuilder.Entity<MyMultiTenantThing>().IsMultiTenant();
modelBuilder.Entity<MyThingWithTenantId>().IsMultiTenant();
}

base.OnModelCreating(modelBuilder);
}
}

// ReSharper disable once ClassNeverInstantiated.Global
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
public object Create(DbContext context)
{
return new object();
}
return new object();
}

public object Create(DbContext context, bool designTime)
{
return new object();
}
return new object();
}
}

public class MyMultiTenantThing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Refer to the solution LICENSE file for more information.

using System.Collections.Generic;
using Finbuckle.MultiTenant.Internal;
using Microsoft.EntityFrameworkCore;

namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantDbContextExtensions;
Expand All @@ -13,7 +14,7 @@ public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext

public TestDbContext(TenantInfo tenantInfo,
DbContextOptions options) :
base(tenantInfo, options)
base(new StaticMultiTenantContextAccessor<TenantInfo>(tenantInfo), options)
{
}
}
Expand Down
Loading

0 comments on commit 00dc5c4

Please sign in to comment.