Stored Procedures
Many enterprise databases expose their logic as stored procedures. Your Web API 2 code calls them, passes parameters, reads results — and turns SQL errors into proper HTTP responses.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Call a stored procedure with ADO.NET and CommandType.StoredProcedure
- Use input and output parameters
- Map a result set to a class with EF6 Database.SqlQuery<T>
- Translate a THROW from SQL into a 409 response
Explanation#
Set command.CommandType = CommandType.StoredProcedure and use the procedure name as the command text. Add parameters exactly as for normal SQL.
Output parameters need Direction = ParameterDirection.Output; read their Value after the command runs.
EF6 can map procedure results to any class whose property names match the columns: db.Database.SqlQuery<OrderSummary>("EXEC dbo.usp_CustomerSummary @CustomerId", new SqlParameter(...)).
Procedures report business errors with THROW 50001, 'message', 1. In C# that arrives as a SqlException with Number 50001 — catch it and return 409 or 400.
Procedures keep data logic close to the data and let DBAs tune it. The trade-off: logic is split between C# and SQL, and is harder to unit test. Many teams use procedures for complex reports and transactional operations, and EF6 for simple CRUD.
Syntax#
CREATE OR ALTER PROCEDURE dbo.usp_PlaceOrder
@ProductId INT, @Quantity INT, @OrderId INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION;
UPDATE dbo.Products SET Stock = Stock - @Quantity
WHERE Id = @ProductId AND Stock >= @Quantity;
IF @@ROWCOUNT = 0
BEGIN
ROLLBACK;
THROW 50001, 'Not enough stock.', 1;
END
INSERT INTO dbo.Orders (ProductId, Quantity) VALUES (@ProductId, @Quantity);
SET @OrderId = SCOPE_IDENTITY();
COMMIT;
END
Code example: Calling procedures from a repository and a controller#
using System.Configuration;
using System.Data;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Net;
using System.Threading.Tasks;
using System.Web.Http;
using ShopApi.Data; // ShopDbContext
public class OrderSummary
{
public int OrderCount { get; set; }
public decimal TotalSpent { get; set; }
}
public class OrderProcedures
{
private readonly string _cs = ConfigurationManager.ConnectionStrings["ShopDb"].ConnectionString;
// ADO.NET: input + output parameters
public async Task<int> PlaceOrderAsync(int productId, int quantity)
{
using (var connection = new SqlConnection(_cs))
using (var command = new SqlCommand("dbo.usp_PlaceOrder", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@ProductId", SqlDbType.Int).Value = productId;
command.Parameters.Add("@Quantity", SqlDbType.Int).Value = quantity;
var orderId = command.Parameters.Add("@OrderId", SqlDbType.Int);
orderId.Direction = ParameterDirection.Output;
await connection.OpenAsync();
await command.ExecuteNonQueryAsync();
return (int)orderId.Value;
}
}
// EF6: map a result set to a class
public async Task<OrderSummary> GetCustomerSummaryAsync(int customerId)
{
using (var db = new ShopDbContext())
{
return await db.Database
.SqlQuery<OrderSummary>("EXEC dbo.usp_CustomerSummary @CustomerId",
new SqlParameter("@CustomerId", customerId))
.SingleAsync();
}
}
}
public class PlaceOrderRequest
{
public int ProductId { get; set; }
public int Quantity { get; set; }
}
public class OrdersController : ApiController
{
private readonly OrderProcedures _procedures = new OrderProcedures();
[HttpPost, Route("api/orders")]
public async Task<IHttpActionResult> Place(PlaceOrderRequest request)
{
try
{
int orderId = await _procedures.PlaceOrderAsync(request.ProductId, request.Quantity);
return Created("api/orders/" + orderId, new { orderId });
}
catch (SqlException ex) when (ex.Number == 50001)
{
return Content(HttpStatusCode.Conflict, new { message = ex.Message });
}
}
[HttpGet, Route("api/customers/{id:int}/summary")]
public async Task<IHttpActionResult> Summary(int id)
{
return Ok(await _procedures.GetCustomerSummaryAsync(id));
}
}
POST /api/orders {"ProductId":3,"Quantity":2} → 201 {"orderId":17}
POST /api/orders {"ProductId":3,"Quantity":99} → 409 {"message":"Not enough stock."}
GET /api/customers/5/summary → 200 {"OrderCount":4,"TotalSpent":2196.00}This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The procedure does the stock check, update and insert in one transaction on the server.
- In C#, CommandType.StoredProcedure plus the procedure name replaces the SQL text.
- @OrderId is an output parameter; its Value is read after ExecuteNonQueryAsync.
- THROW 50001 becomes a SqlException with Number 50001; the exception filter (when) catches only that one and returns 409.
- Database.SqlQuery<OrderSummary> maps the procedure’s columns OrderCount and TotalSpent to the class by name.
Real-world use case#
A bank’s core system exposes transfers only through audited stored procedures. The Web API 2 layer validates input, calls usp_Transfer with parameters, and maps each THROW number to a clear HTTP error.
Common mistakes#
"EXEC usp_Find '" + name + "'" is just as injectable as any other SQL. Use parameters.
Without it, “rows affected” messages can confuse some clients and add network chatter. Start procedures with SET NOCOUNT ON.
Timeouts and deadlocks are server problems, not conflicts. Filter on specific error numbers.
Best practices#
- Use CommandType.StoredProcedure, not "EXEC ..." text, in ADO.NET
- Reserve custom error numbers (50001+) for business errors
- Keep procedures focused and versioned in source control
- Wrap multi-step changes in a transaction inside the procedure
Remember this#
CommandType.StoredProcedure + procedure name.
Output parameter: Direction = Output, read Value after executing.
THROW 50001 in SQL → SqlException.Number 50001 in C#.
Practice exercise#
Your task: Call a procedure dbo.usp_SearchProducts @Term NVARCHAR(50) that returns Id, Name, Price, using EF6 Database.SqlQuery into a ProductRow class, and return the list from GET api/products/proc-search?term=pen.
Need a hint?
Show solution
using System.Data.Entity;
using System.Data.SqlClient;
using System.Threading.Tasks;
using System.Web.Http;
using ShopApi.Data;
public class ProductRow
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProcSearchController : ApiController
{
[HttpGet, Route("api/products/proc-search")]
public async Task<IHttpActionResult> Search(string term = "")
{
using (var db = new ShopDbContext())
{
var rows = await db.Database
.SqlQuery<ProductRow>("EXEC dbo.usp_SearchProducts @Term", new SqlParameter("@Term", term))
.ToListAsync();
return Ok(rows);
}
}
}
Mini quiz#
How do you mark a parameter as an output parameter in ADO.NET?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.