9

How do you determine which column is the culprit when you have 80(+/-) columns to choose from? Using .Net Core (netcoreapp2.2) and EF Core 2.2.4.

Picked up some existing code and there was an attempt to track columns that failed. However, it does not work. I've looked at dozens of examples here and elsewhere and have not found a way to do this in EF Core 2.x.

public int GetColumnMaxLength(string table, EntityEntry entityEntry)
{
    // Just a rough to get the right data - always returns 0 for the moment...
    int result = 0;
    var modelContext = entityEntry.Context;
    var entityType = modelContext.Model.FindEntityType(table); // THIS IS ALWAYS NULL!

    if (entityType != null)
    {
        // Table info 
        var tableName = entityType.Relational().TableName;
        var tableSchema = entityType.Relational().Schema;

        // Column info 
        foreach (var property in entityType.GetProperties())
        {
            var columnName = property.Relational().ColumnName;
            var columnType = property.Relational().ColumnType;
            var isFixedLength = property.Relational().IsFixedLength;
        };
    }
    return result;
}

The above code is being called by this catch portion of a try/catch around the db.SaveAsync(); statement.

catch (Exception ex)
{
    // -----------------------------------------
    // no idea what this was really trying to 
    // do as it barfs out all columns...
    // -----------------------------------------

    var dataInfo = new DataInfo();

    var strLargeValues = new List<Tuple<int, string, string, string>>();

    foreach (var entityEntry in _db.ChangeTracker.Entries().Where(et => et.State != EntityState.Unchanged))
    {
        // -----------------------------------------
        // try to get the column info for all 
        // columns on this table...
        // -----------------------------------------
        dataInfo.GetColumnMaxLength("Subscription", entityEntry);

        foreach (var entry in entityEntry.CurrentValues.Properties)
        {
            var value = entry.PropertyInfo.GetValue(entityEntry.Entity);
            if (value is string s)
            {
                strLargeValues.Add(Tuple.Create(s.Length, s, entry.Name, entityEntry.Entity.GetType().Name));
            }
        }
    }

    var l = strLargeValues.OrderByDescending(v => v.Item1).ToArray();

    foreach (var x in l.Take(100))
    {
        Trace.WriteLine(x.Item4 + " - " + x.Item3 + " - " + x.Item1 + ": " + x.Item2);
    }

    throw;
}

So, the crux of the question is: How do I get the SQL column definition from EF Core?

I want to be able to log the specific table and column when incomingData.Length > targetColumnDefinition.Length


FINAL SOLUTION:

public override int SaveChanges()
{
    using (LogContext.PushProperty("DbContext:Override:Save", nameof(SaveChanges)))
    {
        try
        {
            return base.SaveChanges();
        }
        catch (Exception ex)
        {
            var errorMessage = String.Empty;
            var token = Environment.NewLine;

            foreach (var entityEntry in this.ChangeTracker.Entries().Where(et => et.State != EntityState.Unchanged))
            {
                foreach (var entry in entityEntry.CurrentValues.Properties)
                {
                    var result = entityEntry.GetDatabaseDefinition(entry.Name);
                    var value = entry.PropertyInfo.GetValue(entityEntry.Entity);
                    if (result.IsFixedLength && value.ToLength() > result.MaxLength)
                    {
                        errorMessage = $"{errorMessage}{token}ERROR!! <<< {result.TableName}.{result.ColumnName} {result.ColumnType.ToUpper()} :: {entry.Name}({value.ToLength()}) = {value} >>>";
                        Log.Warning("Cannot save data to SQL column {TableName}.{ColumnName}!  Max length is {LengthTarget} and you are trying to save something that is {LengthSource}.  Column definition is {ColumnType}"
                            , result.TableName
                            , result.ColumnName
                            , result.MaxLength
                            , value.ToLength()
                            , result.ColumnType);
                    }
                }
            }
            throw new Exception(errorMessage, ex);
        }
    }
}
Keith Barrows
  • 24,802
  • 26
  • 88
  • 134
  • Is using plain SQL statements not feasible? – jpgrassi May 13 '19 at 18:29
  • 1
    Which version of SQL Server are you using? 2019 (as of CTP2) includes the table and column names in the message. It's also supposed to be backported to 2017 in [CU12](https://support.microsoft.com/en-us/help/4464082/cumulative-update-12-for-sql-server-2017) and 2016 SP2 in [CU6](https://support.microsoft.com/en-us/help/4488536/cumulative-update-6-for-sql-server-2016-sp2), requiring a trace flag 460 to enable it. If you're running one of those versions, hopefully you can take the update and enable the flag. – madreflection May 13 '19 at 18:38
  • Whatever is loaded on Azure. I don't have control and the contact that does does not have time right now to look. So, left with trying to back engineer field lengths... – Keith Barrows May 13 '19 at 19:43
  • I'm pretty sure the column name will be in the exception message, or an inner exception message. – John Boker May 13 '19 at 19:56
  • Nope. And I think it is because the save is done via a sproc instead of a direct table add/update. The context still gives me the fact it failed, but no further information. I can iterate all the records but so far have not found a way to get the column definition... – Keith Barrows May 13 '19 at 20:17
  • 1
    `FindEntityType` requires the full type name, or else the type itself, `typeof(Subscription)`. – Gert Arnold May 13 '19 at 20:49
  • 2
    You can start from here - there is no need to find the entity type when having `EntityEntry` because it's provided by `Metadata` property, e.g. `var entityType = entityEntry.Metadata;` – Ivan Stoev May 14 '19 at 08:33
  • 1
    Have you tried updating the above to v3.1? I would like to try the above but cannot find some of the missing defs. `GetDatabaseDefinition()`. – IbrarMumtaz Nov 24 '20 at 14:06

2 Answers2

12

On .NET Core 3.1 and EFCore 5.0.2 this logging works with no additional extension methods needed:

try
{
    await context.SaveChangesAsync();
}
catch(Exception ex)
{
    foreach (var entityEntry in context.ChangeTracker.Entries().Where(et => et.State != EntityState.Unchanged))
    {
        foreach (var entry in entityEntry.CurrentValues.Properties)
        { 
            var prop = entityEntry.Property(entry.Name).Metadata;
            var value = entry.PropertyInfo?.GetValue(entityEntry.Entity);
            var valueLength = value?.ToString()?.Length;
            var typemapping = prop.GetTypeMapping();
            var typeSize = ((Microsoft.EntityFrameworkCore.Storage.RelationalTypeMapping) typemapping).Size;
            if (typeSize.HasValue && valueLength > typeSize.Value)
            {
                Log.Error( $"Truncation will occur: {entityEntry.Metadata.GetTableName()}.{prop.GetColumnName()} {prop.GetColumnType()} :: {entry.Name}({valueLength}) = {value}");
            }
        }
    }
    throw ex;
}
Mark Foreman
  • 2,190
  • 18
  • 16
2

As mentioned in the comments, you need the full name and this can be read from the metadata.

public int GetColumnMaxLength(EntityEntry entityEntry)
{
    int result = 0;

    var table = entityEntry.Metadata.Model.FindEntityType(entityEntry.Metadata.ClrType);

    // Column info 
    foreach (var property in table.GetProperties())
    {
        var maxLength = property.GetMaxLength();

        // For sql info, e.g. ColumnType = nvarchar(255):
        var sqlInfo = property.SqlServer();
    };
    return result;
}