diff --git a/docs/en/framework/fundamentals/autofac-integration.md b/docs/en/framework/fundamentals/autofac-integration.md
index ac0abfc3d85..b70568f316d 100644
--- a/docs/en/framework/fundamentals/autofac-integration.md
+++ b/docs/en/framework/fundamentals/autofac-integration.md
@@ -105,3 +105,17 @@ public override void ConfigureServices(ServiceConfigurationContext context)
````
> You should install the [Volo.Abp.Autofac](https://www.nuget.org/packages/Volo.Abp.Autofac) nuget package to the project that you want to use the Autofac API.
+
+## Dynamic Proxy Modules
+
+ABP uses Castle DynamicProxy to create the proxy classes of the services that have [interceptors](../infrastructure/interceptors.md) (like unit of work, auditing, validation and authorization). Generating a new proxy class gets slower as its dynamic module grows, so ABP distributes the proxy classes into 16 dynamic modules by default. You can change this number by setting the `AbpInterceptionRegistrationExtensions.ShardCount` property.
+
+**Example: Set the number of dynamic modules in the `Program.cs` file**
+
+````csharp
+AbpInterceptionRegistrationExtensions.ShardCount = 8;
+
+var builder = WebApplication.CreateBuilder(args);
+````
+
+> Set this property before the application is created. It can not be changed after the first proxy class is generated.
diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpInterceptionRegistrationExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpInterceptionRegistrationExtensions.cs
new file mode 100644
index 00000000000..52503f7abbe
--- /dev/null
+++ b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpInterceptionRegistrationExtensions.cs
@@ -0,0 +1,276 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Autofac.Core;
+using Autofac.Core.Resolving.Pipeline;
+using Autofac.Extras.DynamicProxy;
+using Castle.DynamicProxy;
+using Volo.Abp;
+using Volo.Abp.Autofac;
+
+namespace Autofac.Builder;
+
+///
+/// The class and interface interception of Autofac.Extras.DynamicProxy, generating proxies with
+/// instead of the single static generator that package keeps for the whole process.
+/// Interceptors are read from the same registration metadata as Autofac's, so InterceptedBy and
+/// keep working. It follows Autofac.Extras.DynamicProxy 7.1.0, whose metadata keys it
+/// reads; check it again when that package is upgraded.
+///
+public static class AbpInterceptionRegistrationExtensions
+{
+ // Autofac.Extras.DynamicProxy's metadata keys, written by its InterceptedBy.
+ private const string InterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.InterceptorsPropertyName";
+ private const string AttributeInterceptorsPropertyName = "Autofac.Extras.DynamicProxy.RegistrationExtensions.AttributeInterceptorsPropertyName";
+
+ private static readonly object SyncRoot = new();
+ private static int _shardCount = 16;
+ private static volatile ProxyGenerator? _proxyGenerator;
+
+ ///
+ /// How many dynamic modules proxies are spread over. It is read when is first used,
+ /// which happens while the container is built, and cannot change after that, because a key must keep its shard.
+ /// Defaults to 16.
+ ///
+ public static int ShardCount
+ {
+ get
+ {
+ lock (SyncRoot)
+ {
+ return _shardCount;
+ }
+ }
+ set
+ {
+ Check.Positive(value, nameof(value));
+ lock (SyncRoot)
+ {
+ if (_proxyGenerator != null && value != _shardCount)
+ {
+ throw new AbpException("The proxy shard count cannot change once the proxy generator has been created.");
+ }
+
+ _shardCount = value;
+ }
+ }
+ }
+
+ public static ProxyGenerator ProxyGenerator
+ {
+ get
+ {
+ var generator = _proxyGenerator;
+ if (generator != null)
+ {
+ return generator;
+ }
+
+ lock (SyncRoot)
+ {
+ return _proxyGenerator ??= new ProxyGenerator(new AbpShardedProxyBuilder(_shardCount));
+ }
+ }
+ }
+
+ public static IRegistrationBuilder EnableAbpClassInterceptors(
+ this IRegistrationBuilder registration,
+ ProxyGenerationOptions? options = null)
+ where TConcreteReflectionActivatorData : ConcreteReflectionActivatorData
+ {
+ Check.NotNull(registration, nameof(registration));
+
+ options ??= ProxyGenerationOptions.Default;
+ registration.ActivatorData.ImplementationType = ProxyGenerator.ProxyBuilder.CreateClassProxyType(
+ registration.ActivatorData.ImplementationType,
+ Type.EmptyTypes,
+ options);
+
+ var interceptorServices = GetInterceptorServicesFromAttributes(registration.ActivatorData.ImplementationType);
+ AddInterceptorServicesToMetadata(registration, interceptorServices, AttributeInterceptorsPropertyName);
+
+ // The proxy's constructor takes the options' mixins, then the interceptors, then the options' selector.
+ registration.OnPreparing(e =>
+ {
+ var proxyParameters = new List();
+ var index = 0;
+
+ if (options.HasMixins)
+ {
+ foreach (var mixin in options.MixinData.Mixins)
+ {
+ proxyParameters.Add(new PositionalParameter(index++, mixin));
+ }
+ }
+
+ proxyParameters.Add(new PositionalParameter(index++, GetInterceptorServices(e.Component, registration.ActivatorData.ImplementationType)
+ .Select(s => e.Context.ResolveService(s))
+ .Cast()
+ .ToArray()));
+
+ if (options.Selector != null)
+ {
+ proxyParameters.Add(new PositionalParameter(index, options.Selector));
+ }
+
+ e.Parameters = proxyParameters.Concat(e.Parameters).ToArray();
+ });
+
+ return registration;
+ }
+
+ public static IRegistrationBuilder EnableAbpInterfaceInterceptors(
+ this IRegistrationBuilder registration,
+ ProxyGenerationOptions? options = null)
+ {
+ Check.NotNull(registration, nameof(registration));
+
+ registration.ConfigurePipeline(p => p.Use(PipelinePhase.Activation, MiddlewareInsertionMode.StartOfPhase, (ctx, next) =>
+ {
+ next(ctx);
+
+ EnsureInterfaceInterceptionApplies(ctx.Registration);
+
+ var proxiedInterfaces = ProxiedInterfaces.GetValue(ctx.Instance!.GetType(), static type => new ProxiedInterfaceSet(type));
+ if (proxiedInterfaces.Primary == null)
+ {
+ return;
+ }
+
+ var theInterface = proxiedInterfaces.Primary;
+ var interfaces = proxiedInterfaces.Additional;
+
+ var interceptors = GetInterceptorServices(ctx.Registration, ctx.Instance.GetType())
+ .Select(s => ctx.ResolveService(s))
+ .Cast()
+ .ToArray();
+
+ ctx.Instance = options == null
+ ? ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, interceptors)
+ : ProxyGenerator.CreateInterfaceProxyWithTarget(theInterface, interfaces, ctx.Instance, options, interceptors);
+ }));
+
+ return registration;
+ }
+
+ private static readonly ConditionalWeakTable ProxiedInterfaces = new();
+
+ ///
+ /// The interfaces an interface proxy of a type implements, worked out once per type.
+ ///
+ ///
+ /// The proxy implements the same interfaces whichever comes first, but Castle names it after the first one and copies
+ /// some of that one's attributes to the proxy type. Preferring the class's own I{ClassName} interface gives most
+ /// proxies a name of their own instead of one shared by every implementation of a common interface, which keeps them
+ /// spread over the proxy builder's modules. It is only preferred when neither interface has a public attribute other
+ /// than the compiler's nullable annotations, so the proxy type's attributes stay as they were whatever Castle is
+ /// configured to copy.
+ ///
+ private sealed class ProxiedInterfaceSet
+ {
+ public Type? Primary { get; }
+
+ public Type[] Additional { get; }
+
+ public ProxiedInterfaceSet(Type type)
+ {
+ var interfaces = type.GetInterfaces().Where(ProxyUtil.IsAccessible).ToArray();
+ if (interfaces.Length == 0)
+ {
+ Additional = Type.EmptyTypes;
+ return;
+ }
+
+ var primary = interfaces[0];
+ var own = interfaces.FirstOrDefault(i => i.Name == "I" + type.Name);
+ if (own != null && own != primary && !HasPublicAttributes(primary) && !HasPublicAttributes(own))
+ {
+ primary = own;
+ }
+
+ Primary = primary;
+ Additional = interfaces.Where(i => i != primary).ToArray();
+ }
+
+ // The compiler's nullable annotations are public on .NET but say nothing a caller reads off a proxy type;
+ // counting them would keep nearly every interface of a nullable-enabled project in place.
+ private static readonly string[] NullableAnnotations =
+ {
+ "System.Runtime.CompilerServices.NullableAttribute",
+ "System.Runtime.CompilerServices.NullableContextAttribute",
+ "System.Runtime.CompilerServices.NullablePublicOnlyAttribute"
+ };
+
+ private static bool HasPublicAttributes(Type interfaceType)
+ {
+ return interfaceType.CustomAttributes.Any(attribute =>
+ (attribute.AttributeType.IsPublic || attribute.AttributeType.IsNestedPublic) &&
+ !NullableAnnotations.Contains(attribute.AttributeType.FullName));
+ }
+ }
+
+ private static void EnsureInterfaceInterceptionApplies(IComponentRegistration componentRegistration)
+ {
+ if (componentRegistration.Services
+ .OfType()
+ .Any(s => !s.ServiceType.GetTypeInfo().IsInterface || !ProxyUtil.IsAccessible(s.ServiceType)))
+ {
+ throw new InvalidOperationException(
+ $"The component {componentRegistration} cannot use interface interception as it provides services that are not publicly visible interfaces.");
+ }
+ }
+
+ private static void AddInterceptorServicesToMetadata(
+ IRegistrationBuilder builder,
+ IEnumerable interceptorServices,
+ string metadataKey)
+ {
+ if (builder.RegistrationData.Metadata.TryGetValue(metadataKey, out var existing) && existing is IEnumerable existingServices)
+ {
+ builder.RegistrationData.Metadata[metadataKey] = existingServices.Concat(interceptorServices).Distinct();
+ }
+ else
+ {
+ builder.RegistrationData.Metadata.Add(metadataKey, interceptorServices);
+ }
+ }
+
+ private static IEnumerable GetInterceptorServices(IComponentRegistration registration, Type implType)
+ {
+ var result = Enumerable.Empty();
+
+ if (registration.Metadata.TryGetValue(InterceptorsPropertyName, out var services) && services is IEnumerable existingPropertyServices)
+ {
+ result = result.Concat(existingPropertyServices);
+ }
+
+ return registration.Metadata.TryGetValue(AttributeInterceptorsPropertyName, out services) && services is IEnumerable existingAttributeServices
+ ? result.Concat(existingAttributeServices)
+ : result.Concat(GetInterceptorServicesFromAttributes(implType));
+ }
+
+ private static IEnumerable GetInterceptorServicesFromAttributes(Type implType)
+ {
+ var implTypeInfo = implType.GetTypeInfo();
+ if (!implTypeInfo.IsClass)
+ {
+ return Enumerable.Empty();
+ }
+
+ var classAttributeServices = implTypeInfo
+ .GetCustomAttributes(typeof(InterceptAttribute), true)
+ .Cast()
+ .Select(att => att.InterceptorService);
+
+ var interfaceAttributeServices = implType
+ .GetInterfaces()
+ .SelectMany(i => i.GetTypeInfo().GetCustomAttributes(typeof(InterceptAttribute), true))
+ .Cast()
+ .Select(att => att.InterceptorService);
+
+ return classAttributeServices.Concat(interfaceAttributeServices);
+ }
+
+}
diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs
index d587d556d82..609922f138c 100644
--- a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs
+++ b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs
@@ -132,7 +132,7 @@ private static IRegistrationBuilder
{
if (serviceType.IsInterface)
{
- registrationBuilder = registrationBuilder.EnableInterfaceInterceptors();
+ registrationBuilder = registrationBuilder.EnableAbpInterfaceInterceptors();
}
else
{
@@ -142,7 +142,7 @@ private static IRegistrationBuilder
return registrationBuilder;
}
- (registrationBuilder as IRegistrationBuilder)?.EnableClassInterceptors();
+ (registrationBuilder as IRegistrationBuilder)?.EnableAbpClassInterceptors();
}
foreach (var interceptor in interceptors)
diff --git a/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AbpShardedProxyBuilder.cs b/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AbpShardedProxyBuilder.cs
new file mode 100644
index 00000000000..5c5f3fda46a
--- /dev/null
+++ b/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AbpShardedProxyBuilder.cs
@@ -0,0 +1,76 @@
+using System;
+using Castle.Core.Logging;
+using Castle.DynamicProxy;
+
+namespace Volo.Abp.Autofac;
+
+///
+/// Spreads proxy types over several dynamic modules. Generating a type gets slower as its dynamic module grows, so
+/// keeping every proxy of an application in one module makes generating them all grow quadratically with their
+/// number. A proxy is routed by the simple name of the type it proxies: Castle names a proxy after that name and keeps
+/// names unique only within one module, so types sharing a name must share a shard, or two proxies could get the
+/// same full name, which code that caches by a runtime type name relies on. A key therefore always lands in the same
+/// shard, and is generated and cached there once.
+///
+public class AbpShardedProxyBuilder : IProxyBuilder
+{
+ private readonly DefaultProxyBuilder[] _shards;
+
+ public AbpShardedProxyBuilder(int shardCount)
+ {
+ Check.Positive(shardCount, nameof(shardCount));
+
+ _shards = new DefaultProxyBuilder[shardCount];
+ for (var i = 0; i < shardCount; i++)
+ {
+ _shards[i] = new DefaultProxyBuilder();
+ }
+ }
+
+ public int ShardCount => _shards.Length;
+
+ public ILogger Logger
+ {
+ get => _shards[0].Logger;
+ set
+ {
+ foreach (var shard in _shards)
+ {
+ shard.Logger = value;
+ }
+ }
+ }
+
+ /// The first shard's scope. Proxy generation does not read it; it exists for .
+ public ModuleScope ModuleScope => _shards[0].ModuleScope;
+
+ public Type CreateClassProxyType(Type classToProxy, Type[]? additionalInterfacesToProxy, ProxyGenerationOptions options)
+ {
+ return ShardFor(classToProxy).CreateClassProxyType(classToProxy, additionalInterfacesToProxy, options);
+ }
+
+ public Type CreateClassProxyTypeWithTarget(Type classToProxy, Type[]? additionalInterfacesToProxy, ProxyGenerationOptions options)
+ {
+ return ShardFor(classToProxy).CreateClassProxyTypeWithTarget(classToProxy, additionalInterfacesToProxy, options);
+ }
+
+ public Type CreateInterfaceProxyTypeWithTarget(Type interfaceToProxy, Type[]? additionalInterfacesToProxy, Type targetType, ProxyGenerationOptions options)
+ {
+ return ShardFor(interfaceToProxy).CreateInterfaceProxyTypeWithTarget(interfaceToProxy, additionalInterfacesToProxy, targetType, options);
+ }
+
+ public Type CreateInterfaceProxyTypeWithTargetInterface(Type interfaceToProxy, Type[]? additionalInterfacesToProxy, ProxyGenerationOptions options)
+ {
+ return ShardFor(interfaceToProxy).CreateInterfaceProxyTypeWithTargetInterface(interfaceToProxy, additionalInterfacesToProxy, options);
+ }
+
+ public Type CreateInterfaceProxyTypeWithoutTarget(Type interfaceToProxy, Type[]? additionalInterfacesToProxy, ProxyGenerationOptions options)
+ {
+ return ShardFor(interfaceToProxy).CreateInterfaceProxyTypeWithoutTarget(interfaceToProxy, additionalInterfacesToProxy, options);
+ }
+
+ private DefaultProxyBuilder ShardFor(Type proxiedType)
+ {
+ return _shards[(StringComparer.Ordinal.GetHashCode(proxiedType.Name) & int.MaxValue) % _shards.Length];
+ }
+}
diff --git a/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider.cs b/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider.cs
new file mode 100644
index 00000000000..d0511a34f3c
--- /dev/null
+++ b/framework/src/Volo.Abp.Autofac/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Linq;
+using Autofac.Core;
+using Autofac.Core.Activators.Reflection;
+using Autofac.Extensions.DependencyInjection;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Autofac;
+
+public class AutofacServiceImplementationTypeProvider : IServiceImplementationTypeProvider, ITransientDependency
+{
+ protected IServiceProvider ServiceProvider { get; }
+
+ public AutofacServiceImplementationTypeProvider(IServiceProvider serviceProvider)
+ {
+ ServiceProvider = serviceProvider;
+ }
+
+ public virtual Type? GetImplementationTypeOrNull(Type serviceType)
+ {
+ if (ServiceProvider is not AutofacServiceProvider autofacServiceProvider)
+ {
+ return null;
+ }
+
+ var registry = autofacServiceProvider.LifetimeScope.ComponentRegistry;
+ var service = new TypedService(serviceType);
+ if (!registry.TryGetServiceRegistration(service, out var serviceRegistration))
+ {
+ return null;
+ }
+
+ // A decorator or other service middleware may return another object than the registration creates.
+ if (registry.ServiceMiddlewareFor(service).Any())
+ {
+ return null;
+ }
+
+ // The reflection activator always creates its type (a class proxy, when the service is intercepted);
+ // a delegate may create any type.
+ return serviceRegistration.Registration.Activator is ReflectionActivator activator
+ ? activator.LimitType
+ : null;
+ }
+}
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IServiceImplementationTypeProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IServiceImplementationTypeProvider.cs
new file mode 100644
index 00000000000..183ec7d15f0
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IServiceImplementationTypeProvider.cs
@@ -0,0 +1,13 @@
+using System;
+
+namespace Volo.Abp.DependencyInjection;
+
+public interface IServiceImplementationTypeProvider
+{
+ ///
+ /// Gets the type of every instance the container creates for , without creating one.
+ /// Returns null when the registration does not fix the type (such as a factory method or a decorator) or the
+ /// container can not tell it.
+ ///
+ Type? GetImplementationTypeOrNull(Type serviceType);
+}
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/NullServiceImplementationTypeProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/NullServiceImplementationTypeProvider.cs
new file mode 100644
index 00000000000..5a676c22d20
--- /dev/null
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/NullServiceImplementationTypeProvider.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Volo.Abp.DependencyInjection;
+
+[Dependency(TryRegister = true)]
+public class NullServiceImplementationTypeProvider : IServiceImplementationTypeProvider, ITransientDependency
+{
+ public Type? GetImplementationTypeOrNull(Type serviceType)
+ {
+ return null;
+ }
+}
diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/ObjectHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/ObjectHelper.cs
index ffe9e506232..c38f0f5d59b 100644
--- a/framework/src/Volo.Abp.Core/Volo/Abp/ObjectHelper.cs
+++ b/framework/src/Volo.Abp.Core/Volo/Abp/ObjectHelper.cs
@@ -3,12 +3,15 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using System.Runtime.CompilerServices;
namespace Volo.Abp;
public static class ObjectHelper
{
- private static readonly ConcurrentDictionary _cachedObjectProperties = new();
+ // Keyed by the object's type itself, since different types can share a full name (such as proxies from different
+ // generators), and weakly, so a collectible type can still be unloaded.
+ private static readonly ConditionalWeakTable> _cachedObjectProperties = new();
public static void TrySetProperty(
TObject obj,
@@ -25,10 +28,16 @@ public static void TrySetProperty(
Func valueFactory,
params Type[]? ignoreAttributeTypes)
{
- var cacheKey =
- $"{obj?.GetType().FullName}-{propertySelector}-{(ignoreAttributeTypes != null ? "-" + string.Join("-", ignoreAttributeTypes.Select(x => x.FullName)) : "")}";
+ if (obj == null)
+ {
+ return;
+ }
+
+ var cacheKey = $"{propertySelector}-{(ignoreAttributeTypes != null ? "-" + string.Join("-", ignoreAttributeTypes.Select(x => x.FullName)) : "")}";
- var property = _cachedObjectProperties.GetOrAdd(cacheKey, PropertyFactory);
+ var property = _cachedObjectProperties
+ .GetValue(obj.GetType(), _ => new ConcurrentDictionary())
+ .GetOrAdd(cacheKey, PropertyFactory);
property?.SetValue(obj, valueFactory(obj));
return;
diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
index eb16a6910c6..0a67bb7bbb8 100644
--- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
+++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
@@ -20,6 +21,10 @@ public class EntityChangeEventHelper : IEntityChangeEventHelper, ITransientDepen
{
private const string UnitOfWorkEventRecordEntityPropName = "_Abp_Entity";
+ // Holds each constructor, so its compiled invoke stub stays alive. With Activator.CreateInstance, once many entity
+ // types are in use, a GC can drop the stub, and it is generated and compiled again on the next call.
+ private static readonly ConditionalWeakTable> EventDataFactories = new();
+
public ILogger Logger { get; set; }
public ILocalEventBus LocalEventBus { get; set; }
public IDistributedEventBus DistributedEventBus { get; set; }
@@ -129,7 +134,7 @@ protected virtual void TriggerEventWithEntity(
{
var entityType = ProxyHelper.UnProxy(entityOrEto).GetType();
var eventType = genericEventType.MakeGenericType(entityType);
- var eventData = Activator.CreateInstance(eventType, entityOrEto)!;
+ var eventData = CreateEventData(eventType, entityOrEto);
var currentUow = UnitOfWorkManager.Current;
if (currentUow == null)
@@ -167,6 +172,22 @@ protected virtual void TriggerEventWithEntity(
}
}
+ private static object CreateEventData(Type eventType, object entityOrEto)
+ {
+ return EventDataFactories.GetValue(eventType, static type =>
+ {
+ var constructors = type.GetConstructors();
+ if (constructors.Length != 1 ||
+ Array.Exists(constructors[0].GetParameters(), static parameter => parameter.IsDefined(typeof(ParamArrayAttribute), false)))
+ {
+ return entity => Activator.CreateInstance(type, entity)!;
+ }
+
+ var constructor = constructors[0];
+ return entity => constructor.Invoke(new[] { entity });
+ })(entityOrEto);
+ }
+
public bool IsSameEntityEventRecord(UnitOfWorkEventRecord record1, UnitOfWorkEventRecord record2)
{
if (record1.EventType != record2.EventType)
diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs
index e59cd956133..c0dee298d8b 100644
--- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs
+++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs
@@ -18,6 +18,10 @@ namespace Volo.Abp.EventBus;
public abstract class EventBusBase : IEventBus
{
+ // Holds each constructor, so its compiled invoke stub stays alive. With Activator.CreateInstance, once many types are
+ // in use, a GC can drop the stub, and it is generated and compiled again on the next call.
+ private static readonly ConditionalWeakTable> BaseEventDataFactories = new();
+
protected IServiceScopeFactory ServiceScopeFactory { get; }
protected ICurrentTenant CurrentTenant { get; }
@@ -185,12 +189,28 @@ protected virtual async Task TriggerHandlersAsync(Type eventType, object eventDa
{
var baseEventType = actualEventType.GetGenericTypeDefinition().MakeGenericType(baseArg);
var constructorArgs = ((IEventDataWithInheritableGenericArgument)resolvedEventData).GetConstructorArgs();
- var baseEventData = Activator.CreateInstance(baseEventType, constructorArgs)!;
+ var baseEventData = CreateBaseEventData(baseEventType, constructorArgs);
await PublishToEventBusAsync(baseEventType, baseEventData);
}
}
}
+ private static object CreateBaseEventData(Type baseEventType, object[] constructorArgs)
+ {
+ return BaseEventDataFactories.GetValue(baseEventType, static type =>
+ {
+ var constructors = type.GetConstructors();
+ if (constructors.Length != 1 ||
+ Array.Exists(constructors[0].GetParameters(), static parameter => parameter.IsDefined(typeof(ParamArrayAttribute), false)))
+ {
+ return args => Activator.CreateInstance(type, args)!;
+ }
+
+ var constructor = constructors[0];
+ return args => constructor.Invoke(args);
+ })(constructorArgs);
+ }
+
protected virtual (List Factories, Type? ActualEventType) ResolveHandlerFactories(
Type eventType,
object eventData)
diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerInvoker.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerInvoker.cs
index 9ee7c6cceb0..8ccb9df80b5 100644
--- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerInvoker.cs
+++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventHandlerInvoker.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
@@ -8,31 +9,29 @@ namespace Volo.Abp.EventBus;
public class EventHandlerInvoker : IEventHandlerInvoker, ISingletonDependency
{
- private readonly ConcurrentDictionary _cache;
+ // Keyed by the handler type itself, since different types can share a full name (such as proxies from different
+ // generators), and weakly, so a collectible handler type can still be unloaded.
+ private readonly ConditionalWeakTable> _cache;
public EventHandlerInvoker()
{
- _cache = new ConcurrentDictionary();
+ _cache = new ConditionalWeakTable>();
}
public async Task InvokeAsync(IEventHandler eventHandler, object eventData, Type eventType)
{
- var cacheItem = _cache.GetOrAdd($"{eventHandler.GetType().FullName}-{eventType.FullName}", _ =>
+ var handlerType = eventHandler.GetType();
+ var handlerCache = _cache.GetValue(handlerType, static _ => new ConcurrentDictionary());
+ if (!handlerCache.TryGetValue(eventType, out var cacheItem))
{
- var item = new EventHandlerInvokerCacheItem();
+ cacheItem = CreateCacheItem(eventHandler, eventType);
- if (typeof(ILocalEventHandler<>).MakeGenericType(eventType).IsInstanceOfType(eventHandler))
+ // A handler that cannot handle the event fails below; it is not cached, so it keeps no event type alive.
+ if (cacheItem.Local != null || cacheItem.Distributed != null)
{
- item.Local = (IEventHandlerMethodExecutor?)Activator.CreateInstance(typeof(LocalEventHandlerMethodExecutor<>).MakeGenericType(eventType));
+ cacheItem = handlerCache.GetOrAdd(eventType, cacheItem);
}
-
- if (typeof(IDistributedEventHandler<>).MakeGenericType(eventType).IsInstanceOfType(eventHandler))
- {
- item.Distributed = (IEventHandlerMethodExecutor?)Activator.CreateInstance(typeof(DistributedEventHandlerMethodExecutor<>).MakeGenericType(eventType));
- }
-
- return item;
- });
+ }
if (cacheItem.Local != null)
{
@@ -49,4 +48,22 @@ public async Task InvokeAsync(IEventHandler eventHandler, object eventData, Type
throw new AbpException("The object instance is not an event handler. Object type: " + eventHandler.GetType().AssemblyQualifiedName);
}
}
+
+ private static EventHandlerInvokerCacheItem CreateCacheItem(IEventHandler eventHandler, Type eventType)
+ {
+ var item = new EventHandlerInvokerCacheItem();
+
+ // Checked on the instance: a handler can provide its interfaces at run time (IDynamicInterfaceCastable).
+ if (typeof(ILocalEventHandler<>).MakeGenericType(eventType).IsInstanceOfType(eventHandler))
+ {
+ item.Local = (IEventHandlerMethodExecutor?)Activator.CreateInstance(typeof(LocalEventHandlerMethodExecutor<>).MakeGenericType(eventType));
+ }
+
+ if (typeof(IDistributedEventHandler<>).MakeGenericType(eventType).IsInstanceOfType(eventHandler))
+ {
+ item.Distributed = (IEventHandlerMethodExecutor?)Activator.CreateInstance(typeof(DistributedEventHandlerMethodExecutor<>).MakeGenericType(eventType));
+ }
+
+ return item;
+ }
}
diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IocEventHandlerFactory.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IocEventHandlerFactory.cs
index 1831158e9d5..7a51a2f16f1 100644
--- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IocEventHandlerFactory.cs
+++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IocEventHandlerFactory.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp.DependencyInjection;
namespace Volo.Abp.EventBus;
@@ -15,6 +18,8 @@ public class IocEventHandlerFactory : IEventHandlerFactory, IDisposable
protected IServiceScopeFactory ScopeFactory { get; }
+ private StrongBox? _implementationType;
+
public IocEventHandlerFactory(IServiceScopeFactory scopeFactory, Type handlerType)
{
ScopeFactory = scopeFactory;
@@ -28,10 +33,59 @@ public IocEventHandlerFactory(IServiceScopeFactory scopeFactory, Type handlerTyp
public IEventHandlerDisposeWrapper GetHandler()
{
var scope = ScopeFactory.CreateScope();
- return new EventHandlerDisposeWrapper(
- (IEventHandler)scope.ServiceProvider.GetRequiredService(HandlerType),
- () => scope.Dispose()
- );
+ try
+ {
+ var handler = (IEventHandler)scope.ServiceProvider.GetRequiredService(HandlerType);
+
+ var implementationType = Volatile.Read(ref _implementationType)?.Value;
+ if (implementationType != null && implementationType != handler.GetType())
+ {
+ // The container created another type than its registration told, so it is not asked again.
+ Volatile.Write(ref _implementationType, new StrongBox(null));
+ }
+
+ return new EventHandlerDisposeWrapper(handler, () => scope.Dispose());
+ }
+ catch
+ {
+ try
+ {
+ scope.Dispose();
+ }
+ catch
+ {
+ // Report why the handler could not be created rather than why its scope could not be disposed.
+ }
+
+ throw;
+ }
+ }
+
+ ///
+ /// Gets the type of the handlers this factory creates when the container can tell it without creating one,
+ /// otherwise null. The container is asked once.
+ ///
+ internal Type? GetImplementationTypeOrNull()
+ {
+ var implementationType = Volatile.Read(ref _implementationType);
+ if (implementationType == null)
+ {
+ // Never replaces what GetHandler wrote meanwhile.
+ Interlocked.CompareExchange(ref _implementationType, new StrongBox(FindImplementationType()), null);
+ implementationType = Volatile.Read(ref _implementationType)!;
+ }
+
+ return implementationType.Value;
+ }
+
+ private Type? FindImplementationType()
+ {
+ using (var scope = ScopeFactory.CreateScope())
+ {
+ return scope.ServiceProvider
+ .GetService()?
+ .GetImplementationTypeOrNull(HandlerType);
+ }
}
public bool IsInFactories(List handlerFactories)
diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs
index 729afa29157..8d5913e5580 100644
--- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs
+++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs
@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.DependencyInjection;
@@ -217,23 +218,17 @@ protected override IEnumerable GetHandlerFac
foreach (var handlerFactory in HandlerFactories.Where(hf => ShouldTriggerEventForHandler(eventType, hf.Key)))
{
- foreach (var factory in handlerFactory.Value)
+ foreach (var factory in handlerFactory.Value.Locking(factories => factories.ToList()))
{
- handlerFactoryList.Add(new Tuple(
- factory,
- handlerFactory.Key,
- ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0));
+ handlerFactoryList.Add(new Tuple(factory, handlerFactory.Key, GetHandlerOrder(factory)));
}
}
foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => eventNames.Contains(aehf.Key)))
{
- foreach (var factory in handlerFactory.Value)
+ foreach (var factory in handlerFactory.Value.Locking(factories => factories.ToList()))
{
- handlerFactoryList.Add(new Tuple(
- factory,
- typeof(DynamicEventData),
- ReflectionHelper.GetAttributesOfMemberOrDeclaringType(factory.GetHandler().EventHandler.GetType()).FirstOrDefault()?.Order ?? 0));
+ handlerFactoryList.Add(new Tuple(factory, typeof(DynamicEventData), GetHandlerOrder(factory)));
}
}
@@ -252,16 +247,9 @@ protected override IEnumerable GetDynamicHan
foreach (var handlerFactory in DynamicEventHandlerFactories.Where(aehf => aehf.Key == eventName))
{
- foreach (var factory in handlerFactory.Value)
+ foreach (var factory in handlerFactory.Value.Locking(factories => factories.ToList()))
{
- using var handler = factory.GetHandler();
- var handlerType = handler.EventHandler.GetType();
- handlerFactoryList.Add(new Tuple(
- factory,
- typeof(DynamicEventData),
- ReflectionHelper
- .GetAttributesOfMemberOrDeclaringType(handlerType)
- .FirstOrDefault()?.Order ?? 0));
+ handlerFactoryList.Add(new Tuple(factory, typeof(DynamicEventData), GetHandlerOrder(factory)));
}
}
@@ -269,6 +257,92 @@ protected override IEnumerable GetDynamicHan
new EventTypeWithEventHandlerFactories(x.Item2, new List { x.Item1 })).ToArray();
}
+ private static readonly ConditionalWeakTable> HandlerOrders = new();
+
+ ///
+ /// Disposes a handler the bus created for itself, so that a failure there neither fails the publish nor replaces
+ /// the exception that failed it.
+ ///
+ private void DisposeQuietly(IDisposable disposable, string message)
+ {
+ try
+ {
+ disposable.Dispose();
+ }
+ catch (Exception ex)
+ {
+ try
+ {
+ Logger.LogWarning(ex, message);
+ }
+ catch
+ {
+ // A failing logger must not replace the exception that failed the publish.
+ }
+ }
+ }
+
+ ///
+ /// Gets the order of the handler the factory creates.
+ /// When the handler type is known without creating a handler (the built-in factories of a fixed type, and an
+ /// IoC handler whose registration in the container fixes its type), the order is read from that type.
+ /// Otherwise the handler is created to read its type and is disposed at once, on each publish. Callers
+ /// enumerate a snapshot of the factories, since that dispose runs handler code which may unsubscribe.
+ ///
+ protected virtual int GetHandlerOrder(IEventHandlerFactory factory)
+ {
+ var handlerType = GetFixedHandlerTypeOrNull(factory);
+ if (handlerType == null)
+ {
+ var handler = factory.GetHandler();
+ try
+ {
+ handlerType = handler.EventHandler.GetType();
+ }
+ finally
+ {
+ DisposeQuietly(handler, "Could not dispose an event handler created to read its order.");
+ }
+ }
+
+ return GetHandlerOrderOfType(handlerType);
+ }
+
+ ///
+ /// The order is read from a type's attributes once and kept with the type, which may still be unloaded.
+ ///
+ private static int GetHandlerOrderOfType(Type handlerType)
+ {
+ return HandlerOrders.GetValue(handlerType, static type => new StrongBox(ReflectionHelper
+ .GetAttributesOfMemberOrDeclaringType(type)
+ .FirstOrDefault()?.Order ?? 0)).Value;
+ }
+
+ private static Type? GetFixedHandlerTypeOrNull(IEventHandlerFactory factory)
+ {
+ var factoryType = factory.GetType();
+
+ if (factoryType == typeof(SingleInstanceHandlerFactory))
+ {
+ return ((SingleInstanceHandlerFactory)factory).HandlerInstance.GetType();
+ }
+
+ // Only the built-in transient factories: a derived one may override how the handler is created.
+ if (factoryType == typeof(TransientEventHandlerFactory) ||
+ factoryType.IsGenericType && factoryType.GetGenericTypeDefinition() == typeof(TransientEventHandlerFactory<>))
+ {
+ return ((TransientEventHandlerFactory)factory).HandlerType;
+ }
+
+ // Only the built-in IoC factory, for the same reason.
+ if (factoryType == typeof(IocEventHandlerFactory))
+ {
+ return ((IocEventHandlerFactory)factory).GetImplementationTypeOrNull();
+ }
+
+ return null;
+ }
+
protected override Type? GetEventTypeByEventName(string eventName)
{
return EventTypes.GetOrDefault(eventName);
diff --git a/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider_Tests.cs b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider_Tests.cs
new file mode 100644
index 00000000000..503dad0b915
--- /dev/null
+++ b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/AutofacServiceImplementationTypeProvider_Tests.cs
@@ -0,0 +1,138 @@
+using System.Threading.Tasks;
+using Autofac;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.Testing;
+using Xunit;
+
+namespace Volo.Abp.Autofac;
+
+public class AutofacServiceImplementationTypeProvider_Tests : AbpIntegratedTest
+{
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddTransient();
+ services.OnRegistered(context =>
+ {
+ if (context.ImplementationType == typeof(TypeProviderInterceptedService))
+ {
+ context.Interceptors.TryAdd();
+ }
+ });
+
+ var builder = services.GetObject();
+ builder.RegisterType();
+ builder.RegisterType().As();
+ builder.Register(_ => new TypeProviderService()).As();
+ builder.RegisterType().As();
+ builder.RegisterType().As();
+ builder.RegisterDecorator();
+ }
+
+ [Fact]
+ public void Should_Replace_The_Null_Provider()
+ {
+ GetRequiredService().ShouldBeOfType();
+ }
+
+ [Fact]
+ public void Should_Tell_The_Type_A_Registration_Creates()
+ {
+ var provider = GetRequiredService();
+
+ provider.GetImplementationTypeOrNull(typeof(TypeProviderService)).ShouldBe(typeof(TypeProviderService));
+ provider.GetImplementationTypeOrNull(typeof(ITypeProviderService)).ShouldBe(typeof(TypeProviderService));
+ provider.GetImplementationTypeOrNull(typeof(TypeProviderReplaced)).ShouldBe(typeof(TypeProviderReplacement));
+ }
+
+ [Fact]
+ public void Should_Tell_The_Proxy_Type_Of_An_Intercepted_Class()
+ {
+ var type = GetRequiredService().GetImplementationTypeOrNull(typeof(TypeProviderInterceptedService));
+
+ type.ShouldNotBeNull();
+ type.ShouldNotBe(typeof(TypeProviderInterceptedService));
+ type.ShouldBe(GetRequiredService().GetType());
+ }
+
+ [Fact]
+ public void Should_Not_Tell_A_Type_It_Can_Not_Know()
+ {
+ var provider = GetRequiredService();
+
+ provider.GetImplementationTypeOrNull(typeof(TypeProviderCreatedByMethod)).ShouldBeNull();
+ provider.GetImplementationTypeOrNull(typeof(ITypeProviderDecoratedService)).ShouldBeNull();
+ provider.GetImplementationTypeOrNull(typeof(TypeProviderNotRegistered)).ShouldBeNull();
+ }
+
+ [Fact]
+ public void Should_Not_Tell_A_Type_For_Another_Container()
+ {
+ var services = new ServiceCollection();
+ services.AddTransient();
+ using var serviceProvider = services.BuildServiceProvider();
+
+ new AutofacServiceImplementationTypeProvider(serviceProvider)
+ .GetImplementationTypeOrNull(typeof(TypeProviderService))
+ .ShouldBeNull();
+ }
+
+ public interface ITypeProviderService
+ {
+ }
+
+ public interface ITypeProviderDecoratedService
+ {
+ }
+
+ public class TypeProviderService : TypeProviderCreatedByMethod, ITypeProviderService, ITypeProviderDecoratedService
+ {
+ }
+
+ public class TypeProviderCreatedByMethod
+ {
+ }
+
+ public class TypeProviderReplaced
+ {
+ }
+
+ public class TypeProviderReplacement : TypeProviderReplaced
+ {
+ }
+
+ public class TypeProviderNotRegistered
+ {
+ }
+
+ public class TypeProviderDecorator : ITypeProviderDecoratedService
+ {
+ public TypeProviderDecorator(ITypeProviderDecoratedService inner)
+ {
+ }
+ }
+
+ public class TypeProviderInterceptedService
+ {
+ public virtual Task DoItAsync()
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ public class TypeProviderInterceptor : AbpInterceptor
+ {
+ public override Task InterceptAsync(IAbpMethodInvocation invocation)
+ {
+ return invocation.ProceedAsync();
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/AbpShardedProxy_Tests.cs b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/AbpShardedProxy_Tests.cs
new file mode 100644
index 00000000000..ed0c08892f9
--- /dev/null
+++ b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/AbpShardedProxy_Tests.cs
@@ -0,0 +1,725 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Autofac;
+using Autofac.Builder;
+using Autofac.Extras.DynamicProxy;
+using Castle.DynamicProxy;
+using Shouldly;
+using Volo.Abp;
+using Xunit;
+
+namespace Volo.Abp.Autofac.Interception;
+
+public class AbpShardedProxy_Tests
+{
+ [Theory]
+ [InlineData(1)]
+ [InlineData(16)]
+ public void The_Same_Key_Should_Get_The_Same_Proxy_Type(int shardCount)
+ {
+ var builder = new AbpShardedProxyBuilder(shardCount);
+
+ var proxyType = builder.CreateClassProxyType(typeof(Calculator), Type.EmptyTypes, ProxyGenerationOptions.Default);
+
+ builder.CreateClassProxyType(typeof(Calculator), Type.EmptyTypes, ProxyGenerationOptions.Default).ShouldBeSameAs(proxyType);
+ builder.CreateClassProxyType(typeof(Calculator), new[] { typeof(IExtra) }, ProxyGenerationOptions.Default).ShouldNotBeSameAs(proxyType);
+ }
+
+ [Fact]
+ public void Interface_Proxy_Keys_Should_Be_Reused_And_Kept_Apart()
+ {
+ var builder = new AbpShardedProxyBuilder(16);
+ var options = ProxyGenerationOptions.Default;
+
+ var withTarget = builder.CreateInterfaceProxyTypeWithTarget(typeof(ICalculator), Type.EmptyTypes, typeof(Calculator), options);
+ builder.CreateInterfaceProxyTypeWithTarget(typeof(ICalculator), Type.EmptyTypes, typeof(Calculator), options).ShouldBeSameAs(withTarget);
+ builder.CreateInterfaceProxyTypeWithTarget(typeof(ICalculator), new[] { typeof(IExtra) }, typeof(Calculator), options).ShouldNotBeSameAs(withTarget);
+
+ var withTargetInterface = builder.CreateInterfaceProxyTypeWithTargetInterface(typeof(ICalculator), Type.EmptyTypes, options);
+ builder.CreateInterfaceProxyTypeWithTargetInterface(typeof(ICalculator), Type.EmptyTypes, options).ShouldBeSameAs(withTargetInterface);
+
+ var withoutTarget = builder.CreateInterfaceProxyTypeWithoutTarget(typeof(ICalculator), Type.EmptyTypes, options);
+ builder.CreateInterfaceProxyTypeWithoutTarget(typeof(ICalculator), Type.EmptyTypes, options).ShouldBeSameAs(withoutTarget);
+
+ new[] { withTarget, withTargetInterface, withoutTarget }.Select(proxyType => proxyType.FullName).Distinct().Count().ShouldBe(3);
+ }
+
+ [Fact]
+ public async Task Concurrent_First_Requests_Should_Get_One_Proxy_Type_Per_Key()
+ {
+ var builder = new AbpShardedProxyBuilder(4);
+ var targets = DistinctlyNamedTypes();
+
+ var results = await Task.WhenAll(Enumerable.Range(0, 8).Select(_ => Task.Run(() =>
+ targets.Select(target => builder.CreateClassProxyType(target, Type.EmptyTypes, ProxyGenerationOptions.Default)).ToArray())));
+
+ for (var i = 0; i < targets.Length; i++)
+ {
+ results.Select(result => result[i]).Distinct().ShouldHaveSingleItem();
+ }
+
+ results[0].Select(proxyType => proxyType.Module).Distinct().Count().ShouldBeGreaterThan(1);
+ }
+
+ [Fact]
+ public void Proxies_Of_Types_Sharing_A_Simple_Name_Should_Get_Distinct_Names()
+ {
+ var builder = new AbpShardedProxyBuilder(16);
+ var targets = SameNamedTypes().Concat(new[] { typeof(FirstOwner.Handler), typeof(SecondOwner.Handler) }).ToArray();
+
+ var proxyTypes = targets
+ .Select(target => builder.CreateClassProxyType(target, Type.EmptyTypes, ProxyGenerationOptions.Default))
+ .ToArray();
+
+ proxyTypes.Select(proxyType => proxyType.FullName).Distinct().Count().ShouldBe(targets.Length);
+ proxyTypes.Select(proxyType => proxyType.AssemblyQualifiedName).Distinct().Count().ShouldBe(targets.Length);
+ }
+
+ [Fact]
+ public void Interface_Proxies_Of_One_Interface_With_Different_Targets_Should_Get_Distinct_Names()
+ {
+ var builder = new AbpShardedProxyBuilder(16);
+ var targets = CalculatorTargets();
+
+ var proxyTypes = targets
+ .Select(target => builder.CreateInterfaceProxyTypeWithTarget(typeof(ICalculator), Type.EmptyTypes, target, ProxyGenerationOptions.Default))
+ .ToArray();
+
+ proxyTypes.Distinct().Count().ShouldBe(targets.Length);
+ proxyTypes.Select(proxyType => proxyType.FullName).Distinct().Count().ShouldBe(targets.Length);
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Name_The_Proxy_After_The_Class_Own_Interface()
+ {
+ using var container = Build(builder => builder.RegisterType()
+ .As()
+ .As()
+ .EnableAbpInterfaceInterceptors()
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ var calculator = container.Resolve();
+
+ calculator.GetType().FullName.ShouldStartWith("Castle.Proxies.IOwnInterfaceCalculatorProxy");
+ calculator.Add(1, 2).ShouldBe(3);
+ ((IExtra)calculator).Seven().ShouldBe(7);
+ container.Resolve().Calls.ShouldBe(new[] { "counted:Add", "counted:Seven" });
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Keep_The_Attributes_Autofac_Copies_To_The_Proxy()
+ {
+ using var container = Build(builder =>
+ {
+ builder.RegisterType().Keyed("autofac")
+ .EnableInterfaceInterceptors().InterceptedBy(typeof(RecordingInterceptor));
+ builder.RegisterType().Keyed("abp")
+ .EnableAbpInterfaceInterceptors().InterceptedBy(typeof(RecordingInterceptor));
+ });
+
+ // The class's own interface is not the first one, and the first one has an attribute Castle copies.
+ typeof(TaggedCalculator).GetInterfaces().First().ShouldBe(typeof(ITaggedExtra));
+ var fromAutofac = container.ResolveKeyed("autofac").GetType();
+ var fromAbp = container.ResolveKeyed("abp").GetType();
+
+ fromAutofac.GetCustomAttribute().ShouldNotBeNull().Tag.ShouldBe("extra");
+ fromAbp.GetCustomAttribute().ShouldNotBeNull().Tag.ShouldBe("extra");
+ fromAbp.GetInterfaces().OrderBy(i => i.FullName).ShouldBe(fromAutofac.GetInterfaces().OrderBy(i => i.FullName));
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Keep_The_Attributes_Castle_Is_Told_To_Copy_To_The_Proxy()
+ {
+ global::Castle.DynamicProxy.Generators.AttributesToAlwaysReplicate.Add(typeof(InheritedProxyTagAttribute));
+ using var container = Build(builder =>
+ {
+ builder.RegisterType().Keyed("autofac")
+ .EnableInterfaceInterceptors().InterceptedBy(typeof(RecordingInterceptor));
+ builder.RegisterType().Keyed("abp")
+ .EnableAbpInterfaceInterceptors().InterceptedBy(typeof(RecordingInterceptor));
+ });
+
+ typeof(ReplicatedCalculator).GetInterfaces().First().ShouldBe(typeof(IReplicatedExtra));
+ var fromAutofac = container.ResolveKeyed("autofac").GetType();
+ var fromAbp = container.ResolveKeyed("abp").GetType();
+
+ fromAutofac.GetCustomAttribute().ShouldNotBeNull().Tag.ShouldBe("replicated");
+ fromAbp.GetCustomAttribute().ShouldNotBeNull().Tag.ShouldBe("replicated");
+ }
+
+ [Fact]
+ public void Properties_Should_Be_Set_On_Same_Named_Proxies_From_Different_Generators()
+ {
+ // Two generators name a proxy of the same interface the same way, as Autofac's own and ABP's do side by side.
+ var first = (IPropertyTarget)new ProxyGenerator(new DefaultProxyBuilder())
+ .CreateInterfaceProxyWithTarget(typeof(IPropertyTarget), new PropertyTarget01());
+ var second = (IPropertyTarget)new ProxyGenerator(new DefaultProxyBuilder())
+ .CreateInterfaceProxyWithTarget(typeof(IPropertyTarget), new PropertyTarget02());
+ first.GetType().FullName.ShouldBe(second.GetType().FullName);
+ first.GetType().ShouldNotBe(second.GetType());
+
+ ObjectHelper.TrySetProperty(first, x => x.Value, () => 7);
+ ObjectHelper.TrySetProperty(second, x => x.Value, () => 42);
+
+ ((IPropertyTarget)ProxyUtil.GetUnproxiedInstance(first)).Value.ShouldBe(7);
+ ((IPropertyTarget)ProxyUtil.GetUnproxiedInstance(second)).Value.ShouldBe(42);
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Name_The_Proxy_After_A_Nullable_Annotated_Own_Interface()
+ {
+ using var container = Build(builder => builder.RegisterType()
+ .As()
+ .EnableAbpInterfaceInterceptors()
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ typeof(INullableCalculator).CustomAttributes.ShouldContain(a => a.AttributeType.Name == "NullableContextAttribute");
+ container.Resolve().GetType().FullName.ShouldStartWith("Castle.Proxies.INullableCalculatorProxy");
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Keep_A_Runtime_Attribute_Of_The_Compiler_Namespace_On_The_Proxy()
+ {
+ using var container = Build(builder => builder.RegisterType()
+ .As()
+ .EnableAbpInterfaceInterceptors()
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ typeof(ForwardedCalculator).GetInterfaces().First().ShouldBe(typeof(IForwardedExtra));
+ var proxyType = container.Resolve().GetType();
+ proxyType.FullName.ShouldStartWith("Castle.Proxies.IForwardedExtraProxy");
+ proxyType.GetCustomAttributes(typeof(System.Runtime.CompilerServices.TypeForwardedFromAttribute), false).ShouldNotBeEmpty();
+ }
+
+ [Fact]
+ public void Interface_Proxies_Of_A_Shared_Interface_Should_Keep_Distinct_Names_For_Caches_Keyed_By_Name()
+ {
+ var targets = PropertyTargets();
+ using var container = Build(builder =>
+ {
+ for (var i = 0; i < targets.Length; i++)
+ {
+ builder.RegisterType(targets[i]).Keyed(i).EnableAbpInterfaceInterceptors().InterceptedBy(typeof(RecordingInterceptor));
+ }
+ });
+
+ var proxies = Enumerable.Range(0, targets.Length).Select(i => container.ResolveKeyed(i)).ToArray();
+
+ proxies.Select(proxy => proxy.GetType().FullName).Distinct().Count().ShouldBe(targets.Length);
+ for (var i = 0; i < proxies.Length; i++)
+ {
+ var value = i;
+ ObjectHelper.TrySetProperty(proxies[i], x => x.Value, () => value);
+ ((IPropertyTarget)ProxyUtil.GetUnproxiedInstance(proxies[i])).Value.ShouldBe(i);
+ }
+ }
+
+ [Fact]
+ public async Task Every_Proxy_Kind_Should_Intercept_Through_The_Sharded_Builder()
+ {
+ var generator = new ProxyGenerator(new AbpShardedProxyBuilder(4));
+ var interceptor = new RecordingInterceptor("counted");
+
+ generator.CreateClassProxy(interceptor).Add(1, 2).ShouldBe(3);
+ generator.CreateClassProxyWithTarget(new Calculator(), interceptor).Add(1, 2).ShouldBe(3);
+ generator.CreateInterfaceProxyWithTarget(new Calculator(), interceptor).Add(1, 2).ShouldBe(3);
+ generator.CreateInterfaceProxyWithTargetInterface(new Calculator(), interceptor).Add(1, 2).ShouldBe(3);
+ generator.CreateInterfaceProxyWithoutTarget(new ReturningInterceptor(7)).Add(1, 2).ShouldBe(7);
+ (await generator.CreateClassProxy(interceptor).AddAsync(2, 3)).ShouldBe(5);
+ Should.Throw(() => generator.CreateClassProxy(interceptor).Fail());
+
+ interceptor.Calls.Count.ShouldBe(6);
+ }
+
+ [Fact]
+ public void Class_Interception_Should_Pass_The_Options_Selector_To_The_Proxy()
+ {
+ var options = new ProxyGenerationOptions { Selector = new NoInterceptorsSelector() };
+ using var container = Build(builder => builder.RegisterType()
+ .EnableAbpClassInterceptors(options)
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ container.Resolve().Add(1, 2).ShouldBe(3);
+
+ container.Resolve().Calls.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public void Class_Interception_Should_Pass_The_Options_Mixins_To_The_Proxy()
+ {
+ var options = new ProxyGenerationOptions();
+ options.AddMixinInstance(new Extra());
+ using var container = Build(builder => builder.RegisterType()
+ .EnableAbpClassInterceptors(options)
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ var calculator = container.Resolve();
+
+ calculator.Add(1, 2).ShouldBe(3);
+ ((IExtra)calculator).Seven().ShouldBe(7);
+ container.Resolve().Calls.ShouldBe(new[] { "counted:Add", "counted:Seven" });
+ }
+
+ [Fact]
+ public void Class_Interception_Should_Run_Explicit_Then_Attribute_Interceptors()
+ {
+ using var container = Build(builder => builder.RegisterType()
+ .EnableAbpClassInterceptors()
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ container.Resolve().Add(1, 2).ShouldBe(3);
+
+ CallsOf(container).ShouldBe(new[] { "counted:Add", "attribute:Add" });
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Run_Explicit_Named_And_Attribute_Interceptors()
+ {
+ using var container = Build(builder => builder.RegisterType()
+ .As()
+ .EnableAbpInterfaceInterceptors()
+ .InterceptedBy(typeof(RecordingInterceptor))
+ .InterceptedBy("named"));
+
+ container.Resolve().Add(1, 2).ShouldBe(3);
+
+ CallsOf(container).ShouldBe(new[] { "counted:Add", "named:Add", "attribute:Add" });
+ }
+
+ [Fact]
+ public void Interface_Interception_Should_Pass_The_Options_To_The_Proxy()
+ {
+ var options = new ProxyGenerationOptions { Selector = new NoInterceptorsSelector() };
+ using var container = Build(builder => builder.RegisterType()
+ .As()
+ .EnableAbpInterfaceInterceptors(options)
+ .InterceptedBy(typeof(RecordingInterceptor)));
+
+ container.Resolve().Add(1, 2).ShouldBe(3);
+
+ container.Resolve().Calls.ShouldBeEmpty();
+ }
+
+ [Fact]
+ public async Task The_Shard_Count_Should_Be_Fixed_Once_The_Generator_Exists()
+ {
+ var generators = await Task.WhenAll(Enumerable.Range(0, 8)
+ .Select(_ => Task.Run(() => AbpInterceptionRegistrationExtensions.ProxyGenerator)));
+ var generator = generators.Distinct().ShouldHaveSingleItem();
+ var shardCount = AbpInterceptionRegistrationExtensions.ShardCount;
+
+ generator.ProxyBuilder.ShouldBeOfType().ShardCount.ShouldBe(shardCount);
+ AbpInterceptionRegistrationExtensions.ShardCount = shardCount;
+ Should.Throw(() => AbpInterceptionRegistrationExtensions.ShardCount = shardCount + 1);
+ Should.Throw(() => AbpInterceptionRegistrationExtensions.ShardCount = 0);
+ AbpInterceptionRegistrationExtensions.ShardCount.ShouldBe(shardCount);
+ }
+
+ private static IContainer Build(Action register)
+ {
+ var builder = new ContainerBuilder();
+ builder.RegisterType().AsSelf().SingleInstance()
+ .WithParameter(new TypedParameter(typeof(string), "counted"));
+ builder.Register(_ => new RecordingInterceptor("named")).Named("named").SingleInstance();
+ builder.Register(_ => new AttributeInterceptor()).AsSelf().SingleInstance();
+ register(builder);
+ return builder.Build();
+ }
+
+ private static List CallsOf(IContainer container)
+ {
+ return container.Resolve().Recorded
+ .Concat(((RecordingInterceptor)container.ResolveNamed("named")).Recorded)
+ .Concat(container.Resolve().Recorded)
+ .OrderBy(call => call.Order)
+ .Select(call => call.Name)
+ .ToList();
+ }
+
+ private static Type[] DistinctlyNamedTypes()
+ {
+ return new[]
+ {
+ typeof(Spread01), typeof(Spread02), typeof(Spread03), typeof(Spread04), typeof(Spread05), typeof(Spread06),
+ typeof(Spread07), typeof(Spread08), typeof(Spread09), typeof(Spread10), typeof(Spread11), typeof(Spread12)
+ };
+ }
+
+ private static Type[] CalculatorTargets()
+ {
+ return new[]
+ {
+ typeof(Calculator), typeof(Calculator), typeof(Calculator), typeof(Calculator),
+ typeof(Calculator), typeof(Calculator), typeof(Calculator), typeof(Calculator),
+ typeof(Calculator), typeof(Calculator), typeof(Calculator), typeof(Calculator)
+ };
+ }
+
+ private static Type[] PropertyTargets()
+ {
+ return new[]
+ {
+ typeof(PropertyTarget01), typeof(PropertyTarget02), typeof(PropertyTarget03), typeof(PropertyTarget04),
+ typeof(PropertyTarget05), typeof(PropertyTarget06), typeof(PropertyTarget07), typeof(PropertyTarget08),
+ typeof(PropertyTarget09), typeof(PropertyTarget10), typeof(PropertyTarget11), typeof(PropertyTarget12)
+ };
+ }
+
+ private static Type[] SameNamedTypes()
+ {
+ return new[]
+ {
+ typeof(int), typeof(long), typeof(short), typeof(byte), typeof(string), typeof(Guid), typeof(DateTime),
+ typeof(decimal), typeof(double), typeof(float), typeof(char), typeof(bool), typeof(object), typeof(Uri),
+ typeof(Version), typeof(TimeSpan)
+ }
+ .Select(argument => typeof(Target<>).MakeGenericType(argument))
+ .ToArray();
+ }
+
+ public interface ICalculator
+ {
+ int Add(int a, int b);
+ }
+
+ public class Calculator : ICalculator
+ {
+ public virtual int Add(int a, int b) => a + b;
+ }
+
+#nullable enable
+ public interface INullableCalculator
+ {
+ string? Describe(string? value);
+
+ string Name();
+ }
+
+ public class NullableCalculator : IExtra, INullableCalculator
+ {
+ public int Seven() => 7;
+
+ public string? Describe(string? value) => value;
+
+ public string Name() => "name";
+ }
+#nullable restore
+
+ public interface IOwnInterfaceCalculator
+ {
+ int Add(int a, int b);
+ }
+
+ public class OwnInterfaceCalculator : IExtra, IOwnInterfaceCalculator
+ {
+ public int Add(int a, int b) => a + b;
+
+ public int Seven() => 7;
+ }
+
+ [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class, Inherited = false)]
+ public class ProxyTagAttribute : Attribute
+ {
+ public string Tag { get; }
+
+ public ProxyTagAttribute(string tag)
+ {
+ Tag = tag;
+ }
+ }
+
+ [ProxyTag("extra")]
+ public interface ITaggedExtra
+ {
+ int Eight();
+ }
+
+ [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class)]
+ public class InheritedProxyTagAttribute : Attribute
+ {
+ public string Tag { get; }
+
+ public InheritedProxyTagAttribute(string tag)
+ {
+ Tag = tag;
+ }
+ }
+
+ [InheritedProxyTag("replicated")]
+ public interface IReplicatedExtra
+ {
+ int Nine();
+ }
+
+ public interface IReplicatedCalculator
+ {
+ int Add(int a, int b);
+ }
+
+ public class ReplicatedCalculator : IReplicatedExtra, IReplicatedCalculator
+ {
+ public int Add(int a, int b) => a + b;
+
+ public int Nine() => 9;
+ }
+
+ [System.Runtime.CompilerServices.TypeForwardedFrom("Original, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")]
+ public interface IForwardedExtra
+ {
+ int Ten();
+ }
+
+ public interface IForwardedCalculator
+ {
+ int Add(int a, int b);
+ }
+
+ public class ForwardedCalculator : IForwardedExtra, IForwardedCalculator
+ {
+ public int Add(int a, int b) => a + b;
+
+ public int Ten() => 10;
+ }
+
+ public interface ITaggedCalculator
+ {
+ int Add(int a, int b);
+ }
+
+ public class TaggedCalculator : ITaggedExtra, ITaggedCalculator
+ {
+ public int Add(int a, int b) => a + b;
+
+ public int Eight() => 8;
+ }
+
+ public interface IPropertyTarget
+ {
+ int Value { get; set; }
+ }
+
+ public class PropertyTarget01 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget02 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget03 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget04 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget05 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget06 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget07 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget08 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget09 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget10 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget11 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public class PropertyTarget12 : IPropertyTarget
+ {
+ public int Value { get; set; }
+ }
+
+ public interface IExtra
+ {
+ int Seven();
+ }
+
+ public class Calculator : ICalculator
+ {
+ public virtual int Add(int a, int b) => a + b;
+
+ public virtual Task AddAsync(int a, int b) => Task.FromResult(a + b);
+
+ public virtual int Fail() => throw new InvalidOperationException("Failed.");
+ }
+
+ [Intercept(typeof(AttributeInterceptor))]
+ public class AttributedCalculator : Calculator
+ {
+ }
+
+ public class Extra : IExtra
+ {
+ public int Seven() => 7;
+ }
+
+ public class Spread01
+ {
+ public virtual int Value() => 1;
+ }
+
+ public class Spread02
+ {
+ public virtual int Value() => 2;
+ }
+
+ public class Spread03
+ {
+ public virtual int Value() => 3;
+ }
+
+ public class Spread04
+ {
+ public virtual int Value() => 4;
+ }
+
+ public class Spread05
+ {
+ public virtual int Value() => 5;
+ }
+
+ public class Spread06
+ {
+ public virtual int Value() => 6;
+ }
+
+ public class Spread07
+ {
+ public virtual int Value() => 7;
+ }
+
+ public class Spread08
+ {
+ public virtual int Value() => 8;
+ }
+
+ public class Spread09
+ {
+ public virtual int Value() => 9;
+ }
+
+ public class Spread10
+ {
+ public virtual int Value() => 10;
+ }
+
+ public class Spread11
+ {
+ public virtual int Value() => 11;
+ }
+
+ public class Spread12
+ {
+ public virtual int Value() => 12;
+ }
+
+ public class FirstOwner
+ {
+ public class Handler
+ {
+ public virtual int Value() => 1;
+ }
+ }
+
+ public class SecondOwner
+ {
+ public class Handler
+ {
+ public virtual int Value() => 2;
+ }
+ }
+
+ public class Target
+ {
+ public virtual T? Echo(T? value) => value;
+ }
+
+ public class RecordingInterceptor : IInterceptor
+ {
+ private static int _order;
+
+ private readonly string _name;
+
+ public RecordingInterceptor(string name)
+ {
+ _name = name;
+ }
+
+ public List Calls => Recorded.Select(call => call.Name).ToList();
+
+ public List<(int Order, string Name)> Recorded { get; } = new();
+
+ public void Intercept(IInvocation invocation)
+ {
+ lock (Recorded)
+ {
+ Recorded.Add((System.Threading.Interlocked.Increment(ref _order), $"{_name}:{invocation.Method.Name}"));
+ }
+
+ invocation.Proceed();
+ }
+ }
+
+ public class AttributeInterceptor : RecordingInterceptor
+ {
+ public AttributeInterceptor()
+ : base("attribute")
+ {
+ }
+ }
+
+ public class ReturningInterceptor : IInterceptor
+ {
+ private readonly int _value;
+
+ public ReturningInterceptor(int value)
+ {
+ _value = value;
+ }
+
+ public void Intercept(IInvocation invocation)
+ {
+ invocation.ReturnValue = _value;
+ }
+ }
+
+ public class NoInterceptorsSelector : IInterceptorSelector
+ {
+ public IInterceptor[] SelectInterceptors(Type type, System.Reflection.MethodInfo method, IInterceptor[] interceptors)
+ {
+ return Array.Empty();
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/Autofac_Interface_Interception_Test.cs b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/Autofac_Interface_Interception_Test.cs
new file mode 100644
index 00000000000..ec0b7987d96
--- /dev/null
+++ b/framework/test/Volo.Abp.Autofac.Tests/Volo/Abp/Autofac/Interception/Autofac_Interface_Interception_Test.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Autofac.Builder;
+using Castle.DynamicProxy;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Volo.Abp.DynamicProxy;
+using Volo.Abp.TestBase.Logging;
+using Volo.Abp.Testing;
+using Xunit;
+
+namespace Volo.Abp.Autofac.Interception;
+
+public class Autofac_Interface_Interception_Test : AbpAsyncIntegratedTest, IAsyncLifetime
+{
+ protected override Task> SetAbpApplicationCreationOptionsAsync()
+ {
+ return Task.FromResult>(options => options.UseAutofac());
+ }
+
+ protected override Task BeforeAddApplicationAsync(IServiceCollection services)
+ {
+ services.AddTransient();
+ services.AddTransient();
+ services.OnRegistered(registration =>
+ {
+ if (typeof(InterfaceInterceptionTarget) == registration.ImplementationType)
+ {
+ registration.Interceptors.Add();
+ }
+ });
+
+ return Task.CompletedTask;
+ }
+
+ [Fact]
+ public async Task Should_Intercept_A_Service_Registered_By_Its_Interface()
+ {
+ var target = GetRequiredService();
+
+ ProxyUtil.IsProxy(target).ShouldBeTrue();
+ (await target.GetValueAsync()).ShouldBe(42);
+ // Read from the target itself: reading Logs through the proxy is intercepted too.
+ ((InterfaceInterceptionTarget)ProxyUtil.GetUnproxiedInstance(target)).Logs.ShouldBe(new[]
+ {
+ "SimpleAsyncInterceptor_InterceptAsync_BeforeInvocation",
+ "ExecutingGetValueAsync",
+ "SimpleAsyncInterceptor_InterceptAsync_AfterInvocation"
+ });
+ }
+
+ [Fact]
+ public void Should_Generate_The_Proxy_With_The_Sharded_Generator()
+ {
+ var target = GetRequiredService();
+
+ var targetType = typeof(InterfaceInterceptionTarget);
+ var additionalInterfaces = targetType.GetInterfaces()
+ .Where(ProxyUtil.IsAccessible)
+ .Where(i => i != typeof(IInterfaceInterceptionTarget))
+ .ToArray();
+ var proxyType = AbpInterceptionRegistrationExtensions.ProxyGenerator.ProxyBuilder.CreateInterfaceProxyTypeWithTarget(
+ typeof(IInterfaceInterceptionTarget), additionalInterfaces, targetType, ProxyGenerationOptions.Default);
+
+ // The same key returns the cached type only from the generator that built the resolved proxy.
+ target.GetType().ShouldBeSameAs(proxyType);
+ }
+
+ public interface IInterfaceInterceptionTarget : ICanLogOnObject
+ {
+ Task GetValueAsync();
+ }
+
+ public class InterfaceInterceptionTarget : IInterfaceInterceptionTarget
+ {
+ public List Logs { get; } = new();
+
+ public Task GetValueAsync()
+ {
+ Logs.Add("ExecutingGetValueAsync");
+ return Task.FromResult(42);
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/ObjectHelper_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/ObjectHelper_Tests.cs
index 0c47fa829c1..f5f411f3b28 100644
--- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/ObjectHelper_Tests.cs
+++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/ObjectHelper_Tests.cs
@@ -1,4 +1,8 @@
-using System.Runtime.Serialization;
+using System;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
using Shouldly;
using Xunit;
@@ -6,6 +10,62 @@ namespace Volo.Abp;
public class ObjectHelper_Tests
{
+ [Fact]
+ public void A_Collectible_Type_Should_Not_Be_Kept_Alive_By_The_Property_Cache()
+ {
+ var type = UseCollectibleType();
+
+ for (var i = 0; i < 10 && type.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ type.IsAlive.ShouldBeFalse();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static WeakReference UseCollectibleType()
+ {
+ var builder = AssemblyBuilder
+ .DefineDynamicAssembly(new AssemblyName("Collectible" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.RunAndCollect)
+ .DefineDynamicModule("Collectible")
+ .DefineType("CollectibleObject", TypeAttributes.Public, typeof(object), new[] { typeof(IHasValue) });
+ builder.DefineDefaultConstructor(MethodAttributes.Public);
+ var field = builder.DefineField("_value", typeof(int), FieldAttributes.Private);
+ const MethodAttributes accessorAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final |
+ MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName;
+ var getter = builder.DefineMethod("get_Value", accessorAttributes, typeof(int), Type.EmptyTypes);
+ var il = getter.GetILGenerator();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldfld, field);
+ il.Emit(OpCodes.Ret);
+ var setter = builder.DefineMethod("set_Value", accessorAttributes, null, new[] { typeof(int) });
+ il = setter.GetILGenerator();
+ il.Emit(OpCodes.Ldarg_0);
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Stfld, field);
+ il.Emit(OpCodes.Ret);
+ var property = builder.DefineProperty("Value", PropertyAttributes.None, typeof(int), null);
+ property.SetGetMethod(getter);
+ property.SetSetMethod(setter);
+ builder.DefineMethodOverride(getter, typeof(IHasValue).GetProperty(nameof(IHasValue.Value))!.GetMethod!);
+ builder.DefineMethodOverride(setter, typeof(IHasValue).GetProperty(nameof(IHasValue.Value))!.SetMethod!);
+ var type = builder.CreateType()!;
+
+ // The cache keeps the type's own PropertyInfo, which refers back to the type.
+ var instance = (IHasValue)Activator.CreateInstance(type)!;
+ ObjectHelper.TrySetProperty(instance, x => x.Value, () => 42);
+ instance.Value.ShouldBe(42);
+
+ return new WeakReference(type);
+ }
+
+ public interface IHasValue
+ {
+ int Value { get; set; }
+ }
+
[Fact]
public void TrySetProperty_Test()
{
diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper_Tests.cs
new file mode 100644
index 00000000000..c83dd845c94
--- /dev/null
+++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper_Tests.cs
@@ -0,0 +1,288 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using AutoMapper;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Shouldly;
+using Volo.Abp.Autofac;
+using Volo.Abp.AutoMapper;
+using Volo.Abp.Domain.Entities.Events.Distributed;
+using Volo.Abp.EventBus.Distributed;
+using Volo.Abp.EventBus.Local;
+using Volo.Abp.Modularity;
+using Volo.Abp.Testing;
+using Volo.Abp.Uow;
+using Xunit;
+
+namespace Volo.Abp.Domain.Entities.Events;
+
+public class EntityChangeEventHelper_Tests : AbpIntegratedTest
+{
+ private readonly EntityChangeEventHelper _entityChangeEventHelper;
+ private readonly IUnitOfWorkManager _unitOfWorkManager;
+ private readonly ILocalEventBus _localEventBus;
+ private readonly IDistributedEventBus _distributedEventBus;
+ private readonly AbpDistributedEntityEventOptions _options;
+
+ public EntityChangeEventHelper_Tests()
+ {
+ _entityChangeEventHelper = GetRequiredService();
+ _unitOfWorkManager = GetRequiredService();
+ _localEventBus = GetRequiredService();
+ _distributedEventBus = GetRequiredService();
+ _options = GetRequiredService>().Value;
+ }
+
+ [Fact]
+ public async Task Should_Create_A_New_Created_Event_For_Each_Entity()
+ {
+ var receivedEvents = new List>();
+ using var subscription = _localEventBus.Subscribe>(
+ eventData =>
+ {
+ receivedEvents.Add(eventData);
+ return Task.CompletedTask;
+ });
+ var first = new Student(1);
+ var second = new Student(2);
+
+ using var uow = _unitOfWorkManager.Begin();
+ _entityChangeEventHelper.PublishEntityCreatedEvent(first);
+ _entityChangeEventHelper.PublishEntityCreatedEvent(second);
+
+ receivedEvents.ShouldBeEmpty();
+ await uow.CompleteAsync();
+
+ receivedEvents.Count.ShouldBe(2);
+ receivedEvents[0].Entity.ShouldBeSameAs(first);
+ receivedEvents[1].Entity.ShouldBeSameAs(second);
+ receivedEvents[0].ShouldNotBeSameAs(receivedEvents[1]);
+ }
+
+ [Fact]
+ public async Task Should_Create_A_New_Updated_Event_For_Each_Entity()
+ {
+ var receivedEvents = new List>();
+ using var subscription = _localEventBus.Subscribe>(
+ eventData =>
+ {
+ receivedEvents.Add(eventData);
+ return Task.CompletedTask;
+ });
+ var first = new Student(1);
+ var second = new Student(2);
+
+ using var uow = _unitOfWorkManager.Begin();
+ _entityChangeEventHelper.PublishEntityUpdatedEvent(first);
+ _entityChangeEventHelper.PublishEntityUpdatedEvent(second);
+
+ receivedEvents.ShouldBeEmpty();
+ await uow.CompleteAsync();
+
+ receivedEvents.Count.ShouldBe(2);
+ receivedEvents[0].Entity.ShouldBeSameAs(first);
+ receivedEvents[1].Entity.ShouldBeSameAs(second);
+ receivedEvents[0].ShouldNotBeSameAs(receivedEvents[1]);
+ }
+
+ [Fact]
+ public async Task Should_Create_A_New_Deleted_Event_For_Each_Entity()
+ {
+ var receivedEvents = new List>();
+ using var subscription = _localEventBus.Subscribe>(
+ eventData =>
+ {
+ receivedEvents.Add(eventData);
+ return Task.CompletedTask;
+ });
+ var first = new Student(1);
+ var second = new Student(2);
+
+ using var uow = _unitOfWorkManager.Begin();
+ _entityChangeEventHelper.PublishEntityDeletedEvent(first);
+ _entityChangeEventHelper.PublishEntityDeletedEvent(second);
+
+ receivedEvents.ShouldBeEmpty();
+ await uow.CompleteAsync();
+
+ receivedEvents.Count.ShouldBe(2);
+ receivedEvents[0].Entity.ShouldBeSameAs(first);
+ receivedEvents[1].Entity.ShouldBeSameAs(second);
+ receivedEvents[0].ShouldNotBeSameAs(receivedEvents[1]);
+ }
+
+ [Fact]
+ public async Task Should_Create_Distributed_Events_With_The_Mapped_Eto()
+ {
+ _options.AutoEventSelectors.Add();
+ _options.EtoMappings.Add();
+ var receivedEvents = new List>();
+ using var subscription = _distributedEventBus.Subscribe>(
+ eventData =>
+ {
+ receivedEvents.Add(eventData);
+ return Task.CompletedTask;
+ });
+
+ using var uow = _unitOfWorkManager.Begin();
+ _entityChangeEventHelper.PublishEntityCreatedEvent(new Student(1));
+ _entityChangeEventHelper.PublishEntityCreatedEvent(new Student(2));
+
+ receivedEvents.ShouldBeEmpty();
+ await uow.CompleteAsync();
+
+ receivedEvents.Count.ShouldBe(2);
+ receivedEvents[0].Entity.GetType().ShouldBe(typeof(PersonEto));
+ receivedEvents[0].Entity.Id.ShouldBe(1);
+ receivedEvents[1].Entity.Id.ShouldBe(2);
+ receivedEvents[0].ShouldNotBeSameAs(receivedEvents[1]);
+ }
+
+ [Fact]
+ public async Task Should_Create_An_Entity_Event_With_A_Params_Constructor()
+ {
+ var helper = new ParamsEntityChangeEventHelper(
+ _unitOfWorkManager,
+ GetRequiredService(),
+ GetRequiredService>())
+ {
+ LocalEventBus = _localEventBus
+ };
+ var receivedEntities = new List();
+ using var subscription = _localEventBus.Subscribe>(
+ eventData =>
+ {
+ receivedEntities.Add(eventData.Entity);
+ return Task.CompletedTask;
+ });
+ var student = new Student(42);
+
+ using var uow = _unitOfWorkManager.Begin();
+ helper.Publish(student);
+ receivedEntities.ShouldBeEmpty();
+ await uow.CompleteAsync();
+
+ receivedEntities.Count.ShouldBe(1);
+ receivedEntities[0].ShouldBeSameAs(student);
+ }
+
+ [Fact]
+ public async Task A_Collectible_Entity_Event_Type_Should_Not_Be_Kept_Alive_By_The_Constructor_Cache()
+ {
+ var handled = false;
+ using var subscription = _localEventBus.Subscribe>>(
+ eventData =>
+ {
+ eventData.Entity.ShouldNotBeNull();
+ handled = true;
+ return Task.CompletedTask;
+ });
+
+ var type = await PublishCollectibleEntityAsync(_entityChangeEventHelper, _unitOfWorkManager);
+
+ handled.ShouldBeTrue();
+ for (var i = 0; i < 10 && type.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ type.IsAlive.ShouldBeFalse();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static async Task PublishCollectibleEntityAsync(EntityChangeEventHelper helper, IUnitOfWorkManager manager)
+ {
+ var builder = AssemblyBuilder
+ .DefineDynamicAssembly(new AssemblyName("Collectible" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.RunAndCollect)
+ .DefineDynamicModule("Collectible")
+ .DefineType("CollectibleEntity", TypeAttributes.Public, typeof(Entity));
+ builder.DefineDefaultConstructor(MethodAttributes.Public);
+ var entityType = builder.CreateType()!;
+
+ using var uow = manager.Begin();
+ helper.PublishEntityCreatedEvent(Activator.CreateInstance(entityType)!);
+ await uow.CompleteAsync();
+
+ return new WeakReference(typeof(EntityCreatedEventData<>).MakeGenericType(entityType));
+ }
+
+ protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
+ {
+ options.UseAutofac();
+ }
+
+ [DependsOn(
+ typeof(AbpAutofacModule),
+ typeof(AbpDddDomainModule),
+ typeof(AbpAutoMapperModule)
+ )]
+ public class TestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.AddAutoMapperObjectMapper();
+ Configure(options =>
+ {
+ options.AddMaps(validate: true);
+ });
+ }
+ }
+
+ public class TestAutoMapperProfile : Profile
+ {
+ public TestAutoMapperProfile()
+ {
+ CreateMap();
+ }
+ }
+
+ public class Person : Entity
+ {
+ public Person(int id)
+ : base(id)
+ {
+ }
+ }
+
+ public class Student : Person
+ {
+ public Student(int id)
+ : base(id)
+ {
+ }
+ }
+
+ public class PersonEto
+ {
+ public int Id { get; set; }
+ }
+
+ public class ParamsEntityEventData : EntityEventData
+ {
+ public ParamsEntityEventData(params TEntity[] entities)
+ : base(entities[0])
+ {
+ }
+ }
+
+ private class ParamsEntityChangeEventHelper : EntityChangeEventHelper
+ {
+ public ParamsEntityChangeEventHelper(
+ IUnitOfWorkManager unitOfWorkManager,
+ IEntityToEtoMapper entityToEtoMapper,
+ IOptions options)
+ : base(unitOfWorkManager, entityToEtoMapper, options)
+ {
+ }
+
+ public void Publish(object entity)
+ {
+ TriggerEventWithEntity(LocalEventBus, typeof(ParamsEntityEventData<>), entity, entity);
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_RegisteredHandlerType_Tests.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_RegisteredHandlerType_Tests.cs
new file mode 100644
index 00000000000..b6a28527977
--- /dev/null
+++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus_RegisteredHandlerType_Tests.cs
@@ -0,0 +1,72 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Autofac;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Xunit;
+
+namespace Volo.Abp.EventBus.Distributed;
+
+public class LocalDistributedEventBus_RegisteredHandlerType_Tests : LocalDistributedEventBusTestBase
+{
+ protected override void AfterAddApplication(IServiceCollection services)
+ {
+ // Registered with Autofac directly, so the handler is not also subscribed to the application's bus by convention.
+ var builder = services.GetObject();
+ builder.RegisterType().SingleInstance();
+ builder.RegisterType();
+ }
+
+ [Fact]
+ public async Task A_Distributed_Ioc_Handler_Should_Be_Created_Once_Per_Publish()
+ {
+ var counter = GetRequiredService();
+ var factory = new IocEventHandlerFactory(GetRequiredService(), typeof(CountedDistributedEventHandler));
+
+ using (DistributedEventBus.Subscribe(typeof(CountedDistributedEventData), factory))
+ {
+ for (var i = 0; i < 3; i++)
+ {
+ await DistributedEventBus.PublishAsync(new CountedDistributedEventData(), onUnitOfWorkComplete: false);
+ }
+ }
+
+ counter.Handled.ShouldBe(3);
+ counter.Created.ShouldBe(3);
+ }
+
+ public class CountedDistributedEventData
+ {
+ }
+
+ public class CreationCounter
+ {
+ private int _created;
+ private int _handled;
+
+ public int Created => _created;
+
+ public int Handled => _handled;
+
+ public void OnCreated() => Interlocked.Increment(ref _created);
+
+ public void OnHandled() => Interlocked.Increment(ref _handled);
+ }
+
+ public class CountedDistributedEventHandler : IDistributedEventHandler
+ {
+ private readonly CreationCounter _counter;
+
+ public CountedDistributedEventHandler(CreationCounter counter)
+ {
+ _counter = counter;
+ _counter.OnCreated();
+ }
+
+ public Task HandleEventAsync(CountedDistributedEventData eventData)
+ {
+ _counter.OnHandled();
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventBusTestModule.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventBusTestModule.cs
index 45d32d63ed2..4764bd0b854 100644
--- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventBusTestModule.cs
+++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventBusTestModule.cs
@@ -1,8 +1,9 @@
+using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
namespace Volo.Abp.EventBus;
-[DependsOn(typeof(AbpEventBusModule))]
+[DependsOn(typeof(AbpEventBusModule), typeof(AbpAutofacModule))]
public class EventBusTestModule : AbpModule
{
}
diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventHandlerInvoker_Tests.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventHandlerInvoker_Tests.cs
index 294debbb097..83da7dd3810 100644
--- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventHandlerInvoker_Tests.cs
+++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/EventHandlerInvoker_Tests.cs
@@ -1,5 +1,10 @@
using System;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
+using Castle.DynamicProxy;
using Shouldly;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
@@ -70,6 +75,154 @@ public async Task Should_Invoke_DistributedEventHandler_With_EntityCreatedEto()
localHandler.EntityCreatedCount.ShouldBe(1);
}
+ [Fact]
+ public async Task Proxies_Sharing_A_Full_Name_Should_Each_Be_Invoked_By_What_They_Implement()
+ {
+ // Two dynamic modules can each name a proxy of the same interface the same way.
+ var localOnly = (ILocalEventHandler)new ProxyGenerator(new DefaultProxyBuilder())
+ .CreateInterfaceProxyWithTarget(typeof(ILocalEventHandler), new LocalOnlyEventHandler());
+ var both = new BothEventHandler();
+ var localAndDistributed = (ILocalEventHandler)new ProxyGenerator(new DefaultProxyBuilder())
+ .CreateInterfaceProxyWithTarget(typeof(ILocalEventHandler), new[] { typeof(IDistributedEventHandler) }, both);
+ localOnly.GetType().FullName.ShouldBe(localAndDistributed.GetType().FullName);
+ localOnly.GetType().ShouldNotBe(localAndDistributed.GetType());
+
+ await _eventHandlerInvoker.InvokeAsync(localOnly, new MyEventData(), typeof(MyEventData));
+ await _eventHandlerInvoker.InvokeAsync(localAndDistributed, new MyEventData(), typeof(MyEventData));
+
+ both.LocalCount.ShouldBe(1);
+ both.DistributedCount.ShouldBe(1);
+ }
+
+ [Fact]
+ public async Task A_Collectible_Handler_Type_Should_Not_Be_Kept_Alive_By_The_Invoker_Cache()
+ {
+ var type = await UseCollectibleHandlerAsync(_eventHandlerInvoker);
+
+ for (var i = 0; i < 10 && type.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ type.IsAlive.ShouldBeFalse();
+ }
+
+ [Fact]
+ public async Task A_Collectible_Event_Type_A_Handler_Cannot_Handle_Should_Not_Be_Kept_Alive_By_The_Invoker_Cache()
+ {
+ var type = await UseCollectibleEventTypeAsync(_eventHandlerInvoker);
+
+ for (var i = 0; i < 10 && type.IsAlive; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+
+ type.IsAlive.ShouldBeFalse();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static async Task UseCollectibleEventTypeAsync(IEventHandlerInvoker invoker)
+ {
+ var eventType = AssemblyBuilder
+ .DefineDynamicAssembly(new AssemblyName("Collectible" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.RunAndCollect)
+ .DefineDynamicModule("Collectible")
+ .DefineType("CollectibleEvent", TypeAttributes.Public)
+ .CreateType()!;
+
+ await Should.ThrowAsync(() => invoker.InvokeAsync(new MyLocalEventHandler(), Activator.CreateInstance(eventType)!, eventType));
+
+ return new WeakReference(eventType);
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static async Task UseCollectibleHandlerAsync(IEventHandlerInvoker invoker)
+ {
+ var handlerInterface = typeof(ILocalEventHandler);
+ var builder = AssemblyBuilder
+ .DefineDynamicAssembly(new AssemblyName("Collectible" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.RunAndCollect)
+ .DefineDynamicModule("Collectible")
+ .DefineType("CollectibleHandler", TypeAttributes.Public, typeof(object), new[] { handlerInterface });
+ builder.DefineDefaultConstructor(MethodAttributes.Public);
+ var method = builder.DefineMethod(
+ nameof(ILocalEventHandler.HandleEventAsync),
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
+ typeof(Task),
+ new[] { typeof(MyEventData) });
+ var il = method.GetILGenerator();
+ il.Emit(OpCodes.Call, typeof(Task).GetProperty(nameof(Task.CompletedTask))!.GetMethod!);
+ il.Emit(OpCodes.Ret);
+ builder.DefineMethodOverride(method, handlerInterface.GetMethod(nameof(ILocalEventHandler.HandleEventAsync))!);
+ var type = builder.CreateType()!;
+
+ await invoker.InvokeAsync((IEventHandler)Activator.CreateInstance(type)!, new MyEventData(), typeof(MyEventData));
+
+ return new WeakReference(type);
+ }
+
+ [Fact]
+ public async Task A_Handler_That_Provides_Its_Interface_At_Run_Time_Should_Be_Invoked()
+ {
+ var handler = new RunTimeInterfaceEventHandler();
+
+ await _eventHandlerInvoker.InvokeAsync(handler, new MyEventData(), typeof(MyEventData));
+
+ handler.Count.ShouldBe(1);
+ }
+
+ public class RunTimeInterfaceEventHandler : IEventHandler, IDynamicInterfaceCastable
+ {
+ public int Count { get; set; }
+
+ public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented)
+ {
+ return interfaceType.Equals(typeof(ILocalEventHandler).TypeHandle);
+ }
+
+ public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType)
+ {
+ return typeof(IRunTimeLocalEventHandler).TypeHandle;
+ }
+ }
+
+ [DynamicInterfaceCastableImplementation]
+ public interface IRunTimeLocalEventHandler : ILocalEventHandler
+ {
+ Task ILocalEventHandler.HandleEventAsync(MyEventData eventData)
+ {
+ ((RunTimeInterfaceEventHandler)this).Count++;
+ return Task.CompletedTask;
+ }
+ }
+
+ public class LocalOnlyEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(MyEventData eventData)
+ {
+ return Task.CompletedTask;
+ }
+ }
+
+ public class BothEventHandler : ILocalEventHandler, IDistributedEventHandler
+ {
+ public int LocalCount { get; private set; }
+
+ public int DistributedCount { get; private set; }
+
+ Task ILocalEventHandler.HandleEventAsync(MyEventData eventData)
+ {
+ LocalCount++;
+ return Task.CompletedTask;
+ }
+
+ Task IDistributedEventHandler.HandleEventAsync(MyEventData eventData)
+ {
+ DistributedCount++;
+ return Task.CompletedTask;
+ }
+ }
+
public class MyEventData
{
}
diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_Order_Test.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_Order_Test.cs
index cdf261b2b63..7674b594772 100644
--- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_Order_Test.cs
+++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/EventBus_Order_Test.cs
@@ -1,4 +1,9 @@
-using System.Threading.Tasks;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.DependencyInjection;
using Xunit;
@@ -17,11 +22,379 @@ public async Task Handler_Should_Execute_By_Order()
HandlerExecuteOrder.ShouldBe("321");
}
+ [Fact]
+ public async Task An_Ioc_Handler_Should_Be_Created_Once_Per_Publish()
+ {
+ CountedOrderEventHandler.Reset();
+
+ for (var i = 0; i < 3; i++)
+ {
+ await LocalEventBus.PublishAsync(new CountedOrderEventData());
+ }
+
+ // Its registration tells its type, so it is not created to read its order.
+ CountedOrderEventHandler.Created.ShouldBe(3);
+ CountedOrderEventHandler.Handled.ShouldBe(3);
+ CountedOrderEventHandler.Disposed.ShouldBe(3);
+ }
+
+ [Fact]
+ public void Listing_The_Factories_Should_Return_The_Subscribed_Factories_Without_Creating_A_Handler()
+ {
+ CountedOrderEventHandler.Reset();
+
+ var factories = ((LocalEventBus)LocalEventBus).GetEventHandlerFactories(typeof(CountedOrderEventData));
+
+ factories.SelectMany(x => x.EventHandlerFactories).ShouldContain(x => x is IocEventHandlerFactory);
+ CountedOrderEventHandler.Created.ShouldBe(0);
+ CountedOrderEventHandler.Disposed.ShouldBe(0);
+ CountedOrderEventHandler.Handled.ShouldBe(0);
+ }
+
+ [Fact]
+ public async Task Ordering_Should_Not_Create_A_Handler_Whose_Type_Is_Known()
+ {
+ CountedTransientEventHandler.Reset();
+ using (LocalEventBus.Subscribe())
+ {
+ for (var i = 0; i < 3; i++)
+ {
+ await LocalEventBus.PublishAsync(new CountedTransientEventData());
+ }
+ }
+
+ CountedTransientEventHandler.Created.ShouldBe(3);
+ CountedTransientEventHandler.Disposed.ShouldBe(3);
+ }
+
+ [Fact]
+ public async Task Order_Should_Come_From_The_Handler_The_Factory_Creates()
+ {
+ HandlerExecuteOrder = "";
+ using (LocalEventBus.Subscribe(typeof(ReplacedOrderEventData), new SwitchingHandlerFactory(new ReplacementOrderEventHandler())))
+ {
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ }
+
+ HandlerExecuteOrder.ShouldBe("R0");
+ }
+
+ [Fact]
+ public async Task Order_Should_Follow_The_Type_The_Factory_Creates_Now()
+ {
+ var factory = new SwitchingHandlerFactory(new ReplacementOrderEventHandler());
+ using (LocalEventBus.Subscribe(typeof(ReplacedOrderEventData), factory))
+ {
+ HandlerExecuteOrder = "";
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ HandlerExecuteOrder.ShouldBe("R0");
+
+ factory.Handler = new OriginalOrderEventHandler();
+ HandlerExecuteOrder = "";
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ HandlerExecuteOrder.ShouldBe("0O");
+ }
+ }
+
+ [Fact]
+ public async Task A_Handler_That_Unsubscribes_When_Disposed_Should_Not_Break_The_Publish()
+ {
+ HandlerExecuteOrder = "";
+ var factory = new SelfUnsubscribingHandlerFactory(LocalEventBus);
+ LocalEventBus.Subscribe(typeof(ReplacedOrderEventData), factory);
+
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ HandlerExecuteOrder.ShouldBe("R0");
+
+ HandlerExecuteOrder = "";
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ HandlerExecuteOrder.ShouldBe("0");
+ }
+
+ [Fact]
+ public async Task A_Handler_Whose_Type_Is_Known_Should_Still_Be_Ordered()
+ {
+ HandlerExecuteOrder = "";
+ using (LocalEventBus.Subscribe())
+ using (LocalEventBus.Subscribe(typeof(ReplacedOrderEventData), new TransientEventHandlerFactory(typeof(OriginalOrderEventHandler))))
+ {
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ }
+
+ HandlerExecuteOrder.ShouldBe("R0O");
+ }
+
+ [Fact]
+ public async Task A_Derived_Transient_Factory_Should_Be_Ordered_By_The_Handler_It_Creates()
+ {
+ HandlerExecuteOrder = "";
+ using (LocalEventBus.Subscribe(typeof(ReplacedOrderEventData), new ReplacingTransientHandlerFactory()))
+ {
+ await LocalEventBus.PublishAsync(new ReplacedOrderEventData());
+ }
+
+ HandlerExecuteOrder.ShouldBe("R0");
+ }
+
+ [Fact]
+ public async Task Typed_And_Dynamic_Handlers_Should_Be_Ordered_Together()
+ {
+ HandlerExecuteOrder = "";
+ var eventName = EventNameAttribute.GetNameOrDefault();
+ using (LocalEventBus.Subscribe())
+ using (LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new EarlyDynamicOrderEventHandler())))
+ {
+ await LocalEventBus.PublishAsync(new NamedOrderEventData());
+ }
+
+ HandlerExecuteOrder.ShouldBe("DT");
+ }
+
+ [Fact]
+ public async Task A_Dynamic_Handler_That_Unsubscribes_When_Disposed_Should_Not_Break_The_Publish()
+ {
+ var eventName = "UnmappedOrderEvent-" + Guid.NewGuid().ToString("N");
+ LocalEventBus.Subscribe(eventName, new SelfUnsubscribingDynamicHandlerFactory(LocalEventBus, eventName));
+ using (LocalEventBus.Subscribe(eventName, new SingleInstanceHandlerFactory(new LateDynamicOrderEventHandler())))
+ {
+ HandlerExecuteOrder = "";
+ await LocalEventBus.PublishAsync(eventName, new { Value = 1 });
+ HandlerExecuteOrder.ShouldBe("DL");
+
+ HandlerExecuteOrder = "";
+ await LocalEventBus.PublishAsync(eventName, new { Value = 2 });
+ HandlerExecuteOrder.ShouldBe("L");
+ }
+ }
+
+ [Fact]
+ public async Task An_Ioc_Handler_Replaced_In_A_Container_That_Can_Not_Tell_Its_Type_Should_Be_Ordered_By_The_Replacement()
+ {
+ var services = new ServiceCollection();
+ services.AddTransient();
+ await using var provider = services.BuildServiceProvider();
+ ReplacementIocOrderEventHandler.Reset();
+
+ HandlerExecuteOrder = "";
+ using (LocalEventBus.Subscribe(typeof(IocReplacedOrderEventData), new IocEventHandlerFactory(provider.GetRequiredService(), typeof(OriginalIocOrderEventHandler))))
+ using (LocalEventBus.Subscribe())
+ {
+ await LocalEventBus.PublishAsync(new IocReplacedOrderEventData());
+ }
+
+ HandlerExecuteOrder.ShouldBe("R0");
+ // Microsoft's container does not tell the type, so the handler is also created to read its order.
+ ReplacementIocOrderEventHandler.Created.ShouldBe(2);
+ ReplacementIocOrderEventHandler.Disposed.ShouldBe(2);
+ }
+
+ [Fact]
+ public async Task Publishing_While_Others_Subscribe_And_Unsubscribe_Should_Not_Fail()
+ {
+ var handled = 0;
+ using var stable = LocalEventBus.Subscribe(_ =>
+ {
+ Interlocked.Increment(ref handled);
+ return Task.CompletedTask;
+ });
+
+ var publishers = Enumerable.Range(0, 4).Select(_ => Task.Run(async () =>
+ {
+ for (var i = 0; i < 200; i++)
+ {
+ await LocalEventBus.PublishAsync(new ConcurrentOrderEventData());
+ }
+ }));
+ var subscribers = Enumerable.Range(0, 4).Select(_ => Task.Run(() =>
+ {
+ for (var i = 0; i < 200; i++)
+ {
+ LocalEventBus.Subscribe().Dispose();
+ }
+ }));
+
+ await Task.WhenAll(publishers.Concat(subscribers));
+
+ handled.ShouldBe(800);
+ }
+
public class MyOrderEventHandlerEventData
{
}
+ public class CountedOrderEventData
+ {
+
+ }
+
+ public class CountedOrderEventHandler : ILocalEventHandler, ITransientDependency, IDisposable
+ {
+ private static int _created;
+ private static int _disposed;
+
+ public static int Created => _created;
+
+ public static int Disposed => _disposed;
+
+ public static int Handled => _handled;
+
+ private static int _handled;
+
+ public CountedOrderEventHandler()
+ {
+ Interlocked.Increment(ref _created);
+ }
+
+ public static void Reset()
+ {
+ _created = 0;
+ _disposed = 0;
+ _handled = 0;
+ }
+
+ public Task HandleEventAsync(CountedOrderEventData eventData)
+ {
+ Interlocked.Increment(ref _handled);
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ Interlocked.Increment(ref _disposed);
+ }
+ }
+
+ public class CountedTransientEventData
+ {
+
+ }
+
+ public class CountedTransientEventHandler : ILocalEventHandler, IDisposable
+ {
+ private static int _created;
+ private static int _disposed;
+
+ public static int Created => _created;
+
+ public static int Disposed => _disposed;
+
+ public CountedTransientEventHandler()
+ {
+ Interlocked.Increment(ref _created);
+ }
+
+ public static void Reset()
+ {
+ _created = 0;
+ _disposed = 0;
+ }
+
+ public Task HandleEventAsync(CountedTransientEventData eventData)
+ {
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ Interlocked.Increment(ref _disposed);
+ }
+ }
+
+ public class ReplacedOrderEventData
+ {
+
+ }
+
+ public class ZeroOrderEventHandler : ILocalEventHandler, ITransientDependency
+ {
+ public Task HandleEventAsync(ReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "0";
+ return Task.CompletedTask;
+ }
+ }
+
+ [LocalEventHandlerOrder(10)]
+ public class OriginalOrderEventHandler : ILocalEventHandler
+ {
+ public virtual Task HandleEventAsync(ReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "O";
+ return Task.CompletedTask;
+ }
+ }
+
+ [LocalEventHandlerOrder(-10)]
+ public class ReplacementOrderEventHandler : OriginalOrderEventHandler
+ {
+ public override Task HandleEventAsync(ReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "R";
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// Registered for , but creates its replacement.
+ ///
+ public class ReplacingTransientHandlerFactory : TransientEventHandlerFactory
+ {
+ protected override IEventHandler CreateHandler()
+ {
+ return new ReplacementOrderEventHandler();
+ }
+ }
+
+ ///
+ /// Unsubscribes itself when its first handler is disposed, which ordering does before the handler runs.
+ ///
+ public class SelfUnsubscribingHandlerFactory : IEventHandlerFactory
+ {
+ private readonly ILocalEventBus _localEventBus;
+
+ public SelfUnsubscribingHandlerFactory(ILocalEventBus localEventBus)
+ {
+ _localEventBus = localEventBus;
+ }
+
+ public IEventHandlerDisposeWrapper GetHandler()
+ {
+ return new EventHandlerDisposeWrapper(
+ new ReplacementOrderEventHandler(),
+ () => _localEventBus.Unsubscribe(typeof(ReplacedOrderEventData), this));
+ }
+
+ public bool IsInFactories(List handlerFactories)
+ {
+ return handlerFactories.Contains(this);
+ }
+ }
+
+ ///
+ /// A factory whose handler type is not the one it was registered for and may change, as a replaced or
+ /// conditionally registered service can: the order is read from the handler it creates now.
+ ///
+ public class SwitchingHandlerFactory : IEventHandlerFactory
+ {
+ public IEventHandler Handler { get; set; }
+
+ public SwitchingHandlerFactory(IEventHandler handler)
+ {
+ Handler = handler;
+ }
+
+ public IEventHandlerDisposeWrapper GetHandler()
+ {
+ return new EventHandlerDisposeWrapper(Handler);
+ }
+
+ public bool IsInFactories(List handlerFactories)
+ {
+ return handlerFactories.Contains(this);
+ }
+ }
+
public class MyOrderEventHandler : ILocalEventHandler, ITransientDependency
{
public Task HandleEventAsync(MyOrderEventHandlerEventData eventData)
@@ -51,4 +424,133 @@ public Task HandleEventAsync(MyOrderEventHandlerEventData eventData)
}
}
+ [EventName("Volo.Abp.EventBus.Local.NamedOrderEvent")]
+ public class NamedOrderEventData
+ {
+ }
+
+ public class IocReplacedOrderEventData
+ {
+ }
+
+ public class ConcurrentOrderEventData
+ {
+ }
+
+ [LocalEventHandlerOrder(10)]
+ public class LateTypedOrderEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(NamedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "T";
+ return Task.CompletedTask;
+ }
+ }
+
+ [LocalEventHandlerOrder(-10)]
+ public class EarlyDynamicOrderEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(DynamicEventData eventData)
+ {
+ HandlerExecuteOrder += "D";
+ return Task.CompletedTask;
+ }
+ }
+
+ [LocalEventHandlerOrder(10)]
+ public class LateDynamicOrderEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(DynamicEventData eventData)
+ {
+ HandlerExecuteOrder += "L";
+ return Task.CompletedTask;
+ }
+ }
+
+ ///
+ /// Unsubscribes itself from an event name no event type maps to when its first handler is disposed.
+ ///
+ public class SelfUnsubscribingDynamicHandlerFactory : IEventHandlerFactory
+ {
+ private readonly ILocalEventBus _localEventBus;
+ private readonly string _eventName;
+
+ public SelfUnsubscribingDynamicHandlerFactory(ILocalEventBus localEventBus, string eventName)
+ {
+ _localEventBus = localEventBus;
+ _eventName = eventName;
+ }
+
+ public IEventHandlerDisposeWrapper GetHandler()
+ {
+ return new EventHandlerDisposeWrapper(
+ new EarlyDynamicOrderEventHandler(),
+ () => _localEventBus.Unsubscribe(_eventName, this));
+ }
+
+ public bool IsInFactories(List handlerFactories)
+ {
+ return handlerFactories.Contains(this);
+ }
+ }
+
+ [LocalEventHandlerOrder(10)]
+ public class OriginalIocOrderEventHandler : ILocalEventHandler
+ {
+ public virtual Task HandleEventAsync(IocReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "O";
+ return Task.CompletedTask;
+ }
+ }
+
+ [LocalEventHandlerOrder(-10)]
+ public class ReplacementIocOrderEventHandler : OriginalIocOrderEventHandler, IDisposable
+ {
+ private static int _created;
+ private static int _disposed;
+
+ public static int Created => _created;
+
+ public static int Disposed => _disposed;
+
+ public ReplacementIocOrderEventHandler()
+ {
+ Interlocked.Increment(ref _created);
+ }
+
+ public static void Reset()
+ {
+ _created = 0;
+ _disposed = 0;
+ }
+
+ public override Task HandleEventAsync(IocReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "R";
+ return Task.CompletedTask;
+ }
+
+ public void Dispose()
+ {
+ Interlocked.Increment(ref _disposed);
+ }
+ }
+
+ public class ZeroOrderIocEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(IocReplacedOrderEventData eventData)
+ {
+ HandlerExecuteOrder += "0";
+ return Task.CompletedTask;
+ }
+ }
+
+ public class ConcurrentOrderEventHandler : ILocalEventHandler
+ {
+ public Task HandleEventAsync(ConcurrentOrderEventData eventData)
+ {
+ return Task.CompletedTask;
+ }
+ }
}
diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/GenericInheritanceTest.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/GenericInheritanceTest.cs
index ee6abe0a55a..6352068e501 100644
--- a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/GenericInheritanceTest.cs
+++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/GenericInheritanceTest.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Domain.Entities;
@@ -44,6 +49,199 @@ public async Task Should_Trigger_For_Inherited_Generic_2()
triggeredEvent.ShouldBe(true);
}
+ [Fact]
+ public async Task Should_Trigger_For_Inherited_Generic_Every_Time()
+ {
+ var triggeredIds = new List();
+
+ LocalEventBus.Subscribe>(
+ eventData =>
+ {
+ triggeredIds.Add(eventData.Entity.Id);
+ return Task.CompletedTask;
+ });
+
+ await LocalEventBus.PublishAsync(new EntityChangedEventData