AsyncAwait
EntityFramework
StoredProcedures
DatabaseOperations
CSharp

Call multiple stored procedures using Async/Await and EntityFramework

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Calling stored procedures asynchronously from Entity Framework is straightforward. Calling several of them correctly is where the design questions start. The main rule is that one DbContext instance cannot execute multiple operations concurrently, so you must choose between sequential awaits on one context or true parallelism with separate contexts.

Sequential Calls on a Single DbContext

If the procedures are part of one logical workflow, the safest option is to run them one after another on the same context. This avoids thread-safety issues and keeps the transaction boundary easier to reason about.

csharp
1using Microsoft.EntityFrameworkCore;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5public sealed class ReportRow
6{
7    public int Id { get; set; }
8    public string Name { get; set; } = string.Empty;
9}
10
11public sealed class AppDbContext : DbContext
12{
13    public DbSet<ReportRow> Reports => Set<ReportRow>();
14}
15
16public sealed class ReportService
17{
18    private readonly AppDbContext _db;
19
20    public ReportService(AppDbContext db)
21    {
22        _db = db;
23    }
24
25    public async Task<List<ReportRow>> LoadDataAsync()
26    {
27        await _db.Database.ExecuteSqlRawAsync("EXEC dbo.RefreshSummary");
28
29        return await _db.Reports
30            .FromSqlRaw("EXEC dbo.GetCurrentReport")
31            .ToListAsync();
32    }
33}

This pattern is still asynchronous. The thread is not blocked while SQL Server works, but the database calls are intentionally serialized.

True Parallelism Requires Separate Contexts

If the procedures are independent and you genuinely want overlap, create separate DbContext instances. The cleanest approach in EF Core is IDbContextFactory.

csharp
1using Microsoft.EntityFrameworkCore;
2using System.Threading.Tasks;
3
4public sealed class DashboardService
5{
6    private readonly IDbContextFactory<AppDbContext> _factory;
7
8    public DashboardService(IDbContextFactory<AppDbContext> factory)
9    {
10        _factory = factory;
11    }
12
13    public async Task LoadDashboardAsync()
14    {
15        await using var db1 = await _factory.CreateDbContextAsync();
16        await using var db2 = await _factory.CreateDbContextAsync();
17
18        Task task1 = db1.Database.ExecuteSqlRawAsync("EXEC dbo.RebuildSalesCache");
19        Task task2 = db2.Database.ExecuteSqlRawAsync("EXEC dbo.RebuildInventoryCache");
20
21        await Task.WhenAll(task1, task2);
22    }
23}

This is the right way to do concurrent work. Trying the same thing with one shared DbContext usually fails with an exception about a second operation starting before the first completed.

Returning Result Sets from Stored Procedures

Stored procedures that return rows can be materialized through FromSqlRaw on a mapped entity or keyless entity.

csharp
1using Microsoft.EntityFrameworkCore;
2using System.Collections.Generic;
3using System.Threading.Tasks;
4
5[Keyless]
6public sealed class CustomerSummary
7{
8    public string CustomerName { get; set; } = string.Empty;
9    public decimal Total { get; set; }
10}
11
12public sealed class QueryService
13{
14    private readonly IDbContextFactory<AppDbContext> _factory;
15
16    public QueryService(IDbContextFactory<AppDbContext> factory)
17    {
18        _factory = factory;
19    }
20
21    public async Task<(List<CustomerSummary> A, List<CustomerSummary> B)> LoadAsync()
22    {
23        await using var db1 = await _factory.CreateDbContextAsync();
24        await using var db2 = await _factory.CreateDbContextAsync();
25
26        var taskA = db1.Set<CustomerSummary>()
27            .FromSqlRaw("EXEC dbo.GetTopCustomers")
28            .ToListAsync();
29
30        var taskB = db2.Set<CustomerSummary>()
31            .FromSqlRaw("EXEC dbo.GetRecentCustomers")
32            .ToListAsync();
33
34        await Task.WhenAll(taskA, taskB);
35        return (taskA.Result, taskB.Result);
36    }
37}

The important part is not the syntax. It is the fact that each task owns its own context.

Decide Whether Parallelism Is Worth It

Parallel stored procedure calls are not automatically faster. They increase concurrency on the application side, but they also increase load on the database. If both procedures compete for the same tables, locks, or CPU, running them together may not help.

Use parallelism when the procedures are independent and the database can handle the extra pressure. Otherwise, a simple sequential flow is often the better engineering choice.

Common Pitfalls

The most common mistake is starting multiple async queries on one DbContext. Entity Framework contexts are not thread-safe, and concurrent usage is explicitly unsupported.

Another mistake is using Task.WhenAll because it looks modern, without checking whether the procedures are actually independent. If one procedure must finish before another starts, forcing concurrency only makes the code harder to understand.

Developers also often forget that async improves thread usage, not raw database speed. If SQL Server is the bottleneck, await does not make the stored procedure execute faster. It only prevents your application thread from blocking while it waits.

Finally, be careful with transactions. If you need multiple stored procedures to succeed or fail as one unit, splitting them across separate contexts changes the transaction story. In that case, sequential execution or an explicit distributed design may be more appropriate.

Summary

  • Async stored procedure calls in EF are fine, but one DbContext cannot run multiple operations concurrently.
  • Use sequential awaits on one context for a single workflow.
  • Use separate contexts, often through IDbContextFactory, for true parallel execution.
  • 'Task.WhenAll is useful only when the procedures are independent.'
  • Measure database behavior before assuming parallel stored procedures will improve total latency.

Course illustration
Course illustration

All Rights Reserved.