diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index 1f8b9e22f0..cb9393b1aa 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/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: diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index a93460bbdd..03fb27ab50 100644 --- a/docs/en/Repositories.md +++ b/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 _personRepository; + + public MyDemoService(IRepository 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> 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(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`) 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` interface exposes the standard `IQueryable` 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. diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditLogScope.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditLogScope.cs index 8e9f14e976..e3e8f22d0a 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditLogScope.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditLogScope.cs @@ -1,9 +1,6 @@ -using JetBrains.Annotations; - -namespace Volo.Abp.Auditing; +namespace Volo.Abp.Auditing; public interface IAuditLogScope { - [NotNull] AuditLogInfo Log { get; } } diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingManager.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingManager.cs index a1091b8600..c496a22340 100644 --- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingManager.cs +++ b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/IAuditingManager.cs @@ -1,6 +1,4 @@ -using JetBrains.Annotations; - -namespace Volo.Abp.Auditing; +namespace Volo.Abp.Auditing; public interface IAuditingManager { diff --git a/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs b/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs similarity index 96% rename from framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs rename to framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs index 58047f575d..ea746500d6 100644 --- a/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleDtoExtensions.cs +++ b/framework/src/Volo.Abp.AutoMapper/AutoMapper/AbpAutoMapperExtensibleObjectExtensions.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; -using Volo.Abp; using Volo.Abp.AutoMapper; using Volo.Abp.Data; using Volo.Abp.ObjectExtending; namespace AutoMapper; -public static class AbpAutoMapperExtensibleDtoExtensions +public static class AbpAutoMapperExtensibleObjectExtensions { public static IMappingExpression MapExtraProperties( this IMappingExpression mappingExpression, diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs deleted file mode 100644 index 6c648fd022..0000000000 --- a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs +++ /dev/null @@ -1,22 +0,0 @@ -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.Application/Volo/Abp/Application/Services/ApplicationService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs index ba82e5bdc2..f0d23fa865 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ApplicationService.cs +++ b/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; 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 0a2fa66b77..a877c673f1 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 @@ -92,7 +92,7 @@ public static class ServiceCollectionRepositoryExtensions descriptor = ServiceDescriptor.Transient(serviceType, provider => { var repository = provider.GetRequiredService(implementationType); - ObjectHelper.TrySetProperty(repository.As(), x => x.IsReadOnly, _ => true); + ObjectHelper.TrySetProperty(repository.As(), x => x.IsChangeTrackingEnabled, _ => false); return repository; }); } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs index 536e31e51f..de6632704d 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/AbpDddDomainModule.cs +++ b/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); } } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingHelper.cs new file mode 100644 index 0000000000..d2235afd22 --- /dev/null +++ b/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().ToArray(); + if (attrs.Any()) + { + entityChangeTrackingAttribute = attrs.First(); + return true; + } + + if (methodInfo.DeclaringType != null) + { + //Class declaration + attrs = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType().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); + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptor.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptor.cs new file mode 100644 index 0000000000..307e2dee6a --- /dev/null +++ b/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(); + } + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptorRegistrar.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/ChangeTrackingInterceptorRegistrar.cs new file mode 100644 index 0000000000..0249c570dd --- /dev/null +++ b/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(); + } + } + + private static bool ShouldIntercept(Type type) + { + return !DynamicProxyIgnoreTypes.Contains(type) && ChangeTrackingHelper.IsEntityChangeTrackingType(type.GetTypeInfo()); + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/DisableEntityChangeTrackingAttribute.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/DisableEntityChangeTrackingAttribute.cs new file mode 100644 index 0000000000..98011bda5f --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/DisableEntityChangeTrackingAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Volo.Abp.Domain.ChangeTracking; + +/// +/// Ensures that the change tracking in enabled for the given method or class. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class DisableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute +{ + public DisableEntityChangeTrackingAttribute() + : base(false) + { + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EnableEntityChangeTrackingAttribute.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EnableEntityChangeTrackingAttribute.cs new file mode 100644 index 0000000000..542b60cd74 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EnableEntityChangeTrackingAttribute.cs @@ -0,0 +1,15 @@ +using System; + +namespace Volo.Abp.Domain.ChangeTracking; + +/// +/// Ensures that the change tracking in enabled for the given method or class. +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)] +public class EnableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute +{ + public EnableEntityChangeTrackingAttribute() + : base(true) + { + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EntityChangeTrackingAttribute.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/ChangeTracking/EntityChangeTrackingAttribute.cs new file mode 100644 index 0000000000..3446a49354 --- /dev/null +++ b/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; + } +} 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 6de2aa3208..cd85acce50 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 @@ -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 : public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); - public bool IsReadOnly { get; protected set; } + public ILoggerFactory? LoggerFactory => LazyServiceProvider.LazyGetService(); + + public ILogger Logger => LazyServiceProvider.LazyGetService(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance); + + public IEntityChangeTrackingProvider EntityChangeTrackingProvider => LazyServiceProvider.LazyGetRequiredService(); + + public bool? IsChangeTrackingEnabled { get; protected set; } protected BasicRepositoryBase() { @@ -108,6 +116,24 @@ public abstract class BasicRepositoryBase : { 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 : BasicRepositoryBase, IBasicRepository diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/EntityChangeTrackingProvider.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/EntityChangeTrackingProvider.cs new file mode 100644 index 0000000000..19c10e9a3f --- /dev/null +++ b/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 _current = new AsyncLocal(); + + public IDisposable Change(bool? enabled) + { + var previousValue = Enabled; + _current.Value = enabled; + return new DisposeAction(() => _current.Value = previousValue); + } +} diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IEntityChangeTrackingProvider.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IEntityChangeTrackingProvider.cs new file mode 100644 index 0000000000..f1db1584fa --- /dev/null +++ b/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); +} 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 d50a881c28..dc39255b25 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; } + bool? IsChangeTrackingEnabled { get; } } public interface IRepository : IReadOnlyRepository, IBasicRepository diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs index 1c51954f57..5131486c1e 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/ISupportsExplicitLoading.cs @@ -7,8 +7,8 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories; -public interface ISupportsExplicitLoading - where TEntity : class, IEntity +public interface ISupportsExplicitLoading + where TEntity : class, IEntity { Task EnsureCollectionLoadedAsync( TEntity entity, diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs index cbebf62196..d40449a2e4 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs @@ -15,16 +15,16 @@ namespace Volo.Abp.Domain.Repositories; public static class RepositoryExtensions { - public async static Task EnsureCollectionLoadedAsync( - this IBasicRepository repository, + public async static Task EnsureCollectionLoadedAsync( + this IBasicRepository repository, TEntity entity, Expression>> propertyExpression, CancellationToken cancellationToken = default ) - where TEntity : class, IEntity + where TEntity : class, IEntity where TProperty : class { - var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; + var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; if (repo != null) { await repo.EnsureCollectionLoadedAsync(entity, propertyExpression, cancellationToken); @@ -40,7 +40,7 @@ public static class RepositoryExtensions where TEntity : class, IEntity where TProperty : class { - var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; + var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading; if (repo != null) { await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken); @@ -60,12 +60,12 @@ public static class RepositoryExtensions } } - public async static Task EnsureExistsAsync( - this IRepository repository, + public async static Task EnsureExistsAsync( + this IRepository repository, Expression> expression, CancellationToken cancellationToken = default ) - where TEntity : class, IEntity + where TEntity : class, IEntity { if (!await repository.AnyAsync(expression, cancellationToken)) { @@ -145,6 +145,40 @@ public static class RepositoryExtensions } } + /// + /// Disables change tracking mechanism for the given repository. + /// + /// A repository object + /// + /// A disposable object. Dispose it to restore change tracking mechanism back to its previous state. + /// + public static IDisposable DisableTracking(this IRepository repository) + { + return Tracking(repository, false); + } + + /// + /// Enables change tracking mechanism for the given repository. + /// + /// A repository object + /// + /// A disposable object. Dispose it to restore change tracking mechanism back to its previous state. + /// + 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(), x => x.IsChangeTrackingEnabled, _ => enabled); + return new DisposeAction(_ => + { + ObjectHelper.TrySetProperty(ProxyHelper.UnProxy(repository).As(), x => x.IsChangeTrackingEnabled, _ => previous); + }, repository); + } + private static IUnitOfWorkManager GetUnitOfWorkManager( this IBasicRepository repository, [CallerMemberName] string callingMethodName = nameof(GetUnitOfWorkManager) 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 325ba8345d..1c5d106166 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 @@ -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 : RepositoryBase, IE public async override Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); CheckAndSetId(entity); var dbContext = await GetDbContextAsync(); @@ -124,7 +124,6 @@ public class EfCoreRepository : RepositoryBase, IE public async override Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); var entityArray = entities.ToArray(); if (entityArray.IsNullOrEmpty()) { @@ -160,7 +159,6 @@ public class EfCoreRepository : RepositoryBase, IE public async override Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Attach(entity); @@ -183,8 +181,6 @@ public class EfCoreRepository : RepositoryBase, IE return; } - CheckReadOnly(); - cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -211,7 +207,6 @@ public class EfCoreRepository : RepositoryBase, IE public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Set().Remove(entity); @@ -230,7 +225,6 @@ public class EfCoreRepository : RepositoryBase, IE return; } - CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -294,12 +288,12 @@ public class EfCoreRepository : RepositoryBase, IE [Obsolete("Use GetQueryableAsync method.")] protected override IQueryable GetQueryable() { - return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly); + return DbSet.AsQueryable().AsNoTrackingIf(!ShouldTrackingEntityChange()); } public async override Task> 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 : RepositoryBase, IE public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); var dbContext = await GetDbContextAsync(); var dbSet = dbContext.Set(); @@ -341,7 +334,6 @@ public class EfCoreRepository : RepositoryBase, IE 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)); @@ -445,26 +437,11 @@ 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, IEfCoreRepository, - ISupportsExplicitLoading + ISupportsExplicitLoading where TDbContext : IEfCoreDbContext where TEntity : class, IEntity @@ -491,14 +468,13 @@ public class EfCoreRepository : EfCoreRepository 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 : EfCoreRepository ids, bool autoSave = false, CancellationToken cancellationToken = default) { - CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs index f9b59e2c94..ca98017e0a 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs @@ -407,7 +407,7 @@ public class RepositoryRegistration_Tests public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository { - public bool IsReadOnly { get; set; } + public bool? IsChangeTrackingEnabled { get; set; } } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/ChangeTracking/ChangeTrackingInterceptor_Tests.cs new file mode 100644 index 0000000000..87b487b68a --- /dev/null +++ b/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 +{ + [Fact] + public async Task ReadOnly_Repository_Should_Not_Track_Entities() + { + await AddSomePeopleAsync(); + + var readOnlyRepository = GetRequiredService>(); + + await WithUnitOfWorkAsync(async () => + { + var db = await readOnlyRepository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + + var service = GetRequiredService(); + 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>(); + + await WithUnitOfWorkAsync(async () => + { + var service = GetRequiredService(); + 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(); + 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(); + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + + var entityChangeTrackingProvider = GetRequiredService(); + // 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>(); + 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 _repository; + + public MyService(IRepository repository) + { + _repository = repository; + } + + [DisableEntityChangeTracking] + public virtual async Task> GetPeoplesAsync() + { + await GetPeopleAsync(); + return await _repository.GetListAsync(); + } + + [EnableEntityChangeTracking] + public virtual async Task GetPeopleAsync() + { + var p1 = await _repository.FindAsync(x => x.Name == "people1"); + return p1; + } +} + +public class MyReadOnlyService : MyService +{ + public MyReadOnlyService(IReadOnlyRepository repository) + : base(repository.As>()) + { + } +} + + +[EnableEntityChangeTracking] +public class MyServiceEnableEntityChangeTracking : ITransientDependency +{ + private readonly IRepository _repository; + + public MyServiceEnableEntityChangeTracking(IRepository repository) + { + _repository = repository; + } + + public virtual async Task> GetPeoplesAsync() + { + var p1 = await GetPeopleAsync(); + var p2 = await _repository.FindAsync(x => x.Name == "people2"); + + return new List {p1, p2}; + } + + [DisableEntityChangeTracking] + public virtual async Task GetPeopleAsync() + { + var p1 = await _repository.FindAsync(x => x.Name == "people1"); + return p1; + } +} + +public class MyServiceChangeTrackingByEntityChangeTrackingProvider : ITransientDependency +{ + private readonly IRepository _repository; + + public MyServiceChangeTrackingByEntityChangeTrackingProvider(IRepository repository) + { + _repository = repository; + } + + public virtual async Task> GetPeoplesAsync() + { + return await _repository.GetListAsync(); + } +} 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 index 393b54dbe3..6213a6e114 100644 --- 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 @@ -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>(); + 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 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(async () => + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + using (repository.DisableTracking()) { - var readonlyRepository = GetRequiredService>(); - await readonlyRepository.ToEfCoreRepository().As>().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); }); } diff --git a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlob.cs b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlob.cs index 8b51868e27..214ebff508 100644 --- a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlob.cs +++ b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlob.cs @@ -17,6 +17,10 @@ public class DatabaseBlob : AggregateRoot, IMultiTenant [DisableAuditing] public virtual byte[] Content { get; protected set; } + protected DatabaseBlob() + { + } + public DatabaseBlob(Guid id, Guid containerId, [NotNull] string name, [NotNull] byte[] content, Guid? tenantId = null) : base(id) { diff --git a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobContainer.cs b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobContainer.cs index 0ae8a3ad4b..5186248a40 100644 --- a/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobContainer.cs +++ b/modules/blob-storing-database/src/Volo.Abp.BlobStoring.Database.Domain/Volo/Abp/BlobStoring/Database/DatabaseBlobContainer.cs @@ -11,6 +11,10 @@ public class DatabaseBlobContainer : AggregateRoot, IMultiTenant public virtual string Name { get; protected set; } + protected DatabaseBlobContainer() + { + } + public DatabaseBlobContainer(Guid id, [NotNull] string name, Guid? tenantId = null) : base(id) { diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/Blog.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/Blog.cs index 32ce890ff0..fb976fd957 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/Blog.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/Blog.cs @@ -16,6 +16,10 @@ public class Blog : FullAuditedAggregateRoot, IMultiTenant public virtual Guid? TenantId { get; protected set; } + protected Blog() + { + } + protected internal Blog( Guid id, [NotNull] string name, diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogFeature.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogFeature.cs index 39e2e2c65c..5571e776b1 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogFeature.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Blogs/BlogFeature.cs @@ -13,6 +13,10 @@ public class BlogFeature : FullAuditedAggregateRoot public bool IsEnabled { get; protected internal set; } + protected BlogFeature() + { + } + public BlogFeature(Guid blogId, [NotNull] string featureName, bool isEnabled = true) { BlogId = blogId; diff --git a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs index ab7e23df8b..fa2505eecc 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Domain/Volo/CmsKit/Menus/MenuItem.cs @@ -41,6 +41,10 @@ public class MenuItem : AuditedAggregateRoot, IMultiTenant public Guid? TenantId { get; protected set; } + protected MenuItem() + { + } + public MenuItem(Guid id, [NotNull] string displayName, [NotNull] string url, diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs index 498184b9cc..fa1beb9124 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/IdentityUserAppService.cs @@ -173,7 +173,10 @@ public class IdentityUserAppService : IdentityAppServiceBase, IIdentityUserAppSe (await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); } - (await UserManager.SetLockoutEnabledAsync(user, input.LockoutEnabled)).CheckErrors(); + if (user.Id != CurrentUser.Id) + { + (await UserManager.SetLockoutEnabledAsync(user, input.LockoutEnabled)).CheckErrors(); + } user.Name = input.Name; user.Surname = input.Surname; diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor index f858f99f68..57ab80dc5e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor @@ -129,9 +129,12 @@ @L["DisplayName:IsActive"] - - @L["DisplayName:LockoutEnabled"] - + @if (!IsEditCurrentUser) + { + + @L["DisplayName:LockoutEnabled"] + + } diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs index 00d583c9f0..f09c1aca8e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs @@ -41,6 +41,7 @@ public partial class UserManagement private List UserManagementTableColumns => TableColumns.Get(); private TextRole _passwordTextRole = TextRole.Password; + public bool IsEditCurrentUser { get; set; } public UserManagement() { @@ -119,7 +120,7 @@ public partial class UserManagement try { EditModalSelectedTab = DefaultSelectedTab; - + IsEditCurrentUser = entity.Id == CurrentUser.Id; var userRoleNames = (await AppService.GetRolesAsync(entity.Id)).Items.Select(r => r.Name).ToList(); EditUserRoles = Roles.Select(x => new AssignedRoleViewModel diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml index 079ffa01b9..521c977dae 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml @@ -39,8 +39,10 @@ - - + @if (!Model.IsEditCurrentUser) + { + + } @foreach (var propertyInfo in ObjectExtensionManager.Instance.GetProperties()) { if (!propertyInfo.Name.EndsWith("_Text")) diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs index cf1b7a5a3f..e73706262c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/EditModal.cshtml.cs @@ -24,6 +24,8 @@ public class EditModalModel : IdentityPageModel protected IIdentityUserAppService IdentityUserAppService { get; } + public bool IsEditCurrentUser { get; set; } + public EditModalModel(IIdentityUserAppService identityUserAppService) { IdentityUserAppService = identityUserAppService; @@ -32,10 +34,9 @@ public class EditModalModel : IdentityPageModel public virtual async Task OnGetAsync(Guid id) { var user = await IdentityUserAppService.GetAsync(id); - UserInfo = ObjectMapper.Map(user); - Roles = ObjectMapper.Map, AssignedRoleViewModel[]>((await IdentityUserAppService.GetAssignableRolesAsync()).Items); + IsEditCurrentUser = CurrentUser.Id == id; var userRoleNames = (await IdentityUserAppService.GetRolesAsync(UserInfo.Id)).Items.Select(r => r.Name).ToList(); foreach (var role in Roles) diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index ffb7407ab0..0a472a18cb 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -22,7 +22,6 @@ export class ErrorHandler { protected readonly routerErrorHandlerService = inject(RouterErrorHandlerService); protected readonly httpErrorConfig = inject(HTTP_ERROR_CONFIG); protected readonly customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); - protected readonly defaultHttpErrorHandler = (_, err: HttpErrorResponse) => throwError(() => err); protected readonly httpErrorHandler = inject(HTTP_ERROR_HANDLER, { optional: true }) || this.defaultHttpErrorHandler;