From 2c24f0c706802aa21a5163ef5220bc84d8ea30b9 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 11:33:04 +0800 Subject: [PATCH] Throw `AbpRepositoryIsReadOnlyException` when the read-only repository writes method calls. --- .../Data/AbpRepositoryIsReadOnlyException.cs | 22 +++++ .../EntityFrameworkCore/EfCoreRepository.cs | 88 +++++++++++++------ .../Repositories/ReadOnlyRepository_Tests.cs | 75 ++++++++++++++++ 3 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs new file mode 100644 index 0000000000..6c648fd022 --- /dev/null +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs @@ -0,0 +1,22 @@ +namespace Volo.Abp.Data; + +public class AbpRepositoryIsReadOnlyException : AbpException +{ + /// + /// Creates a new object. + /// + public AbpRepositoryIsReadOnlyException() + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + public AbpRepositoryIsReadOnlyException(string message) + : base(message) + { + + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 96d7b4111e..e2b40d6886 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -1,4 +1,3 @@ -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -6,16 +5,15 @@ using System; using System.Collections.Generic; using System.Data; using System.Linq; -using System.Linq.Dynamic.Core; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Storage; +using Volo.Abp.Data; using Volo.Abp.Domain.Entities; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.DependencyInjection; using Volo.Abp.Guids; -using Volo.Abp.MultiTenancy; namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore; @@ -37,30 +35,50 @@ public class EfCoreRepository : RepositoryBase, IE [Obsolete("Use GetDbContextAsync() method.")] private TDbContext GetDbContext() { + TDbContext dbContext; // Multi-tenancy unaware entities should always use the host connection string if (!EntityHelper.IsMultiTenant()) { using (CurrentTenant.Change(null)) { - return _dbContextProvider.GetDbContext(); + dbContext = _dbContextProvider.GetDbContext(); } } + else + { + dbContext = _dbContextProvider.GetDbContext(); + } + + if (IsReadOnly) + { + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + } - return _dbContextProvider.GetDbContext(); + return dbContext; } - protected virtual Task GetDbContextAsync() + protected virtual async Task GetDbContextAsync() { + TDbContext dbContext; // Multi-tenancy unaware entities should always use the host connection string if (!EntityHelper.IsMultiTenant()) { using (CurrentTenant.Change(null)) { - return _dbContextProvider.GetDbContextAsync(); + dbContext = await _dbContextProvider.GetDbContextAsync(); } } + else + { + dbContext = await _dbContextProvider.GetDbContextAsync(); + } + + if (IsReadOnly) + { + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + } - return _dbContextProvider.GetDbContextAsync(); + return dbContext; } [Obsolete("Use GetDbSetAsync() method.")] @@ -75,7 +93,7 @@ public class EfCoreRepository : RepositoryBase, IE { return (await GetDbContextAsync()).Set(); } - + protected async Task GetDbConnectionAsync() { return (await GetDbContextAsync()).Database.GetDbConnection(); @@ -107,8 +125,9 @@ public class EfCoreRepository : RepositoryBase, IE ); } - public override async Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); CheckAndSetId(entity); var dbContext = await GetDbContextAsync(); @@ -123,8 +142,9 @@ public class EfCoreRepository : RepositoryBase, IE return savedEntity; } - public override async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var entityArray = entities.ToArray(); var dbContext = await GetDbContextAsync(); cancellationToken = GetCancellationToken(cancellationToken); @@ -153,8 +173,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Attach(entity); @@ -169,8 +190,9 @@ public class EfCoreRepository : RepositoryBase, IE return updatedEntity; } - public override async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -195,8 +217,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Set().Remove(entity); @@ -207,8 +230,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -233,26 +257,26 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) + public async override Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) { return includeDetails ? await (await WithDetailsAsync()).ToListAsync(GetCancellationToken(cancellationToken)) : await (await GetDbSetAsync()).ToListAsync(GetCancellationToken(cancellationToken)); } - public override async Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) + public async override Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) { return includeDetails ? await (await WithDetailsAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)) : await (await GetDbSetAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)); } - public override async Task GetCountAsync(CancellationToken cancellationToken = default) + public async override Task GetCountAsync(CancellationToken cancellationToken = default) { return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); } - public override async Task> GetPagedListAsync( + public async override Task> GetPagedListAsync( int skipCount, int maxResultCount, string sorting, @@ -275,17 +299,17 @@ public class EfCoreRepository : RepositoryBase, IE return DbSet.AsQueryable(); } - public override async Task> GetQueryableAsync() + public async override Task> GetQueryableAsync() { return (await GetDbSetAsync()).AsQueryable(); } - protected override async Task SaveChangesAsync(CancellationToken cancellationToken) + protected async override Task SaveChangesAsync(CancellationToken cancellationToken) { await (await GetDbContextAsync()).SaveChangesAsync(cancellationToken); } - public override async Task FindAsync( + public async override Task FindAsync( Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) @@ -299,8 +323,9 @@ public class EfCoreRepository : RepositoryBase, IE .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); } - public override async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); var dbSet = dbContext.Set(); @@ -316,8 +341,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) + public async override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); var dbSet = dbContext.Set(); await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); @@ -358,7 +384,7 @@ public class EfCoreRepository : RepositoryBase, IE return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable()); } - public override async Task> WithDetailsAsync() + public async override Task> WithDetailsAsync() { if (AbpEntityOptions.DefaultWithDetailsFunc == null) { @@ -377,7 +403,7 @@ public class EfCoreRepository : RepositoryBase, IE ); } - public override async Task> WithDetailsAsync(params Expression>[] propertySelectors) + public async override Task> WithDetailsAsync(params Expression>[] propertySelectors) { return IncludeDetails( await GetQueryableAsync(), @@ -421,6 +447,14 @@ public class EfCoreRepository : RepositoryBase, IE true ); } + + protected virtual void CheckReadOnly() + { + if (IsReadOnly) + { + throw new AbpRepositoryIsReadOnlyException($"Can not call {nameof(InsertAsync)}, {nameof(UpdateAsync)}, {nameof(DeleteAsync)}, {nameof(DeleteManyAsync)} methods on a read-only repository!"); + } + } } public class EfCoreRepository : EfCoreRepository, @@ -457,6 +491,7 @@ public class EfCoreRepository : EfCoreRepository : EfCoreRepository ids, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs new file mode 100644 index 0000000000..bd073ce15a --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Repositories; + +public class ReadOnlyRepository_Tests : TestAppTestBase +{ + [Fact] + public async Task ReadOnlyRepository_Should_NoTracking() + { + // Non-read-only repository tracking default + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await repository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(list.Count); + }); + + // Read-only repository no tracking default + await WithUnitOfWorkAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + var db = await readonlyRepository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await readonlyRepository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(0); + }); + + // Read-only repository can tracking manually by AsTracking + await WithUnitOfWorkAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + var db = await readonlyRepository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await (await readonlyRepository.ToEfCoreRepository().GetQueryableAsync()).AsTracking().ToListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(list.Count); + }); + } + + [Fact] + public async Task ReadOnlyRepository_Should_Throw_AbpRepositoryIsReadOnlyException_When_Write_Method_Call() + { + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + await repository.ToEfCoreRepository().InsertAsync(new Person(Guid.NewGuid(), "test", 18)); + var person = await repository.ToEfCoreRepository().FirstOrDefaultAsync(); + person.ShouldNotBeNull(); + }); + + await WithUnitOfWorkAsync(async () => + { + await Assert.ThrowsAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + await readonlyRepository.ToEfCoreRepository().As>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18)); + }); + }); + } +}