1

How to Execute Stored Procedures With EF Core 7

 1 year ago
source link: https://code-maze.com/efcore-execute-stored-procedures/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to Execute Stored Procedures With EF Core 7

Posted by Ivan Matec | Updated Date Nov 22, 2022 | 4

Code Maze Book Collection

BLACK FRIDAY OFFER - 40% OFF

Want to build great APIs? Or become even better at it? Check our Ultimate ASP.NET Core Web API program and learn how to create a full production-ready ASP.NET Core API using only the latest .NET technologies. Bonus materials (Security book, Docker book, and other bonus files) are included in the Premium package!

In this article, we will see how to use stored procedures in Entity Framework Core 7.

Although Entity Framework and LINQ provide a simple way to work with the data, stored procedures still have their place when considering data access and manipulation.

To download the source code for this article, you can visit our GitHub repository.

Setup

Let’s define our data model and stored procedures. 

We will use the same structure as in our previous article

So we have a Student class:

public class Student
public int Id { get; set; }
public string? Name { get; set; }
public int Mark { get; set; }
public int CourseId { get; set; }
public Course? Course { get; set; }
public class Student
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public int Mark { get; set; }

    public int CourseId { get; set; }
    public Course? Course { get; set; }
}

And the Course class:

public class Course
public int Id { get; set; }
public string? Title { get; set; }
public ICollection<Student>? Students { get; set; }
public class Course
{
    public int Id { get; set; }
    public string? Title { get; set; }

    public ICollection<Student>? Students { get; set; }
}

We will also define our stored procedures. 

First FindStudents, a procedure that returns a list of Students filtered by the name:

CREATE OR ALTER PROCEDURE [dbo].[FindStudents]
@SearchFor NVARCHAR(50)
BEGIN
SET NOCOUNT ON;
SELECT * FROM Students WHERE [Name] LIKE '%' + @SearchFor + '%'
CREATE OR ALTER PROCEDURE [dbo].[FindStudents]
    @SearchFor NVARCHAR(50)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT * FROM Students WHERE [Name] LIKE '%' + @SearchFor + '%'
END

Then FindStudentsAlt, a procedure that returns a list of students with only student names and course titles filtered by student name:

CREATE OR ALTER PROCEDURE [dbo].[FindStudentsAlt]
@SearchFor NVARCHAR(50)
BEGIN
SET NOCOUNT ON;
SELECT
StudentName = S.[Name],
CourseTitle = C.Title
Students S
LEFT JOIN Courses C ON S.CourseId = C.Id
WHERE
S.[Name] LIKE '%' + @SearchFor + '%'
CREATE OR ALTER PROCEDURE [dbo].[FindStudentsAlt]
    @SearchFor NVARCHAR(50)
AS
BEGIN
    SET NOCOUNT ON;
    SELECT 
        StudentName = S.[Name],
        CourseTitle = C.Title
    FROM 
        Students S
    LEFT JOIN Courses C ON S.CourseId = C.Id 
    WHERE 
        S.[Name] LIKE '%' + @SearchFor + '%'
END

And finally, we will define a method to update student marks, UpdateStudentMark:

CREATE OR ALTER PROCEDURE UpdateStudentMark
@Id int,
@Mark int
BEGIN
UPDATE
Students
Mark = @Mark
WHERE
Id = @Id;
CREATE OR ALTER PROCEDURE UpdateStudentMark
    @Id int,
    @Mark int
AS
BEGIN
    UPDATE 
        Students
    SET 
        Mark = @Mark
    WHERE 
        Id = @Id;
END

We can see that we do not use SET NOCOUNT ON; in this procedure because we want to get the number of affected rows when we call this procedure.

Using stored procedures can simplify and improve the execution of SQL queries. We can use stored procedures to define complex queries which might become too complicated to read and write in LINQ. 

If we want to use a stored procedure to query the data, the query needs to return the complete set of properties (columns) for EF to map the data to the entity.

Stored procedures in EF 7

We can divide methods to execute stored procedures in EF into two groups, methods to query the data and methods to add/update/delete data.

If we want to query the data, we will use FromSql, FromSqlInterpolated, or FromSqlRaw methods that are available only on the DbSet object.

Learning Web API? Get our eBook ASP.NET Core Web API Best Practices and become an expert for FREE!   >> GET THE BOOK <<

To add, update or remove records, we need to use ExecuteSql, ExecuteSqlInterpolated, or ExecuteSqlRaw methods, which are available for DbContext.Database object.

Let’s take a closer look at these methods. 

DbSet.FromSql

Let’s define a method FindStudentsFromSql that uses the FromSql method to call FindStudents stored procedure and returns a list of Students:

public static List<Student>? FindStudentsFromSql(AppDbContext context, string searchFor)
return context?.Students?.FromSql($"FindStudents {searchFor}").ToList();
public static List<Student>? FindStudentsFromSql(AppDbContext context, string searchFor)
{
    return context?.Students?.FromSql($"FindStudents {searchFor}").ToList();
}

FromSql method is introduced in EF Core 7.0. It takes FormattableString as the only parameter, which means it is safe against SQL injection since all parameter data are wrapped in an DbParameter object. As a parameter string, we pass the name of the procedure and the value for the parameter.

DbSet.FromSqlInterpolated

FromSqlInterpolated is s available since EF Core 3.0, and it is basically the same as FromSql:

public static List<Student>? FindStudentsFromSqlInterpolated(AppDbContext context, string searchFor)
return context?.Students?.FromSqlInterpolated($"FindStudents {searchFor}").ToList();
public static List<Student>? FindStudentsFromSqlInterpolated(AppDbContext context, string searchFor)
{
    return context?.Students?.FromSqlInterpolated($"FindStudents {searchFor}").ToList();
}

Next, when we call the  FindStudentsAlt stored procedure, we cannot map the result to our data model:

public static List<Student>? FindStudentsAltFromSqlInterpolated(AppDbContext context, string searchFor)
return context?.Students?.FromSqlInterpolated($"FindStudentsAlt {searchFor}").ToList();
public static List<Student>? FindStudentsAltFromSqlInterpolated(AppDbContext context, string searchFor)
{
    return context?.Students?.FromSqlInterpolated($"FindStudentsAlt {searchFor}").ToList();
}

As a result, we see it will throw an exception since the underlying stored procedure returns an incomplete set of results:

public void WhenFindStudentsAltFromSqlInterpolated_ThenSuccess()
Assert.Throws<InvalidOperationException>(
() => Methods.FindStudentsAltFromSqlInterpolated(context, "100")
public void WhenFindStudentsAltFromSqlInterpolated_ThenSuccess()
{
    Assert.Throws<InvalidOperationException>(
        () => Methods.FindStudentsAltFromSqlInterpolated(context, "100")
    );
}

DbSet.FromSqlRaw

Unlike FromSql and FromSqlInterpolated, FromSqlRaw is not safe from SQL injection attacks. Therefore, we need to pay extra attention when defining SQL commands.

Learning Web API? Get our eBook ASP.NET Core Web API Best Practices and become an expert for FREE!   >> GET THE BOOK <<

We should never pass concatenated or interpolated strings into this method.

Let’s define how we can use it to call stored procedure:

public static List? FindStudentsFromSqlRaw(AppDbContext context, string searchFor)
return context?.Students?.FromSqlRaw("FindStudents @searchFor",
new SqlParameter("@searchFor", searchFor)).ToList();
public static List? FindStudentsFromSqlRaw(AppDbContext context, string searchFor)
{
    return context?.Students?.FromSqlRaw("FindStudents @searchFor",
        new SqlParameter("@searchFor", searchFor)).ToList();
}

Let’s now show how we can misuse this method and, therefore, abuse it to perform SQL injection attacks.

First, we need to define our unsafe method:

public static List<Student>? FindStudentsFromSqlRawUnsafe(AppDbContext context, string searchFor)
return context?.Students?.FromSqlRaw($"FindStudents @searchFor = '{searchFor}'").ToList();
public static List<Student>? FindStudentsFromSqlRawUnsafe(AppDbContext context, string searchFor)
{
    return context?.Students?.FromSqlRaw($"FindStudents @searchFor = '{searchFor}'").ToList();
}

Here we can see that we call the correct stored procedure, but the @searchFor parameter is not passed as a DbParameter and the SQL command is constructed using interpolated string.

We can still use the method safely:

public void WhenFindStudentsExecuteSqlRawUnsafe_ThenSuccess()
var results = FindMethods.FindStudentsFromSqlRawUnsafe(context, "100");
Assert.True(results?.Count == 9);
public void WhenFindStudentsExecuteSqlRawUnsafe_ThenSuccess()
{
    var results = FindMethods.FindStudentsFromSqlRawUnsafe(context, "100");

    Assert.True(results?.Count == 9);
}

But we can also use it maliciously:

public void WhenFindStudentsExecuteSqlRawUnsafeSqlInjection_ThenSuccess()
var results = FindMethods.FindStudentsFromSqlRawUnsafe(context,
@"xyz'; UPDATE Students SET Name = 'Student 000' WHERE Id = 1; SELECT '");
Assert.True(results?.Count == 0);
public void WhenFindStudentsExecuteSqlRawUnsafeSqlInjection_ThenSuccess()
{
    var results = FindMethods.FindStudentsFromSqlRawUnsafe(context, 
        @"xyz'; UPDATE Students SET Name = 'Student 000' WHERE Id = 1; SELECT '");

    Assert.True(results?.Count == 0);
}

This example will not return any Students, but it will execute an UPDATE (or any other) query and possibly compromise the whole database.

Learning Web API? Get our eBook ASP.NET Core Web API Best Practices and become an expert for FREE!   >> GET THE BOOK <<

Database.ExecuteSql

As we already mentioned, if we need to perform insert, update, or delete operations, we will use ExecuteSql methods on DbContext.Database object.

Here is how we can use the ExecuteSql method to call our UpdateStudentMark stored procedure:

public static int UpdateStudentMarkSql(AppDbContext context, int id, int mark)
return context.Database.ExecuteSql($"UpdateStudentMark @Id={id}, @Mark={mark}");
public static int UpdateStudentMarkSql(AppDbContext context, int id, int mark)
{
    return context.Database.ExecuteSql($"UpdateStudentMark @Id={id}, @Mark={mark}");
}

We can also use the async version:

public async static Task<int> UpdateStudentMarkSqlAsync(AppDbContext context, int id, int mark)
return await context.Database.ExecuteSqlAsync($"UpdateStudentMark @Id={id}, @Mark={mark}");
public async static Task<int> UpdateStudentMarkSqlAsync(AppDbContext context, int id, int mark)
{
    return await context.Database.ExecuteSqlAsync($"UpdateStudentMark @Id={id}, @Mark={mark}");
}

Same as FromSql and FromSqlInterpolated methods, ExecuteSql and ExecuteSqlInterpolated methods are safe against SQL injection attacks.

Database.ExecuteSqlInterpolated

Usage of the ExecuteSqlInterpolated method is the same as for ExecuteSql:

public static int UpdateStudentMarkSqlInterpolated(AppDbContext context, int id, int mark)
return context.Database.ExecuteSqlInterpolated($"UpdateStudentMark @Id={id}, @Mark={mark}");
public static int UpdateStudentMarkSqlInterpolated(AppDbContext context, int id, int mark)
{
    return context.Database.ExecuteSqlInterpolated($"UpdateStudentMark @Id={id}, @Mark={mark}");
}

And the async version:

public async static Task<int> UpdateStudentMarkSqlInterpolatedAsync(AppDbContext context, int id, int mark)
return await context.Database.ExecuteSqlInterpolatedAsync($"UpdateStudentMark @Id={id}, @Mark={mark}");
public async static Task<int> UpdateStudentMarkSqlInterpolatedAsync(AppDbContext context, int id, int mark)
{
    return await context.Database.ExecuteSqlInterpolatedAsync($"UpdateStudentMark @Id={id}, @Mark={mark}");
}

Database.ExecuteSqlRaw

And again, the same as FromSqlRaw, the ExecuteSqlRaw method needs to be used with caution for the same reasons.

Here we see how we can use it properly with both sync:

public static int UpdateStudentMarkSqlRaw(AppDbContext context, int id, int mark)
return context.Database.ExecuteSqlRaw("dbo.UpdateStudentMark @Id, @Mark",
new SqlParameter("@Id", id),
new SqlParameter("@Mark", mark));
public static int UpdateStudentMarkSqlRaw(AppDbContext context, int id, int mark)
{
    return context.Database.ExecuteSqlRaw("dbo.UpdateStudentMark @Id, @Mark",
        new SqlParameter("@Id", id),
        new SqlParameter("@Mark", mark));
}

And async version:

public async static Task UpdateStudentMarkSqlRawAsync(AppDbContext context, int id, int mark)
return await context.Database.ExecuteSqlRawAsync("dbo.UpdateStudentMark @Id, @Mark",
new SqlParameter("@Id", id),
new SqlParameter("@Mark", mark));
public async static Task UpdateStudentMarkSqlRawAsync(AppDbContext context, int id, int mark)
{
    return await context.Database.ExecuteSqlRawAsync("dbo.UpdateStudentMark @Id, @Mark",
        new SqlParameter("@Id", id),
        new SqlParameter("@Mark", mark));
}

Dynamic SQL

Now that we know how to use all these different methods to execute SPs, we are going to show a few more features that they support, which are not so much related only to stored procedures.

Let’s consider a situation where we need to create the query dynamically. It might seem like the interpolated string is the way to go:

public static int UpdateStudentMarkSqlDynamic(AppDbContext context, int id, int mark)
var field1 = "@Id";
var field2 = "@Mark";
return context.Database.ExecuteSql($"UpdateStudentMark {field1}={id}, {field2}={mark}");
public static int UpdateStudentMarkSqlDynamic(AppDbContext context, int id, int mark)
{
    var field1 = "@Id";
    var field2 = "@Mark";

    return context.Database.ExecuteSql($"UpdateStudentMark {field1}={id}, {field2}={mark}");
}

But this is not possible because databases do not allow parameterizing parameter names or any other part of the schema, and this method would throw an SqlException.

In this situation, we can use the raw method to create the query:

public static int UpdateStudentMarkSqlRawDynamic(AppDbContext context, int id, int mark)
var field1 = "@Id";
var field2 = "@Mark";
return context.Database.ExecuteSqlRaw($"dbo.UpdateStudentMark {field1} = @Id, {field2}=@Mark",
new SqlParameter("@Id", id),
new SqlParameter("@Mark", mark));
public static int UpdateStudentMarkSqlRawDynamic(AppDbContext context, int id, int mark)
{
    var field1 = "@Id";
    var field2 = "@Mark";

    return context.Database.ExecuteSqlRaw($"dbo.UpdateStudentMark {field1} = @Id, {field2}=@Mark",
        new SqlParameter("@Id", id),
        new SqlParameter("@Mark", mark));
}

Here we still want to send input values as SqlParameter objects to prevent possible SQL injection attacks.

Combining With LINQ

It is worth pointing out it is possible to combine SQL commands with LINQ operators. 

This is not directly applicable to the stored procedure, so we will define a method that uses a simple SELECT query in combination with LINQ:

Learning Web API? Get our eBook ASP.NET Core Web API Best Practices and become an expert for FREE!   >> GET THE BOOK <<
public static List? FindStudentsFromSqlAndLinq(AppDbContext context, string searchFor)
return context?.Students?
.FromSql($"SELECT * FROM Students")
.Where(m => m.Name != null && m.Name.IndexOf(searchFor) > -1)
.OrderBy(m => m.Mark)
.ToList();
public static List? FindStudentsFromSqlAndLinq(AppDbContext context, string searchFor)
{
    return context?.Students?
        .FromSql($"SELECT * FROM Students")
        .Where(m => m.Name != null && m.Name.IndexOf(searchFor) > -1)
        .OrderBy(m => m.Mark)
        .ToList();
}

We can also inspect the SQL query this method generates:

DECLARE @__searchFor_1 nvarchar(4000) = N'100';
SELECT [e].[Id], [e].[CourseId], [e].[Mark], [e].[Name]
FROM (
SELECT * FROM Students
) AS [e]
WHERE ([e].[Name] IS NOT NULL) AND
CASE WHEN @__searchFor_1 = N'' THEN 0
ELSE CAST(CHARINDEX(@__searchFor_1, [e].[Name]) AS int) - 1 END > -1
ORDER BY [e].[Mark]
DECLARE @__searchFor_1 nvarchar(4000) = N'100';
SELECT [e].[Id], [e].[CourseId], [e].[Mark], [e].[Name]
    FROM (
        SELECT * FROM Students
    ) AS [e]
WHERE ([e].[Name] IS NOT NULL) AND 
CASE WHEN @__searchFor_1 = N'' THEN 0
ELSE CAST(CHARINDEX(@__searchFor_1, [e].[Name]) AS int) - 1 END > -1
ORDER BY [e].[Mark]

We see that LINQ queries are applied on top of the initial SQL query.

Change Tracking

Queries that use the methods we just described follow the same change tracking rules as LINQ queries. EF tracks these results by default, as it tracks any other result from a LINQ query.

Let’s define a method that demonstrates this:

public static int? FindStudentsFromSqlAndUpdateMarks(AppDbContext context, string searchFor)
var students = context?.Students?
.FromSql($"FindStudents {searchFor}").ToList();
if (students != null)
foreach (var student in students)
student.Mark += 1;
return context?.SaveChanges();
public static int? FindStudentsFromSqlAndUpdateMarks(AppDbContext context, string searchFor)
{
    var students = context?.Students?
                .FromSql($"FindStudents {searchFor}").ToList();

    if (students != null)
        foreach (var student in students)
            student.Mark += 1;

    return context?.SaveChanges();
}

The results we get from the stored procedure are valid entities, and we can work with them as with any other collection of entities.

In this case, the Context.SaveChanges method will successfully save updated data to the database.

Conclusion

In this article, we learned how to work with stored procedures from EF Core 7.0.

We also examined the limitations and possible security pitfalls when executing stored procedures through available EF Core methods.

Learning Web API? Get our eBook ASP.NET Core Web API Best Practices and become an expert for FREE!   >> GET THE BOOK <<

Code Maze Book Collection

BLACK FRIDAY OFFER - 40% OFF

Want to build great APIs? Or become even better at it? Check our Ultimate ASP.NET Core Web API program and learn how to create a full production-ready ASP.NET Core API using only the latest .NET technologies. Bonus materials (Security book, Docker book, and other bonus files) are included in the Premium package!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK