diff --git a/src/EFCore/ChangeTracking/ChangeTracker.cs b/src/EFCore/ChangeTracking/ChangeTracker.cs
index 37b99525a87..d52d900412d 100644
--- a/src/EFCore/ChangeTracking/ChangeTracker.cs
+++ b/src/EFCore/ChangeTracking/ChangeTracker.cs
@@ -10,7 +10,6 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
-using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Utilities;
@@ -397,18 +396,6 @@ Task IResettableService.ResetStateAsync(CancellationToken cancellationToken)
return default;
}
- ///
- /// 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
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- [EntityFrameworkInternal]
- public virtual DebugView DebugView
- => new DebugView(
- () => StateManager.ToDebugString(StateManagerDebugStringOptions.ShortDefault),
- () => StateManager.ToDebugString(StateManagerDebugStringOptions.LongDefault));
-
#region Hidden System.Object members
///
diff --git a/src/EFCore/ChangeTracking/EntityEntry.cs b/src/EFCore/ChangeTracking/EntityEntry.cs
index 9007c747703..e7495da61e1 100644
--- a/src/EFCore/ChangeTracking/EntityEntry.cs
+++ b/src/EFCore/ChangeTracking/EntityEntry.cs
@@ -403,14 +403,13 @@ private void Reload(PropertyValues storeValues)
private IEntityFinder Finder
=> InternalEntry.StateManager.CreateEntityFinder(InternalEntry.EntityType);
- #region Hidden System.Object members
-
///
/// Returns a string that represents the current object.
///
/// A string that represents the current object.
- [EditorBrowsable(EditorBrowsableState.Never)]
- public override string ToString() => base.ToString();
+ public override string ToString() => InternalEntry.ToString();
+
+ #region Hidden System.Object members
///
/// Determines whether the specified object is equal to the current object.
diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
index 7d66816501c..d0888a4fc3b 100644
--- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
+++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
@@ -9,11 +10,13 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
+using System.Text;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Storage;
@@ -1673,8 +1676,197 @@ public virtual bool IsLoaded([NotNull] INavigation navigation)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override string ToString()
- => $"{this.BuildCurrentValuesString(EntityType.FindPrimaryKey().Properties)} {EntityState}"
- + $"{(((IUpdateEntry)this).SharedIdentityEntry == null ? "" : " Shared")} {EntityType}";
+ => ToDebugString(StateManagerDebugStringOptions.ShortDefault);
+
+ ///
+ /// 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
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual DebugView DebugView
+ => new DebugView(
+ () => this.ToDebugString(StateManagerDebugStringOptions.ShortDefault),
+ () => this.ToDebugString(StateManagerDebugStringOptions.LongDefault));
+
+ ///
+ /// 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
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public virtual string ToDebugString(StateManagerDebugStringOptions options)
+ {
+ var builder = new StringBuilder();
+ var keyString = this.BuildCurrentValuesString(EntityType.FindPrimaryKey().Properties);
+
+ builder
+ .Append(EntityType.DisplayName())
+ .Append(' ')
+ .Append(SharedIdentityEntry == null ? "(Shared) " : "")
+ .Append(keyString)
+ .Append(' ')
+ .Append(EntityState.ToString());
+
+ if ((options & StateManagerDebugStringOptions.IncludeProperties) != 0)
+ {
+ foreach (var property in EntityType.GetProperties())
+ {
+ builder.AppendLine();
+
+ var currentValue = GetCurrentValue(property);
+ builder
+ .Append(" ")
+ .Append(property.Name)
+ .Append(": ");
+
+ AppendValue(currentValue);
+
+ if (property.IsPrimaryKey())
+ {
+ builder.Append(" PK");
+ }
+ else if (property.IsKey())
+ {
+ builder.Append(" AK");
+ }
+
+ if (property.IsForeignKey())
+ {
+ builder.Append(" FK");
+ }
+
+ if (IsModified(property))
+ {
+ builder.Append(" Modified");
+ }
+
+ if (HasTemporaryValue(property))
+ {
+ builder.Append(" Temporary");
+ }
+
+ if (HasOriginalValuesSnapshot
+ && property.GetOriginalValueIndex() != -1)
+ {
+ var originalValue = GetOriginalValue(property);
+ if (!Equals(originalValue, currentValue))
+ {
+ builder.Append(" Originally ");
+ AppendValue(originalValue);
+ }
+ }
+ }
+ }
+ else
+ {
+ foreach (var alternateKey in EntityType.GetKeys().Where(k => !k.IsPrimaryKey()))
+ {
+ builder
+ .Append(" AK ")
+ .Append(this.BuildCurrentValuesString(alternateKey.Properties));
+ }
+
+ foreach (var foreignKey in EntityType.GetForeignKeys())
+ {
+ builder
+ .Append(" FK ")
+ .Append(this.BuildCurrentValuesString(foreignKey.Properties));
+ }
+ }
+
+ if ((options & StateManagerDebugStringOptions.IncludeNavigations) != 0)
+ {
+ foreach (var navigation in EntityType.GetNavigations())
+ {
+ builder.AppendLine();
+
+ var currentValue = GetCurrentValue(navigation);
+ var targetType = navigation.GetTargetType();
+
+ builder
+ .Append(" ")
+ .Append(navigation.Name)
+ .Append(": ");
+
+ if (currentValue == null)
+ {
+ builder.Append("");
+ }
+ else if (navigation.IsCollection())
+ {
+ builder.Append('[');
+
+ const int maxRelatedToShow = 32;
+ var relatedEntities = ((IEnumerable)currentValue).Cast
- public static string ToDebugString(
- [NotNull] this IStateManager stateManager,
- StateManagerDebugStringOptions options,
- [NotNull] string indent = "")
+ public static string ToDebugString([NotNull] this IStateManager stateManager, StateManagerDebugStringOptions options)
{
var builder = new StringBuilder();
- void AppendValue(object value)
+ foreach (var entry in stateManager.Entries.OrderBy(e => e, EntityEntryComparer.Instance))
{
- if (value == null)
- {
- builder.Append("");
- }
- else if (value.GetType().IsNumeric())
- {
- builder.Append(value);
- }
- else if (value is byte[] bytes)
- {
- builder.AppendBytes(bytes);
- }
- else
- {
- var stringValue = value.ToString();
- if (stringValue.Length > 63)
- {
- stringValue = stringValue.Substring(0, 60) + "...";
- }
-
- builder
- .Append('\'')
- .Append(stringValue)
- .Append('\'');
- }
+ builder.AppendLine(entry.ToDebugString(options));
}
- void AppendRelatedKey(IEntityType targetType, object value)
- {
- var otherEntry = stateManager.TryGetEntry(value, targetType, throwOnTypeMismatch: false);
+ return builder.ToString();
+ }
- builder.Append(
- otherEntry == null
- ? ""
- : otherEntry.BuildCurrentValuesString(targetType.FindPrimaryKey().Properties));
- }
+ private sealed class EntityEntryComparer : IComparer
+ {
+ public static EntityEntryComparer Instance = new EntityEntryComparer();
- foreach (var item in stateManager.Entries
- .Select(
- e =>
- new
- {
- KeyString = e.BuildCurrentValuesString(e.EntityType.FindPrimaryKey().Properties),
- Entry = e
- })
- .OrderBy(e => e.Entry.EntityType.DisplayName())
- .ThenBy(e => e.KeyString))
+ private EntityEntryComparer()
{
- var entry = item.Entry;
- var entityType = entry.EntityType;
-
- builder
- .Append(entityType.DisplayName())
- .Append(' ')
- .Append(item.KeyString)
- .Append(' ')
- .Append(entry.EntityState.ToString());
-
- if ((options & StateManagerDebugStringOptions.IncludeProperties) != 0)
- {
- builder.AppendLine();
-
- foreach (var property in entityType.GetProperties())
- {
- var currentValue = entry.GetCurrentValue(property);
- builder
- .Append(" ")
- .Append(property.Name)
- .Append(": ");
-
- AppendValue(currentValue);
-
- if (property.IsPrimaryKey())
- {
- builder.Append(" PK");
- }
- else if (property.IsKey())
- {
- builder.Append(" AK");
- }
-
- if (property.IsForeignKey())
- {
- builder.Append(" FK");
- }
-
- if (entry.IsModified(property))
- {
- builder.Append(" Modified");
- }
-
- if (entry.HasTemporaryValue(property))
- {
- builder.Append(" Temporary");
- }
-
- if (entry.HasOriginalValuesSnapshot
- && property.GetOriginalValueIndex() != -1)
- {
- var originalValue = entry.GetOriginalValue(property);
- if (!Equals(originalValue, currentValue))
- {
- builder.Append(" Originally ");
- AppendValue(originalValue);
- }
- }
+ }
- builder.AppendLine();
- }
- }
- else
+ public int Compare(InternalEntityEntry x, InternalEntityEntry y)
+ {
+ var result = StringComparer.InvariantCulture.Compare(x.EntityType.Name, y.EntityType.Name);
+ if (result != 0)
{
- foreach (var alternateKey in entityType.GetKeys().Where(k => !k.IsPrimaryKey()))
- {
- builder
- .Append(" AK ")
- .Append(entry.BuildCurrentValuesString(alternateKey.Properties));
- }
-
- foreach (var foreignKey in entityType.GetForeignKeys())
- {
- builder
- .Append(" FK ")
- .Append(entry.BuildCurrentValuesString(foreignKey.Properties));
- }
-
- builder.AppendLine();
+ return result;
}
- if ((options & StateManagerDebugStringOptions.IncludeNavigations) != 0)
+ var primaryKey = x.EntityType.FindPrimaryKey();
+ if (primaryKey != null)
{
- foreach (var navigation in entityType.GetNavigations())
+ var keyProperties = primaryKey.Properties;
+ foreach (var keyProperty in keyProperties)
{
- var currentValue = entry.GetCurrentValue(navigation);
- var targetType = navigation.GetTargetType();
-
- builder
- .Append(" ")
- .Append(navigation.Name)
- .Append(": ");
-
- if (currentValue == null)
- {
- builder.Append("");
- }
- else if (navigation.IsCollection())
+ if (typeof(IComparable).IsAssignableFrom(keyProperty.ClrType))
{
- builder.Append('[');
-
- var relatedEntities = ((IEnumerable)currentValue).Cast