diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH1439/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH1439/Fixture.cs index 0962d06c353..cf61e951e2d 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/GH1439/Fixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH1439/Fixture.cs @@ -91,7 +91,7 @@ public async Task LazyPropertyShouldBeUninitializedAndLoadableAsync() Is.False, "Lazy property initialization status"); Assert.That( - FieldInterceptionHelper.IsInstrumented(e1), + e1 is IFieldInterceptorAccessor, Is.True, "Entity IsInstrumented"); Assert.That( @@ -117,7 +117,7 @@ public async Task LazyPropertyShouldBeUninitializedAndLoadableWithComponentIdAsy Is.False, "Lazy property initialization status"); Assert.That( - FieldInterceptionHelper.IsInstrumented(e2), + e2 is IFieldInterceptorAccessor, Is.True, "Entity IsInstrumented"); Assert.That( diff --git a/src/NHibernate.Test/NHSpecificTest/GH1439/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH1439/Fixture.cs index 5a12bfe77fe..d77e02a0fae 100644 --- a/src/NHibernate.Test/NHSpecificTest/GH1439/Fixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/GH1439/Fixture.cs @@ -79,7 +79,7 @@ public void LazyPropertyShouldBeUninitializedAndLoadable() Is.False, "Lazy property initialization status"); Assert.That( - FieldInterceptionHelper.IsInstrumented(e1), + e1 is IFieldInterceptorAccessor, Is.True, "Entity IsInstrumented"); Assert.That( @@ -105,7 +105,7 @@ public void LazyPropertyShouldBeUninitializedAndLoadableWithComponentId() Is.False, "Lazy property initialization status"); Assert.That( - FieldInterceptionHelper.IsInstrumented(e2), + e2 is IFieldInterceptorAccessor, Is.True, "Entity IsInstrumented"); Assert.That( diff --git a/src/NHibernate/Async/Engine/Cascade.cs b/src/NHibernate/Async/Engine/Cascade.cs index e33a941ba1e..77e8b93bfb1 100644 --- a/src/NHibernate/Async/Engine/Cascade.cs +++ b/src/NHibernate/Async/Engine/Cascade.cs @@ -60,7 +60,7 @@ public async Task CascadeOnAsync(IEntityPersister persister, object parent, obje IType[] types = persister.PropertyTypes; CascadeStyle[] cascadeStyles = persister.PropertyCascadeStyles; - var uninitializedLazyProperties = persister.GetUninitializedLazyProperties(parent); + var uninitializedLazyProperties = persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(parent); for (int i = 0; i < types.Length; i++) { CascadeStyle style = cascadeStyles[i]; diff --git a/src/NHibernate/Async/Event/Default/DefaultMergeEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultMergeEventListener.cs index 40848b4f356..9384b786b79 100644 --- a/src/NHibernate/Async/Event/Default/DefaultMergeEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultMergeEventListener.cs @@ -389,7 +389,7 @@ protected virtual async Task EntityIsDetachedAsync(MergeEvent @event, IDictionar await (CopyValuesAsync(persister, entity, target, source, copyCache, cancellationToken)).ConfigureAwait(false); //copyValues works by reflection, so explicitly mark the entity instance dirty - MarkInterceptorDirty(entity, target); + MarkInterceptorDirty(entity, persister, target); @event.Result = result; } diff --git a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs index d2d0a5e3361..ae54caf3fcb 100644 --- a/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Async/Persister/Entity/AbstractEntityPersister.cs @@ -36,6 +36,7 @@ using Property=NHibernate.Mapping.Property; using NHibernate.SqlTypes; using System.Linq; +using NHibernate.Bytecode; namespace NHibernate.Persister.Entity { diff --git a/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs b/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs new file mode 100644 index 00000000000..df7f58e4fd5 --- /dev/null +++ b/src/NHibernate/Bytecode/IBytecodeEnhancementMetadata.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Engine; +using NHibernate.Intercept; +using NHibernate.Tuple.Entity; + +namespace NHibernate.Bytecode +{ + /// + /// Encapsulates bytecode enhancement information about a particular entity. + /// + /// Author: Steve Ebersole + /// + public interface IBytecodeEnhancementMetadata + { + /// + /// The name of the entity to which this metadata applies. + /// + string EntityName { get; } + + /// + /// Has the entity class been bytecode enhanced for lazy loading? + /// + bool EnhancedForLazyLoading { get; } + + /// + /// Has the information about all lazy properties + /// + LazyPropertiesMetadata LazyPropertiesMetadata { get; } + + /// + /// Has the information about all properties mapped as lazy="no-proxy" + /// + UnwrapProxyPropertiesMetadata UnwrapProxyPropertiesMetadata { get; } + + /// + /// Build and inject an interceptor instance into the enhanced entity. + /// + /// The entity into which built interceptor should be injected. + /// Whether all lazy properties were unfetched or not. + /// The session to which the entity instance belongs. + /// The built and injected interceptor. + // TODO: Remove lazyPropertiesAreUnfetched when interceptor will be injected on entity instantiation + IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session); + + /// + /// Extract the field interceptor instance from the enhanced entity. + /// + /// The entity from which to extract the interceptor. + /// The extracted interceptor. + IFieldInterceptor ExtractInterceptor(object entity); + + /// + /// Retrieve the uninitialized lazy properties from the enhanced entity. + /// + /// The entity from which to retrieve the uninitialized lazy properties. + /// The uninitialized property names. + ISet GetUninitializedLazyProperties(object entity); + + /// + /// Retrieve the uninitialized lazy properties from the entity state. + /// + /// The entity state from which to retrieve the uninitialized lazy properties. + /// The uninitialized property names. + ISet GetUninitializedLazyProperties(object[] entityState); + } +} diff --git a/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs new file mode 100644 index 00000000000..ee70714e9b1 --- /dev/null +++ b/src/NHibernate/Bytecode/LazyPropertiesMetadata.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Iesi.Collections.Generic; +using NHibernate.Util; + +namespace NHibernate.Bytecode +{ + /// + /// Information about all of the bytecode lazy properties for an entity + /// + /// Author: Steve Ebersole + /// + [Serializable] + public class LazyPropertiesMetadata + { + public static LazyPropertiesMetadata NonEnhanced(string entityName) + { + return new LazyPropertiesMetadata(entityName, null); + } + + public static LazyPropertiesMetadata From( + string entityName, + IEnumerable lazyPropertyDescriptors) + { + // TODO: port lazy fetch groups + return new LazyPropertiesMetadata( + entityName, + lazyPropertyDescriptors.ToDictionary(o => o.Name)); + } + + private readonly IDictionary _lazyPropertyDescriptors; + + public LazyPropertiesMetadata( + string entityName, + IDictionary lazyPropertyDescriptors) + { + EntityName = entityName; + _lazyPropertyDescriptors = lazyPropertyDescriptors; + HasLazyProperties = _lazyPropertyDescriptors?.Count > 0; + LazyPropertyNames = HasLazyProperties + ? new ReadOnlySet(new HashSet(_lazyPropertyDescriptors.Keys)) + : CollectionHelper.EmptySet(); + // TODO: port lazy fetch groups + } + + public string EntityName { get; } + + public bool HasLazyProperties { get; } + + public ISet LazyPropertyNames { get; } + + public IEnumerable LazyPropertyDescriptors => + _lazyPropertyDescriptors?.Values ?? Enumerable.Empty(); + } +} diff --git a/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs b/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs new file mode 100644 index 00000000000..a0640ef9470 --- /dev/null +++ b/src/NHibernate/Bytecode/LazyPropertyDescriptor.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Type; + +namespace NHibernate.Bytecode +{ + /// + /// Descriptor for a property which is enabled for bytecode lazy fetching + /// + /// Author: Steve Ebersole + /// + [Serializable] + public class LazyPropertyDescriptor + { + public static LazyPropertyDescriptor From( + Mapping.Property property, + int propertyIndex, + int lazyIndex) + { + // TODO: port lazy fetch groups + + return new LazyPropertyDescriptor( + propertyIndex, + lazyIndex, + property.Name, + property.Type + ); + } + + private LazyPropertyDescriptor( + int propertyIndex, + int lazyIndex, + string name, + IType type) + { + if (propertyIndex < lazyIndex) + { + throw new InvalidOperationException("Property index is lower than the lazy index."); + } + + PropertyIndex = propertyIndex; + LazyIndex = lazyIndex; + Name = name; + Type = type; + } + + /// + /// Access to the index of the property in terms of its position in the entity persister + /// + public int PropertyIndex { get; } + + /// + /// Access to the index of the property in terms of its position within the lazy properties of the persister + /// + public int LazyIndex { get; } + + /// + /// Access to the name of the property + /// + public string Name { get; } + + /// + /// Access to the property's type + /// + public IType Type { get; } + } +} diff --git a/src/NHibernate/Bytecode/NotInstrumentedException.cs b/src/NHibernate/Bytecode/NotInstrumentedException.cs new file mode 100644 index 00000000000..71a392b9b0a --- /dev/null +++ b/src/NHibernate/Bytecode/NotInstrumentedException.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; + +namespace NHibernate.Bytecode +{ + /// + /// Indicates a condition where an instrumented/enhanced class was expected, but the class was not + /// instrumented/enhanced. + /// + /// Author: Steve Ebersole + /// + [Serializable] + public class NotInstrumentedException : HibernateException + { + /// + /// Constructs a NotInstrumentedException. + /// + /// Message explaining the exception condition. + public NotInstrumentedException(string message) : base(message) + { + } + + /// + protected NotInstrumentedException(SerializationInfo info, StreamingContext context) + : base(info, context) {} + } +} diff --git a/src/NHibernate/Bytecode/UnwrapProxyPropertiesMetadata.cs b/src/NHibernate/Bytecode/UnwrapProxyPropertiesMetadata.cs new file mode 100644 index 00000000000..9c2b6f6111e --- /dev/null +++ b/src/NHibernate/Bytecode/UnwrapProxyPropertiesMetadata.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Iesi.Collections.Generic; +using NHibernate.Util; + +namespace NHibernate.Bytecode +{ + /// + /// Information about all properties mapped as lazy="no-proxy" for an entity + /// + [Serializable] + public class UnwrapProxyPropertiesMetadata + { + public static UnwrapProxyPropertiesMetadata NonEnhanced(string entityName) + { + return new UnwrapProxyPropertiesMetadata(entityName, null); + } + + public static UnwrapProxyPropertiesMetadata From( + string entityName, + IEnumerable unwrapProxyPropertyDescriptors) + { + return new UnwrapProxyPropertiesMetadata( + entityName, + unwrapProxyPropertyDescriptors.ToDictionary(o => o.Name)); + } + + private readonly IDictionary _unwrapProxyPropertyDescriptors; + + public UnwrapProxyPropertiesMetadata( + string entityName, + IDictionary unwrapProxyPropertyDescriptors) + { + EntityName = entityName; + _unwrapProxyPropertyDescriptors = unwrapProxyPropertyDescriptors; + HasUnwrapProxyProperties = unwrapProxyPropertyDescriptors?.Count > 0; + UnwrapProxyPropertyNames = HasUnwrapProxyProperties + ? new ReadOnlySet(new HashSet(unwrapProxyPropertyDescriptors.Keys)) + : CollectionHelper.EmptySet(); + } + + public string EntityName { get; } + + public bool HasUnwrapProxyProperties { get; } + + public ISet UnwrapProxyPropertyNames { get; } + + public IEnumerable UnwrapProxyPropertyDescriptors => + _unwrapProxyPropertyDescriptors?.Values ?? Enumerable.Empty(); + + public int GetUnwrapProxyPropertyIndex(string propertyName) + { + if (!_unwrapProxyPropertyDescriptors.TryGetValue(propertyName, out var descriptor)) + { + throw new InvalidOperationException( + $"Property {propertyName} is not mapped as lazy=\"no-proxy\" on entity {EntityName}"); + } + + return descriptor.PropertyIndex; + } + } +} diff --git a/src/NHibernate/Bytecode/UnwrapProxyPropertyDescriptor.cs b/src/NHibernate/Bytecode/UnwrapProxyPropertyDescriptor.cs new file mode 100644 index 00000000000..b4012e1778f --- /dev/null +++ b/src/NHibernate/Bytecode/UnwrapProxyPropertyDescriptor.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Type; + +namespace NHibernate.Bytecode +{ + /// + /// Descriptor for a property which is mapped as lazy="no-proxy" + /// + [Serializable] + public class UnwrapProxyPropertyDescriptor + { + public static UnwrapProxyPropertyDescriptor From( + Mapping.Property property, + int propertyIndex) + { + return new UnwrapProxyPropertyDescriptor( + propertyIndex, + property.Name, + property.Type + ); + } + + private UnwrapProxyPropertyDescriptor( + int propertyIndex, + string name, + IType type) + { + PropertyIndex = propertyIndex; + Name = name; + Type = type; + } + + /// + /// Access to the index of the property in terms of its position in the entity persister + /// + public int PropertyIndex { get; } + + /// + /// Access to the name of the property + /// + public string Name { get; } + + /// + /// Access to the property's type + /// + public IType Type { get; } + } +} diff --git a/src/NHibernate/Engine/Cascade.cs b/src/NHibernate/Engine/Cascade.cs index 85500f7c9f2..a9ba97a214d 100644 --- a/src/NHibernate/Engine/Cascade.cs +++ b/src/NHibernate/Engine/Cascade.cs @@ -113,7 +113,7 @@ public void CascadeOn(IEntityPersister persister, object parent, object anything IType[] types = persister.PropertyTypes; CascadeStyle[] cascadeStyles = persister.PropertyCascadeStyles; - var uninitializedLazyProperties = persister.GetUninitializedLazyProperties(parent); + var uninitializedLazyProperties = persister.EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(parent); for (int i = 0; i < types.Length; i++) { CascadeStyle style = cascadeStyles[i]; diff --git a/src/NHibernate/Engine/EntityEntry.cs b/src/NHibernate/Engine/EntityEntry.cs index aceb9894457..1ae1daaa16d 100644 --- a/src/NHibernate/Engine/EntityEntry.cs +++ b/src/NHibernate/Engine/EntityEntry.cs @@ -233,7 +233,11 @@ public void PostUpdate(object entity, object[] updatedState, object nextVersion) version = nextVersion; Persister.SetPropertyValue(entity, Persister.VersionProperty, nextVersion); } - FieldInterceptionHelper.ClearDirty(entity); + + if (Persister.IsInstrumented) + { + persister.EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(entity)?.ClearDirty(); + } } /// @@ -264,8 +268,8 @@ public bool RequiresDirtyCheck(object entity) { return IsModifiableEntity() - && (Persister.HasMutableProperties || !FieldInterceptionHelper.IsInstrumented(entity) - || FieldInterceptionHelper.ExtractFieldInterceptor(entity).IsDirty); + && (Persister.HasMutableProperties || !Persister.IsInstrumented + || (Persister.EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(entity)?.IsDirty ?? true)); } /// diff --git a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs index 63e94980222..530aed15d26 100644 --- a/src/NHibernate/Event/Default/AbstractSaveEventListener.cs +++ b/src/NHibernate/Event/Default/AbstractSaveEventListener.cs @@ -299,10 +299,11 @@ protected virtual object PerformSaveOrReplicate(object entity, EntityKey key, IE private void MarkInterceptorDirty(object entity, IEntityPersister persister, IEventSource source) { - if (FieldInterceptionHelper.IsInstrumented(entity)) + if (persister.IsInstrumented) { - IFieldInterceptor interceptor = FieldInterceptionHelper.InjectFieldInterceptor(entity, persister.EntityName, persister.MappedClass, null, null, source); - interceptor.MarkDirty(); + var interceptor = persister.EntityMetamodel.BytecodeEnhancementMetadata + .InjectInterceptor(entity, false, source); + interceptor?.MarkDirty(); } } diff --git a/src/NHibernate/Event/Default/DefaultMergeEventListener.cs b/src/NHibernate/Event/Default/DefaultMergeEventListener.cs index b304bd16512..31781db0df4 100644 --- a/src/NHibernate/Event/Default/DefaultMergeEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultMergeEventListener.cs @@ -380,7 +380,7 @@ protected virtual void EntityIsDetached(MergeEvent @event, IDictionary copyCache CopyValues(persister, entity, target, source, copyCache); //copyValues works by reflection, so explicitly mark the entity instance dirty - MarkInterceptorDirty(entity, target); + MarkInterceptorDirty(entity, persister, target); @event.Result = result; } @@ -400,15 +400,12 @@ protected virtual bool InvokeUpdateLifecycle(object entity, IEntityPersister per return false; } - private void MarkInterceptorDirty(object entity, object target) + private void MarkInterceptorDirty(object entity, IEntityPersister persister, object target) { - if (FieldInterceptionHelper.IsInstrumented(entity)) + if (persister.IsInstrumented) { - IFieldInterceptor interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(target); - if (interceptor != null) - { - interceptor.MarkDirty(); - } + var interceptor = persister.EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(target); + interceptor?.MarkDirty(); } } diff --git a/src/NHibernate/Impl/SessionImpl.cs b/src/NHibernate/Impl/SessionImpl.cs index 3e999ddd9df..fa980fa038a 100644 --- a/src/NHibernate/Impl/SessionImpl.cs +++ b/src/NHibernate/Impl/SessionImpl.cs @@ -958,11 +958,11 @@ public override string BestGuessEntityName(object entity) } entity = initializer.GetImplementation(); } - if (FieldInterceptionHelper.IsInstrumented(entity)) + + if (entity is IFieldInterceptorAccessor interceptorAccessor && interceptorAccessor.FieldInterceptor != null) { // NH: support of field-interceptor-proxy - IFieldInterceptor interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(entity); - return interceptor.EntityName; + return interceptorAccessor.FieldInterceptor.EntityName; } EntityEntry entry = persistenceContext.GetEntry(entity); if (entry == null) diff --git a/src/NHibernate/Intercept/FieldInterceptionHelper.cs b/src/NHibernate/Intercept/FieldInterceptionHelper.cs index a1ad727b9d9..84c07b21077 100644 --- a/src/NHibernate/Intercept/FieldInterceptionHelper.cs +++ b/src/NHibernate/Intercept/FieldInterceptionHelper.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NHibernate.Engine; @@ -21,17 +22,20 @@ public static bool IsInstrumented(System.Type entityClass) return Cfg.Environment.BytecodeProvider.ProxyFactoryFactory.IsInstrumented(entityClass); } + [Obsolete("This method has no more usages and will be removed in a future version")] public static bool IsInstrumented(object entity) { return entity is IFieldInterceptorAccessor; } + [Obsolete("This method has no more usages and will be removed in a future version")] public static IFieldInterceptor ExtractFieldInterceptor(object entity) { var fieldInterceptorAccessor = entity as IFieldInterceptorAccessor; return fieldInterceptorAccessor == null ? null : fieldInterceptorAccessor.FieldInterceptor; } + [Obsolete("Use IBytecodeEnhancementMetadata.InjectInterceptor method instead")] public static IFieldInterceptor InjectFieldInterceptor(object entity, string entityName, System.Type mappedClass, ISet uninitializedFieldNames, @@ -48,6 +52,7 @@ public static IFieldInterceptor InjectFieldInterceptor(object entity, string ent return null; } + [Obsolete("This method has no more usages and will be removed in a future version")] public static void ClearDirty(object entity) { IFieldInterceptor interceptor = ExtractFieldInterceptor(entity); @@ -57,6 +62,7 @@ public static void ClearDirty(object entity) } } + [Obsolete("This method has no more usages and will be removed in a future version")] public static void MarkDirty(object entity) { IFieldInterceptor interceptor = ExtractFieldInterceptor(entity); @@ -66,4 +72,4 @@ public static void MarkDirty(object entity) } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/NHibernateUtil.cs b/src/NHibernate/NHibernateUtil.cs index 2831f3d6298..fcc7a894e94 100644 --- a/src/NHibernate/NHibernateUtil.cs +++ b/src/NHibernate/NHibernateUtil.cs @@ -588,9 +588,9 @@ public static bool IsPropertyInitialized(object proxy, string propertyName) entity = proxy; } - if (FieldInterceptionHelper.IsInstrumented(entity)) + if (entity is IFieldInterceptorAccessor interceptorAccessor) { - IFieldInterceptor interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(entity); + var interceptor = interceptorAccessor.FieldInterceptor; return interceptor == null || interceptor.IsInitializedField(propertyName); } else diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index bb5d959f453..85d98154acd 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -26,6 +26,7 @@ using Property=NHibernate.Mapping.Property; using NHibernate.SqlTypes; using System.Linq; +using NHibernate.Bytecode; namespace NHibernate.Persister.Entity { @@ -683,6 +684,9 @@ public virtual string VersionColumnName get { return versionColumnName; } } + // 6.0 TODO: Add into IEntityPersister and simplify .EntityMetamodel.BytecodeEnhancementMetadata external calls + public IBytecodeEnhancementMetadata InstrumentationMetadata => EntityMetamodel.BytecodeEnhancementMetadata; + [Obsolete("Please use RootTableName instead.")] protected internal string VersionedTableName { @@ -1281,7 +1285,7 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS fieldName); } - var uninitializedLazyProperties = GetUninitializedLazyProperties(entity); + var uninitializedLazyProperties = InstrumentationMetadata.GetUninitializedLazyProperties(entity); if (HasCache && session.CacheMode.HasFlag(CacheMode.Get)) { CacheKey cacheKey = session.GenerateCacheKey(id, IdentifierType, EntityName); @@ -3220,7 +3224,7 @@ private bool HasDirtyLazyProperties(int[] dirtyFields, object obj) { // When having a dirty lazy property and the entity is not yet initialized we have to use a dynamic update for // it even if it is disabled in order to have it updated. - return GetUninitializedLazyProperties(obj).Count > 0 && dirtyFields.Any(i => PropertyLaziness[i]); + return InstrumentationMetadata.GetUninitializedLazyProperties(obj).Count > 0 && dirtyFields.Any(i => PropertyLaziness[i]); } public object Insert(object[] fields, object obj, ISessionImplementor session) @@ -3928,17 +3932,17 @@ public virtual bool HasLazyProperties public virtual void AfterReassociate(object entity, ISessionImplementor session) { - if (FieldInterceptionHelper.IsInstrumented(entity)) + if (IsInstrumented) { - IFieldInterceptor interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(entity); + var interceptor = InstrumentationMetadata.ExtractInterceptor(entity); if (interceptor != null) { interceptor.Session = session; } else { - IFieldInterceptor fieldInterceptor = FieldInterceptionHelper.InjectFieldInterceptor(entity, EntityName, MappedClass, null, null, session); - fieldInterceptor.MarkDirty(); + var fieldInterceptor = InstrumentationMetadata.InjectInterceptor(entity, false, session); + fieldInterceptor?.MarkDirty(); } } } @@ -4089,7 +4093,7 @@ public string SelectFragment( public bool IsInstrumented { - get { return EntityTuplizer.IsInstrumented; } + get { return InstrumentationMetadata.EnhancedForLazyLoading; } } public bool HasInsertGeneratedProperties @@ -4107,30 +4111,6 @@ public void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISes EntityTuplizer.AfterInitialize(entity, lazyPropertiesAreUnfetched, session); } - internal ISet GetUninitializedLazyProperties(object entity) - { - return EntityTuplizer.GetUninitializedLazyProperties(entity) ?? new HashSet(lazyPropertyNames); - } - - internal ISet GetUninitializedLazyProperties(object[] state) - { - if (!HasLazyProperties) - { - return CollectionHelper.EmptySet(); - } - - var uninitializedProperties = new HashSet(); - for (var j = 0; j < lazyPropertyNames.Length; j++) - { - if (state[lazyPropertyNumbers[j]] == LazyPropertyInitializer.UnfetchedProperty) - { - uninitializedProperties.Add(lazyPropertyNames[j]); - } - } - - return uninitializedProperties; - } - public virtual bool[] PropertyUpdateability { get { return entityMetamodel.PropertyUpdateability; } diff --git a/src/NHibernate/Persister/Entity/IEntityPersister.cs b/src/NHibernate/Persister/Entity/IEntityPersister.cs index b757d8a7cf8..7ba82488bb9 100644 --- a/src/NHibernate/Persister/Entity/IEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/IEntityPersister.cs @@ -623,59 +623,5 @@ public static int GetBatchSize(this IEntityPersister persister) return 1; } - - //6.0 TODO: Merge into IEntityPersister - internal static ISet GetUninitializedLazyProperties(this IEntityPersister persister, object entity) - { - if (persister is AbstractEntityPersister abstractEntityPersister) - { - return abstractEntityPersister.GetUninitializedLazyProperties(entity); - } - - if (!persister.HasUninitializedLazyProperties(entity)) - { - return CollectionHelper.EmptySet(); - } - - // Assume they are all uninitialized. - var result = new HashSet(); - for (var i = 0; i < persister.PropertyLaziness.Length; i++) - { - if (persister.PropertyLaziness[i]) - { - result.Add(persister.PropertyNames[i]); - } - } - - return result; - } - - /// - /// Get uninitialized lazy properties from entity state - /// - //6.0 TODO: Merge into IEntityPersister - public static ISet GetUninitializedLazyProperties(this IEntityPersister persister, object[] state) - { - if (persister is AbstractEntityPersister abstractEntityPersister) - { - return abstractEntityPersister.GetUninitializedLazyProperties(state); - } - - if (!persister.HasLazyProperties) - { - return CollectionHelper.EmptySet(); - } - - var result = new HashSet(); - for (var i = 0; i < persister.PropertyLaziness.Length; i++) - { - if (persister.PropertyLaziness[i] && state[i] == LazyPropertyInitializer.UnfetchedProperty) - { - result.Add(persister.PropertyNames[i]); - } - } - - return result; - } } } diff --git a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs new file mode 100644 index 00000000000..cd4dc440fa3 --- /dev/null +++ b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataNonPocoImpl.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NHibernate.Bytecode; +using NHibernate.Engine; +using NHibernate.Intercept; +using NHibernate.Util; + +namespace NHibernate.Tuple.Entity +{ + /// + /// Author: Steve Ebersole + /// + [Serializable] + public class BytecodeEnhancementMetadataNonPocoImpl : IBytecodeEnhancementMetadata + { + private readonly string _errorMessage; + + public BytecodeEnhancementMetadataNonPocoImpl(string entityName) + { + EntityName = entityName; + LazyPropertiesMetadata = LazyPropertiesMetadata.NonEnhanced(entityName); + UnwrapProxyPropertiesMetadata = UnwrapProxyPropertiesMetadata.NonEnhanced(entityName); + _errorMessage = $"Entity [{entityName}] is non-poco, and therefore not instrumented"; + } + + /// + public string EntityName { get; } + + /// + public bool EnhancedForLazyLoading => false; + + /// + public LazyPropertiesMetadata LazyPropertiesMetadata { get; } + + /// + public UnwrapProxyPropertiesMetadata UnwrapProxyPropertiesMetadata { get; } + + /// + public IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) + { + throw new NotInstrumentedException(_errorMessage); + } + + /// + public IFieldInterceptor ExtractInterceptor(object entity) + { + throw new NotInstrumentedException(_errorMessage); + } + + /// + public ISet GetUninitializedLazyProperties(object entity) + { + return CollectionHelper.EmptySet(); + } + + /// + public ISet GetUninitializedLazyProperties(object[] entityState) + { + return CollectionHelper.EmptySet(); + } + } +} diff --git a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs new file mode 100644 index 00000000000..2f1ef4007d3 --- /dev/null +++ b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using NHibernate.Bytecode; +using NHibernate.Engine; +using NHibernate.Intercept; +using NHibernate.Mapping; +using NHibernate.Util; + +namespace NHibernate.Tuple.Entity +{ + /// + /// Author: Steve Ebersole + /// + [Serializable] + public class BytecodeEnhancementMetadataPocoImpl : IBytecodeEnhancementMetadata + { + private readonly System.Type _entityType; + + public static IBytecodeEnhancementMetadata From( + PersistentClass persistentClass, + ICollection lazyPropertyDescriptors, + ICollection unwrapProxyPropertyDescriptors) + { + var mappedClass = persistentClass.MappedClass; + var enhancedForLazyLoading = lazyPropertyDescriptors?.Count > 0 || unwrapProxyPropertyDescriptors?.Count > 0; + + // We have to check all subclasses if any of them is enhanced for lazy loading as the + // root class will be enhanced when any of the subclasses is enhanced, even if it + // does not have any lazy properties. + // If we do not check the subclasses, where the root-entity has no lazy properties + // we will eager-load/double-load those properties (NH2488). + if (!enhancedForLazyLoading) + { + foreach (var persistentSubclass in persistentClass.SubclassClosureIterator) + { + enhancedForLazyLoading |= IsEnhancedForLazyLoading(persistentSubclass); + if (enhancedForLazyLoading) + { + break; + } + } + } + + var lazyPropertiesMetadata = enhancedForLazyLoading + ? LazyPropertiesMetadata.From(persistentClass.EntityName, lazyPropertyDescriptors) + : LazyPropertiesMetadata.NonEnhanced(persistentClass.EntityName); + + var unwrapProxyPropertiesMetadata = enhancedForLazyLoading + ? UnwrapProxyPropertiesMetadata.From(persistentClass.EntityName, unwrapProxyPropertyDescriptors) + : UnwrapProxyPropertiesMetadata.NonEnhanced(persistentClass.EntityName); + + return new BytecodeEnhancementMetadataPocoImpl( + persistentClass.EntityName, + mappedClass, + enhancedForLazyLoading, + lazyPropertiesMetadata, + unwrapProxyPropertiesMetadata + ); + } + + /// + /// Check for a if is enhanced for lazy loading. + /// NOTE: The logic was taken from . + /// + /// The persistent class to check. + /// Whether the persistent class is enhanced for lazy loading or not. + private static bool IsEnhancedForLazyLoading(PersistentClass persistentClass) + { + var lazyAvailable = persistentClass.HasPocoRepresentation + && FieldInterceptionHelper.IsInstrumented(persistentClass.MappedClass); + + var lazy = persistentClass.IsLazy && (!persistentClass.HasPocoRepresentation || + !ReflectHelper.IsFinalClass(persistentClass.ProxyInterface)); + lazyAvailable &= lazy; + if (!lazyAvailable) + { + return false; + } + + foreach (var prop in persistentClass.PropertyIterator) + { + // NH: A lazy property is a simple property marked with lazy=true + var islazyProperty = prop.IsLazy && (!prop.IsEntityRelation || prop.UnwrapProxy); + // NH: A Relation (in this case many-to-one or one-to-one) marked as "no-proxy" + var isUnwrapProxy = prop.UnwrapProxy; + + if (islazyProperty || isUnwrapProxy) + { + return true; + } + } + + return false; + } + + public BytecodeEnhancementMetadataPocoImpl( + string entityName, + System.Type entityType, + bool enhancedForLazyLoading, + LazyPropertiesMetadata lazyPropertiesMetadata, + UnwrapProxyPropertiesMetadata unwrapProxyPropertiesMetadata) + { + EntityName = entityName; + _entityType = entityType; + EnhancedForLazyLoading = enhancedForLazyLoading; + LazyPropertiesMetadata = lazyPropertiesMetadata; + UnwrapProxyPropertiesMetadata = unwrapProxyPropertiesMetadata; + } + + /// + public string EntityName { get; } + + /// + public bool EnhancedForLazyLoading { get; } + + /// + public LazyPropertiesMetadata LazyPropertiesMetadata { get; } + + /// + public UnwrapProxyPropertiesMetadata UnwrapProxyPropertiesMetadata { get; } + + /// + public IFieldInterceptor InjectInterceptor(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) + { + if (!EnhancedForLazyLoading) + { + throw new NotInstrumentedException($"Entity class [{_entityType}] is not enhanced for lazy loading"); + } + + if (!(entity is IFieldInterceptorAccessor fieldInterceptorAccessor)) + { + return null; // Can happen when a saved entity is refreshed within the same session NH2860 + } + + if (entity.GetType().BaseType != _entityType) + { + throw new ArgumentException( + $"Passed entity instance [{entity}] is not of expected type [{EntityName}]"); + } + + var fieldInterceptorImpl = new DefaultFieldInterceptor( + session, + LazyPropertiesMetadata.HasLazyProperties && lazyPropertiesAreUnfetched + ? new HashSet(LazyPropertiesMetadata.LazyPropertyNames) + : null, + UnwrapProxyPropertiesMetadata.UnwrapProxyPropertyNames, + EntityName, + _entityType); + fieldInterceptorAccessor.FieldInterceptor = fieldInterceptorImpl; + + return fieldInterceptorImpl; + } + + /// + public IFieldInterceptor ExtractInterceptor(object entity) + { + if (!EnhancedForLazyLoading) + { + throw new NotInstrumentedException($"Entity class [{_entityType}] is not enhanced for lazy loading"); + } + + if (!(entity is IFieldInterceptorAccessor fieldInterceptorAccessor)) + { + return null; + } + + var interceptor = fieldInterceptorAccessor.FieldInterceptor; + if (interceptor == null) + { + return null; + } + + if (_entityType != interceptor.MappedClass) + { + throw new ArgumentException( + $"Passed entity instance [{entity}] is not of expected type [{EntityName}]"); + } + + return fieldInterceptorAccessor.FieldInterceptor; + } + + /// + public ISet GetUninitializedLazyProperties(object entity) + { + var interceptor = LazyPropertiesMetadata.HasLazyProperties ? ExtractInterceptor(entity) : null; + if (interceptor == null) + { + return CollectionHelper.EmptySet(); + } + + return interceptor.GetUninitializedFields() ?? LazyPropertiesMetadata.LazyPropertyNames; + } + + /// + public ISet GetUninitializedLazyProperties(object[] entityState) + { + if (!LazyPropertiesMetadata.HasLazyProperties) + { + return CollectionHelper.EmptySet(); + } + + var uninitializedProperties = new HashSet(); + foreach (var propertyDescriptor in LazyPropertiesMetadata.LazyPropertyDescriptors) + { + if (entityState[propertyDescriptor.PropertyIndex] == LazyPropertyInitializer.UnfetchedProperty) + { + uninitializedProperties.Add(propertyDescriptor.Name); + } + } + + return uninitializedProperties; + } + } +} diff --git a/src/NHibernate/Tuple/Entity/EntityMetamodel.cs b/src/NHibernate/Tuple/Entity/EntityMetamodel.cs index 9f81571059b..7fb104dac86 100644 --- a/src/NHibernate/Tuple/Entity/EntityMetamodel.cs +++ b/src/NHibernate/Tuple/Entity/EntityMetamodel.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; - +using NHibernate.Bytecode; using NHibernate.Engine; using NHibernate.Intercept; using NHibernate.Mapping; @@ -103,6 +103,8 @@ public EntityMetamodel(PersistentClass persistentClass, ISessionFactoryImplement propertySpan = persistentClass.PropertyClosureSpan; properties = new StandardProperty[propertySpan]; List naturalIdNumbers = new List(); + var lazyPropertyDescriptors = new List(); + var unwrapProxyPropertyDescriptors = new List(); propertyNames = new string[propertySpan]; propertyTypes = new IType[propertySpan]; @@ -182,10 +184,12 @@ public EntityMetamodel(PersistentClass persistentClass, ISessionFactoryImplement if (islazyProperty) { hasLazy = true; + lazyPropertyDescriptors.Add(LazyPropertyDescriptor.From(prop, i, lazyPropertyDescriptors.Count)); } if (isUnwrapProxy) { hasUnwrapProxyForProperties = true; + unwrapProxyPropertyDescriptors.Add(UnwrapProxyPropertyDescriptor.From(prop, i)); } propertyLaziness[i] = islazyProperty; @@ -316,6 +320,16 @@ public EntityMetamodel(PersistentClass persistentClass, ISessionFactoryImplement EntityMode = persistentClass.HasPocoRepresentation ? EntityMode.Poco : EntityMode.Map; + if (persistentClass.HasPocoRepresentation) + { + BytecodeEnhancementMetadata = BytecodeEnhancementMetadataPocoImpl + .From(persistentClass, lazyPropertyDescriptors, unwrapProxyPropertyDescriptors); + } + else + { + BytecodeEnhancementMetadata = new BytecodeEnhancementMetadataNonPocoImpl(persistentClass.EntityName); + } + var entityTuplizerFactory = new EntityTuplizerFactory(); var tuplizerClassName = persistentClass.GetTuplizerImplClassName(EntityMode); Tuplizer = tuplizerClassName == null @@ -716,5 +730,7 @@ public int[] NaturalIdentifierProperties { get { return naturalIdPropertyNumbers; } } + + public IBytecodeEnhancementMetadata BytecodeEnhancementMetadata { get; } } } diff --git a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs index 6f697e2c04d..397bdd595d6 100644 --- a/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/IEntityTuplizer.cs @@ -119,23 +119,4 @@ public interface IEntityTuplizer : ITuplizer /// True if uninitialized lazy properties were found; false otherwise. bool HasUninitializedLazyProperties(object entity); } - - internal static class EntityTuplizerExtensions - { - //6.0 TODO: Merge into IEntityTuplizer - internal static ISet GetUninitializedLazyProperties(this IEntityTuplizer entityTuplizer, object entity) - { - if (entityTuplizer is AbstractEntityTuplizer abstractEntityTuplizer) - { - return abstractEntityTuplizer.GetUninitializedLazyProperties(entity); - } - - if (!entityTuplizer.HasUninitializedLazyProperties(entity)) - { - return CollectionHelper.EmptySet(); - } - - return null; // The caller should use all lazy properties as the result - } - } } diff --git a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs index 2deb5f607ab..5dd46ce470a 100644 --- a/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/PocoEntityTuplizer.cs @@ -24,8 +24,6 @@ public class PocoEntityTuplizer : AbstractEntityTuplizer private readonly System.Type proxyInterface; private readonly bool islifecycleImplementor; private readonly bool isValidatableImplementor; - private readonly HashSet lazyPropertyNames = new HashSet(); - private readonly HashSet unwrapProxyPropertyNames = new HashSet(); [NonSerialized] private IReflectionOptimizer optimizer; private readonly IProxyValidator proxyValidator; @@ -59,13 +57,6 @@ public PocoEntityTuplizer(EntityMetamodel entityMetamodel, PersistentClass mappe islifecycleImplementor = typeof(ILifecycle).IsAssignableFrom(mappedClass); isValidatableImplementor = typeof(IValidatable).IsAssignableFrom(mappedClass); - foreach (Mapping.Property property in mappedEntity.PropertyClosureIterator) - { - if (property.IsLazy) - lazyPropertyNames.Add(property.Name); - if (property.UnwrapProxy) - unwrapProxyPropertyNames.Add(property.Name); - } SetReflectionOptimizer(); Instantiator = BuildInstantiator(mappedEntity); @@ -80,16 +71,7 @@ public override System.Type ConcreteProxyClass get { return proxyInterface; } } - public override bool IsInstrumented - { - get - { - // NH: we can't really check for EntityMetamodel.HasLazyProperties and/or EntityMetamodel.HasUnwrapProxyForProperties here - // because this property is used even where subclasses has lazy-properties. - // Checking it here, where the root-entity has no lazy properties we will eager-load/double-load those properties. - return FieldInterceptionHelper.IsInstrumented(MappedClass); - } - } + public override bool IsInstrumented => EntityMetamodel.BytecodeEnhancementMetadata.EnhancedForLazyLoading; public override System.Type MappedClass { @@ -111,12 +93,12 @@ protected override IInstantiator BuildInstantiator(PersistentClass persistentCla if (optimizer == null) { log.Debug("Create Instantiator without optimizer for:{0}", persistentClass.MappedClass.FullName); - return new PocoInstantiator(persistentClass, null, ProxyFactory, EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties); + return new PocoInstantiator(persistentClass, null, ProxyFactory, IsInstrumented); } else { log.Debug("Create Instantiator using optimizer for:{0}", persistentClass.MappedClass.FullName); - return new PocoInstantiator(persistentClass, optimizer.InstantiationOptimizer, ProxyFactory, EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties); + return new PocoInstantiator(persistentClass, optimizer.InstantiationOptimizer, ProxyFactory, IsInstrumented); } } @@ -229,10 +211,9 @@ protected virtual IProxyFactory BuildProxyFactoryInternal(PersistentClass @class public override void AfterInitialize(object entity, bool lazyPropertiesAreUnfetched, ISessionImplementor session) { - if (IsInstrumented && (EntityMetamodel.HasLazyProperties || EntityMetamodel.HasUnwrapProxyForProperties)) + if (IsInstrumented) { - HashSet lazyProps = lazyPropertiesAreUnfetched && EntityMetamodel.HasLazyProperties ? new HashSet(lazyPropertyNames) : null; - FieldInterceptionHelper.InjectFieldInterceptor(entity, EntityName, this.MappedClass ,lazyProps, unwrapProxyPropertyNames, session); + EntityMetamodel.BytecodeEnhancementMetadata.InjectInterceptor(entity, lazyPropertiesAreUnfetched, session); } } @@ -269,7 +250,7 @@ public override bool HasUninitializedLazyProperties(object entity) { if (EntityMetamodel.HasLazyProperties) { - IFieldInterceptor callback = FieldInterceptionHelper.ExtractFieldInterceptor(entity); + var callback = EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(entity); return callback != null && !callback.IsInitialized; } else @@ -285,13 +266,7 @@ internal override ISet GetUninitializedLazyProperties(object entity) return CollectionHelper.EmptySet(); } - var interceptor = FieldInterceptionHelper.ExtractFieldInterceptor(entity); - if (interceptor == null) - { - return CollectionHelper.EmptySet(); - } - - return interceptor.GetUninitializedFields() ?? lazyPropertyNames; + return EntityMetamodel.BytecodeEnhancementMetadata.GetUninitializedLazyProperties(entity); } public override bool IsLifecycleImplementor