diff --git a/src/Lamar/IoC/LamarInstanceHashCollisionException.cs b/src/Lamar/IoC/LamarInstanceHashCollisionException.cs new file mode 100644 index 00000000..1b201030 --- /dev/null +++ b/src/Lamar/IoC/LamarInstanceHashCollisionException.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System; +using System.Linq; +using JasperFx.Core.Reflection; + +namespace Lamar.IoC; + +public class LamarInstanceHashCollisionException : LamarException +{ + public LamarInstanceHashCollisionException(int instanceHash, IEnumerable serviceTypes) : base( + $"Duplicate hash '{instanceHash}' generated for services: {string.Join(", ", serviceTypes.Select(x => x.FullNameInCode()))}") + { + } +} diff --git a/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs b/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs new file mode 100644 index 00000000..e0e9df1e --- /dev/null +++ b/src/Lamar/ServiceRegistry.HashCollisionMitigation.cs @@ -0,0 +1,75 @@ +using Lamar.IoC; +using Lamar.IoC.Instances; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lamar; + +public partial class ServiceRegistry +{ + /// + /// 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. + ///
+ /// + /// + /// + public void MitigateInstanceHashCollisions(int retryLimit = 3, Func instanceRenamePolicy = null) + { + bool shouldMitigateCollisions = true; + int remaininingRetries = retryLimit; + + Func renameInstance = instanceRenamePolicy ?? (name => $"{name}_x"); + + while (shouldMitigateCollisions && remaininingRetries > 0) + { + shouldMitigateCollisions = TryRemoveInstanceHashCollisions(renameInstance); + remaininingRetries--; + } + + var collision = GetInstanceHashCollisions().FirstOrDefault(); + + if (collision != null) + { + throw new LamarInstanceHashCollisionException(collision.Key, collision.Select(x => x.ServiceType)); + } + } + + internal bool TryRemoveInstanceHashCollisions(Func instanceRenamePolicy) + { + var hasCollision = false; + + foreach (var collision in GetInstanceHashCollisions()) + { + hasCollision = true; + HandleInstanceHashCollisions(collision, instanceRenamePolicy); + } + + return hasCollision; + } + + internal void HandleInstanceHashCollisions(IGrouping collision, Func instanceRenamePolicy) + { + var namedInstances = collision.Where(x => x.IsExplicitlyNamed).ToList(); + + if (namedInstances.Count > 1) + { + throw new LamarInstanceHashCollisionException(collision.Key, namedInstances.Select(x => x.ServiceType)); + } + + foreach (var instance in collision.Except(namedInstances)) + { + instance.Name = instanceRenamePolicy(instance.Name); + } + } + + internal IEnumerable> GetInstanceHashCollisions() + { + return this.Select(x => x.ImplementationInstance) + .OfType() + .GroupBy(x => x.Hash) + .Where(x => x.Select(y => y.ServiceType).Distinct().Count() > 1); + } +} \ No newline at end of file