Dynamic Translate to avoid C# syntax errors
Asked Answered
F

1

4

Consider the following database table (SQL Server 2005). I'd like to use this in EF (v6, .net 4.5.1) with the Translate function but after searching seems this is not supported.

CREATE TABLE Foo 
(
     pk INT NOT NULL PRIMARY KEY, 
     Foo VARCHAR(100)
)

Using by-convention mapping that would create a class Foo with a property Foo which is not supported by C# syntax. I tried using the ColumnAttribute:

public partial class Foo
{
    [Key]
    public virtual int pk {get;set;}
    [Column("Foo")]
    public virtual string Name {get;set;}
}

This appears to work, but I'd like to make the initial page request load gobs of data via stored procedure and MARS (and use a generic structure so I can reuse it on other pages), so I called the stored procedure and looped through the result sets, calling ObjectContext.Translate via reflection (similar to the below, but this is abbreviated):

var methTranslate = typeof(ObjectContext).GetMethod("Translate", new[] { typeof(DbDataReader), typeof(string), typeof(MergeOption) });

foreach (var className in classNames)
{
    // ...
    var translateGenericMethod = methTranslate.MakeGenericMethod(classType);
    // ...
    reader.NextResult();
    var enumerable = (IEnumerable)translateGenericMethod.Invoke(ObjectContext, 
        new object[] { reader, entitySet.Name, MergeOption.AppendOnly });
}

From multiple things I've read, the ColumnAttribute mapping is not supported. From MSDN:

EF does not take any mapping into account when it creates entities using the Translate method. It will simply match column names in the result set with property names on your classes.

And sure enough, I get and error:

The data reader is incompatible with the specified 'Namespace.Foo'. A member of the type, 'Name', does not have a corresponding column in the data reader with the same name.

The problem is, I do not see any alternative or way to specify/hint at the mapping. I could change the class name but that is less desirable than the property names.

Any workarounds, or any other way to dynamically load data without using Translate?

Frampton answered 29/9, 2016 at 20:18 Comment(1)
Read stored procedures + dynamic data structures = Dapper.Nessa
S
3

A bit tricky, but doable.

The idea is to utilize the Translate method by implementing and using a custom DbDataReader that performs the required mapping.

Before doing that, let implement a generic DbDataReader class that does just delegating to the underlying DbDataReader:

abstract class DelegatingDbDataReader : DbDataReader
{
    readonly DbDataReader source;
    public DelegatingDbDataReader(DbDataReader source)
    {
        this.source = source;
    }
    public override object this[string name] { get { return source[name]; } }
    public override object this[int ordinal] { get { return source[ordinal]; } }
    public override int Depth { get { return source.Depth; } }
    public override int FieldCount { get { return source.FieldCount; } }
    public override bool HasRows { get { return source.HasRows; } }
    public override bool IsClosed { get { return source.IsClosed; } }
    public override int RecordsAffected { get { return source.RecordsAffected; } }
    public override bool GetBoolean(int ordinal) { return source.GetBoolean(ordinal); }
    public override byte GetByte(int ordinal) { return source.GetByte(ordinal); }
    public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) { return source.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); }
    public override char GetChar(int ordinal) { return source.GetChar(ordinal); }
    public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) { return source.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); }
    public override string GetDataTypeName(int ordinal) { return source.GetDataTypeName(ordinal); }
    public override DateTime GetDateTime(int ordinal) { return source.GetDateTime(ordinal); }
    public override decimal GetDecimal(int ordinal) { return source.GetDecimal(ordinal); }
    public override double GetDouble(int ordinal) { return source.GetDouble(ordinal); }
    public override IEnumerator GetEnumerator() { return source.GetEnumerator(); }
    public override Type GetFieldType(int ordinal) { return source.GetFieldType(ordinal); }
    public override float GetFloat(int ordinal) { return source.GetFloat(ordinal); }
    public override Guid GetGuid(int ordinal) { return source.GetGuid(ordinal); }
    public override short GetInt16(int ordinal) { return source.GetInt16(ordinal); }
    public override int GetInt32(int ordinal) { return source.GetInt32(ordinal); }
    public override long GetInt64(int ordinal) { return source.GetInt64(ordinal); }
    public override string GetName(int ordinal) { return source.GetName(ordinal); }
    public override int GetOrdinal(string name) { return source.GetOrdinal(name); }
    public override string GetString(int ordinal) { return source.GetString(ordinal); }
    public override object GetValue(int ordinal) { return source.GetValue(ordinal); }
    public override int GetValues(object[] values) { return source.GetValues(values); }
    public override bool IsDBNull(int ordinal) { return source.IsDBNull(ordinal); }
    public override bool NextResult() { return source.NextResult(); }
    public override bool Read() { return source.Read(); }
    public override void Close() { source.Close(); }
    public override T GetFieldValue<T>(int ordinal) { return source.GetFieldValue<T>(ordinal); }
    public override Task<T> GetFieldValueAsync<T>(int ordinal, CancellationToken cancellationToken) { return source.GetFieldValueAsync<T>(ordinal, cancellationToken); }
    public override Type GetProviderSpecificFieldType(int ordinal) { return source.GetProviderSpecificFieldType(ordinal); }
    public override object GetProviderSpecificValue(int ordinal) { return source.GetProviderSpecificValue(ordinal); }
    public override int GetProviderSpecificValues(object[] values) { return source.GetProviderSpecificValues(values); }
    public override DataTable GetSchemaTable() { return source.GetSchemaTable(); }
    public override Stream GetStream(int ordinal) { return source.GetStream(ordinal); }
    public override TextReader GetTextReader(int ordinal) { return source.GetTextReader(ordinal); }
    public override Task<bool> IsDBNullAsync(int ordinal, CancellationToken cancellationToken) { return source.IsDBNullAsync(ordinal, cancellationToken); }
    public override Task<bool> ReadAsync(CancellationToken cancellationToken) { return source.ReadAsync(cancellationToken); }
    public override int VisibleFieldCount { get { return source.VisibleFieldCount; } }
}

Nothing fancy - annoyingly overriding all abstract/meaningful virtual members and delegate to the underlying object.

Now the reader that performs name mapping:

class MappingDbDataReader : DelegatingDbDataReader
{
    Dictionary<string, string> nameToSourceNameMap;
    public MappingDbDataReader(DbDataReader source, Dictionary<string, string> nameToSourceNameMap) : base(source)
    {
        this.nameToSourceNameMap = nameToSourceNameMap;
    }
    private string GetSourceName(string name)
    {
        string sourceName;
        return nameToSourceNameMap.TryGetValue(name, out sourceName) ? sourceName : name;
    }
    public override object this[string name]
    {
        get { return base[GetSourceName(name)]; }
    }
    public override string GetName(int ordinal)
    {
        string sourceName = base.GetName(ordinal);
        return nameToSourceNameMap
            .Where(item => item.Value.Equals(sourceName, StringComparison.OrdinalIgnoreCase))
            .Select(item => item.Key)
            .FirstOrDefault() ?? sourceName;
    }
    public override int GetOrdinal(string name)
    {
        return base.GetOrdinal(GetSourceName(name));
    }
}

Again, nothing fancy. Override a few methods and perform a name to column name and vice versa mapping.

Finally, a helper method that does what you are asking:

public static class EntityUtils
{
    public static ObjectResult<T> ReadSingleResult<T>(this DbContext dbContext, DbDataReader dbReader)
        where T : class
    {
        var objectContext = ((IObjectContextAdapter)dbContext).ObjectContext;
        var columnMappings = objectContext.GetPropertyMappings(typeof(T))
            .ToDictionary(m => m.Property.Name, m => m.Column.Name);
        var mappingReader = new MappingDbDataReader(dbReader, columnMappings);
        return objectContext.Translate<T>(mappingReader);
    }

    static IEnumerable<ScalarPropertyMapping> GetPropertyMappings(this ObjectContext objectContext, Type clrEntityType)
    {
        var metadata = objectContext.MetadataWorkspace;

        // Get the part of the model that contains info about the actual CLR types
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));

        // Get the entity type from the model that maps to the CLR type
        var entityType = metadata
                .GetItems<EntityType>(DataSpace.OSpace)
                      .Single(e => objectItemCollection.GetClrType(e) == clrEntityType);

        // Get the entity set that uses this entity type
        var entitySet = metadata
            .GetItems<EntityContainer>(DataSpace.CSpace)
                  .Single()
                  .EntitySets
                  .Single(s => s.ElementType.Name == entityType.Name);

        // Find the mapping between conceptual and storage model for this entity set
        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                      .Single()
                      .EntitySetMappings
                      .Single(s => s.EntitySet == entitySet);

        // Find the storage property (column) mappings
        var propertyMappings = mapping
            .EntityTypeMappings.Single()
            .Fragments.Single()
            .PropertyMappings
            .OfType<ScalarPropertyMapping>();


        return propertyMappings;
    }

ReadSingleResult is the helper method in question. The GetPropertyMappings method is using part of the code from EF6.1 Get Mapping Between Properties and Columns.

Sample usage similar to the provided example:

var readMethodBase = typeof(EntityUtils).GetMethod("ReadSingleResult", new[] { typeof(DbContext), typeof(DbDataReader) });

foreach (var className in classNames)
{
    // ...
    var readMethod = readMethodBase.MakeGenericMethod(classType);
    var result = ((IEnumerable)readMethod.Invoke(null, new object[] { dbContext, dbReader }))
        .Cast<dynamic>()
        .ToList();
    // ...
    dbReader.NextResult();
}

Hope that helps.

Stenophagous answered 30/9, 2016 at 19:39 Comment(4)
This appears to work but one thing Translate was doing was adding the results to the DbContext. It appears this requires doing that explicitly and when I added while (enumarator.MoveNext()) { this.Entry(enumarator.Current).State = EntityState.Unchanged; }; to the foreach loop, the loading took 30+ seconds :( I'll mark this as solution but maybe I'm trying to accomplish the wrong problem.Frampton
@Matthew Actually one difference is that I used a simpler Translate overload. You can take the custom data reader from my sample and use the overload with entitySet.Name, MergeOption.AppendOnly as in your original code.Stenophagous
AH! my fault for copy-and-pasting without thinking. That took it down to 2.3 seconds. Thanks!Frampton
So basically, you can't use Translate to read entities, because it doesn't actually translate the column names, and it even throws an exception if you try to load a table-per-hierarchy type, because it's too dumb to read the Discriminator column and instantiate the right subtype instead of throwing an error about not being able to construct an instance of an abstract base type. Worthless. Impossible to read entities from a sproc with EF.Bursitis

© 2022 - 2024 — McMap. All rights reserved.