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(new Student(1))); + await LocalEventBus.PublishAsync(new EntityChangedEventData(new Student(2))); + await LocalEventBus.PublishAsync(new EntityChangedEventData(new Student(3))); + + triggeredIds.ShouldBe(new[] { 1, 2, 3 }); + } + + [Fact] + public async Task Should_Trigger_For_Inherited_Generic_With_Multiple_Constructors() + { + var triggeredEvent = false; + + LocalEventBus.Subscribe>( + eventData => + { + eventData.Entity.Id.ShouldBe(42); + triggeredEvent = true; + return Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(new MultipleConstructorsEventData(new Student(42))); + + triggeredEvent.ShouldBe(true); + } + + [Theory] + [InlineData(0, false)] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(0, true)] + [InlineData(1, true)] + [InlineData(2, true)] + public async Task Should_Trigger_For_Inherited_Generic_With_Params(int tagCount, bool useArrayArgument) + { + var tags = new[] { "first", "second" }[..tagCount]; + var student = new Student(42); + var receivedEvents = new List>(); + + using var subscription = LocalEventBus.Subscribe>( + eventData => + { + receivedEvents.Add(eventData); + return Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(new ParamsEventData(student, useArrayArgument, tags)); + + receivedEvents.Count.ShouldBe(1); + receivedEvents[0].Entity.ShouldBeSameAs(student); + receivedEvents[0].Tags.ShouldBe(tags); + if (useArrayArgument) + { + receivedEvents[0].Tags.ShouldBeSameAs(tags); + } + } + + [Fact] + public async Task Should_Select_The_Constructor_For_Each_Publish() + { + var receivedEvents = new List>(); + using var subscription = LocalEventBus.Subscribe>( + eventData => + { + receivedEvents.Add(eventData); + return Task.CompletedTask; + }); + + await LocalEventBus.PublishAsync(new MultipleConstructorsEventData(new Student(1))); + await LocalEventBus.PublishAsync(new MultipleConstructorsEventData(new Student(2), "updated")); + await LocalEventBus.PublishAsync(new MultipleConstructorsEventData(new Student(3))); + + receivedEvents.Count.ShouldBe(3); + receivedEvents[0].Entity.Id.ShouldBe(1); + receivedEvents[0].Reason.ShouldBeNull(); + receivedEvents[1].Entity.Id.ShouldBe(2); + receivedEvents[1].Reason.ShouldBe("updated"); + receivedEvents[2].Entity.Id.ShouldBe(3); + receivedEvents[2].Reason.ShouldBeNull(); + } + + [Fact] + public async Task A_Collectible_Event_Type_Should_Not_Be_Kept_Alive_By_The_Constructor_Cache() + { + var handled = false; + using var subscription = LocalEventBus.Subscribe>( + eventData => + { + eventData.Entity.Id.ShouldBe(42); + handled = true; + return Task.CompletedTask; + }); + + var type = await PublishCollectibleEventAsync(LocalEventBus); + + 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 PublishCollectibleEventAsync(ILocalEventBus eventBus) + { + var builder = AssemblyBuilder + .DefineDynamicAssembly(new AssemblyName("Collectible" + Guid.NewGuid().ToString("N")), AssemblyBuilderAccess.RunAndCollect) + .DefineDynamicModule("Collectible") + .DefineType("CollectibleEvent", TypeAttributes.Public); + var parameter = builder.DefineGenericParameters("TEntity")[0]; + var baseType = typeof(EntityChangedEventData<>).MakeGenericType(parameter); + builder.SetParent(baseType); + var constructor = builder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { parameter }); + var il = constructor.GetILGenerator(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Call, TypeBuilder.GetConstructor(baseType, typeof(EntityChangedEventData<>).GetConstructors()[0])); + il.Emit(OpCodes.Ret); + var definition = builder.CreateType()!; + var eventType = definition.MakeGenericType(typeof(Student)); + + await eventBus.PublishAsync(eventType, Activator.CreateInstance(eventType, new Student(42))!); + + return new WeakReference(definition.MakeGenericType(typeof(Person))); + } + + public class ParamsEventData : IEventDataWithInheritableGenericArgument + { + public TEntity Entity { get; } + + public string[] Tags { get; } + + public bool UseArrayArgument { get; } + + public ParamsEventData(TEntity entity, bool useArrayArgument, params string[] tags) + { + Entity = entity; + UseArrayArgument = useArrayArgument; + Tags = tags; + } + + public object[] GetConstructorArgs() + { + if (UseArrayArgument) + { + return new object[] { Entity!, UseArrayArgument, Tags }; + } + + var args = new object[Tags.Length + 2]; + args[0] = Entity!; + args[1] = UseArrayArgument; + Array.Copy(Tags, 0, args, 2, Tags.Length); + return args; + } + } + + public class MultipleConstructorsEventData : IEventDataWithInheritableGenericArgument + { + public TEntity Entity { get; } + + public string Reason { get; } + + public MultipleConstructorsEventData(TEntity entity) + { + Entity = entity; + } + + public MultipleConstructorsEventData(TEntity entity, string reason) + : this(entity) + { + Reason = reason; + } + + public object[] GetConstructorArgs() + { + return Reason == null ? new object[] { Entity! } : new object[] { Entity!, Reason }; + } + } + public class Person : Entity { public Person() diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/IocEventHandlerFactory_Tests.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/IocEventHandlerFactory_Tests.cs new file mode 100644 index 00000000000..87873413ab7 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/IocEventHandlerFactory_Tests.cs @@ -0,0 +1,241 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.EventBus.Local; + +public class IocEventHandlerFactory_Tests : EventBusTestBase +{ + protected override void AfterAddApplication(IServiceCollection services) + { + AddThrowingHandler(services); + } + + [Fact] + public void A_Handler_That_Fails_To_Construct_Should_Not_Leave_Its_Scope_Undisposed_With_Autofac() + { + var tracker = GetRequiredService(); + var factory = new IocEventHandlerFactory(GetRequiredService(), typeof(ThrowingHandler)); + + Should.Throw(() => factory.GetHandler()); + + tracker.Created.ShouldBe(1); + tracker.Disposed.ShouldBe(1); + } + + [Fact] + public void A_Handler_That_Fails_To_Construct_Should_Not_Leave_Its_Scope_Undisposed_With_Microsoft_DI() + { + var services = new ServiceCollection(); + AddThrowingHandler(services); + using var provider = services.BuildServiceProvider(); + var tracker = provider.GetRequiredService(); + var factory = new IocEventHandlerFactory(provider.GetRequiredService(), typeof(ThrowingHandler)); + + Should.Throw(() => factory.GetHandler()).Message.ShouldBe("Handler construction failed."); + + tracker.Created.ShouldBe(1); + tracker.Disposed.ShouldBe(1); + } + + [Fact] + public async Task Publishing_To_A_Handler_That_Fails_To_Construct_Should_Dispose_Every_Scope_It_Opened() + { + var tracker = GetRequiredService(); + using (LocalEventBus.Subscribe(typeof(ThrowingHandlerEventData), new IocEventHandlerFactory(GetRequiredService(), typeof(ThrowingHandler)))) + { + await Should.ThrowAsync(() => LocalEventBus.PublishAsync(new ThrowingHandlerEventData())); + } + + tracker.Created.ShouldBeGreaterThan(0); + tracker.Disposed.ShouldBe(tracker.Created); + } + + [Fact] + public void A_Handler_That_Fails_To_Construct_Should_Report_Its_Own_Exception_When_Its_Scope_Fails_To_Dispose() + { + var services = new ServiceCollection(); + services.AddScoped(); + services.AddTransient(); + using var provider = services.BuildServiceProvider(); + var factory = new IocEventHandlerFactory(provider.GetRequiredService(), typeof(HandlerWithDisposeFailingDependency)); + + Should.Throw(() => factory.GetHandler()).Message.ShouldBe("Handler construction failed."); + } + + [Fact] + public void A_Resolved_Handler_Should_Keep_Its_Scope_Until_It_Is_Disposed() + { + var tracker = GetRequiredService(); + var factory = new IocEventHandlerFactory(GetRequiredService(), typeof(WorkingHandler)); + + var wrapper = factory.GetHandler(); + wrapper.EventHandler.ShouldBeOfType().Dependency.IsDisposed.ShouldBeFalse(); + tracker.Created.ShouldBe(1); + tracker.Disposed.ShouldBe(0); + + wrapper.Dispose(); + tracker.Disposed.ShouldBe(1); + } + + [Fact] + public async Task A_Handler_That_Fails_To_Construct_While_Ordering_Should_Dispose_Both_Without_Invoking_Either() + { + // A Microsoft container of its own: it does not tell the handler types, so both are created to read their + // order, and neither is also subscribed to the application's bus by convention. + var services = new ServiceCollection(); + AddThrowingHandler(services); + services.AddTransient(); + services.AddTransient(); + await using var provider = services.BuildServiceProvider(); + var tracker = provider.GetRequiredService(); + var scopeFactory = provider.GetRequiredService(); + using (LocalEventBus.Subscribe(typeof(OrderingFailureEventData), new IocEventHandlerFactory(scopeFactory, typeof(EarlierWorkingHandler)))) + using (LocalEventBus.Subscribe(typeof(OrderingFailureEventData), new IocEventHandlerFactory(scopeFactory, typeof(LaterThrowingHandler)))) + { + await Should.ThrowAsync(() => LocalEventBus.PublishAsync(new OrderingFailureEventData())); + } + + tracker.Handled.ShouldBe(0); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + private static void AddThrowingHandler(IServiceCollection services) + { + services.AddSingleton(); + services.AddScoped(); + services.AddTransient(); + services.AddTransient(); + } + + public class ThrowingHandlerEventData + { + } + + public class OrderingFailureEventData + { + } + + public class DisposeFailingDependency : IDisposable + { + public void Dispose() + { + throw new ApplicationException("Scope disposal failed."); + } + } + + public class HandlerWithDisposeFailingDependency : ILocalEventHandler + { + public HandlerWithDisposeFailingDependency(DisposeFailingDependency dependency) + { + throw new InvalidOperationException("Handler construction failed."); + } + + public Task HandleEventAsync(OrderingFailureEventData eventData) + { + return Task.CompletedTask; + } + } + + public class ScopeTracker + { + private int _created; + private int _disposed; + private int _handled; + + public int Created => _created; + + public int Disposed => _disposed; + + public int Handled => _handled; + + public void OnHandled() => Interlocked.Increment(ref _handled); + + public void OnCreated() => Interlocked.Increment(ref _created); + + public void OnDisposed() => Interlocked.Increment(ref _disposed); + } + + public class ScopedDependency : IDisposable + { + private readonly ScopeTracker _tracker; + + public ScopeTracker Tracker => _tracker; + + public bool IsDisposed { get; private set; } + + public ScopedDependency(ScopeTracker tracker) + { + _tracker = tracker; + _tracker.OnCreated(); + } + + public void Dispose() + { + IsDisposed = true; + _tracker.OnDisposed(); + } + } + + public class ThrowingHandler : ILocalEventHandler + { + public ThrowingHandler(ScopedDependency dependency) + { + throw new InvalidOperationException("Handler construction failed."); + } + + public Task HandleEventAsync(ThrowingHandlerEventData eventData) + { + return Task.CompletedTask; + } + } + + public class WorkingHandler : ILocalEventHandler + { + public ScopedDependency Dependency { get; } + + public WorkingHandler(ScopedDependency dependency) + { + Dependency = dependency; + } + + public Task HandleEventAsync(ThrowingHandlerEventData eventData) + { + Dependency.Tracker.OnHandled(); + return Task.CompletedTask; + } + } + + public class EarlierWorkingHandler : ILocalEventHandler + { + private readonly ScopedDependency _dependency; + + public EarlierWorkingHandler(ScopedDependency dependency) + { + _dependency = dependency; + } + + public Task HandleEventAsync(OrderingFailureEventData eventData) + { + _dependency.Tracker.OnHandled(); + return Task.CompletedTask; + } + } + + public class LaterThrowingHandler : ILocalEventHandler + { + public LaterThrowingHandler(ScopedDependency dependency) + { + throw new InvalidOperationException("Handler construction failed."); + } + + public Task HandleEventAsync(OrderingFailureEventData eventData) + { + return Task.CompletedTask; + } + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_HandlerConstruction_Tests.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_HandlerConstruction_Tests.cs new file mode 100644 index 00000000000..66700de8b71 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_HandlerConstruction_Tests.cs @@ -0,0 +1,218 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EventBus.Local; + +public class LocalEventBus_HandlerConstruction_Tests : EventBusTestBase +{ + [Fact] + public async Task A_Later_Handler_Should_Be_Created_After_An_Earlier_One_Ran() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient(); + await using var provider = services.BuildServiceProvider(); + var scopeFactory = provider.GetRequiredService(); + + using (LocalEventBus.Subscribe(typeof(StateEventData), new IocEventHandlerFactory(scopeFactory, typeof(StateReadingHandler)))) + using (LocalEventBus.Subscribe(typeof(StateEventData), new IocEventHandlerFactory(scopeFactory, typeof(StateWritingHandler)))) + { + await LocalEventBus.PublishAsync(new StateEventData()); + } + + provider.GetRequiredService().SeenByReader.ShouldBe(42); + } + + [Fact] + public async Task A_Handler_Created_To_Read_Its_Order_Should_Not_Stop_The_Publish_When_It_Fails_To_Dispose() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddTransient(); + await using var provider = services.BuildServiceProvider(); + + using (LocalEventBus.Subscribe(typeof(StateEventData), new IocEventHandlerFactory(provider.GetRequiredService(), typeof(DisposeFailingStateHandler)))) + { + // The instance that handled the event still fails to dispose afterwards, as it did before. + await Should.ThrowAsync(() => LocalEventBus.PublishAsync(new StateEventData())); + } + + provider.GetRequiredService().Value.ShouldBe(1); + } + + [Fact] + public async Task A_Handler_That_Fails_To_Resolve_While_Its_Order_Is_Read_Should_Still_Be_Disposed() + { + var factory = new LazilyFailingHandlerFactory(); + var eventName = "Volo.Abp.EventBus.Local.LocalEventBus_HandlerConstruction_Tests.LazilyFailing"; + using (LocalEventBus.Subscribe(eventName, factory)) + { + (await Should.ThrowAsync(() => LocalEventBus.PublishAsync(eventName, new { }, onUnitOfWorkComplete: false))) + .Message.ShouldBe("Lazy resolution failed."); + } + + factory.Disposed.ShouldBe(1); + } + + public class LazilyFailingHandlerFactory : IEventHandlerFactory + { + public int Disposed { get; private set; } + + public IEventHandlerDisposeWrapper GetHandler() + { + return new LazilyFailingHandlerWrapper(this); + } + + public bool IsInFactories(List handlerFactories) + { + return handlerFactories.Contains(this); + } + + private class LazilyFailingHandlerWrapper : IEventHandlerDisposeWrapper + { + private readonly LazilyFailingHandlerFactory _factory; + + public LazilyFailingHandlerWrapper(LazilyFailingHandlerFactory factory) + { + _factory = factory; + } + + public IEventHandler EventHandler => throw new System.InvalidOperationException("Lazy resolution failed."); + + public void Dispose() + { + _factory.Disposed++; + } + } + } + + [Fact] + public async Task A_Derived_Bus_Should_Choose_The_Handlers_Of_A_Mapped_Dynamic_Event() + { + var bus = ActivatorUtilities.CreateInstance(ServiceProvider); + var handled = 0; + using (bus.Subscribe(_ => + { + handled++; + return Task.CompletedTask; + })) + { + await bus.TriggerHandlersAsync(typeof(DynamicEventData), new DynamicEventData(FilteredEventData.Name, new { })); + await bus.PublishAsync(new FilteredEventData(), onUnitOfWorkComplete: false); + } + + handled.ShouldBe(0); + bus.FilteredCalls.ShouldBe(2); + } + + [EventName(Name)] + public class FilteredEventData + { + public const string Name = "Volo.Abp.EventBus.Local.LocalEventBus_HandlerConstruction_Tests.Filtered"; + } + + // Kept out of conventional registration, which would make it the application's bus. + [DisableConventionalRegistration] + public class FilteringLocalEventBus : LocalEventBus + { + public int FilteredCalls { get; private set; } + + public FilteringLocalEventBus( + IOptions options, + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + IEventHandlerInvoker eventHandlerInvoker) + : base(options, serviceScopeFactory, currentTenant, unitOfWorkManager, eventHandlerInvoker) + { + } + + protected override IEnumerable GetHandlerFactories(System.Type eventType) + { + if (eventType == typeof(FilteredEventData)) + { + FilteredCalls++; + return Enumerable.Empty(); + } + + return base.GetHandlerFactories(eventType); + } + } +} + +public class StateEventData +{ +} + +public class ConstructionState +{ + public int Value { get; set; } + + public int SeenByReader { get; set; } = -1; +} + +public class DisposeFailingStateHandler : ILocalEventHandler, System.IDisposable +{ + private readonly ConstructionState _state; + + public DisposeFailingStateHandler(ConstructionState state) + { + _state = state; + } + + public Task HandleEventAsync(StateEventData eventData) + { + _state.Value++; + return Task.CompletedTask; + } + + public void Dispose() + { + throw new System.InvalidOperationException("Dispose failed."); + } +} + +[LocalEventHandlerOrder(-1)] +public class StateWritingHandler : ILocalEventHandler +{ + private readonly ConstructionState _state; + + public StateWritingHandler(ConstructionState state) + { + _state = state; + } + + public Task HandleEventAsync(StateEventData eventData) + { + _state.Value = 42; + return Task.CompletedTask; + } +} + +[LocalEventHandlerOrder(1)] +public class StateReadingHandler : ILocalEventHandler +{ + private readonly ConstructionState _state; + private readonly int _valueWhenCreated; + + public StateReadingHandler(ConstructionState state) + { + _state = state; + _valueWhenCreated = state.Value; + } + + public Task HandleEventAsync(StateEventData eventData) + { + _state.SeenByReader = _valueWhenCreated; + return Task.CompletedTask; + } +} diff --git a/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_RegisteredHandlerType_Tests.cs b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_RegisteredHandlerType_Tests.cs new file mode 100644 index 00000000000..7b4b87cd953 --- /dev/null +++ b/framework/test/Volo.Abp.EventBus.Tests/Volo/Abp/EventBus/Local/LocalEventBus_RegisteredHandlerType_Tests.cs @@ -0,0 +1,620 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Autofac; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EventBus.Local; + +/// +/// An IoC handler whose registration tells its type is ordered by that type, so it is created only to handle the event. +/// +public class LocalEventBus_RegisteredHandlerType_Tests : EventBusTestBase +{ + protected override void AfterAddApplication(IServiceCollection services) + { + // Registered with Autofac directly: registered through the service collection, they would also be subscribed + // to the application's bus by convention. + var builder = services.GetObject(); + builder.RegisterType().SingleInstance(); + builder.RegisterType().InstancePerLifetimeScope(); + builder.RegisterType().SingleInstance(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType().As(); + builder.Register(context => context.Resolve().CreateReplacement + ? new ReplacementOfSwitchedHandler(context.Resolve()) + : new SwitchedHandler(context.Resolve())) + .As(); + builder.RegisterType() + .OnActivating(e => e.ReplaceInstance(new ReplacementOfActivatedHandler(e.Context.Resolve()))); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + builder.RegisterType(); + + services.OnRegistered(context => + { + if (context.ImplementationType == typeof(InterceptedHandler)) + { + context.Interceptors.TryAdd(); + } + }); + } + + [Fact] + public async Task An_Ioc_Handler_Should_Be_Created_Once_Per_Publish() + { + var tracker = GetRequiredService(); + using (Subscribe()) + { + for (var i = 0; i < 3; i++) + { + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + } + } + + tracker.Log.Count.ShouldBe(3); + tracker.Created.ShouldBe(3); + tracker.Disposed.ShouldBe(3); + } + + [Fact] + public async Task A_Replaced_Ioc_Handler_Should_Be_Ordered_By_The_Replacement_And_Created_Once() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + } + + tracker.Log.ShouldBe(new[] { "replacement", "first disposed=1" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + [Fact] + public async Task A_Handler_Registered_With_A_Factory_Method_Should_Be_Ordered_By_What_It_Creates_On_Each_Publish() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + tracker.Log.ShouldBe(new[] { "first disposed=1", "switched" }); + + tracker.CreateReplacement = true; + tracker.Log.Clear(); + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + tracker.Log.ShouldBe(new[] { "switched replacement", "first disposed=5" }); + } + + // Its type can only be read from a handler, which is created to read it on each publish. + tracker.Created.ShouldBe(6); + tracker.Disposed.ShouldBe(6); + } + + [Fact] + public async Task A_Handler_Replaced_When_Activated_Should_Be_Ordered_By_What_Is_Created_From_Then_On() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + // The registration can not tell the replacement, so the first publish orders by the registered type. + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + tracker.Log.ShouldBe(new[] { "first disposed=0", "activated replacement" }); + + // Having created another type, the factory reads the type from a handler from then on. + tracker.Log.Clear(); + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + tracker.Log.ShouldBe(new[] { "activated replacement", "first disposed=4" }); + } + } + + [Fact] + public async Task A_Later_Handler_Should_Be_Created_After_An_Earlier_One_Ran() + { + var state = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + for (var i = 0; i < 2; i++) + { + state.Value = 0; + state.SeenByReader = -1; + + await LocalEventBus.PublishAsync(new StateEventData()); + + state.SeenByReader.ShouldBe(42); + } + } + } + + [Fact] + public async Task Each_Handler_Should_Release_Its_Scope_Before_The_Next_One_Runs() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + } + + tracker.Log.ShouldBe(new[] { "first disposed=0", "second disposed=1" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + [Fact] + public async Task A_Handler_That_Throws_Should_Not_Stop_The_Next_One_Or_Keep_Any_Scope() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + (await Should.ThrowAsync(() => LocalEventBus.PublishAsync(new RegisteredTypeEventData()))) + .Message.ShouldBe("Handling failed."); + } + + tracker.Log.ShouldBe(new[] { "throwing", "second disposed=1" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + [Fact] + public async Task A_Handler_That_Fails_To_Construct_Should_Stop_The_Publish_After_The_Earlier_Ones_Ran() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + using (Subscribe()) + { + (await Should.ThrowAsync(() => LocalEventBus.PublishAsync(new RegisteredTypeEventData()))) + .GetBaseException().Message.ShouldBe("Construction failed."); + } + + tracker.Log.ShouldBe(new[] { "first disposed=0" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + [Fact] + public async Task A_Dynamic_Ioc_Handler_Should_Be_Created_Once_Per_Publish() + { + var tracker = GetRequiredService(); + var eventName = "RegisteredTypeDynamic-" + Guid.NewGuid().ToString("N"); + using (LocalEventBus.Subscribe(eventName, CreateFactory())) + { + await LocalEventBus.PublishAsync(eventName, new { Value = 1 }); + } + + tracker.Log.ShouldBe(new[] { "dynamic" }); + tracker.Created.ShouldBe(1); + tracker.Disposed.ShouldBe(1); + } + + [Fact] + public async Task A_Publish_Stopped_Before_A_Handler_Should_Not_Create_It() + { + var tracker = GetRequiredService(); + using (LocalEventBus.Subscribe(typeof(NamedEventData), CreateFactory())) + using (LocalEventBus.Subscribe(NamedEventData.Name, CreateFactory())) + { + // The typed handler's data cannot be converted, which ends the publish after the dynamic handler ran. + await Should.ThrowAsync(() => ((LocalEventBus)LocalEventBus).TriggerHandlersAsync( + typeof(DynamicEventData), + new DynamicEventData(NamedEventData.Name, new { Value = "not a number" }))); + } + + tracker.Log.ShouldBe(new[] { "named dynamic" }); + tracker.Created.ShouldBe(1); + tracker.Disposed.ShouldBe(1); + } + + [Fact] + public async Task A_Publish_From_A_Handler_Should_Create_And_Release_Its_Own_Handlers() + { + var tracker = GetRequiredService(); + using (Subscribe()) + using (Subscribe()) + { + await LocalEventBus.PublishAsync(new RegisteredTypeEventData()); + } + + tracker.Log.ShouldBe(new[] { "inner disposed=0", "outer done" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + [Fact] + public async Task An_Intercepted_Handler_Should_Be_Created_Once_Per_Publish() + { + InterceptedHandler.Reset(); + CountingInterceptor.Reset(); + + for (var i = 0; i < 2; i++) + { + await LocalEventBus.PublishAsync(new InterceptedEventData()); + } + + // The registration creates a class proxy, the type every handler it creates has. + InterceptedHandler.Created.ShouldBe(2); + InterceptedHandler.Handled.ShouldBe(2); + CountingInterceptor.Intercepted.ShouldBe(2); + } + + [Fact] + public async Task A_Derived_Bus_Should_Order_By_The_Registered_Type_Too() + { + var tracker = GetRequiredService(); + var derived = ActivatorUtilities.CreateInstance(ServiceProvider); + using (derived.Subscribe(typeof(RegisteredTypeEventData), CreateFactory())) + using (derived.Subscribe(typeof(RegisteredTypeEventData), CreateFactory())) + { + await derived.PublishAsync(new RegisteredTypeEventData(), onUnitOfWorkComplete: false); + } + + tracker.Log.ShouldBe(new[] { "replacement", "first disposed=1" }); + tracker.Created.ShouldBe(2); + tracker.Disposed.ShouldBe(2); + } + + private IDisposable Subscribe() + { + return LocalEventBus.Subscribe(typeof(TEvent), CreateFactory()); + } + + private IocEventHandlerFactory CreateFactory() + { + return new IocEventHandlerFactory(GetRequiredService(), typeof(THandler)); + } + + public class RegisteredTypeEventData + { + } + + public class InnerEventData + { + } + + public class InterceptedEventData + { + } + + [EventName(Name)] + public class NamedEventData + { + public const string Name = "Volo.Abp.EventBus.Local.LocalEventBus_RegisteredHandlerType_Tests.Named"; + + public int Value { get; set; } + } + + public class Tracker + { + private int _created; + private int _disposed; + + public List Log { get; } = new(); + + public bool CreateReplacement { get; set; } + + public int Created => _created; + + public int Disposed => _disposed; + + public void OnCreated() => Interlocked.Increment(ref _created); + + public void OnDisposed() => Interlocked.Increment(ref _disposed); + } + + public class TrackedDependency : IDisposable + { + public Tracker Tracker { get; } + + public TrackedDependency(Tracker tracker) + { + Tracker = tracker; + Tracker.OnCreated(); + } + + public void Dispose() + { + Tracker.OnDisposed(); + } + } + + public abstract class TrackedHandler : ILocalEventHandler + { + protected TrackedDependency Dependency { get; } + + protected TrackedHandler(TrackedDependency dependency) + { + Dependency = dependency; + } + + public virtual Task HandleEventAsync(RegisteredTypeEventData eventData) + { + Dependency.Tracker.Log.Add(GetLogEntry()); + return Task.CompletedTask; + } + + protected abstract string GetLogEntry(); + } + + [LocalEventHandlerOrder(-1)] + public class FirstHandler : TrackedHandler + { + public FirstHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => $"first disposed={Dependency.Tracker.Disposed}"; + } + + [LocalEventHandlerOrder(1)] + public class SecondHandler : TrackedHandler + { + public SecondHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => $"second disposed={Dependency.Tracker.Disposed}"; + } + + [LocalEventHandlerOrder(1)] + public class ReplacedHandler : TrackedHandler + { + public ReplacedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "replaced"; + } + + [LocalEventHandlerOrder(-10)] + public class ReplacementOfReplacedHandler : ReplacedHandler + { + public ReplacementOfReplacedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "replacement"; + } + + [LocalEventHandlerOrder(1)] + public class SwitchedHandler : TrackedHandler + { + public SwitchedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "switched"; + } + + [LocalEventHandlerOrder(-10)] + public class ReplacementOfSwitchedHandler : SwitchedHandler + { + public ReplacementOfSwitchedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "switched replacement"; + } + + [LocalEventHandlerOrder(1)] + public class ActivatedHandler : TrackedHandler + { + public ActivatedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "activated"; + } + + [LocalEventHandlerOrder(-10)] + public class ReplacementOfActivatedHandler : ActivatedHandler + { + public ReplacementOfActivatedHandler(TrackedDependency dependency) + : base(dependency) + { + } + + protected override string GetLogEntry() => "activated replacement"; + } + + [LocalEventHandlerOrder(-1)] + public class ThrowingWhileHandlingHandler : TrackedHandler + { + public ThrowingWhileHandlingHandler(TrackedDependency dependency) + : base(dependency) + { + } + + public override Task HandleEventAsync(RegisteredTypeEventData eventData) + { + base.HandleEventAsync(eventData); + throw new InvalidOperationException("Handling failed."); + } + + protected override string GetLogEntry() => "throwing"; + } + + public class ConstructionFailingHandler : ILocalEventHandler + { + public ConstructionFailingHandler(TrackedDependency dependency) + { + throw new InvalidOperationException("Construction failed."); + } + + public Task HandleEventAsync(RegisteredTypeEventData eventData) + { + return Task.CompletedTask; + } + } + + public class DynamicHandler : ILocalEventHandler + { + private readonly TrackedDependency _dependency; + + public DynamicHandler(TrackedDependency dependency) + { + _dependency = dependency; + } + + public Task HandleEventAsync(DynamicEventData eventData) + { + _dependency.Tracker.Log.Add("dynamic"); + return Task.CompletedTask; + } + } + + public class OuterHandler : ILocalEventHandler + { + private readonly TrackedDependency _dependency; + private readonly ILocalEventBus _localEventBus; + + public OuterHandler(TrackedDependency dependency, ILocalEventBus localEventBus) + { + _dependency = dependency; + _localEventBus = localEventBus; + } + + public async Task HandleEventAsync(RegisteredTypeEventData eventData) + { + await _localEventBus.PublishAsync(new InnerEventData(), onUnitOfWorkComplete: false); + _dependency.Tracker.Log.Add("outer done"); + } + } + + public class InnerHandler : ILocalEventHandler + { + private readonly TrackedDependency _dependency; + + public InnerHandler(TrackedDependency dependency) + { + _dependency = dependency; + } + + public Task HandleEventAsync(InnerEventData eventData) + { + _dependency.Tracker.Log.Add($"inner disposed={_dependency.Tracker.Disposed}"); + return Task.CompletedTask; + } + } + + [LocalEventHandlerOrder(-1)] + public class NamedDynamicHandler : ILocalEventHandler + { + private readonly TrackedDependency _dependency; + + public NamedDynamicHandler(TrackedDependency dependency) + { + _dependency = dependency; + } + + public Task HandleEventAsync(DynamicEventData eventData) + { + _dependency.Tracker.Log.Add("named dynamic"); + return Task.CompletedTask; + } + } + + [LocalEventHandlerOrder(1)] + public class NamedTypedHandler : ILocalEventHandler + { + private readonly TrackedDependency _dependency; + + public NamedTypedHandler(TrackedDependency dependency) + { + _dependency = dependency; + } + + public Task HandleEventAsync(NamedEventData eventData) + { + _dependency.Tracker.Log.Add("named typed"); + return Task.CompletedTask; + } + } + + public class InterceptedHandler : ILocalEventHandler, ITransientDependency + { + private static int _created; + private static int _handled; + + public static int Created => _created; + + public static int Handled => _handled; + + public InterceptedHandler() + { + Interlocked.Increment(ref _created); + } + + public static void Reset() + { + _created = 0; + _handled = 0; + } + + public virtual Task HandleEventAsync(InterceptedEventData eventData) + { + Interlocked.Increment(ref _handled); + return Task.CompletedTask; + } + } + + public class CountingInterceptor : AbpInterceptor, ITransientDependency + { + private static int _intercepted; + + public static int Intercepted => _intercepted; + + public static void Reset() + { + _intercepted = 0; + } + + public override async Task InterceptAsync(IAbpMethodInvocation invocation) + { + Interlocked.Increment(ref _intercepted); + await invocation.ProceedAsync(); + } + } + + // Kept out of conventional registration, which would make it the application's bus. + [DisableConventionalRegistration] + public class DerivedLocalEventBus : LocalEventBus + { + public DerivedLocalEventBus( + IOptions options, + IServiceScopeFactory serviceScopeFactory, + ICurrentTenant currentTenant, + IUnitOfWorkManager unitOfWorkManager, + IEventHandlerInvoker eventHandlerInvoker) + : base(options, serviceScopeFactory, currentTenant, unitOfWorkManager, eventHandlerInvoker) + { + } + } +}