Подтвердить что ты не робот

Как использовать Entity Framework для сопоставления результатов хранимой процедуры с объектом с иными параметрами

Я пытаюсь создать базовый пример с использованием Entity Framework, чтобы выполнить сопоставление вывода хранимой процедуры SQL Server с сущностью на С#, но объект имеет разные (дружественные) параметры имен, а не более критические имена, Я также пытаюсь сделать это с синтаксисом Fluent (т.е. Не edmx).


Что работает....

Сохраненная процедура возвращает значения, называемые: UT_ID, UT_LONG_NM, UT_STR_AD, UT_CITY_AD, UT_ST_AD, UT_ZIP_CD_AD, UT_CT

Если я создаю такой объект...

public class DBUnitEntity
{
    public Int16 UT_ID { get; set; }
    public string UT_LONG_NM { get; set; }
    public string UT_STR_AD { get; set; }
    public string UT_CITY_AD { get; set; }
    public string UT_ST_AD { get; set; }
    public Int32 UT_ZIP_CD_AD { get; set; }
    public string UT_CT { get; set; } 
}

и EntityTypeConfiguration, как это...

public class DbUnitMapping: EntityTypeConfiguration<DBUnitEntity>
{
        public DbUnitMapping()
        {
            HasKey(t => t.UT_ID);
        }
}

..., который я добавляю в OnModelCreating из DbContext, тогда я могу получить объекты просто отлично из базы данных, что приятно, используя это....

var allUnits = _context.Database.SqlQuery<DBUnitEntity>(StoredProcedureHelper.GetAllUnitsProc);

НО, что не работает

Если я хочу сущность, подобную этой, с более дружественными именами....

public class UnitEntity : IUnit
{
    public Int16 UnitId { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string State { get; set; }
    public Int32 Zip { get; set; }
    public string Category { get; set; }
}

и EntityTypeConfiguration, как это...

    public UnitMapping()
    {
        HasKey(t => t.UnitId);

        Property(t => t.UnitId).HasColumnName("UT_ID");
        Property(t => t.Name).HasColumnName("UT_LONG_NM");
        Property(t => t.Address).HasColumnName("UT_STR_AD");
        Property(t => t.City).HasColumnName("UT_CITY_AD");
        Property(t => t.State).HasColumnName("UT_ST_AD");
        Property(t => t.Zip).HasColumnName("UT_ZIP_CD_AD");
        Property(t => t.Category).HasColumnName("UT_CT");
    }

Когда я пытаюсь получить данные, я получаю сообщение System.Data.EntityCommandExecutionException с сообщением....

"Устройство чтения данных несовместимо с указанным" DataAccess.EFCodeFirstSample.UnitEntity ". Элемент типа" UnitId "не имеет соответствующего столбца в устройстве чтения данных с тем же именем".

Если я добавлю свойство "хранимой процедуры с именем" к сущности, оно отправляется и жалуется на следующее "неизвестное" свойство.

Не работает ли "HasColumnName", как я ожидаю/хочу, чтобы он был в этой первой хранимой процедуре, написанной в коде, в стиле EF?


Update:

Пробовал использовать DataAnnotations (ключ из ComponentModel и столбец из EntityFramework)... ala

public class UnitEntity : IUnit
{
    [Key]
    [Column("UT_ID")]
    public Int16 UnitId { get; set; }
    public string Name { get; set; }

Это исключило необходимость любой EntityTypeConfiguration вообще для DBUnitEntity с идентичным именованием базы данных (т.е. просто добавлением атрибута [Key]), но ничего не сделал для объекта с именами свойств, которые не соответствуют базе данных (та же ошибка, что и раньше).

Я не возражаю против использования аннотаций ComponentModel в модели, но я действительно не хочу использовать аннотации EntityFramework в модели, если я могу помочь (не хочу привязывать модель к любому конкретному доступу к данным рамки)

4b9b3361

Ответ 1

Из Сначала введите код Framework Entity Framework (стр. 155):

Метод SQLQuery всегда пытается сопоставить столбцы с собственностью на основе имени свойства... Нет, что сопоставление имен столбцов и свойств не учитывает никакого сопоставления. Например, если вы сопоставили свойство DestinationId с столбцом Id в таблице Destination, метод SqlQuery не использовал бы это сопоставление.

Таким образом, вы не можете использовать сопоставления при вызове хранимой процедуры. Одним из способов является изменение хранимой процедуры для возврата результата с псевдонимами для каждого столбца, который будет соответствовать вашим именам свойств объекта.

Select UT_STR_AD as Address From SomeTable и т.д.

Ответ 2

Это не использует Entity Framework, но это связано с dbcontext. Я потратил часы на часы, прочесывая интернет и используя точку заглядывания все впустую. Я читал некоторые, где ColumnAttribute игнорируется для SqlQueryRaw. Но я создал что-то с отражением, дженериками, sql datareader и Activator. Я собираюсь проверить это на нескольких других процессах. Если есть какая-либо другая проверка ошибок, которая должна войти, комментарий.

public static List<T> SqlQuery<T>( DbContext db, string sql, params object[] parameters)
    {

        List<T> Rows = new List<T>();
        using (SqlConnection con = new SqlConnection(db.Database.Connection.ConnectionString))
        {
            using (SqlCommand cmd = new SqlCommand(sql, con))
            {
                cmd.CommandType = CommandType.StoredProcedure;
                foreach (var param in parameters)
                    cmd.Parameters.Add(param);
                con.Open();
                using (SqlDataReader dr = cmd.ExecuteReader())
                {
                    if (dr.HasRows)
                    {
                        var dictionary = typeof(T).GetProperties().ToDictionary(
                   field => CamelCaseToUnderscore(field.Name), field => field.Name);
                        while (dr.Read())
                        {
                            T tempObj = (T)Activator.CreateInstance(typeof(T));
                            foreach (var key in dictionary.Keys)
                            {
                                PropertyInfo propertyInfo = tempObj.GetType().GetProperty(dictionary[key], BindingFlags.Public | BindingFlags.Instance);
                                if (null != propertyInfo && propertyInfo.CanWrite)
                                    propertyInfo.SetValue(tempObj, Convert.ChangeType(dr[key], propertyInfo.PropertyType), null);
                            }
                            Rows.Add(tempObj);
                        }
                    }
                    dr.Close();
                }
            }
        }
        return Rows;
    }

    private static string CamelCaseToUnderscore(string str)
    {
        return Regex.Replace(str, @"(?<!_)([A-Z])", "_$1").TrimStart('_').ToLower();
    }

Также необходимо знать, что все наши хранимые процедуры возвращают нижнее нижнее нижнее подчеркивание. CamelCaseToUnderscore построен специально для него.

Теперь BigDeal может сопоставляться с big_deal

Вы должны иметь возможность называть его так

Namespace.SqlQuery<YourObj>(db, "name_of_stored_proc", new SqlParameter("@param",value),,,,,,,);

Ответ 3

Пример, опубликованный "DeadlyChambers", замечательный, но я хотел бы расширить этот пример, чтобы включить ColumnAttribute, который вы можете использовать с EF, чтобы добавить к свойствам, чтобы сопоставить поле SQL с классом.

Ex.

[Column("sqlFieldName")]
public string AdjustedName { get; set; }

Вот модифицированный код.
Этот код также включает параметр, который позволяет настраивать сопоставления, если необходимо, путем передачи словаря.
Вам понадобится конвертер типов, отличный от Convert.ChangeType, для таких вещей, как типы с нулевым значением.
Ex. Если у вас есть бит, бит в базе данных и с нулевым значением boolean в .NET, вы получите проблему с преобразованием типа.

/// <summary>
/// WARNING: EF does not use the ColumnAttribute when mapping from SqlQuery. So this is a "fix" that uses "lots" of REFLECTION
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="database"></param>
/// <param name="sqlCommandString"></param>
/// <param name="modelPropertyName_sqlPropertyName">Model Property Name and SQL Property Name</param>
/// <param name="sqlParameters">SQL Parameters</param>
/// <returns></returns>
public static List<T> SqlQueryMapped<T>(this System.Data.Entity.Database database, 
    string sqlCommandString, 
    Dictionary<string,string> modelPropertyName_sqlPropertyName, 
    params System.Data.SqlClient.SqlParameter[] sqlParameters)
{
    List<T> listOfT = new List<T>();

    using (var cmd = database.Connection.CreateCommand())
    {
        cmd.CommandText = sqlCommandString;
        if (cmd.Connection.State != System.Data.ConnectionState.Open)
        {
            cmd.Connection.Open();
        }

        cmd.Parameters.AddRange(sqlParameters);

        using (var dataReader = cmd.ExecuteReader())
        {
            if (dataReader.HasRows)
            {
                // HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
                var convertTo = typeof(GenericExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(mi => mi.Name == "ConvertTo").Where(m => m.GetParameters().Count() == 1).FirstOrDefault();

                // now build a new list of the SQL properties to map
                // NOTE: this method is used because GetOrdinal can throw an exception if column is not found by name
                Dictionary<string, int> sqlPropertiesAttributes = new Dictionary<string, int>();
                for (int index = 0; index < dataReader.FieldCount; index++)
                {
                    sqlPropertiesAttributes.Add(dataReader.GetName(index), index);
                }

                while (dataReader.Read())
                {
                    // create a new instance of T
                    T newT = (T)Activator.CreateInstance(typeof(T));

                    // get a list of the model properties
                    var modelProperties = newT.GetType().GetProperties();

                    // now map the SQL property to the EF property
                    foreach (var propertyInfo in modelProperties)
                    {
                        if (propertyInfo != null && propertyInfo.CanWrite)
                        {
                            // determine if the given model property has a different map then the one based on the column attribute
                            string sqlPropertyToMap = (propertyInfo.GetCustomAttribute<ColumnAttribute>()?.Name ?? propertyInfo.Name);
                            string sqlPropertyName;
                            if (modelPropertyName_sqlPropertyName!= null && modelPropertyName_sqlPropertyName.TryGetValue(propertyInfo.Name, out sqlPropertyName))
                            {
                                sqlPropertyToMap = sqlPropertyName;
                            }

                            // find the SQL value based on the column name or the property name
                            int columnIndex;
                            if (sqlPropertiesAttributes.TryGetValue(sqlPropertyToMap, out columnIndex))
                            {
                                var sqlValue = dataReader.GetValue(columnIndex);

                                // ignore this property if it is DBNull
                                if (Convert.IsDBNull(sqlValue))
                                {
                                    continue;
                                }

                                // HACK: you can't use extension methods without a type at design time. So this is a way to call an extension method through reflection.
                                var newValue = convertTo.MakeGenericMethod(propertyInfo.PropertyType).Invoke(null, new object[] { sqlValue });

                                propertyInfo.SetValue(newT, newValue);
                            }
                        }
                    }

                    listOfT.Add(newT);
                }
            }
        }
    }

    return listOfT;
}