Working with stored procedures using the Entity Framework (EF) is a challenge because depending on what the stored procedure does determines how you call it using EF. This inconsistency, along with the names of the methods you call to submit a stored procedure changing from one version of EF to the next, can lead to much frustration for developers. This article won't solve these issues, but it will show you how to make calls to stored procedures using the version of Entity Framework in .NET 5.x. You're going to learn how to retrieve data from a stored procedure, how to pass parameters, return a scalar value, modify data, and handle multiple result sets.
Of course, as I've been preaching for the past 20+ years, you should create your own classes as a wrapper around EF to make calls to stored procedures. If you do, as you move from one version of .NET to the next, all you have to do is to change your methods and not all of the code in your application. For an example of how to put a wrapper around the Entity Framework, see my blog post entitled “Simplify Calling Stored Procedures with the Entity Framework” at https://www.pdsa.com/blog.
Follow Along with This Article
You may follow along with the steps in this article to build an MVC application using .NET 5.x if you wish. The simple Web application shows you how to interact with a product table in a SQL Server database. I'm going to use Visual Studio Code, MVC, .NET 5.x, C#, the Entity Framework, and the Adventure Works Lite sample database for SQL Server. You can get the Adventure Works Lite database on my GitHub at https://github.com/PaulDSheriff/AdventureWorksLT.
In the GitHub repository is a file named AdventureWorksLT.bak
that you can use to restore the SQL Server database. If you're unable to restore the backup, there's a SalesLT-Product.sql
file that you can use to create the product table with data in any SQL Server database.
Create the MVC Core Project
Create an MVC application using the .NET CLI from within Visual Studio Code. Startup Visual Studio Code and open a terminal window by clicking the Terminal > New Terminal menu. Navigate to the folder where you normally place your development projects. For example, I'm going to create my project under the D:\Samples folder
. Once I open a terminal window, I type in the command:
CD D:\Samples
Once you're in your development folder, create a new folder named EFSample
using the MKDIR (or the MD) command.
MKDIR EFSample
Change to the new directory using the CD command in the terminal window, as shown in the command below.
CD EFSample
Create a new MVC Core project in the EFSample folder by executing the dotnet new mvc command.
dotnet new mvc
Once the command has run, open the folder in VS Code by clicking the File > Open Folder… menu. A few seconds after opening the folder, you should be prompted with a dialog that looks like Figure 1. Click on the Yes button to add the required assets to the new project.
Try It Out
To ensure that everything is created correctly, click the Run > Start Debugging menu and a Web page should be displayed in your default browser. If you get an error about your connection not being private, open the \Properties\launchSettings.json
file and under the “EFSample” object, modify the applicationUrl
property to look like the following: “http://localhost:5000”.
Set Up EF Classes
To work with a table in a database, you need to add EF into your project and set up an entity
class to map each field in your table to a property in a class. In addition, you need to create a DbContext
class, add a connection string to point to your database table and configure some DbSet properties in your DbContext
class. Finally, you need to create an instance of the DbContext
class in the Startup
class to allow this instance to be used with Dependency Injection (DI).
Add Entity Framework
To use the Entity Framework in the .NET 5.x application, add a package to your EFSample
project. Go back to the terminal window that should still be open in the EFSample
folder. Type in the following command to add the Entity Framework to the project.
dotnet add package
Microsoft.EntityFrameworkCore.SqlServer
Add a Product Entity Class
An entity
class is a representation of a table or view contained in your database. In Listing 1, you can see an example of a Product
class that has a property for each field in the SalesLT.Product table contained in the AdventureWorksLT sample database. You need to have a [Table] attribute to identity the table and schema in which this table is located. You may have additional data annotations such as the [Key] or [Required] attributes above various properties, however, I've left most of them off for this sample.
In your project, right mouse-click on the Models folder and add a new file named Product.cs
. Into this file, add the code shown in Listing 1. In addition to the properties, add an override of the ToString()
method. This method is useful when looking at lists of data in the debugger as you're going to do throughout this article.
Listing 1: Create a Product entity class to represent a single row of data in the Product table.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EFSample.Models
{
[Table("Product", Schema = "SalesLT")]
public partial class Product
{
[Key]
public int ? ProductID { get; set; }
public string Name { get; set; }
public string ProductNumber { get; set; }
public string Color { get; set; }
public decimal StandardCost { get; set; }
public decimal ListPrice { get; set; }
public string Size { get; set; }
public decimal ? Weight { get; set; }
public DateTime SellStartDate { get; set; }
public DateTime ? SellEndDate { get; set; }
public DateTime ? DiscontinuedDate { get; set; }
public override string ToString()
{
return Name;
}
}
}
Create DbContext Class
An essential piece of using EF is an instance of a DbContext
class. Right mouse-click on the Models folder and add a new file named AdvDbContext.cs
. Add the code shown in the code snippet below. You can think of a DbContext
class as a database that contains properties of the type DbSet<T>
. Each property relates to a single table and/or view in the database. For now, you only need a single DbSet<Product>
property named Products
.
using Microsoft.EntityFrameworkCore;
namespace EFSample.Models
{
public class AdvDbContext: DbContext
{
public AdvDbContext(DbContextOptions<AdvDbContext> options) : base(options) {}
public DbSet<Product> Products { get; set; }
}
}
Add Connection String
The Entity Framework needs a connection string in order to connect to your database. Open the appSettings.json
file and add a property named ConnectionStrings
within the literal
object in this file. This property's value is a literal
object with a single
property named DefaultConnection
. The value for this property is the actual connection string. Change the values for Server
, Initial Catalog and Integrated Security as appropriate for your database and server.
"ConnectionStrings": {
"DefaultConnection": "Server=Localhost;
Initial Catalog=AdventureWorksLT;
Integrated Security=True;"
}
Add DbContext to the Startup Class
Instead of creating an instance of the AdvDbContext
class each time you need to use it, it's best to allow .NET to use DI to pass in an instance of the DbContext to any class that needs it. Open the Startup.cs
file and add two using statements at the top of this file in order to add the AdvDbContext
class into the collection of services for this application.
using EFSample.Models;
using Microsoft.EntityFrameworkCore;
Within the Startup
class, locate the ConfigureServices()
method. Just after the one line that's already in this method, add an instance of the AdvDbContext to the services collection using the AddDbContext()
method. To this method, you pass an options
object where you tell it you're using SQL Server as your database, and you retrieve the connection string from the appSettings.json
file using the Configuration
class.
// Setup DB
Contextservices.AddDbContext<AdvDbContext>options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
Call a Stored Procedure
You now have everything configured correctly in order to connect to your database and call any stored procedures within this database. In this first sample, build a stored procedure without any parameters and call that stored procedure to load a collection of product
objects.
Create a Stored Procedure
Go to your SQL Server database that contains the SalesLT.Product
table and add a new stored procedure named Product_GetAll
. Place this stored procedure in the SalesLT
schema. This stored procedure retrieves all rows of product data and returns them to your application.
CREATE PROCEDURE [SalesLT].[Product_GetAll]ASBEGIN SELECT * FROM SalesLT.Product;END
Modify the Home Controller
To use the AdvDbContext
class and the services of EF in your controller, add two using statements at the top of the HomeController.cs
file just like you did in the Startup
class.
using EFSample.Models;
using Microsoft.EntityFrameworkCore;
Tell .NET to inject the instance of the AdvDbContext
object you created in the Startup
class by adding a readonly
field of the type AdvDbContext
.
private readonly AdvDbContext _db;
Next, add another parameter to the HomeController constructor. .NET looks at all parameters being passed into the HomeController and if they're in the collection of services, automatically fills in the parameters with an instance from the services collection
object.
public HomeController(ILogger<HomeController> logger, AdvDbContext dbContext)
{
_logger = logger;
_db = dbContext;
}
Now that you have the instance of the AdvDbContext
class, you simply need to call the appropriate
method with the name of the stored procedure you just created. Add a new method named GetAll()
to the HomeController
class and make it look like the following code snippet.
public IActionResult GetAll()
{
List<Product> list;
string sql = "EXEC SalesLT.Product_GetAll";
list = _db.Products.FromSqlRaw<Product>(sql).ToList();
Debugger.Break();
return View("Index");
}
In the above code, you create a variable named list
of the type List<Product>
. Create a variable to hold the SQL statement with the name of the stored procedure to call on the database server. Use the FromSqlRaw<Product>
(sql)
method on the DbSet<Product>
collection you defined in the AdvDbContext
class to invoke the stored procedure. After the stored procedure returns the rows, this method maps each row to a Product
object and builds the collection of products in the variable named list
. I put the Debugger.Break()
method after the call to the database so you can hover over the list variable and see all the data returned.
Modify the Index Page
To call the GetAll()
method you just created in the HomeController
class, open the index.cshtml
file located in the \Views\Home
folder. Modify the code in this file to match the code shown below.
@ {
ViewData["Title"] = "Home Page";
}
<h1>Call Stored Procedures</h1>
<a asp-action="GetAll" class="btn btn-primary">Get All</a>
Try It Out
After typing in all of the above code, try your changes by clicking on the Run > Start Debugging
menu. When the home page loads, click the Get All
hyperlink. The debugger should now come up and you can hover over the list
variable to view the list of products returned. The ToString()
method you added to the Product
class is what displays each product's name when you look at the variable in the debugger.
Pass Parameters to Stored Procedures
Many stored procedures have one or more parameters required for it to function correctly. EF allows you to pass in parameters to your stored procedures.
Create a Stored Procedure with a Parameter
In the AdventureWorksLT database, add a new stored procedure named Product_Get
that accepts a single integer parameter named ProductID
. This parameter is used to select a single product using the primary key field (ProductID) in the Product table.
CREATE PROCEDURE [SalesLT].[Product_Get] @ProductID intAS
BEGIN
SELECT * FROM SalesLT.Product
WHERE ProductID = @ProductID;
END
Modify the Home Controller Class
If you've been using .NET and SQL Server for any length of time, you're familiar with the SqlClient
namespace. This namespace contains various
classes needed to interact with the SQL Server database using ADO.NET. EF is simply a wrapper around ADO.NET, so it makes sense that at some point, you're going to need to use some specific ADO.NET
classes to use some of the services of EF. Open the HomeController.cs
file and add a Using statement at the top of the file.
using Microsoft.Data.SqlClient;
Add new method (Listing 2) named GetAProduct()
to this class in order to call the Product_Get stored procedure. In this method, create a variable named parms
of the type List<SqlParameter>
. Create as many SQL parameter
objects as you need for the stored procedure you're calling. In this simple sample, I only need to set the ParameterName
and Value
properties of the SqlParameter
object. There are many other properties you can set such as Direction
, IsNullable
, DbType
, etc., however, they're not needed for calling this stored procedure.
Listing 2: Use the SqlParameter class to pass parameters to your stored procedures.
public IActionResult GetAProduct()
{
List<Product> list;
string sql = "EXEC SalesLT.Product_Get @ProductID";
List<SqlParameter> parms = new List<SqlParameter>
{
// Create parameter(s)
new SqlParameter { ParameterName = "@ProductID", Value = 706 }
};
list = _db.Products.FromSqlRaw<Product>(sql, parms.ToArray()).ToList();
Debugger.Break();
return View("Index");
}
Modify the Index Page
Open the index.cshtml
file and add a new anchor tag immediately below the previous one you added that looks like the following code snippet.
<a asp-action="GetAProduct" class="btn btn-primary">Get A Product</a>
Try It Out
Try out this new method by clicking the Run > Start Debugging
menu. Click on the Get A Product
hyperlink on the home page. The debugger should now come up and you can hover over the list variable to view the single product returned.
Return a Scalar Value
Some stored procedures you write may not return a set of data, but rather a single value such as a string or integer value. This is called a scalar
value and needs to be handled in a special way when using the Entity Framework. Create a stored procedure named Product_CountAll
in your database that looks like the following code.
CREATE PROCEDURE [SalesLT].[Product_CountAll] AS
BEGIN
SELECT Count(*) As [Value]
FROM SalesLT.Product
END
Add Class to Get Integer Value
Notice in the Product_CountAll
stored procedure you just created the scalar value from the Count(*)
function is assigned to a variable named Value
. Everything in EF needs to be assigned to a property in a class, so create a new class named ScalarInt
. Right mouse-click on the Models folder and add a new file named ScalarInt.cs
and add the following code into this new file. The property you create in this class must match the name of the variable you're using in your stored procedure for the scalar value you're returning.
namespace EFSample.Models
{
public partial class ScalarInt
{
public int Value { get; set; }
public override string ToString()
{
return Value.ToString();
}
}
}
Modify the AdvDbContext Class
To use this ScalarInt
class to call the Product_CountAll
stored procedure, create a property in the AdvDbContext
class of the type DbSet<ScalarInt>
. Open the AdvDbContext.cs
file and add the following property.
public DbSet<ScalarInt> ScalarIntValue { get; set; }
When you use a DbSet<T>
property, EF assumes that you're following good relational database practices such as the use of primary keys. However, the ScalarInt
class doesn't correspond to a table and thus doesn't have a primary key. It's up to you to tell EF that there's no primary key by adding a line of code in the OnModelCreating()
event. Override the following event in your AdvDbContext
class and inform EF that that the ScalarInt
class has no primary key.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ScalarInt>().HasNoKey();
}
Modify the Home Controller
When calling a stored procedure that simply returns a single scalar value, you use the FromSqlRaw()
method just like you've done before. However, you need to turn this single value into an enumerable list, so call the AsEnumerable()
method after the call to the FromSqlRaw()
method to create this list. Invoke the FirstOrDefault()
method to return a single
object. In this way, you get the single instance of the ScalarInt
class you are looking for with the Value
property in this instance filled in with the number of rows counted by this stored procedure.
public IActionResult CountAll()
{
ScalarInt value;
string sql = "EXEC SalesLT.Product_CountAll";
value = _db.ScalarIntValue.FromSqlRaw<ScalarInt>(sql).AsEnumerable().FirstOrDefault();
Debugger.Break();
return View("Index");
}
Modify the Index Page
Open the index.cshtml
file and add a new anchor tag immediately below the previous one you added that looks like the following code snippet.
<a asp-action="CountAll" class="btn btn-primary">Count All Products</a>
Try It Out
Try out this new method by clicking the Run > Start Debugging
menu. Click on the Count All Products
hyperlink on the home page. The debugger should now come up and you can hover over the value variable to view the total count of product records.
Modify Data Using a Stored Procedure
So far in this article, you've learned to retrieve data from stored procedures both with and without parameters. You also learned to retrieve a single scalar value from a stored procedure. To call a data modification stored procedure such as one that performs an INSERT
, UPDATE
, or DELETE
, you use a different method on a different object in the DbContext
object. To illustrate an UPDATE
, create a stored procedure named Product_UpdateListPrice
.
CREATE PROCEDURE [SalesLT] .[Product_UpdateListPrice] @ProductID int, @ListPrice money AS
BEGIN
UPDATE SalesLT.Product
SET ListPrice = @ListPrice
WHERE ProductID = @ProductID;
END
Modify the Home Controller
When calling any data modification stored procedure use the ExecuteSqlRaw()
method on the Database
property of your DbContext
object. This method returns an integer value of the number of rows affected by the statement in that stored procedure. In your HomeController
class, add a new method named UpdateListPrice()
to call the Product_UpdateListPrice
stored procedure you just created, as shown in Listing 3.
Listing 3: Use the ExecuteSqlRaw() method on the Database object to call a data modification stored procedure.
public IActionResult UpdateListPrice()
{
int rowsAffected;
string sql = "EXEC SalesLT.Product_UpdateListPrice @ProductID, @ListPrice";
List<SqlParameter> parms = new List<SqlParameter>
{
// Create parameters
new SqlParameter { ParameterName = "@ProductID", Value = 706 },
new SqlParameter { ParameterName = "@ListPrice", Value = 1500 }
};
rowsAffected = _db.Database.ExecuteSqlRaw(sql, parms.ToArray());
Debugger.Break();
return View("Index");
}
Modify the Index Page
Open the index.cshtml
file and add a new anchor tag immediately below the previous one you added that looks like the following code snippet.
<a asp-action="UpdateListPrice" class="btn btn-primary">Update List Price</a>
Try It Out
Try out this new method by clicking the Run > Start Debugging
menu. Click on the Update List Price
hyperlink on the home page. The debugger should now come up and you can hover over the rowsAffected
variable to view the total number of records affected. If you've done everything correctly, the value should be one (1). If you look in the Product
table, you should also see the ListPrice
field for the Product 706 has been modified to the value 1500.
Handle Multiple Result Sets
A common performance-improving technique is to return multiple result sets from a single stored procedure. Doing this requires less client- and server-side resources than submitting multiple calls to the database and returning multiple result sets individually. To illustrate how to retrieve multiple result sets using EF, create the following stored procedure in the Adventure Works LT database.
CREATE PROCEDURE [SalesLT].[MultipleResultsColors] AS
BEGIN
SELECT * FROM SalesLT.Product WHERE Color = 'Black';
SELECT * FROM SalesLT.Product WHERE Color = 'Red';
END
Modify the Home Controller
When working with multiple result sets, you are going to get close to pure ADO.NET code rather than EF code. As such, you need to open the HomeController.cs
file and add two using statements that once again you may be familiar with as they are used frequently with ADO.NET.
using System.Data;
using System.Data.Common;
Next, add a new method named MultipleResultSets()
as shown in Listing 4. In this method, you create two different List<Product>
variables: one to hold all black products and one to hold all red products. Using the Database
object on your DbContext
object, you call the GetDbConnection()
method to retrieve the ADO.NET database
object. From this object, you can create a command
object using the CreateCommand()
method. Fill in the CommandText
property of the command
object with the SQL statement you created with the call to the stored procedure. Open the connection on the database
object and you're now ready to call the stored procedure.
Listing 4: When retrieving multiple result sets, you are almost writing pure ADO.NET code.
public IActionResult MultipleResultSets()
{
List<Product> black = new List<Product>();
List<Product> red = new List<Product>();
DbCommand cmd;
DbDataReader rdr;
string sql = "EXEC SalesLT.MultipleResultsColors";
// Build command object
cmd = _db.Database.GetDbConnection().CreateCommand();
cmd.CommandText = sql;
// Open database connection
_db.Database.OpenConnection();
// Create a DataReader
rdr = cmd.ExecuteReader(CommandBehavior.CloseConnection);
// Build collection of Black products
while (rdr.Read())
{
black.Add(new Product
{
ProductID = rdr.GetInt32(0),
Name = rdr.GetString(1),
ProductNumber = rdr.GetString(2)
});
}
// Advance to the next result set
rdr.NextResult();
// Build collection of Red products
while (rdr.Read())
{
red.Add(new Product
{
ProductID = rdr.GetInt32(0),
Name = rdr.GetString(1),
ProductNumber = rdr.GetString(2)
});
}
Debugger.Break();
// Close Reader and Database Connection
rdr.Close();
return View("Index");
}
Call the stored procedure using the ExecuteReader()
method of the command
object. Pass in an enumeration to tell ADO.NET to close the connection when the reader is closed. Once you have the data reader
object, call the Read()
method to loop through each record in the first result set. This method returns a false
when there are no more records in the first result set. Each time through the loop, create a new Product
object with any properties you want, and add them to the black
list of products.
When you're done with the first result set, call the NextResult()
method on the data reader
object to advance to the next result set. Loop through all these records until the Read()
method returns a false
value. Each time through the loop, create a new Product
object and add any properties you want. Add each new Product
object to the red
list of products. When you are done with all the results, be sure to call the Close()
method on the reader
object to close it and the database connection. Note that I didn't use any exception handling in this method, but you definitely should!
Modify the Index Page
Open the index.cshtml
file and add a new anchor tag immediately below the previous one you added that looks like the following code snippet.
<a asp-action="MultipleResultSets" class="btn btn-primary"> Multiple Result Sets</a>
Try It Out
Try out this new method by clicking the Run > Start Debugging
menu. Click on the Multiple Result Sets
hyperlink on the home page. The debugger should now come up and you can hover over the black and the red variables to view the different lists of products.
Summary
In this article, you learned how to make calls to stored procedures from the Entity Framework in .NET 5.x. There are some differences from previous versions of the Entity Framework, but nothing too dramatic that you can't figure out readily enough. Calling stored procedures with and without parameters can be a little different. Retrieving data and retrieving scalar values is done differently, so it's good to keep these samples around so you can look them up when you need to. In some cases, such as multiple result sets, you use almost pure ADO.NET code.