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 95f908d004..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,6 +215,12 @@ They can all be seen as below: ![generic-repositories](images/generic-repositories.png) +#### Read Only Repositories behavior in Entity Framework Core + +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. + +> 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 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. 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.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs index c63a5442c7..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 @@ -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,33 @@ public static class ServiceCollectionRepositoryExtensions IServiceCollection services, Type serviceType, Type implementationType, - bool replaceExisting) + bool replaceExisting, + bool isReadOnlyRepository = false) { + ServiceDescriptor descriptor; + + if (isReadOnlyRepository) + { + services.TryAddTransient(implementationType); + descriptor = ServiceDescriptor.Transient(serviceType, provider => + { + var repository = provider.GetRequiredService(implementationType); + ObjectHelper.TrySetProperty(repository.As(), x => x.IsReadOnly, _ => true); + return repository; + }); + } + else + { + descriptor = ServiceDescriptor.Transient(serviceType, implementationType); + } + 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..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,6 +34,8 @@ public abstract class BasicRepositoryBase : public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); + public bool IsReadOnly { get; protected set; } + 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/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 96d7b4111e..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 @@ -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; @@ -75,7 +73,7 @@ public class EfCoreRepository : RepositoryBase, IE { return (await GetDbContextAsync()).Set(); } - + protected async Task GetDbConnectionAsync() { return (await GetDbContextAsync()).Database.GetDbConnection(); @@ -107,8 +105,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 +122,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 +153,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 +170,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 +197,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 +210,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 +237,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)); + : await (await GetQueryableAsync()).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)); + : await (await GetQueryableAsync()).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)); + return await (await GetQueryableAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); } - public override async Task> GetPagedListAsync( + public async override Task> GetPagedListAsync( int skipCount, int maxResultCount, string sorting, @@ -261,7 +265,7 @@ public class EfCoreRepository : RepositoryBase, IE { var queryable = includeDetails ? await WithDetailsAsync() - : await GetDbSetAsync(); + : await GetQueryableAsync(); return await queryable .OrderByIf>(!sorting.IsNullOrWhiteSpace(), sorting) @@ -272,20 +276,20 @@ public class EfCoreRepository : RepositoryBase, IE [Obsolete("Use GetQueryableAsync method.")] protected override IQueryable GetQueryable() { - return DbSet.AsQueryable(); + return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly); } - public override async Task> GetQueryableAsync() + public async override Task> GetQueryableAsync() { - return (await GetDbSetAsync()).AsQueryable(); + return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(IsReadOnly); } - 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) @@ -294,13 +298,14 @@ public class EfCoreRepository : RepositoryBase, IE ? await (await WithDetailsAsync()) .Where(predicate) .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()) + : await (await GetQueryableAsync()) .Where(predicate) .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 +321,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 +364,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 +383,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 +427,21 @@ public class EfCoreRepository : RepositoryBase, IE true ); } + + protected virtual void CheckReadOnly() + { + if (IsReadOnly) + { + 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!"); + } + } } public class EfCoreRepository : EfCoreRepository, @@ -452,11 +473,14 @@ 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) { + CheckReadOnly(); var entity = await FindAsync(id, cancellationToken: cancellationToken); if (entity == null) { @@ -468,6 +492,7 @@ public class 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/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..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 @@ -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; } } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions 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)); + }); + }); + } +}