Browse Source

Merge pull request #17421 from abpframework/IsReadOnly-NoTracking

Use NoTracking for readonly repositories for EF core.
pull/17462/head
Halil İbrahim Kalkan 3 years ago
committed by GitHub
parent
commit
d5089ff839
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      docs/en/Entity-Framework-Core.md
  2. 8
      docs/en/Repositories.md
  3. 22
      framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs
  4. 33
      framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs
  5. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs
  6. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
  7. 7
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs
  8. 83
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  9. 11
      framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs
  10. 42
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs
  11. 75
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs

8
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:

8
docs/en/Repositories.md

@ -205,8 +205,6 @@ Methods:
- `WithDetails()` 1 overload
- `WithDetailsAsync()` 1 overload
Where as the `IReadOnlyBasicRepository<Tentity, TKey>` 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<TEntity, TKey>` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository<TEntity>` for your entity.

22
framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs

@ -0,0 +1,22 @@
namespace Volo.Abp.Data;
public class AbpRepositoryIsReadOnlyException : AbpException
{
/// <summary>
/// Creates a new <see cref="AbpRepositoryIsReadOnlyException"/> object.
/// </summary>
public AbpRepositoryIsReadOnlyException()
{
}
/// <summary>
/// Creates a new <see cref="AbpRepositoryIsReadOnlyException"/> object.
/// </summary>
/// <param name="message">Exception message</param>
public AbpRepositoryIsReadOnlyException(string message)
: base(message)
{
}
}

33
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<TEntity>
var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType);
if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType))
{
RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting);
RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting, true);
}
//IBasicRepository<TEntity>
@ -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<TEntity, TKey>
var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType);
if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
{
RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting);
RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true);
}
//IBasicRepository<TEntity, TKey>
@ -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<IRepository>(), 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);
}
}
}

2
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs

@ -34,6 +34,8 @@ public abstract class BasicRepositoryBase<TEntity> :
public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
public bool IsReadOnly { get; protected set; }
protected BasicRepositoryBase()
{

2
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs

@ -12,7 +12,7 @@ namespace Volo.Abp.Domain.Repositories;
/// </summary>
public interface IRepository
{
bool IsReadOnly { get; }
}
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>, IBasicRepository<TEntity>

7
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<TEntity>).AssemblyQualifiedName, nameof(repository));
}
public static IQueryable<TEntity> AsNoTrackingIf<TEntity>(this IQueryable<TEntity> queryable, bool condition)
where TEntity : class, IEntity
{
return condition ? queryable.AsNoTracking() : queryable;
}
}

83
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<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
{
return (await GetDbContextAsync()).Set<TEntity>();
}
protected async Task<IDbConnection> GetDbConnectionAsync()
{
return (await GetDbContextAsync()).Database.GetDbConnection();
@ -107,8 +105,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
);
}
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
CheckAndSetId(entity);
var dbContext = await GetDbContextAsync();
@ -123,8 +122,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return savedEntity;
}
public override async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task InsertManyAsync(IEnumerable<TEntity> 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<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
@ -169,8 +170,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return updatedEntity;
}
public override async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -195,8 +197,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, 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<TEntity>().Remove(entity);
@ -207,8 +210,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -233,26 +237,26 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
public async override Task<List<TEntity>> 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<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
public async override Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> 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<long> GetCountAsync(CancellationToken cancellationToken = default)
public async override Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken));
return await (await GetQueryableAsync()).LongCountAsync(GetCancellationToken(cancellationToken));
}
public override async Task<List<TEntity>> GetPagedListAsync(
public async override Task<List<TEntity>> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
@ -261,7 +265,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
{
var queryable = includeDetails
? await WithDetailsAsync()
: await GetDbSetAsync();
: await GetQueryableAsync();
return await queryable
.OrderByIf<TEntity, IQueryable<TEntity>>(!sorting.IsNullOrWhiteSpace(), sorting)
@ -272,20 +276,20 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
[Obsolete("Use GetQueryableAsync method.")]
protected override IQueryable<TEntity> GetQueryable()
{
return DbSet.AsQueryable();
return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly);
}
public override async Task<IQueryable<TEntity>> GetQueryableAsync()
public async override Task<IQueryable<TEntity>> 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<TEntity> FindAsync(
public async override Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
@ -294,13 +298,14 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, 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<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
public async override Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set<TEntity>();
@ -316,8 +321,9 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
}
}
public override async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
public async override Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set<TEntity>();
await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken));
@ -358,7 +364,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable());
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync()
public async override Task<IQueryable<TEntity>> WithDetailsAsync()
{
if (AbpEntityOptions.DefaultWithDetailsFunc == null)
{
@ -377,7 +383,7 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
);
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
public async override Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
{
return IncludeDetails(
await GetQueryableAsync(),
@ -421,6 +427,21 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, 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<TDbContext, TEntity, TKey> : EfCoreRepository<TDbContext, TEntity>,
@ -452,11 +473,14 @@ public class EfCoreRepository<TDbContext, TEntity, TKey> : EfCoreRepository<TDbC
{
return includeDetails
? await (await WithDetailsAsync()).OrderBy(e => 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<TDbContext, TEntity, TKey> : EfCoreRepository<TDbC
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken);

11
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);

42
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<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
//MyTestAggregateRootWithGuidPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
@ -69,24 +69,24 @@ public class RepositoryRegistration_Tests
//Assert
//MyTestAggregateRootWithoutPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
//MyTestAggregateRootWithGuidPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
//MyTestEntityWithInt32Pk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IReadOnlyBasicRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
}
@ -114,20 +114,20 @@ public class RepositoryRegistration_Tests
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithoutPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithoutPk>));
//MyTestAggregateRootWithGuidPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IReadOnlyBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
//MyTestEntityWithInt32Pk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IRepository<MyTestEntityWithInt32Pk>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IReadOnlyBasicRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
services.ShouldContainTransient(typeof(IRepository<MyTestEntityWithInt32Pk, int>), typeof(MyTestDefaultRepository<MyTestEntityWithInt32Pk, int>));
}
@ -209,10 +209,10 @@ public class RepositoryRegistration_Tests
services.ShouldNotContainService(typeof(IRepository<MyTestAggregateRootWithoutPk>));
//MyTestAggregateRootWithGuidPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestDefaultRepository<MyTestAggregateRootWithGuidPk, Guid>));
}
@ -234,11 +234,11 @@ public class RepositoryRegistration_Tests
new MyTestRepositoryRegistrar(options).AddRepositories();
//MyTestAggregateRootWithGuidPk
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IReadOnlyBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository<MyTestAggregateRootWithGuidPk, Guid>));
services.ShouldContainTransient(typeof(IBasicRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
services.ShouldContainTransient(typeof(IRepository<MyTestAggregateRootWithGuidPk, Guid>), typeof(MyTestAggregateRootWithDefaultPkCustomRepository));
}
@ -407,7 +407,7 @@ public class RepositoryRegistration_Tests
public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository
{
public bool IsReadOnly { get; set; }
}
public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions

75
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<AbpEntityFrameworkCoreTestModule>
{
[Fact]
public async Task ReadOnlyRepository_Should_NoTracking()
{
// Non-read-only repository tracking default
await WithUnitOfWorkAsync(async () =>
{
var repository = GetRequiredService<IRepository<Person, Guid>>();
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<IReadOnlyRepository<Person, Guid>>();
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<IReadOnlyRepository<Person, Guid>>();
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<IRepository<Person, Guid>>();
await repository.ToEfCoreRepository().InsertAsync(new Person(Guid.NewGuid(), "test", 18));
var person = await repository.ToEfCoreRepository().FirstOrDefaultAsync();
person.ShouldNotBeNull();
});
await WithUnitOfWorkAsync(async () =>
{
await Assert.ThrowsAsync<AbpRepositoryIsReadOnlyException>(async () =>
{
var readonlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
await readonlyRepository.ToEfCoreRepository().As<EfCoreRepository<TestAppDbContext, Person, Guid>>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18));
});
});
}
}
Loading…
Cancel
Save