Entity Framework Core 存储过程

9
我正在使用EF Core(code-first)工作,并希望从存储过程中获取数据。返回的结果应该是基于返回结果定义的类。
如果返回类型是实体之一,我可以使其工作。例如:
_context.Set<Entity>().FromSql("dbo.Stored_Proc").ToList(); 

但是,如果我的返回值不是上下文中的实体,则无法完成。

任何帮助将不胜感激。


所以你想让Entity Framework不返回实体吗?你确定你不是在寻找类似于Dapper的东西吗? - Jeroen Mostert
我想返回数据,但不是直接映射到表的实体。我的数据与上下文中的任何实体都不完全匹配。 - GSH
对于最简单和最完整的答案,请查看此链接:https://dev59.com/Fl4b5IYBdhLWcg3w3k-Y#75465142 - Ash K
你可以考虑使用用户定义函数来替代存储过程。请参考https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping。 - undefined
4个回答

16

这很简单。

以下是三个步骤:

  1. 创建一个与您的SP返回相同属性的模型类,例如:

    public class SPModel
    {
        public int Id { get; set; }
        public DateTime? CreatedDateTime { get; set; }
        etc...
    }
  1. 像这样将该模型类插入到您的dbContext类中:
    public class YourDbContext : DbContext
    {
        public virtual DbSet<SPModel> YourDbSet { get; set; }
        ....
    }

请不要将此类映射到任何SQL表中!

  1. 在调用存储过程时使用它,例如:
  1. 在调用存储过程时使用它,例如:
    var spModels = await dbContext.YourDbSet
        .FromSqlRaw("EXECUTE yourStoredProcedure {0},{1}", param1, param2)
        .ToListAsync();

一些有用的内容:

  • 你可以使用 try/catch 块来查看是否错过了某些属性
  • 您可以扩展您的模型类以添加新属性,但避免使用“set”访问器,例如:public bool IsLoaded { get; }
  • 如果您的 SP 返回一些可空类型,请小心处理,在这种情况下,模型也必须具有可空类型

正确。我在谈论EF Core。https://learn.microsoft.com/en-us/ef/core/querying/raw-sql 我在我的项目中使用它。至少在EF Core 1.1中。 - Semionoff
1
啊...抱歉,是我疏忽了。我忘记在顶部添加“using”指令了。我真是太傻了。难怪编译器无法识别“FromSql”方法。 - Firanto
无论如何...我正在使用PostgreSQL,对FromSql()中的参数感到头痛。我有一个DateTime值,我想将其转换为PostgreSQL中的date,但系统会自动将DateTime转换为timestamp without timezone。是否有任何方法可以像在AdoDB中使用的那样指定NpgsqlDbType?我的意思是,在AdoDB中,我们可以使用这个:cmd.Parameters.Add("param", NpgsqlDbType.Date);。如何在FromSql()中实现这个功能? - Firanto
没事了... FromSql() 可以使用 SqlParameter 对象来定义参数。在 PostgreSQL 中,我使用 NpgsqlParameterSqlParameternpgsql 实现)。问题解决了。 :) - Firanto
1
此外,您需要设置 modelBuilder.Entity<SPModel>().HasNoKey(); 并使用 [NotMapped] 属性标记 SPModel - Roman Borovets
显示剩余2条评论

3
Entity Framework Net Core 2.0: 执行存储过程并将结果映射为自定义对象列表
EF Core对存储过程的支持类似于早期版本的EF Code First。
您需要通过从EF继承DbContext类来创建DbContext类。使用DbContext执行存储过程。
我决定编写一些方法来帮助我执行存储过程和对象映射其结果。如果您有一个用于选择表中所有行的存储过程,则可以实现此操作。
第一步是编写一个方法,从DbContext创建DbCommand。
public static DbCommand LoadStoredProc(
  this DbContext context, string storedProcName)
{
  var cmd = context.Database.GetDbConnection().CreateCommand();
  cmd.CommandText = storedProcName;
  cmd.CommandType = System.Data.CommandType.StoredProcedure;
  return cmd;
}

使用以下方法将参数传递给存储过程。
public static DbCommand WithSqlParam(
  this DbCommand cmd, string paramName, object paramValue)
{
  if (string.IsNullOrEmpty(cmd.CommandText))
    throw new InvalidOperationException(
      "Call LoadStoredProc before using this method");
  var param = cmd.CreateParameter();
  param.ParameterName = paramName;
  param.Value = paramValue;
  cmd.Parameters.Add(param);
  return cmd;
}

最后,为了将结果映射为自定义对象列表,请使用MapToList方法。
private static List<T> MapToList<T>(this DbDataReader dr)
{
  var objList = new List<T>();
  var props = typeof(T).GetRuntimeProperties();

  var colMapping = dr.GetColumnSchema()
    .Where(x => props.Any(y => y.Name.ToLower() == x.ColumnName.ToLower()))
    .ToDictionary(key => key.ColumnName.ToLower());

  if (dr.HasRows)
  {
    while (dr.Read())
    {
      T obj = Activator.CreateInstance<T>();
      foreach (var prop in props)
      {
        var val = 
          dr.GetValue(colMapping[prop.Name.ToLower()].ColumnOrdinal.Value);
          prop.SetValue(obj, val == DBNull.Value ? null : val);
      }
      objList.Add(obj);
    }
  }
  return objList;
}

现在我们准备使用ExecuteStoredProc方法执行存储过程,并将其映射到作为T传递的列表类型。
public static async Task<List<T>> ExecuteStoredProc<T>(this DbCommand command)
{
  using (command)
  {
    if (command.Connection.State == System.Data.ConnectionState.Closed)
    command.Connection.Open();
    try
    {
      using (var reader = await command.ExecuteReaderAsync())
      {
        return reader.MapToList<T>();
      }
    }
    catch(Exception e)
    {
      throw (e);
    }
    finally
    {
      command.Connection.Close();
    }
  }
}

例如,要执行名为“StoredProcedureName”的存储过程,其中包含名为“firstparamname”和“secondparamname”的两个参数,这是其实现方式。
List<MyType> myTypeList = new List<MyType>();
using(var context = new MyDbContext())
{
  myTypeList = context.LoadStoredProc("StoredProcedureName")
  .WithSqlParam("firstparamname", firstParamValue)
  .WithSqlParam("secondparamname", secondParamValue).
  .ExecureStoredProc<MyType>();
}

我希望那就是你需要的。

1
不想说,但有人在没有给你信用的情况下制作了一个你代码的git仓库。https://www.sinclairinat0r.com/2017/05/06/entity-framework-core--mapping-stored-procedures,-fluently - sksallaj
3
嗨@sksallaj,请不要挑起争端,因为我的代码自2017年5月以来就已经发布在我的博客和GitHub存储库上了(实际上甚至在那之前就存在了)。 https://github.com/snickler/EFCore-FluentStoredProcedure/commit/97f70d9b24fd8c81de01b61b32e6e611606447ac - snickler
2
谢谢回复!我日期看错了!你的代码仓库真棒,帮助了我很多。 - sksallaj

3

可以通过以下扩展实现,而无需定义任何DbQuery或DbSet。适用于Efcore 3及以上版本。

public class CustomType 
{
   public int Id { get; set; }
   public string Name { get; set; }
}

public static class DbContextExtensions
{
    public static IList<T> SqlQuery<T>(this DbContext context, string sql, params object[] parameters) where T : class
    {
        using (var dbcontext = new ContextForQueryType<T>(context.Database.GetDbConnection()))
        {
            return dbcontext.Set<T>().FromSqlRaw(sql, parameters).AsNoTracking().ToList();
        }
    }

    public static async Task<IList<T>> SqlQueryAsync<T>(this DbContext context, string sql, params object[] parameters) where T : class
    {
        using (var dbcontext = new ContextForQueryType<T>(context.Database.GetDbConnection()))
        {
            return await dbcontext.Set<T>().FromSqlRaw(sql, parameters).AsNoTracking().ToListAsync();
        }
    }

private class ContextForQueryType<T> : DbContext where T : class
{
    private readonly System.Data.Common.DbConnection connection;

    public ContextForQueryType(System.Data.Common.DbConnection connection)
    {
        this.connection = connection;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(connection, options => options.EnableRetryOnFailure());

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<T>().HasNoKey();
        base.OnModelCreating(modelBuilder);
    }
  } 
}

并且像这样执行:

var param = new SqlParameter("@IdParam", SqlDbType.VarChar, 10);
param.Value = Id.ToString();

string sqlQuery = "Exec [dbo].[usp_get_custom_type] @IdParam";

await context.SqlQueryAsync<CustomType>(sqlQuery);

0

我尝试了所有其他解决方案,但对我没有用。但我找到了一个正确的解决方案,它可能对这里的某个人有帮助。

我的原始答案- https://dev59.com/Fl4b5IYBdhLWcg3w3k-Y#57224037

要调用存储过程并将结果返回到EF Core模型列表中,我们必须遵循3个步骤。

第一步。 您需要添加一个新类,就像您的实体类一样。该类应具有与SP中所有列相对应的属性。例如,如果您的SP返回名为IdName的两列,则您的新类应该是:

public class MySPModel
{
    public int Id {get; set;}
    public string Name {get; set;}
}

步骤2。

然后,您必须将一个DbQuery属性添加到您的DBContext类中,以用于您的SP。

public partial class Sonar_Health_AppointmentsContext : DbContext
{
        public virtual DbSet<Booking> Booking { get; set; } // your existing DbSets
        ...

        public virtual DbQuery<MySPModel> MySP { get; set; } // your new DbQuery
        ...
}

步骤三。

现在,您将能够从您的DBContext调用并获取您的SP的结果。

var result = await _context.Query<MySPModel>().AsNoTracking().FromSql(string.Format("EXEC {0} {1}", functionName, parameter)).ToListAsync();

我正在使用通用的UnitOfWork & Repository。因此,我的执行SP的函数是

/// <summary>
/// Execute function. Be extra care when using this function as there is a risk for SQL injection
/// </summary>
public async Task<IEnumerable<T>> ExecuteFuntion<T>(string functionName, string parameter) where T : class
{
    return await _context.Query<T>().AsNoTracking().FromSql(string.Format("EXEC {0} {1}", functionName, parameter)).ToListAsync();
}

希望对某人有所帮助!!!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接