diff --git a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index b79965aa12..bd6f64a529 100644 --- a/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -9,6 +10,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; using Volo.Abp.Domain.Entities; using Volo.Abp.Guids; +using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Uow; @@ -17,12 +19,21 @@ namespace Volo.Abp.EntityFrameworkCore public abstract class AbpDbContext : DbContext where TDbContext : DbContext { + public Guid? CurrentTenantId => CurrentTenant?.Id; + + protected virtual bool IsMayHaveTenantFilterEnabled => true; //TODO: Change this when data filtering system is full implemented + + public ICurrentTenant CurrentTenant { get; set; } + public IGuidGenerator GuidGenerator { get; set; } + private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo = typeof(AbpDbContext).GetMethod(nameof(ConfigureGlobalFilters), BindingFlags.Instance | BindingFlags.NonPublic); + protected AbpDbContext(DbContextOptions options) : base(options) { GuidGenerator = SimpleGuidGenerator.Instance; + CurrentTenant = NullCurrentTenant.Instance; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -32,6 +43,10 @@ namespace Volo.Abp.EntityFrameworkCore foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { ConfigureConcurrencyStamp(entityType); + + ConfigureGlobalFiltersMethodInfo + .MakeGenericMethod(entityType.ClrType) + .Invoke(this, new object[] { modelBuilder, entityType }); } } @@ -135,5 +150,82 @@ namespace Volo.Abp.EntityFrameworkCore } } } + + protected void ConfigureGlobalFilters(ModelBuilder modelBuilder, IMutableEntityType entityType) + where TEntity : class + { + if (entityType.BaseType == null && ShouldFilterEntity(entityType)) + { + var filterExpression = CreateFilterExpression(); + if (filterExpression != null) + { + modelBuilder.Entity().HasQueryFilter(filterExpression); + } + } + } + + protected virtual bool ShouldFilterEntity(IMutableEntityType entityType) where TEntity : class + { + if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) + { + return true; + } + + return false; + } + + protected virtual Expression> CreateFilterExpression() + where TEntity : class + { + Expression> expression = null; + + if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) + { + /* This condition should normally be defined as below: + * !IsMayHaveTenantFilterEnabled || ((IMayHaveTenant)e).TenantId == CurrentTenantId + * But this causes a problem with EF Core (see https://github.com/aspnet/EntityFrameworkCore/issues/9502) + * So, we made a workaround to make it working. It works same as above. + */ + Expression> multiTenantFilter = e => ((IMultiTenant)e).TenantId == CurrentTenantId || (((IMultiTenant)e).TenantId == CurrentTenantId) == IsMayHaveTenantFilterEnabled; + expression = expression == null ? multiTenantFilter : CombineExpressions(expression, multiTenantFilter); + } + + return expression; + } + + protected virtual Expression> CombineExpressions(Expression> expression1, Expression> expression2) + { + var parameter = Expression.Parameter(typeof(T)); + + var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter); + var left = leftVisitor.Visit(expression1.Body); + + var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter); + var right = rightVisitor.Visit(expression2.Body); + + return Expression.Lambda>(Expression.AndAlso(left, right), parameter); + } + + class ReplaceExpressionVisitor : ExpressionVisitor + { + private readonly Expression _oldValue; + private readonly Expression _newValue; + + public ReplaceExpressionVisitor(Expression oldValue, Expression newValue) + { + _oldValue = oldValue; + _newValue = newValue; + } + + public override Expression Visit(Expression node) + { + if (node == _oldValue) + { + return _newValue; + } + + return base.Visit(node); + } + } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/MultiTenancy/NullCurrentTenant.cs b/src/Volo.Abp/Volo/Abp/MultiTenancy/NullCurrentTenant.cs index 8b8b4a2189..87c641fbde 100644 --- a/src/Volo.Abp/Volo/Abp/MultiTenancy/NullCurrentTenant.cs +++ b/src/Volo.Abp/Volo/Abp/MultiTenancy/NullCurrentTenant.cs @@ -5,6 +5,8 @@ namespace Volo.Abp.MultiTenancy { public class NullCurrentTenant : ICurrentTenant, ISingletonDependency { + public static NullCurrentTenant Instance { get; } = new NullCurrentTenant(); + public Guid? Id { get; } = null; } } \ No newline at end of file diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.Designer.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.Designer.cs new file mode 100644 index 0000000000..697ae217b1 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.Designer.cs @@ -0,0 +1,68 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.Internal; +using System; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; + +namespace Volo.Abp.EntityFrameworkCore.Tests.Migrations +{ + [DbContext(typeof(TestAppDbContext))] + [Migration("20171026084129_Added_TenantId_To_Person")] + partial class Added_TenantId_To_Person + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("Volo.Abp.TestApp.Domain.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Age"); + + b.Property("Name"); + + b.Property("TenantId"); + + b.HasKey("Id"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Volo.Abp.TestApp.Domain.Phone", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Number"); + + b.Property("PersonId"); + + b.Property("Type"); + + b.HasKey("Id"); + + b.HasIndex("PersonId"); + + b.ToTable("AppPhones"); + }); + + modelBuilder.Entity("Volo.Abp.TestApp.Domain.Phone", b => + { + b.HasOne("Volo.Abp.TestApp.Domain.Person") + .WithMany("Phones") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.cs new file mode 100644 index 0000000000..0553e4e716 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/20171026084129_Added_TenantId_To_Person.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace Volo.Abp.EntityFrameworkCore.Tests.Migrations +{ + public partial class Added_TenantId_To_Person : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "TenantId", + table: "People", + type: "BLOB", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "TenantId", + table: "People"); + } + } +} diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/TestAppDbContextModelSnapshot.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/TestAppDbContextModelSnapshot.cs index be31c871b2..1ea7b55f65 100644 --- a/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/TestAppDbContextModelSnapshot.cs +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Migrations/TestAppDbContextModelSnapshot.cs @@ -29,6 +29,8 @@ namespace Volo.Abp.EntityFrameworkCore.Tests.Migrations b.Property("Name"); + b.Property("TenantId"); + b.HasKey("Id"); b.ToTable("People"); diff --git a/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/MultiTenant_Filter_Tests.cs b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/MultiTenant_Filter_Tests.cs new file mode 100644 index 0000000000..84e7cc44c6 --- /dev/null +++ b/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/MultiTenant_Filter_Tests.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using Shouldly; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TestApp; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Repositories +{ + public class MultiTenant_Filter_Tests : EntityFrameworkCoreTestBase + { + private ICurrentTenant _fakeCurrentTenant; + private readonly IRepository _personRepository; + + public MultiTenant_Filter_Tests() + { + _personRepository = GetRequiredService>(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + _fakeCurrentTenant = Substitute.For(); + services.AddSingleton(_fakeCurrentTenant); + } + + [Fact] + public async Task Should_Get_Person_For_Current_Tenant() + { + //TenantId = null + + _fakeCurrentTenant.Id.Returns((Guid?)null); + + var people = await _personRepository.GetListAsync(); + people.Count.ShouldBe(1); + people.Any(p => p.Name == "Douglas").ShouldBeTrue(); + + //TenantId = TestDataBuilder.TenantId1 + + _fakeCurrentTenant.Id.Returns(TestDataBuilder.TenantId1); + + people = await _personRepository.GetListAsync(); + people.Count.ShouldBe(2); + people.Any(p => p.Name == TestDataBuilder.TenantId1 + "-Person1").ShouldBeTrue(); + people.Any(p => p.Name == TestDataBuilder.TenantId1 + "-Person2").ShouldBeTrue(); + + //TenantId = TestDataBuilder.TenantId2 + + _fakeCurrentTenant.Id.Returns(TestDataBuilder.TenantId2); + + people = await _personRepository.GetListAsync(); + people.Count.ShouldBe(0); + } + } +} diff --git a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs index f3651c0783..dd3e8f6b39 100644 --- a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs +++ b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/Person.cs @@ -1,11 +1,14 @@ using System; using System.Collections.ObjectModel; using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.TestApp.Domain { - public class Person : AggregateRoot + public class Person : AggregateRoot, IMultiTenant { + public virtual Guid? TenantId { get; set; } + public virtual string Name { get; set; } public virtual int Age { get; set; } @@ -17,11 +20,12 @@ namespace Volo.Abp.TestApp.Domain } - public Person(Guid id, string name, int age) + public Person(Guid id, string name, int age, Guid? tenantId = null) { Id = id; Name = name; Age = age; + TenantId = tenantId; Phones = new Collection(); } diff --git a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs index 4bfec673bf..dcc16fbef0 100644 --- a/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs +++ b/test/Volo.Abp.TestApp/Volo/Abp/TestApp/TestDataBuilder.cs @@ -7,6 +7,9 @@ namespace Volo.Abp.TestApp { public class TestDataBuilder : ITransientDependency { + public static Guid TenantId1 { get; } = new Guid("55687dce-595c-41b4-a024-2a5e991ac8f4"); + public static Guid TenantId2 { get; } = new Guid("f522d19f-5a86-4278-98fb-0577319c544a"); + private readonly IRepository _personRepository; public TestDataBuilder(IRepository personRepository) @@ -24,7 +27,14 @@ namespace Volo.Abp.TestApp var douglas = new Person(Guid.NewGuid(), "Douglas", 42); douglas.Phones.Add(new Phone(douglas.Id, "123456789")); douglas.Phones.Add(new Phone(douglas.Id, "123456780", PhoneType.Home)); + _personRepository.Insert(douglas); + + var tenant1Person1 = new Person(Guid.NewGuid(), TenantId1 + "-Person1", 42, TenantId1); + var tenant1Person2 = new Person(Guid.NewGuid(), TenantId1 + "-Person2", 43, TenantId1); + + _personRepository.Insert(tenant1Person1); + _personRepository.Insert(tenant1Person2); } } } \ No newline at end of file