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().Take(maxRelatedToShow + 1).ToList(); + + for (var i = 0; i < relatedEntities.Count; i++) + { + if (i != 0) + { + builder.Append(", "); + } + + if (i < 32) + { + AppendRelatedKey(targetType, relatedEntities[i]); + } + else + { + builder.Append("..."); + } + } + + builder.Append(']'); + } + else + { + AppendRelatedKey(targetType, currentValue); + } + } + } + + return builder.ToString(); + + void AppendValue(object value) + { + 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('\''); + } + } + + void AppendRelatedKey(IEntityType targetType, object value) + { + var otherEntry = StateManager.TryGetEntry(value, targetType, throwOnTypeMismatch: false); + + builder.Append( + otherEntry == null + ? "" + : otherEntry.BuildCurrentValuesString(targetType.FindPrimaryKey().Properties)); + } + } IUpdateEntry IUpdateEntry.SharedIdentityEntry => SharedIdentityEntry; diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index f957a3a74b9..80d011b4f30 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -1254,5 +1254,16 @@ public virtual void OnStateChanged(InternalEntityEntry internalEntityEntry, Enti @event?.Invoke(Context.ChangeTracker, new EntityStateChangedEventArgs(internalEntityEntry, oldState, newState)); } + + /// + /// 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)); } } diff --git a/src/EFCore/ChangeTracking/Internal/StateManagerExtensions.cs b/src/EFCore/ChangeTracking/Internal/StateManagerExtensions.cs index 58d63ce7091..b988ecd2248 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManagerExtensions.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManagerExtensions.cs @@ -7,9 +7,6 @@ using System.Linq; using System.Text; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Update; namespace Microsoft.EntityFrameworkCore.ChangeTracking.Internal { @@ -61,196 +58,56 @@ public static IReadOnlyList ToList( /// 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 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().Take(33).ToList(); + result = Comparer.DefaultInvariant.Compare( + x.GetCurrentValue(keyProperty), + y.GetCurrentValue(keyProperty)); - for (var i = 0; i < relatedEntities.Count; i++) + if (result != 0) { - if (i != 0) - { - builder.Append(", "); - } - - if (i < 32) - { - AppendRelatedKey(targetType, relatedEntities[i]); - } - else - { - builder.Append("..."); - } + return result; } - - builder.Append(']'); - } - else - { - AppendRelatedKey(targetType, currentValue); } - - builder.AppendLine(); } } - } - return builder.ToString(); + return 0; + } } } } diff --git a/src/EFCore/Update/UpdateEntryExtensions.cs b/src/EFCore/Update/UpdateEntryExtensions.cs index 82bf137a2af..cf33167e112 100644 --- a/src/EFCore/Update/UpdateEntryExtensions.cs +++ b/src/EFCore/Update/UpdateEntryExtensions.cs @@ -28,7 +28,16 @@ public static string BuildCurrentValuesString( [NotNull] IEnumerable properties) => "{" + string.Join( - ", ", properties.Select(p => p.Name + ": " + Convert.ToString(entry.GetCurrentValue(p), CultureInfo.InvariantCulture))) + ", ", properties.Select( + p => + { + var currentValue = entry.GetCurrentValue(p); + return p.Name + + ": " + + (currentValue == null + ? "" + : Convert.ToString(currentValue, CultureInfo.InvariantCulture)); + })) + "}"; /// @@ -44,7 +53,16 @@ public static string BuildOriginalValuesString( [NotNull] IEnumerable properties) => "{" + string.Join( - ", ", properties.Select(p => p.Name + ": " + Convert.ToString(entry.GetOriginalValue(p), CultureInfo.InvariantCulture))) + ", ", properties.Select( + p => + { + var originalValue = entry.GetOriginalValue(p); + return p.Name + + ": " + + (originalValue == null + ? "" + : Convert.ToString(originalValue, CultureInfo.InvariantCulture)); + })) + "}"; } }