Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/EFCore/ChangeTracking/ChangeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,44 @@ public virtual IEnumerable<EntityEntry<TEntity>> Entries<TEntity>()
.Select(e => new EntityEntry<TEntity>(e));
}

/// <summary>
/// Returns tracked entities that are in a given state from a fast cache.
/// </summary>
/// <param name="added">Entities in EntityState.Added state</param>
/// <param name="modified">Entities in Modified.Added state</param>
/// <param name="deleted">Entities in Modified.Deleted state</param>
/// <param name="unchanged">Entities in Modified.Unchanged state</param>
/// <returns>An entry for each entity that matched the search criteria.</returns>
public IEnumerable<EntityEntry> GetEntriesForState(
bool added = false,
bool modified = false,
bool deleted = false,
bool unchanged = false)
{
return StateManager.GetEntriesForState(added, modified, deleted, unchanged)
.Select(e => new EntityEntry(e));
}

/// <summary>
/// Returns tracked entities that are in a given state from a fast cache.
/// </summary>
/// <param name="added">Entities in EntityState.Added state</param>
/// <param name="modified">Entities in Modified.Added state</param>
/// <param name="deleted">Entities in Modified.Deleted state</param>
/// <param name="unchanged">Entities in Modified.Unchanged state</param>
/// <returns>An entry for each entity that matched the search criteria.</returns>
public IEnumerable<EntityEntry<TEntity>> GetEntriesForState<TEntity>(
bool added = false,
bool modified = false,
bool deleted = false,
bool unchanged = false)
where TEntity : class
{
return StateManager.GetEntriesForState(added, modified, deleted, unchanged)
.Where(e => e.Entity is TEntity)
.Select(e => new EntityEntry<TEntity>(e));
}

private void TryDetectChanges()
{
if (AutoDetectChangesEnabled)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,6 @@ public TProperty GetRelationshipSnapshotValue<TProperty>(IPropertyBase propertyB
public object? GetRelationshipSnapshotValue(IPropertyBase propertyBase)
=> _relationshipsSnapshot.GetValue(this, propertyBase);


/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
Expand Down
24 changes: 24 additions & 0 deletions src/EFCore/ChangeTracking/Internal/InternalEntryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,30 @@ public void SetOriginalValue(
}
}

/// <summary>
/// Refreshes the property value with the value from the database
/// </summary>
/// <param name="propertyBase">Property</param>
/// <param name="value">New value from database</param>
/// <param name="mergeOption">MergeOption</param>
/// <param name="updateEntityState">Sets the EntityState to Unchanged if MergeOption.OverwriteChanges else calls ChangeDetector to determine changes</param>
public void ReloadValue(IPropertyBase propertyBase, object? value, MergeOption mergeOption, bool updateEntityState)
{
var property = (IProperty)propertyBase;
EnsureOriginalValues();
bool isModified = IsModified(property);
_originalValues.SetValue(property, value, -1);
if (mergeOption == MergeOption.OverwriteChanges || !isModified)
SetProperty(propertyBase, value, isMaterialization: true, setModified: false);
if (updateEntityState)
{
if (mergeOption == MergeOption.OverwriteChanges)
SetEntityState(EntityState.Unchanged);
else
((StateManager as StateManager)?.ChangeDetector as ChangeDetector)?.DetectValueChange(this, property);
}
}

private void ReorderOriginalComplexCollectionEntries(IComplexProperty complexProperty, IList? newOriginalCollection)
{
Check.DebugAssert(HasOriginalValuesSnapshot, "This should only be called when original values are present");
Expand Down
31 changes: 31 additions & 0 deletions src/EFCore/Extensions/EntityFrameworkQueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2883,6 +2883,37 @@ public static IQueryable<TEntity> AsTracking<TEntity>(

#endregion

#region Refreshing

internal static readonly MethodInfo RefreshMethodInfo
= typeof(EntityFrameworkQueryableExtensions).GetMethod(
nameof(Refresh), [typeof(IQueryable<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(MergeOption)])!;


/// <summary>
/// Specifies that the current Entity Framework LINQ query should refresh already loaded objects with the specified merge option.
/// </summary>
/// <typeparam name="T">The type of entity being queried.</typeparam>
/// <param name="source">The source query.</param>
/// <param name="mergeOption">The MergeOption</param>
/// <returns>A new query annotated with the given tag.</returns>
public static IQueryable<T> Refresh<T>(
this IQueryable<T> source,
[NotParameterized] MergeOption mergeOption)
{
return
source.Provider is EntityQueryProvider
? source.Provider.CreateQuery<T>(
Expression.Call(
instance: null,
method: RefreshMethodInfo.MakeGenericMethod(typeof(T)),
arg0: source.Expression,
arg1: Expression.Constant(mergeOption)))
: source;
}

#endregion

#region Tagging

internal static readonly MethodInfo TagWithMethodInfo
Expand Down
27 changes: 27 additions & 0 deletions src/EFCore/MergeOption.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// The different ways that new objects loaded from the database can be merged with existing objects already in memory.
/// </summary>
public enum MergeOption
{
/// <summary>
/// Will only append new (top level-unique) rows. This is the default behavior.
/// </summary>
AppendOnly = 0,

/// <summary>
/// The incoming values for this row will be written to both the current value and
/// the original value versions of the data for each column.
/// </summary>
OverwriteChanges = 1,

/// <summary>
/// The incoming values for this row will be written to the original value version
/// of each column. The current version of the data in each column will not be changed.
/// </summary>
PreserveChanges = 2
}
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,14 @@ private static void VerifyReturnType(Expression expression, ParameterExpression
return visitedExpression;
}

if (genericMethodDefinition == EntityFrameworkQueryableExtensions.RefreshMethodInfo)
{
var visitedExpression = Visit(methodCallExpression.Arguments[0]);
_queryCompilationContext.RefreshMergeOption = methodCallExpression.Arguments[1].GetConstantValue<MergeOption>();

return visitedExpression;
}

return null;
}

Expand Down
5 changes: 5 additions & 0 deletions src/EFCore/Query/QueryCompilationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ public QueryCompilationContext(QueryCompilationContextDependencies dependencies,
/// </summary>
public virtual bool IgnoreAutoIncludes { get; internal set; }

/// <summary>
/// A value indicating how already loaded objects should be merged and refreshed with the results of this query.
/// </summary>
public virtual MergeOption RefreshMergeOption { get; internal set; }

/// <summary>
/// The set of tags applied to this query.
/// </summary>
Expand Down
83 changes: 80 additions & 3 deletions src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ protected ShapedQueryCompilingExpressionVisitor(
dependencies.EntityMaterializerSource,
dependencies.LiftableConstantFactory,
queryCompilationContext.QueryTrackingBehavior,
queryCompilationContext.SupportsPrecompiledQuery);
queryCompilationContext.SupportsPrecompiledQuery,
queryCompilationContext.RefreshMergeOption);

_constantVerifyingExpressionVisitor = new ConstantVerifyingExpressionVisitor(dependencies.TypeMappingSource);
_materializationConditionConstantLifter = new MaterializationConditionConstantLifter(dependencies.LiftableConstantFactory);
Expand Down Expand Up @@ -378,7 +379,8 @@ private sealed class StructuralTypeMaterializerInjector(
IStructuralTypeMaterializerSource materializerSource,
ILiftableConstantFactory liftableConstantFactory,
QueryTrackingBehavior queryTrackingBehavior,
bool supportsPrecompiledQuery)
bool supportsPrecompiledQuery,
MergeOption mergeOption)
: ExpressionVisitor
{
private static readonly ConstructorInfo MaterializationContextConstructor
Expand Down Expand Up @@ -411,6 +413,8 @@ private static readonly MethodInfo CreateNullKeyValueInNoTrackingQueryMethod
private readonly bool _queryStateManager =
queryTrackingBehavior is QueryTrackingBehavior.TrackAll or QueryTrackingBehavior.NoTrackingWithIdentityResolution;

private readonly MergeOption _MergeOption = mergeOption;

private readonly ISet<IEntityType> _visitedEntityTypes = new HashSet<IEntityType>();
private readonly MaterializationConditionConstantLifter _materializationConditionConstantLifter = new(liftableConstantFactory);
private int _currentEntityIndex;
Expand Down Expand Up @@ -525,7 +529,15 @@ private Expression ProcessStructuralTypeShaper(StructuralTypeShaperExpression sh
Assign(
instanceVariable, Convert(
MakeMemberAccess(entryVariable, EntityMemberInfo),
clrType))),
clrType)),
// Update the existing entity with new property values from the database
// if the merge option is not AppendOnly
_MergeOption != MergeOption.AppendOnly
? UpdateExistingEntityWithDatabaseValues(
entryVariable,
concreteEntityTypeVariable,
materializationContextVariable,
shaper) : Empty()),
MaterializeEntity(
shaper, materializationContextVariable, concreteEntityTypeVariable, instanceVariable,
entryVariable))));
Expand Down Expand Up @@ -764,5 +776,70 @@ private BlockExpression CreateFullMaterializeExpression(

return Block(blockExpressions);
}

/// <summary>
/// Creates an expression to update an existing tracked entity with values from the database,
/// similar to the EntityEntry.Reload() method.
/// </summary>
/// <param name="entryVariable">The variable representing the existing InternalEntityEntry.</param>
/// <param name="concreteEntityTypeVariable">The variable representing the concrete entity type.</param>
/// <param name="materializationContextVariable">The materialization context variable.</param>
/// <param name="shaper">The structural type shaper expression.</param>
/// <returns>An expression that updates the existing entity with database values.</returns>
private Expression UpdateExistingEntityWithDatabaseValues(
ParameterExpression entryVariable,
ParameterExpression concreteEntityTypeVariable,
ParameterExpression materializationContextVariable,
StructuralTypeShaperExpression shaper)
{
var updateExpressions = new List<Expression>();
var typeBase = shaper.StructuralType;

if (typeBase is not IEntityType entityType)
{
// For complex types, we don't update existing instances
return Empty();
}

var valueBufferExpression = Call(materializationContextVariable, MaterializationContext.GetValueBufferMethod);

// Get all properties to update (exclude key properties which should not change)
var propertiesToUpdate = entityType.GetProperties()
.Where(p => !p.IsPrimaryKey())
.ToList();

var setReloadValueMethod = typeof(InternalEntityEntry)
.GetMethod(nameof(InternalEntityEntry.ReloadValue), new[] { typeof(IPropertyBase), typeof(object), typeof(MergeOption), typeof(bool) })!;

// Update original values similar to EntityEntry.Reload()
// This ensures that the original values snapshot reflects the database state
var dbProperties = propertiesToUpdate.Where(p => !p.IsShadowProperty());
int count = dbProperties.Count();
int i = 0;
foreach (var property in dbProperties)
{
i++;
var newValue = valueBufferExpression.CreateValueBufferReadValueExpression(
property.ClrType,
property.GetIndex(),
property);

var setOriginalValueExpression = Call(
entryVariable,
setReloadValueMethod,
Constant(property),
property.ClrType.IsValueType && property.IsNullable
? (Expression)Convert(newValue, typeof(object))
: Convert(newValue, typeof(object)),
Constant(_MergeOption),
Constant(i == count));

updateExpressions.Add(setOriginalValueExpression);
}

return updateExpressions.Count > 0
? (Expression)Block(updateExpressions)
: Empty();
}
}
}
Loading