From d7cf09578921e45001b14c285b985cc9a586894c Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 10:35:59 +0800 Subject: [PATCH 1/8] Add `IsReadOnly` to `IRepository`. --- .../ServiceCollectionRepositoryExtensions.cs | 29 +++++++++---- .../Repositories/BasicRepositoryBase.cs | 3 ++ .../Abp/Domain/Repositories/IRepository.cs | 2 +- .../ServiceCollectionShouldlyExtensions.cs | 11 +++++ .../RepositoryRegistration_Tests.cs | 42 +++++++++---------- 5 files changed, 58 insertions(+), 29 deletions(-) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs index c63a5442c7..f1e5068bc6 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; @@ -17,13 +18,13 @@ public static class ServiceCollectionRepositoryExtensions var readOnlyBasicRepositoryInterface = typeof(IReadOnlyBasicRepository<>).MakeGenericType(entityType); if (readOnlyBasicRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyBasicRepositoryInterface, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyBasicRepositoryInterface, repositoryImplementationType, replaceExisting, true); //IReadOnlyRepository var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType); if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting, true); } //IBasicRepository @@ -48,13 +49,13 @@ public static class ServiceCollectionRepositoryExtensions var readOnlyBasicRepositoryInterfaceWithPk = typeof(IReadOnlyBasicRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyBasicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true); //IReadOnlyRepository var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true); } //IBasicRepository @@ -80,15 +81,29 @@ public static class ServiceCollectionRepositoryExtensions IServiceCollection services, Type serviceType, Type implementationType, - bool replaceExisting) + bool replaceExisting, + bool isReadOnly = false) { + var descriptor = ServiceDescriptor.Transient(serviceType, implementationType); + + if (isReadOnly) + { + services.AddTransient(implementationType); + descriptor = ServiceDescriptor.Transient(serviceType, provider => + { + var repository = provider.GetRequiredService(implementationType); + ObjectHelper.TrySetProperty(repository.As(), x => x.IsReadOnly, _ => true); + return repository; + }); + } + if (replaceExisting) { - services.Replace(ServiceDescriptor.Transient(serviceType, implementationType)); + services.Replace(descriptor); } else { - services.TryAddTransient(serviceType, implementationType); + services.TryAdd(descriptor); } } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs index 0d86045eea..44d10b36ae 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs @@ -34,6 +34,9 @@ public abstract class BasicRepositoryBase : public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); + + public bool IsReadOnly { get; set; } = false; + protected BasicRepositoryBase() { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs index 3c7ae81875..d50a881c28 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs @@ -12,7 +12,7 @@ namespace Volo.Abp.Domain.Repositories; /// public interface IRepository { - + bool IsReadOnly { get; } } public interface IRepository : IReadOnlyRepository, IBasicRepository diff --git a/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs b/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs index 6c3f8e5090..541b62f58d 100644 --- a/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs +++ b/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs @@ -17,6 +17,17 @@ public static class ServiceCollectionShouldlyExtensions serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); } + public static void ShouldContainTransientImplementationFactory(this IServiceCollection services, Type serviceType) + { + var serviceDescriptor = services.FirstOrDefault(s => s.ServiceType == serviceType); + + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.ImplementationType.ShouldBeNull(); + serviceDescriptor.ImplementationFactory.ShouldNotBeNull(); + serviceDescriptor.ImplementationInstance.ShouldBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + public static void ShouldContainSingleton(this IServiceCollection services, Type serviceType, Type implementationType = null) { var serviceDescriptor = services.FirstOrDefault(s => s.ServiceType == serviceType); diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs index e6e17b253c..c12909bc98 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs @@ -31,15 +31,15 @@ public class RepositoryRegistration_Tests //Assert //MyTestAggregateRootWithoutPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); @@ -69,24 +69,24 @@ public class RepositoryRegistration_Tests //Assert //MyTestAggregateRootWithoutPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestEntityWithInt32Pk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -114,20 +114,20 @@ public class RepositoryRegistration_Tests services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); //MyTestEntityWithInt32Pk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -209,10 +209,10 @@ public class RepositoryRegistration_Tests services.ShouldNotContainService(typeof(IRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -234,11 +234,11 @@ public class RepositoryRegistration_Tests new MyTestRepositoryRegistrar(options).AddRepositories(); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); } @@ -407,7 +407,7 @@ public class RepositoryRegistration_Tests public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository { - + public bool IsReadOnly { get; set; } = false; } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions From 2c24f0c706802aa21a5163ef5220bc84d8ea30b9 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 11:33:04 +0800 Subject: [PATCH 2/8] 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)); + }); + }); + } +} From 599b39fa5210841502325a0b06e366546a4c5b08 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 13:21:06 +0800 Subject: [PATCH 3/8] Update Repositories.md --- docs/en/Repositories.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 95f908d004..57115f903b 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -217,6 +217,12 @@ They can all be seen as below: ![generic-repositories](images/generic-repositories.png) +#### Read Only Repositories behavior in EF Core + +EF Core will use No-tracking queries for read-only repositories. This means that the entities returned from the repository will not be tracked by the EF Core's change tracker. This is a performance optimization. + +However, if you need to track the entities, You can still add [AsTracking](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) to make specific queries tracking. + ### Generic Repository without a Primary Key If your entity does not have an Id primary key (it may have a composite primary key for instance) then you cannot use the `IRepository` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository` for your entity. From f47a09d6024956c8f6df39d437d157b26cfffb28 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 13:30:19 +0800 Subject: [PATCH 4/8] Update `AbpRepositoryIsReadOnlyException's` message. --- .../Repositories/EntityFrameworkCore/EfCoreRepository.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 e2b40d6886..2f89ac2b8a 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 @@ -452,7 +452,14 @@ public class EfCoreRepository : RepositoryBase, IE { if (IsReadOnly) { - throw new AbpRepositoryIsReadOnlyException($"Can not call {nameof(InsertAsync)}, {nameof(UpdateAsync)}, {nameof(DeleteAsync)}, {nameof(DeleteManyAsync)} methods on a read-only repository!"); + throw new AbpRepositoryIsReadOnlyException($"Can not call " + + $"{nameof(InsertAsync)}, " + + $"{nameof(InsertManyAsync)}, " + + $"{nameof(UpdateAsync)}, " + + $"{nameof(UpdateManyAsync)}, " + + $"{nameof(DeleteAsync)}, " + + $"{nameof(DeleteManyAsync)}, " + + $"{nameof(DeleteDirectAsync)} methods on a read-only repository!"); } } } From 1344d44948135bd330344709164a9a1bd7899fc4 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 21 Aug 2023 13:46:54 +0800 Subject: [PATCH 5/8] Rename `isReadOnly` to `isReadOnlyRepository`. --- .../ServiceCollectionRepositoryExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs index f1e5068bc6..ae85fbe24c 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs @@ -82,11 +82,11 @@ public static class ServiceCollectionRepositoryExtensions Type serviceType, Type implementationType, bool replaceExisting, - bool isReadOnly = false) + bool isReadOnlyRepository = false) { var descriptor = ServiceDescriptor.Transient(serviceType, implementationType); - if (isReadOnly) + if (isReadOnlyRepository) { services.AddTransient(implementationType); descriptor = ServiceDescriptor.Transient(serviceType, provider => From da3e2cdc32aade5b6e30813179d05ad561fc6c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 23 Aug 2023 17:37:54 +0300 Subject: [PATCH 6/8] Enhance the documentation part for readonly repositories. --- docs/en/Entity-Framework-Core.md | 8 ++++++++ docs/en/Repositories.md | 8 +++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index e2cfd21a05..1f8b9e22f0 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -594,6 +594,14 @@ Whenever you access to a property/collection, EF Core automatically performs an See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) of the EF Core. +## Read-Only Repositories + +ABP Framework provides read-only [repository](Repositories.md) interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`) to explicitly indicate that your purpose is to query data, but not change it. If so, you can inject these interfaces into your services. + +Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. + +> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. + ## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 57115f903b..a93460bbdd 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -205,8 +205,6 @@ Methods: - `WithDetails()` 1 overload - `WithDetailsAsync()` 1 overload - - Where as the `IReadOnlyBasicRepository` provides the following methods: - `GetCountAsync()` @@ -217,11 +215,11 @@ They can all be seen as below: ![generic-repositories](images/generic-repositories.png) -#### Read Only Repositories behavior in EF Core +#### Read Only Repositories behavior in Entity Framework Core -EF Core will use No-tracking queries for read-only repositories. This means that the entities returned from the repository will not be tracked by the EF Core's change tracker. This is a performance optimization. +Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. If you need to track the entities, you can still uses [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method. -However, if you need to track the entities, You can still add [AsTracking](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) to make specific queries tracking. +> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. ### Generic Repository without a Primary Key From 547bb83667b7aedbe0559931514f774c39496d53 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 24 Aug 2023 09:35:52 +0800 Subject: [PATCH 7/8] Change some code base on review cmment. --- .../ServiceCollectionRepositoryExtensions.cs | 8 ++++++-- .../Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs | 3 +-- .../Domain/Repositories/RepositoryRegistration_Tests.cs | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs index ae85fbe24c..0a2fa66b77 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs @@ -84,11 +84,11 @@ public static class ServiceCollectionRepositoryExtensions bool replaceExisting, bool isReadOnlyRepository = false) { - var descriptor = ServiceDescriptor.Transient(serviceType, implementationType); + ServiceDescriptor descriptor; if (isReadOnlyRepository) { - services.AddTransient(implementationType); + services.TryAddTransient(implementationType); descriptor = ServiceDescriptor.Transient(serviceType, provider => { var repository = provider.GetRequiredService(implementationType); @@ -96,6 +96,10 @@ public static class ServiceCollectionRepositoryExtensions return repository; }); } + else + { + descriptor = ServiceDescriptor.Transient(serviceType, implementationType); + } if (replaceExisting) { diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs index 44d10b36ae..6de2aa3208 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs @@ -34,8 +34,7 @@ public abstract class BasicRepositoryBase : public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); - - public bool IsReadOnly { get; set; } = false; + public bool IsReadOnly { get; protected set; } protected BasicRepositoryBase() { diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs index c12909bc98..f9b59e2c94 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs @@ -407,7 +407,7 @@ public class RepositoryRegistration_Tests public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository { - public bool IsReadOnly { get; set; } = false; + public bool IsReadOnly { get; set; } } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions From fe95e8ddd0d236dc4003d96a1a90cb64f6f7c619 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 24 Aug 2023 10:29:35 +0800 Subject: [PATCH 8/8] `AsNoTracking` for `Queryable` instead of `DbContext`. --- .../EfCoreRepositoryExtensions.cs | 7 +++ .../EntityFrameworkCore/EfCoreRepository.cs | 48 ++++++------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs index 94dba628d7..614ba135ff 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Entities; @@ -44,4 +45,10 @@ public static class EfCoreRepositoryExtensions throw new ArgumentException("Given repository does not implement " + typeof(IEfCoreRepository).AssemblyQualifiedName, nameof(repository)); } + + public static IQueryable AsNoTrackingIf(this IQueryable queryable, bool condition) + where TEntity : class, IEntity + { + return condition ? queryable.AsNoTracking() : queryable; + } } 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 2f89ac2b8a..b881c07245 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 @@ -35,50 +35,30 @@ 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)) { - dbContext = _dbContextProvider.GetDbContext(); + return _dbContextProvider.GetDbContext(); } } - else - { - dbContext = _dbContextProvider.GetDbContext(); - } - - if (IsReadOnly) - { - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - } - return dbContext; + return _dbContextProvider.GetDbContext(); } - protected virtual async Task GetDbContextAsync() + protected virtual Task GetDbContextAsync() { - TDbContext dbContext; // Multi-tenancy unaware entities should always use the host connection string if (!EntityHelper.IsMultiTenant()) { using (CurrentTenant.Change(null)) { - dbContext = await _dbContextProvider.GetDbContextAsync(); + return _dbContextProvider.GetDbContextAsync(); } } - else - { - dbContext = await _dbContextProvider.GetDbContextAsync(); - } - - if (IsReadOnly) - { - dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; - } - return dbContext; + return _dbContextProvider.GetDbContextAsync(); } [Obsolete("Use GetDbSetAsync() method.")] @@ -261,19 +241,19 @@ public class EfCoreRepository : RepositoryBase, IE { return includeDetails ? await (await WithDetailsAsync()).ToListAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).ToListAsync(GetCancellationToken(cancellationToken)); + : await (await GetQueryableAsync()).ToListAsync(GetCancellationToken(cancellationToken)); } 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)); + : await (await GetQueryableAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)); } public async override Task GetCountAsync(CancellationToken cancellationToken = default) { - return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); + return await (await GetQueryableAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); } public async override Task> GetPagedListAsync( @@ -285,7 +265,7 @@ public class EfCoreRepository : RepositoryBase, IE { var queryable = includeDetails ? await WithDetailsAsync() - : await GetDbSetAsync(); + : await GetQueryableAsync(); return await queryable .OrderByIf>(!sorting.IsNullOrWhiteSpace(), sorting) @@ -296,12 +276,12 @@ public class EfCoreRepository : RepositoryBase, IE [Obsolete("Use GetQueryableAsync method.")] protected override IQueryable GetQueryable() { - return DbSet.AsQueryable(); + return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly); } public async override Task> GetQueryableAsync() { - return (await GetDbSetAsync()).AsQueryable(); + return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(IsReadOnly); } protected async override Task SaveChangesAsync(CancellationToken cancellationToken) @@ -318,7 +298,7 @@ public class EfCoreRepository : RepositoryBase, IE ? await (await WithDetailsAsync()) .Where(predicate) .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()) + : await (await GetQueryableAsync()) .Where(predicate) .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); } @@ -493,7 +473,9 @@ public class EfCoreRepository : EfCoreRepository e.Id).FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).FindAsync(new object[] { id }, GetCancellationToken(cancellationToken)); + : IsReadOnly + ? await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken)) + : await (await GetDbSetAsync()).FindAsync(new object[] {id}, GetCancellationToken(cancellationToken)); } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)