diff --git a/src/Lamar.Testing/Bugs/Bug_398_service_registry_hash_collisions.cs b/src/Lamar.Testing/Bugs/Bug_398_service_registry_hash_collisions.cs new file mode 100644 index 00000000..c1868447 --- /dev/null +++ b/src/Lamar.Testing/Bugs/Bug_398_service_registry_hash_collisions.cs @@ -0,0 +1,142 @@ +using Lamar.IoC; +using Shouldly; +using System; +using Xunit; + +namespace Lamar.Testing.Bugs; + +public class Bug_398_service_registry_hash_collisions +{ + [Fact] + public void GetHashCollisions_RegistryWithCollisions() + { + var registry = GetRegistryWithCollisions(); + + var collisions = registry.GetInstanceHashCollisions(); + + collisions.ShouldNotBeEmpty(); + } + + [Fact] + public void GetHashCollisions_RegistryWithoutCollisions() + { + var registry = GetRegistryWithoutCollisions(); + + var collisions = registry.GetInstanceHashCollisions(); + + collisions.ShouldBeEmpty(); + } + + [Fact] + public void TryRemoveHashCollisions_RegistryWithCollisions() + { + var registry = GetRegistryWithCollisions(); + + var removed = registry.TryRemoveInstanceHashCollisions(); + + removed.ShouldBeTrue(); + } + + [Fact] + public void TryRemoveHashCollisions_RegistryWithoutCollisions() + { + var registry = GetRegistryWithoutCollisions(); + + var removed = registry.TryRemoveInstanceHashCollisions(); + + removed.ShouldBeFalse(); + } + + [Fact] + public void HandleInstanceHashCollisions() + { + var registry = GetRegistryWithCollisions(); + + registry.TryRemoveInstanceHashCollisions(); + + registry.GetInstanceHashCollisions().ShouldBeEmpty(); + + var container = new Container(registry); + container.GetInstance().ShouldBeOfType(); + container.GetInstance().ShouldBeOfType(); + } + + [Fact] + public void MitigateInstanceHashCollisions() + { + var registry = GetRegistryWithCollisions(); + + registry.MitigateInstanceHashCollisions(); + + registry.GetInstanceHashCollisions().ShouldBeEmpty(); + + var container = new Container(registry); + container.GetInstance().ShouldBeOfType(); + container.GetInstance().ShouldBeOfType(); + } + + [Fact] + public void MitigateInstanceHashCollisionsThrowsAfterLimitReached() + { + var registry = GetRegistryWithCollisions(); + + var mitigateCollisions = () => registry.MitigateInstanceHashCollisions(0); + + mitigateCollisions.ShouldThrow(); + } + + [Fact] + public void MitigateInstanceHashCollisionsUsesCustomRenamePolicy() + { + var registry = GetRegistryWithCollisions(); + + var instanceFoo = registry.For().Use(); + var instanceBar = registry.For().Use(); + + instanceFoo.Hash = instanceBar.Hash = 1; + + registry.MitigateInstanceHashCollisions(instanceRenamePolicy: x => $"{x}.updated"); + + instanceFoo.Name.ShouldEndWith(".updated"); + instanceBar.Name.ShouldEndWith(".updated"); + } + + [Fact] + public void NamedCollisionThrows() + { + var registry = new ServiceRegistry(); + + registry.For().Use().Named("foo").Hash = 1; + registry.For().Use().Named("bar").Hash = 1; + + Action handleCollisions = () => registry.TryRemoveInstanceHashCollisions(); + + handleCollisions.ShouldThrow(); + } + + + private ServiceRegistry GetRegistryWithCollisions() + { + var registry = new ServiceRegistry(); + + registry.For().Use().Hash = 1; + registry.For().Use().Hash = 1; + + return registry; + } + + private ServiceRegistry GetRegistryWithoutCollisions() + { + var registry = new ServiceRegistry(); + + registry.For().Use().Hash = 1; + registry.For().Use().Hash = 2; + + return registry; + } + + public interface IFoo { } + public interface IBar { } + public class Foo : IFoo { } + public class Bar : IBar { } +} diff --git a/src/Lamar/IoC/LamarInstanceHashCollisionException.cs b/src/Lamar/IoC/LamarInstanceHashCollisionException.cs index 1b201030..164b1bf9 100644 --- a/src/Lamar/IoC/LamarInstanceHashCollisionException.cs +++ b/src/Lamar/IoC/LamarInstanceHashCollisionException.cs @@ -7,8 +7,13 @@ namespace Lamar.IoC; public class LamarInstanceHashCollisionException : LamarException { + public int InstanceHash { get; } + public IEnumerable ServiceTypes { get; } + public LamarInstanceHashCollisionException(int instanceHash, IEnumerable serviceTypes) : base( $"Duplicate hash '{instanceHash}' generated for services: {string.Join(", ", serviceTypes.Select(x => x.FullNameInCode()))}") { + InstanceHash = instanceHash; + ServiceTypes = serviceTypes; } } diff --git a/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs b/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs index e0e9df1e..6d5f06c0 100644 --- a/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs +++ b/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs @@ -8,10 +8,13 @@ namespace Lamar; public partial class ServiceRegistry { + internal static readonly Func DefaultInstanceRenamePolicy = name => $"{name}_x"; + /// /// Finds and mitigates hash code collisions in the current instance.
/// Hash codes are regenerated by renaming the instances where collisions exist with the provided .
- /// When the is met, and a hash collision is detected, a is thrown. + /// When the is met, and a hash collision is detected, a is thrown.
+ /// If no is specified, the will be used. ///
/// /// @@ -21,7 +24,7 @@ public void MitigateInstanceHashCollisions(int retryLimit = 3, Func renameInstance = instanceRenamePolicy ?? (name => $"{name}_x"); + Func renameInstance = instanceRenamePolicy ?? DefaultInstanceRenamePolicy; while (shouldMitigateCollisions && remaininingRetries > 0) { @@ -37,6 +40,8 @@ public void MitigateInstanceHashCollisions(int retryLimit = 3, Func TryRemoveInstanceHashCollisions(DefaultInstanceRenamePolicy); + internal bool TryRemoveInstanceHashCollisions(Func instanceRenamePolicy) { var hasCollision = false; @@ -50,6 +55,8 @@ internal bool TryRemoveInstanceHashCollisions(Func instanceRenam return hasCollision; } + internal void HandleInstanceHashCollisions(IGrouping collision) => HandleInstanceHashCollisions(collision, DefaultInstanceRenamePolicy); + internal void HandleInstanceHashCollisions(IGrouping collision, Func instanceRenamePolicy) { var namedInstances = collision.Where(x => x.IsExplicitlyNamed).ToList();