Как использовать 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 в модели, если я могу помочь (не хочу привязывать модель к любому конкретному доступу к данным рамки)
Ответы
Ответ 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;
}