MVC 5 Entity Framework 6 Execute Stored Procedure

2020-05-29 02:04发布

问题:

I'm stuck. I have an existing application with an extremely large database and extensive library of stored procedures and functions. All I want to do is use a DbContext to execute a stored procedure and return a set of data or map to one of the entities in the context. Is that something magical I haven't discovered on the net somewhere? Someone, anyone, please help. Here's what I've got so far (and it doesn't return anything, the result is -1):

var contacts = db.Database.ExecuteSqlCommand("Contact_Search @LastName, @FirstName",
    new SqlParameter("@LastName", GetDataValue(args.LastName)),
    new SqlParameter("@FirstName", GetDataValue(args.FirstName)));

Executing that returns -1. I also tried something to the effect of this with no success:

DbRawSqlQuery<Contact> data = db.Database.SqlQuery<Contact>
                                   ("EXEC Contact_Search @LastName, @FirstName",
                                       GetDataValue(args.LastName), 
                                       GetDataValue(args.FirstName));

I understand that I could add an edmx and map to a stored procedure that way, but that is not the preferred method. Again, our database contains nearly 450 million records and a library of almost 3,000 stored procedures and functions. It would be a nightmare to maintain. Am I even starting in the right direction? Is Entity Framework the right choice?

回答1:

Wow, it seems right after I give up, I somehow stumble upon the answer. I found a FANTASTIC post about executing stored procedures and after reading up, this was my solution:

var contacts = db.Database.SqlQuery<Contact>("Contact_Search @LastName, @FirstName",

So, many thanks to Anuraj for his excellent post! The key to my solution was to first use SqlQuery instead of ExecuteSqlCommand, and also to execute the method mapping to my entity model (Contact).



回答2:

This code is better than SqlQuery() because SqlQuery() doesn't recognise the [Column] attribute. Here it is on a silver platter.

public static class StoredProcedureExtensions {   
/// <summary>
/// Execute Stored Procedure and return result in an enumerable object.
/// </summary>
/// <typeparam name="TEntity">Type of enumerable object class to return.</typeparam>
/// <param name="commandText">SQL query.</param>
/// <param name="parameters">SQL parameters.</param>
/// <param name="readOnly">Determines whether to attach and track changes for saving. Defaults to true and entities will not be tracked and thus a faster call.</param>
/// <returns>IEnumerable of entity type.</returns>
public static IEnumerable<TEntity> GetStoredProcedureResults<TEntity>(this DbContext dbContext, string query, Dictionary<string, object> parameters, bool readOnly = true) where TEntity : class, new()
{
  SqlParameter[] sqlParameterArray = DbContextExtensions.DictionaryToSqlParameters(parameters);

  return dbContext.GetStoredProcedureResults<TEntity>(query, sqlParameterArray, readOnly);
}

/// <summary>
/// Execute Stored Procedure and return result in an enumerable object.
/// </summary>
/// <typeparam name="TEntity">Type of enumerable object class to return.</typeparam>
/// <param name="commandText">SQL query.</param>
/// <param name="parameters">SQL parameters.</param>
/// <param name="readOnly">Determines whether to attach and track changes for saving. Defaults to true and entities will not be tracked and thus a faster call.</param>
/// <returns>IEnumerable of entity type.</returns>
public static IEnumerable<TEntity> GetStoredProcedureResults<TEntity>(this DbContext dbContext, string commandText, SqlParameter[] sqlParameterArray = null, bool readOnly = true) where TEntity : class, new()
{
  string infoMsg = commandText;
  try
  {
    //---- For a better error message
    if (sqlParameterArray != null)
    {
      foreach (SqlParameter p in sqlParameterArray)
      {
        infoMsg += string.Format(" {0}={1}, ", p.ParameterName, p.Value == null ? "(null)" : p.Value.ToString());
      }
      infoMsg = infoMsg.Trim().TrimEnd(',');
    }

    ///////////////////////////
    var reader = GetReader(dbContext, commandText, sqlParameterArray, CommandType.StoredProcedure);
    ///////////////////////////

    ///////////////////////////
    List<TEntity> results = GetListFromDataReader<TEntity>(reader);
    ///////////////////////////

    if(readOnly == false)
    {
      DbSet entitySet = dbContext.Set<TEntity>(); // For attaching the entities so EF can track changes
      results.ForEach(n => entitySet.Attach(n));  // Add tracking to each entity
    }

    reader.Close();
    return results.AsEnumerable();
  }
  catch (Exception ex)
  {
    throw new Exception("An error occurred while executing GetStoredProcedureResults(). " + infoMsg + ". Check the inner exception for more details.\r\n" + ex.Message, ex);
  }
}

//========================================= Private methods
#region Private Methods

private static DbDataReader GetReader(DbContext dbContext, string commandText, SqlParameter[] sqlParameterArray, CommandType commandType)
{
  var command = dbContext.Database.Connection.CreateCommand();
  command.CommandText = commandText;
  command.CommandType = commandType;
  if (sqlParameterArray != null) command.Parameters.AddRange(sqlParameterArray);

  dbContext.Database.Connection.Open();
  var reader = command.ExecuteReader(CommandBehavior.CloseConnection);
  return reader;
}

private static List<TEntity> GetListFromDataReader<TEntity>(DbDataReader reader) where TEntity : class, new()
{
  PropertyInfo[]                entityProperties = typeof(TEntity).GetProperties();
  IEnumerable<string>           readerColumnNames = (reader.GetSchemaTable().Select()).Select(r => r.ItemArray[0].ToString().ToUpper()); // uppercase reader column names. 
  List<MappingPropertyToColumn> propertyToColumnMappings = GetPropertyToColumnMappings<TEntity>(); // Maps the entity property names to the corresponding names of the columns in the reader

  var entityList = new List<TEntity>(); // Fill this
  while (reader.Read())
  {
    var element = Activator.CreateInstance<TEntity>();
    foreach (var entityProperty in entityProperties)
    {
      MappingPropertyToColumn mapping = propertyToColumnMappings._Find(entityProperty.Name);
      if (mapping == null) // This property has a [Not Mapped] attribute
      {
        continue; // Skip this one
      }

      var o = (object)reader[mapping.ColumnName]; // mapping must match all mapped properties to columns. If result set does not contain a column, then throw error like EF would.

      bool hasValue = o.GetType() != typeof(DBNull);

      if (mapping.IsEnum && hasValue) // Enum
      {
        entityProperty.SetValue(element, Enum.Parse(mapping.UnderlyingType, o.ToString()));
      }
      else
      {
        if (hasValue)
        { 
          entityProperty.SetValue(element, ChangeType(o, entityProperty.PropertyType)); 
        }
      }
    }
    entityList.Add(element);
  }

  return entityList;
}

public static object ChangeType(object value, Type conversion)
{
  var t = conversion;

  if (t.IsGenericType && t.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
  {
    if (value == null)
    {
      return null;
    }

    t = Nullable.GetUnderlyingType(t);
  }

  return Convert.ChangeType(value, t);
}

private static List<MappingPropertyToColumn> GetPropertyToColumnMappings<TEntity>() where TEntity : new()
{
  var type = typeof(TEntity);
  List<MappingPropertyToColumn> databaseMappings = new List<MappingPropertyToColumn>();

  foreach (var entityProperty in type.GetProperties())
  {
    bool isEnum = entityProperty.PropertyType.IsEnum;

    // [Not Mapped] Not Mapped Attribute
    var notMapped = entityProperty.GetCustomAttributes(false).FirstOrDefault(attribute => attribute is NotMappedAttribute);
    if (notMapped != null) // This property has a [Not Mapped] attribute
    {
      continue; // Skip this property 
    }

    // Determine if property is an enum
    Type underlyingType = null;
    if (entityProperty.PropertyType.IsGenericType && entityProperty.PropertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
    {
      underlyingType = Nullable.GetUnderlyingType(entityProperty.PropertyType); ;
      if (underlyingType != null && underlyingType.IsEnum)
      {
        isEnum = true;
      }
    }

    // [Column("tbl_columnname")] Column Name Attribute for mapping
    var columnMapping = entityProperty.GetCustomAttributes(false).FirstOrDefault(attribute => attribute is ColumnAttribute);

    if (columnMapping != null)
    {
      databaseMappings.Add(new MappingPropertyToColumn { PropertyName = entityProperty.Name, ColumnName = ((ColumnAttribute)columnMapping).Name.ToUpper(), IsEnum = isEnum, UnderlyingType = underlyingType }); // SQL case insensitive
    }
    else
    {
      databaseMappings._AddProperty(entityProperty.Name, entityProperty.Name, isEnum); // C# case sensitive
    }
  }

  return databaseMappings;
}

//====================================== Class for holding column mappings and other info for each property
private class MappingPropertyToColumn
{
  private string _propertyName;
  public string PropertyName
  {
    get { return _propertyName; }
    set { _propertyName = value; }
  }

  private string _columnName;
  public string ColumnName
  {
    get { return _columnName; }
    set { _columnName = value; }
  }

  private bool _isNullableEnum;
  public bool IsEnum
  {
    get { return _isNullableEnum; }
    set { _isNullableEnum = value; }
  }

  private Type _underlyingType;
  public Type UnderlyingType
  {
    get { return _underlyingType; }
    set { _underlyingType = value; }
  }

}

//======================================================= List<MappingPropertyToColumn> Extension methods
#region List<MappingPropertyToColumn> Extension methods
private static bool _ContainsKey<T>(this List<T> list, string key) where T : MappingPropertyToColumn
{
  return list.Any(x => x.PropertyName == key);
}
private static MappingPropertyToColumn _Find<T>(this List<T> list, string key) where T : MappingPropertyToColumn
{
  return list.Where(x => x.PropertyName == key).FirstOrDefault();
}
private static void _AddProperty<T>(this List<T> list, string propertyName, string columnName, bool isEnum, Type underlyingType = null) where T : MappingPropertyToColumn
{
  list.Add((T)new MappingPropertyToColumn { PropertyName = propertyName, ColumnName = columnName, IsEnum = isEnum, UnderlyingType = underlyingType }); // C# case sensitive
}
#endregion

#endregion  }