Browse Source

Merge pull request #17491 from abpframework/IsChangeTrackingEnabled

Introduce ` RepositoryInterceptor` and `Enable/DisableTracking()` extension methods.
pull/17543/head
Halil İbrahim Kalkan 3 years ago
committed by GitHub
parent
commit
9971dc9d8b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      docs/en/Entity-Framework-Core.md
  2. 73
      docs/en/Repositories.md
  3. 22
      framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs
  4. 1
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs
  5. 2
      framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs
  6. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs
  7. 53
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingHelper.cs
  8. 30
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptor.cs
  9. 22
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptorRegistrar.cs
  10. 15
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/DisableEntityChangeTrackingAttribute.cs
  11. 15
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EnableEntityChangeTrackingAttribute.cs
  12. 14
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EntityChangeTrackingAttribute.cs
  13. 28
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs
  14. 19
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/EntityChangeTrackingProvider.cs
  15. 10
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IEntityChangeTrackingProvider.cs
  16. 2
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs
  17. 34
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs
  18. 33
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  19. 2
      framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs
  20. 168
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs
  21. 41
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs

6
docs/en/Entity-Framework-Core.md

@ -598,10 +598,14 @@ See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/queryi
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.
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 use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document).
> 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.
## Enabling / Disabling the Change Tracking
In addition to the read-only repositories, ABP allows to manually control the change tracking behavior for querying objects. Please see the *Enabling / Disabling the Change Tracking* section of the [Repositories documentation](Repositories.md) to learn how to use it.
## 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:

73
docs/en/Repositories.md

@ -176,6 +176,77 @@ Some features (like soft-delete, multi-tenancy and audit logging) won't work, so
The `EnsureExistsAsync` extension method accepts entity id or entities query expression to ensure entities exist, otherwise, it will throw `EntityNotFoundException`.
### Enabling / Disabling the Change Tracking
ABP provides repository extension methods and attributes those can be used to control the change tracking behavior for queried entities in the underlying database provider.
Disabling change tracking can gain performance if you query many entities from the database for read-only purposes. Querying single or a few entities won't make much performance difference, but you are free to use it whenever you like.
> If the underlying database provider doesn't support change tracking, then this system won't have any effect. [Entity Framework Core](Entity-Framework-Core.md) supports change tracking, for example, while the [MongoDB](MongoDB.md) provider doesn't support it.
#### Repository Extension Methods for Change Tracking
Change tracking is enabled unless you explicitly disable it.
**Example: Using the `DisableTracking` extension method**
````csharp
public class MyDemoService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
public MyDemoService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
public async Task DoItAsync()
{
// Change tracking is enabled in that point (by default)
using (_personRepository.DisableTracking())
{
// Change tracking is disabled in that point
var list = await _personRepository.GetPagedListAsync(0, 100, "Name ASC");
}
// Change tracking is enabled in that point (by default)
}
}
````
> `DisableTracking` extension method returns a `IDisposable` object, so you can safely **restore** the change tracking behavior to the **previous state** one the `using` block ends. Basically, `DisableTracking` method ensures that the change tracking is disabled inside the `using` block, but doesn't affect outside of the `using` block. That means, if change tracking was already disabled, `DisableTracking` and the disposable return value do nothing.
`EnableTracking()` method works exactly opposite to the `DisableTracking()` method. You typically won't use it (because the change tracking is already enabled by default), but it is there in case of you need that.
#### Attributes for Change Tracking
You typically use the `DisableTracking()` method for the application service methods those only returns data, but doesn't make any change on entities. For such cases, you can use the `DisableEntityChangeTracking` attribute on your method/class as a shortcut to disable the change tracking for whole method body.
**Example: Using the `DisableEntityChangeTracking` attribute on a method**
````csharp
[DisableEntityChangeTracking]
public virtual async Task<List<PersonDto>> GetListAsync()
{
/* We disabled the change tracking in this method
because we won't change the people objects */
var people = await _personRepository.GetListAsync();
return ObjectMapper.Map<List<Person>, List<PersonDto>(people);
}
````
`EnableEntityChangeTracking` can be used for the opposite purpose, and it ensures that the change tracking is enabled for a given method. Since the change tracking is enabled by default, `EnableEntityChangeTracking` may be needed only if you know that your method is called from a context that disables the change tracking.
`DisableEntityChangeTracking` and `EnableEntityChangeTracking` attributes can be used on a **method** or on a **class** (which affects all of the class methods).
ABP uses dynamic proxying to make these attributes working. There are some rules here:
* If you are **not injecting** the service over an interface (like `IPersonAppService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted.
> Change tracking behavior doesn't affect tracking entity objects returned from `InsertAsync` and `UpdateAsync` methods. The objects returned from these methods are always tracked (if the underlying provider has the change tracking feature) and any change you made to these objects are saved into the database.
## Other Generic Repository Types
Standard `IRepository<TEntity, TKey>` interface exposes the standard `IQueryable<TEntity>` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`.
@ -217,7 +288,7 @@ They can all be seen as below:
#### 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.
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 use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document).
> 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.

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

@ -1,22 +0,0 @@
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)
{
}
}

1
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs

@ -1,4 +1,3 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;

2
framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs

@ -92,7 +92,7 @@ public static class ServiceCollectionRepositoryExtensions
descriptor = ServiceDescriptor.Transient(serviceType, provider =>
{
var repository = provider.GetRequiredService(implementationType);
ObjectHelper.TrySetProperty(repository.As<IRepository>(), x => x.IsReadOnly, _ => true);
ObjectHelper.TrySetProperty(repository.As<IRepository>(), x => x.IsChangeTrackingEnabled, _ => false);
return repository;
});
}

2
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs

@ -2,6 +2,7 @@
using Volo.Abp.Auditing;
using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.Domain.ChangeTracking;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus;
using Volo.Abp.ExceptionHandling;
@ -30,5 +31,6 @@ public class AbpDddDomainModule : AbpModule
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddConventionalRegistrar(new AbpRepositoryConventionalRegistrar());
context.Services.OnRegistered(ChangeTrackingInterceptorRegistrar.RegisterIfNeeded);
}
}

53
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingHelper.cs

@ -0,0 +1,53 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Volo.Abp.Domain.Repositories;
namespace Volo.Abp.Domain.ChangeTracking;
public static class ChangeTrackingHelper
{
public static bool IsEntityChangeTrackingType(TypeInfo implementationType)
{
return HasEntityChangeTrackingAttribute(implementationType) || AnyMethodHasEntityChangeTrackingAttribute(implementationType);
}
public static bool IsEntityChangeTrackingMethod([NotNull] MethodInfo methodInfo, out EntityChangeTrackingAttribute? entityChangeTrackingAttribute)
{
Check.NotNull(methodInfo, nameof(methodInfo));
//Method declaration
var attrs = methodInfo.GetCustomAttributes(true).OfType<EntityChangeTrackingAttribute>().ToArray();
if (attrs.Any())
{
entityChangeTrackingAttribute = attrs.First();
return true;
}
if (methodInfo.DeclaringType != null)
{
//Class declaration
attrs = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType<EntityChangeTrackingAttribute>().ToArray();
if (attrs.Any())
{
entityChangeTrackingAttribute = attrs.First();
return true;
}
}
entityChangeTrackingAttribute = null;
return false;
}
private static bool AnyMethodHasEntityChangeTrackingAttribute(TypeInfo implementationType)
{
return implementationType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(HasEntityChangeTrackingAttribute);
}
private static bool HasEntityChangeTrackingAttribute(MemberInfo memberInfo)
{
return memberInfo.IsDefined(typeof(EntityChangeTrackingAttribute), true);
}
}

30
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptor.cs

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Domain.ChangeTracking;
public class ChangeTrackingInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IEntityChangeTrackingProvider _entityChangeTrackingProvider;
public ChangeTrackingInterceptor(IEntityChangeTrackingProvider entityChangeTrackingProvider)
{
_entityChangeTrackingProvider = entityChangeTrackingProvider;
}
public async override Task InterceptAsync(IAbpMethodInvocation invocation)
{
if (!ChangeTrackingHelper.IsEntityChangeTrackingMethod(invocation.Method, out var changeTrackingAttribute))
{
await invocation.ProceedAsync();
return;
}
using (_entityChangeTrackingProvider.Change(changeTrackingAttribute?.IsEnabled))
{
await invocation.ProceedAsync();
}
}
}

22
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptorRegistrar.cs

@ -0,0 +1,22 @@
using System;
using System.Reflection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Domain.ChangeTracking;
public class ChangeTrackingInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<ChangeTrackingInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
return !DynamicProxyIgnoreTypes.Contains(type) && ChangeTrackingHelper.IsEntityChangeTrackingType(type.GetTypeInfo());
}
}

15
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/DisableEntityChangeTrackingAttribute.cs

@ -0,0 +1,15 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
/// <summary>
/// Ensures that the change tracking in enabled for the given method or class.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class DisableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute
{
public DisableEntityChangeTrackingAttribute()
: base(false)
{
}
}

15
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EnableEntityChangeTrackingAttribute.cs

@ -0,0 +1,15 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
/// <summary>
/// Ensures that the change tracking in enabled for the given method or class.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute
{
public EnableEntityChangeTrackingAttribute()
: base(true)
{
}
}

14
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EntityChangeTrackingAttribute.cs

@ -0,0 +1,14 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public abstract class EntityChangeTrackingAttribute : Attribute
{
public virtual bool IsEnabled { get; set; }
public EntityChangeTrackingAttribute(bool isEnabled)
{
IsEnabled = isEnabled;
}
}

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

@ -4,6 +4,8 @@ using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
@ -34,7 +36,13 @@ public abstract class BasicRepositoryBase<TEntity> :
public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
public bool IsReadOnly { get; protected set; }
public ILoggerFactory? LoggerFactory => LazyServiceProvider.LazyGetService<ILoggerFactory>();
public ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance);
public IEntityChangeTrackingProvider EntityChangeTrackingProvider => LazyServiceProvider.LazyGetRequiredService<IEntityChangeTrackingProvider>();
public bool? IsChangeTrackingEnabled { get; protected set; }
protected BasicRepositoryBase()
{
@ -108,6 +116,24 @@ public abstract class BasicRepositoryBase<TEntity> :
{
return CancellationTokenProvider.FallbackToProvider(preferredValue);
}
protected virtual bool ShouldTrackingEntityChange()
{
// If IsChangeTrackingEnabled is set, it has the highest priority. This generally means the repository is read-only.
if (IsChangeTrackingEnabled.HasValue)
{
return IsChangeTrackingEnabled.Value;
}
// If Interface/Class/Method has Enable/DisableEntityChangeTrackingAttribute, it has the second highest priority.
if (EntityChangeTrackingProvider.Enabled.HasValue)
{
return EntityChangeTrackingProvider.Enabled.Value;
}
// Default behavior is tracking entity change.
return true;
}
}
public abstract class BasicRepositoryBase<TEntity, TKey> : BasicRepositoryBase<TEntity>, IBasicRepository<TEntity, TKey>

19
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/EntityChangeTrackingProvider.cs

@ -0,0 +1,19 @@
using System;
using System.Threading;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Domain.Repositories;
public class EntityChangeTrackingProvider : IEntityChangeTrackingProvider, ISingletonDependency
{
public bool? Enabled => _current.Value;
private readonly AsyncLocal<bool?> _current = new AsyncLocal<bool?>();
public IDisposable Change(bool? enabled)
{
var previousValue = Enabled;
_current.Value = enabled;
return new DisposeAction(() => _current.Value = previousValue);
}
}

10
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IEntityChangeTrackingProvider.cs

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Domain.Repositories;
public interface IEntityChangeTrackingProvider
{
bool? Enabled { get; }
IDisposable Change(bool? enabled);
}

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; }
bool? IsChangeTrackingEnabled { get; }
}
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>, IBasicRepository<TEntity>

34
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs

@ -145,6 +145,40 @@ public static class RepositoryExtensions
}
}
/// <summary>
/// Disables change tracking mechanism for the given repository.
/// </summary>
/// <param name="repository">A repository object</param>
/// <returns>
/// A disposable object. Dispose it to restore change tracking mechanism back to its previous state.
/// </returns>
public static IDisposable DisableTracking(this IRepository repository)
{
return Tracking(repository, false);
}
/// <summary>
/// Enables change tracking mechanism for the given repository.
/// </summary>
/// <param name="repository">A repository object</param>
/// <returns>
/// A disposable object. Dispose it to restore change tracking mechanism back to its previous state.
/// </returns>
public static IDisposable EnableTracking(this IRepository repository)
{
return Tracking(repository, true);
}
private static IDisposable Tracking(this IRepository repository, bool enabled)
{
var previous = repository.IsChangeTrackingEnabled;
ObjectHelper.TrySetProperty(ProxyHelper.UnProxy(repository).As<IRepository>(), x => x.IsChangeTrackingEnabled, _ => enabled);
return new DisposeAction<IRepository>(_ =>
{
ObjectHelper.TrySetProperty(ProxyHelper.UnProxy(repository).As<IRepository>(), x => x.IsChangeTrackingEnabled, _ => previous);
}, repository);
}
private static IUnitOfWorkManager GetUnitOfWorkManager<TEntity>(
this IBasicRepository<TEntity> repository,
[CallerMemberName] string callingMethodName = nameof(GetUnitOfWorkManager)

33
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -9,6 +9,7 @@ using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.EntityFrameworkCore;
@ -107,7 +108,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
public async override Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
CheckAndSetId(entity);
var dbContext = await GetDbContextAsync();
@ -124,7 +124,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
public async override Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var entityArray = entities.ToArray();
if (entityArray.IsNullOrEmpty())
{
@ -160,7 +159,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
public async override Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
@ -183,8 +181,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return;
}
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -211,7 +207,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckReadOnly();
var dbContext = await GetDbContextAsync();
dbContext.Set<TEntity>().Remove(entity);
@ -230,7 +225,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
return;
}
CheckReadOnly();
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
@ -294,12 +288,12 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
[Obsolete("Use GetQueryableAsync method.")]
protected override IQueryable<TEntity> GetQueryable()
{
return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly);
return DbSet.AsQueryable().AsNoTrackingIf(!ShouldTrackingEntityChange());
}
public async override Task<IQueryable<TEntity>> GetQueryableAsync()
{
return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(IsReadOnly);
return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(!ShouldTrackingEntityChange());
}
protected async override Task SaveChangesAsync(CancellationToken cancellationToken)
@ -323,7 +317,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
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>();
@ -341,7 +334,6 @@ public class EfCoreRepository<TDbContext, TEntity> : RepositoryBase<TEntity>, IE
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));
@ -445,21 +437,6 @@ 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>,
@ -491,14 +468,13 @@ 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))
: IsReadOnly
: !ShouldTrackingEntityChange()
? 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)
{
@ -510,7 +486,6 @@ 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);

2
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; }
public bool? IsChangeTrackingEnabled { get; set; }
}
public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions

168
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs

@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.ChangeTracking;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.EntityFrameworkCore.ChangeTracking;
public class ChangeTrackingInterceptor_Tests : TestAppTestBase<AbpEntityFrameworkCoreTestModule>
{
[Fact]
public async Task ReadOnly_Repository_Should_Not_Track_Entities()
{
await AddSomePeopleAsync();
var readOnlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
await WithUnitOfWorkAsync(async () =>
{
var db = await readOnlyRepository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var service = GetRequiredService<MyReadOnlyService>();
var list = await service.GetPeoplesAsync();
list.Count.ShouldBeGreaterThan(0);
// RepositoryInterceptor always not track entities
db.ChangeTracker.Entries().Count().ShouldBe(0);
});
}
[Fact]
public async Task RepositoryInterceptor_Test()
{
await AddSomePeopleAsync();
var repository = GetRequiredService<IRepository<Person, Guid>>();
await WithUnitOfWorkAsync(async () =>
{
var service = GetRequiredService<MyService>();
var db = await repository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await service.GetPeoplesAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(1); // Track one entity from GetPeopleAsync
});
await WithUnitOfWorkAsync(async () =>
{
var service = GetRequiredService<MyServiceEnableEntityChangeTracking>();
var db = await repository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var list = await service.GetPeoplesAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(1); // Track one entity from GetPeoplesAsync
});
await WithUnitOfWorkAsync(async () =>
{
var service = GetRequiredService<MyServiceChangeTrackingByEntityChangeTrackingProvider>();
var db = await repository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
var entityChangeTrackingProvider = GetRequiredService<IEntityChangeTrackingProvider>();
// Disable entity change tracking
using (entityChangeTrackingProvider.Change(false))
{
var list = await service.GetPeoplesAsync();
list.Count.ShouldBeGreaterThan(0);
db.ChangeTracker.Entries().Count().ShouldBe(0);
}
});
}
private async Task AddSomePeopleAsync()
{
var repository = GetRequiredService<IRepository<Person, Guid>>();
await repository.InsertAsync(new Person(Guid.NewGuid(), "people1", 18));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people2", 19));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people3", 20));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people4", 21));
}
}
public class MyService : ITransientDependency
{
private readonly IRepository<Person, Guid> _repository;
public MyService(IRepository<Person, Guid> repository)
{
_repository = repository;
}
[DisableEntityChangeTracking]
public virtual async Task<List<Person>> GetPeoplesAsync()
{
await GetPeopleAsync();
return await _repository.GetListAsync();
}
[EnableEntityChangeTracking]
public virtual async Task<Person> GetPeopleAsync()
{
var p1 = await _repository.FindAsync(x => x.Name == "people1");
return p1;
}
}
public class MyReadOnlyService : MyService
{
public MyReadOnlyService(IReadOnlyRepository<Person, Guid> repository)
: base(repository.As<IRepository<Person, Guid>>())
{
}
}
[EnableEntityChangeTracking]
public class MyServiceEnableEntityChangeTracking : ITransientDependency
{
private readonly IRepository<Person, Guid> _repository;
public MyServiceEnableEntityChangeTracking(IRepository<Person, Guid> repository)
{
_repository = repository;
}
public virtual async Task<List<Person>> GetPeoplesAsync()
{
var p1 = await GetPeopleAsync();
var p2 = await _repository.FindAsync(x => x.Name == "people2");
return new List<Person> {p1, p2};
}
[DisableEntityChangeTracking]
public virtual async Task<Person> GetPeopleAsync()
{
var p1 = await _repository.FindAsync(x => x.Name == "people1");
return p1;
}
}
public class MyServiceChangeTrackingByEntityChangeTrackingProvider : ITransientDependency
{
private readonly IRepository<Person, Guid> _repository;
public MyServiceChangeTrackingByEntityChangeTrackingProvider(IRepository<Person, Guid> repository)
{
_repository = repository;
}
public virtual async Task<List<Person>> GetPeoplesAsync()
{
return await _repository.GetListAsync();
}
}

41
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs

@ -2,13 +2,9 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
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 Volo.Abp.Uow;
using Xunit;
@ -55,23 +51,42 @@ public class ReadOnlyRepository_Tests : TestAppTestBase<AbpEntityFrameworkCoreTe
}
[Fact]
public async Task ReadOnlyRepository_Should_Throw_AbpRepositoryIsReadOnlyException_When_Write_Method_Call()
public async Task Repository_Should_Support_Tracking_Or_NoTracking()
{
var repository = GetRequiredService<IRepository<Person, Guid>>();
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 repository.InsertAsync(new Person(Guid.NewGuid(), "people1", 18));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people2", 19));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people3", 20));
await repository.InsertAsync(new Person(Guid.NewGuid(), "people4", 21));
});
await WithUnitOfWorkAsync(async () =>
{
await Assert.ThrowsAsync<AbpRepositoryIsReadOnlyException>(async () =>
var db = await repository.GetDbContextAsync();
db.ChangeTracker.Entries().Count().ShouldBe(0);
using (repository.DisableTracking())
{
var readonlyRepository = GetRequiredService<IReadOnlyRepository<Person, Guid>>();
await readonlyRepository.ToEfCoreRepository().As<EfCoreRepository<TestAppDbContext, Person, Guid>>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18));
});
var p1 = await repository.FindAsync(x => x.Name == "people1");
p1.ShouldNotBeNull();
db.ChangeTracker.Entries().Count().ShouldBe(0);
}
var p2 = await repository.FindAsync(x => x.Name == "people2");
p2.ShouldNotBeNull();
db.ChangeTracker.Entries().Count().ShouldBe(1);
repository.DisableTracking();
var p3 = await repository.FindAsync(x => x.Name == "people3");
p3.ShouldNotBeNull();
db.ChangeTracker.Entries().Count().ShouldBe(1);
repository.EnableTracking();
var p4 = await repository.FindAsync(x => x.Name == "people4");
p4.ShouldNotBeNull();
db.ChangeTracker.Entries().Count().ShouldBe(2);
});
}

Loading…
Cancel
Save