5

I have to call a stored procedure which is selecting records from multiple tables.

I have tried the following code, but it's returning null for columns from other tables than the entity class.

private async Task<IEnumerable<TEntity>> InvokeStoredProcedureAsync(string input = "")
{
    var storedProcedureName = "sp_BulkSelect";

    using (var db = new MyDbContext(_options))
    {
        var result = await db.Set<TEntity>().FromSql(storedProcedureName + " @inputIds", new SqlParameter("inputIds", input)).ToListAsync();
        return result;
    }
}

Stored procedure:

SELECT 
    [MainTable].[Id],
    [Table1Id],
    [Table2Id],
    [MainTable].[Table1Code],
    [Table2].[Table2Code]
FROM
    [MainTable] [MainTable]
LEFT JOIN 
    [Table1] [Table1] ON [MainTable].Table1Id = [Table1].[Id]
LEFT JOIN 
    [Table2] [Table2] ON [MainTable].[Table2Id] = [Table2].[Id];

MainTable class:

[Table("MainTable")]
public class MainTable : FullAuditedEntity
{

    [ForeignKey("Table1Id")]
    public virtual Table1 Table1 { get; set; }
    public virtual int Table1Id { get; set; }

    [ForeignKey("Table2Id")]
    public virtual Table2 Table2 { get; set; }
    public virtual int? Table2Id { get; set; }      

}

So when I call this stored procedure, Table1Code and Table2Code are missing in the return value.

I tried to add the following code in MainTable class, but its also not working.

[NotMapped]
public virtual string Table2Code { get; set; }

[NotMapped]
public virtual string Table1Code { get; set; }

Then I removed [NotMapped] from both the properties and added migration, in this case, its returning proper value. But It will add two columns in MainTable. It's really a BAD design.

So my question is how to select columns from multiple tables in the stored procedure in Entity Framework Core.

I'm using EF Core 2.0.

I think there has to be some way to call the stored procedure with using Entity and then map it to any class because select columns from multiple tables using join is a very basic requirement.

I tried the similar solution, but its giving compilation error.

'DatabaseFacade' does not contain a definition for 'SqlQuery' and no extension method 'SqlQuery' accepting a first argument of type 'DatabaseFacade' could be found (are you missing a using directive or an assembly reference?)

Vivek Nuna
  • 25,472
  • 25
  • 109
  • 197
  • "When I call this SP, it's returning `null` value for `Table1Code`" → Well, where's `Table1Code` defined? – aaron Feb 03 '18 at 07:14
  • "I tried to add the following code in MainArticle class, but its also not working." → How is `MainArticle` related to `MainTable`? – aaron Feb 03 '18 at 07:15
  • @aaron I have modified the question, please check – Vivek Nuna Feb 03 '18 at 07:36
  • It was typing mistake – Vivek Nuna Feb 03 '18 at 07:44
  • Where's `Table1Code` defined? – aaron Feb 03 '18 at 07:45
  • Initially, It's not defined in `MainTable`. But later I tried `[NotMapped] Table1Code` and then `Table1Code` in `MainTable`. – Vivek Nuna Feb 03 '18 at 07:47
  • Possible duplicate of [SqlQuery into a \[NotMapped\] field?](https://stackoverflow.com/questions/25391619/sqlquery-into-a-notmapped-field) – aaron Feb 03 '18 at 08:12
  • Tried, But it's giving compilation error. 'DatabaseFacade' does not contain a definition for 'SqlQuery' and no extension method 'SqlQuery' accepting a first argument of type 'DatabaseFacade' could be found (are you missing a using directive or an assembly reference?) – Vivek Nuna Feb 03 '18 at 09:08
  • See https://stackoverflow.com/questions/35631903/raw-sql-query-without-dbset-entity-framework-core/46013305#46013305 – aaron Feb 03 '18 at 09:25
  • It's not possible currently. Eventually in the next EF Core 2.1 release - see **Read-only view types in the model** in the [Roadmap](https://github.com/aspnet/EntityFrameworkCore/wiki/Roadmap) – Ivan Stoev Feb 03 '18 at 13:16
  • Then what’s workaround for this now? And when can I expect for core 2.1 release? – Vivek Nuna Feb 03 '18 at 14:49

4 Answers4

4

The complete idea to get data from a stored procedure is as follows:

  1. You need to add an entity that has the same properties as the procedures select query has.
  2. Add the entity to your DbContext and Create a migration. Change the code in the Up() and Down() methods of the migration so that it creates the procedure in the database.
  3. Now use the FromSql() method to get the data a normal entity data.

Here is some code that can guide you. Suppose you have these entities in your application domain:

  1. Student
  2. Parent
  3. SchoolClass
  4. Section
  5. Enrollment

Migrations up method

protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "StudentDetails");

        migrationBuilder.Sql(
            @"create proc GetStudentDetail
            @ssid int,
            @sectionId int = null
            as
            select Id, name, Gender, RollNumber, Status, Type,
            FatherName, FatherContact, SchoolClass, Section,
            SsId, SectionId, EnrollmentId
            from 
            (
                SELECT stu.Id, stu.name, stu.Gender, en.RollNumber, en.Status, en.Type,
                p.FatherName, p.FatherContact, sc.Name as SchoolClass, sec.Name as Section,
                ss.SessionId as SsId, sec.Id as SectionId, en.Id as EnrollmentId,
                en.EntryDate, row_number() over (partition by studentid order by en.entrydate desc) as rowno
                from SchoolSessions ss
                join SchoolClasses sc on ss.SessionId = sc.ssid
                join Sections sec on sc.Id = sec.ClassId
                join Enrollments en on sec.id = en.SectionId
                join Students stu on en.StudentId = stu.Id
                join parents p on stu.ParentId = p.Id 
                where ss.SessionId = @ssid 
            ) A
            where rowno = 1 and
            (SectionId = @sectionId or @sectionId is null)"
            );
    }

Migrations down method

protected override void Down(MigrationBuilder migrationBuilder)
    {

        migrationBuilder.Sql("drop proc GetStudentDetail");

        migrationBuilder.CreateTable(
            name: "StudentDetails",
            columns: table => new
            {
                Id = table.Column<int>(nullable: false)
                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                EnrollmentId = table.Column<int>(nullable: false),
                FatherContact = table.Column<string>(nullable: true),
                FatherName = table.Column<string>(nullable: true),
                Gender = table.Column<int>(nullable: false),
                Name = table.Column<string>(nullable: true),
                RollNumber = table.Column<string>(nullable: true),
                SchoolClass = table.Column<string>(nullable: true),
                Section = table.Column<string>(nullable: true),
                SectionId = table.Column<int>(nullable: false),
                SsId = table.Column<int>(nullable: false),
                Status = table.Column<int>(nullable: false),
                Type = table.Column<int>(nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_StudentDetails", x => x.Id);
            });
    }

The fake entity: All properties in this entity are coming from the above-said entities. You can call it a fake entity.

public class StudentDetail
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Gender Gender { get; set; }
    public string RollNumber { get; set; }
    public StudentStatus Status { get; set; }
    public StudentType Type { get; set; }
    public string FatherName { get; set; }
    public string FatherContact { get; set; }
    public string SchoolClass { get; set; }
    public string Section { get; set; }
    public int SsId { get; set; }
    public int SectionId { get; set; }
    public int EnrollmentId { get; set; }
}

Service layer to get data

public IEnumerable<StudentDetail> GetStudentDetails(int ssid)
    {
        var ssidParam = new SqlParameter("@ssid", ssid);
        var result = _appDbContext.StudentDetails.FromSql("exec GetStudentDetail @ssid", ssidParam).AsNoTracking().ToList();
        return result;
    }
vivek
  • 1,595
  • 2
  • 18
  • 35
  • It will create StudentDetails table in DB? – Vivek Nuna Feb 03 '18 at 11:25
  • Yes. But in migration, I have dropped the table. Also, Note that it is just a workaround, not a proper solution. I read it on EF Git repository issues while working on my project. But I don't remember the link. Sorry about that. – vivek Feb 03 '18 at 11:26
  • What’s difference between these two Up and Down method? – Vivek Nuna Feb 03 '18 at 11:29
  • When these Up and Down methods will be called? – Vivek Nuna Feb 03 '18 at 11:31
  • Actually, I had two migration. First one creates the `StudentDetails` table in the database. Later using a new migration that I created for the Stored procedure, I dropped the table and created the stored procedure instead. – vivek Feb 03 '18 at 11:32
  • So if let’s say I have added the migration like this. And run update-database. So this procedure will be be created and this table will be dropped? I’m wondering that how will it work? – Vivek Nuna Feb 03 '18 at 11:34
  • The `Up()` method will be called when you apply this migration. The `Down()` method is called when you un-apply the migration. – vivek Feb 03 '18 at 11:35
  • Do you think, you are answering my question? – Vivek Nuna Feb 03 '18 at 11:37
  • This is working fine in my project. You should try this first, then only you will get it. I will update the link from the git repo very soon. Thanks – vivek Feb 03 '18 at 11:37
  • How to add Entity to DBContext in step 2? – Vivek Nuna Feb 03 '18 at 11:39
  • `public DbSet StudentDetails {get; set;}` in your `DbContext` class. – vivek Feb 03 '18 at 11:44
  • 2
    You don't need to fiddle with the stored procedure or table in a Migration if you're using database-first. Just add an entity matching the shape of the stored procedure output. And 99.99% of the time if you have stored procedures, you should be using a database-first workflow. – David Browne - Microsoft Feb 03 '18 at 21:14
  • @David, For database-first approach, you are correct. But what about the code first approach, If I need to use a stored procedure which is complex enough as in the given code. It is not supported to run the stored procedure by now in EF-Core and I learned this workaround from the Git repository from some issue that I don't remember now. – vivek Feb 05 '18 at 05:15
  • "what about the code first approach" Normally you would switch to a database-first workflow if you want to use stored procedures, views, triggers, .... What you post _would_ work, it's just unusual to mix stored procedures and code-first. – David Browne - Microsoft Feb 05 '18 at 15:33
  • I don't get it that why you are creating table in Down(). If we don't create it! will it still work? – Zeeshan Ahmad Khalil Aug 07 '20 at 08:06
  • I found, in part, the conceptual issue as to when the migration actiuallt runs on teh database is: (1) Add the table migration ... add-mgration sp1 (2) Edit the migration text under Migrations. (3) Then do update-database In editing the Up this added the Stored Procedure only and the Down deleted the Stored Procedure only. :} – David Jones Apr 17 '23 at 12:48
  • Still got a problem though in _appDbContext.StudentDetails FromSql is not an option. Am I missing something? – David Jones Apr 17 '23 at 12:56
  • 1
    @DavidJones What's your EF Core version? It's possible that the EF Core has some changes related to this function. – vivek Apr 17 '23 at 15:29
  • Was EF V 6.5 updating to V7.0 – David Jones Apr 18 '23 at 06:51
  • OK Upgraded to 7.01 and can do .FromSql. Thx. But can only supply one parameter. Got around that with a "fudge". – David Jones Apr 18 '23 at 19:18
2

That's how it works in EF Core 2.1:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Query<YourModel>();
}

SqlParameter value1Input = new SqlParameter("@Param1", value1 ?? (object)DBNull.Value);
SqlParameter value2Input = new SqlParameter("@Param2", value2 ?? (object)DBNull.Value);

List<YourModel> result;
using (var db = new MyDbContext(_options))
{
    result = await db.Query<YourModel>().FromSql("STORED_PROCEDURE @Param1, @Param2", value1Input, value2Input).ToListAsync();
}

Source

Der_Meister
  • 4,771
  • 2
  • 46
  • 53
1

Below steps works in DB Design first approach EF core 3.1.0

1) Suppose SP(sp_BulkSelect) return 3 column by multiple tables(col1 int,col2 string,col3 string)

2) create fake Class which will hold this Data, DataType should be same as per SP column

  public Class1
      {
       public int col1 {get;set;}
       public string col2 {get;set;}
       public string col3 {get;set;}
      }

3) Used Modelbuilder to map call and entity in MyDbContext class

 modelBuilder.Entity<Class1>(entity =>
            {
                entity.HasNoKey();
                entity.Property(e => e.col1);
                entity.Property(e => e.col2);
                entity.Property(e => e.col3);

            });

4) create fake table in MyDbContext class

  public virtual DbSet<Class1> Class_1 { get; set; }

5) Used MyDbContext class to call SP

 var Class1data = MyDbContext.Class_1.FromSqlRaw("EXECUTE sp_BulkSelect {0}", id);
PrashSE
  • 177
  • 1
  • 4
0

1- Create a View from select part of sql query --without where condition--.

2- Then generate your model from database and Entity Framework generates a model for your view.

3- Now you can execute your stored procedure using generated model from view

dbContext.GeneratedView.FromSqlRaw("MyStoredProcedure {0}, {1} ", param1, param2)

Mojtaba G
  • 56
  • 3