Как использовать EF Core со stored procedure?
EF Core вызывает хранимые процедуры через FromSqlRaw для SELECT с маппингом в DbSet, через ExecuteSqlRawAsync для DML без результата, и через SqlQuery<T> для скалярных значений. EF Core 7+ позволяет связать CRUD-операции с процедурами через InsertUsingStoredProcedure и аналоги.
EF Core и Stored Procedures
EF Core поддерживает вызов хранимых процедур для чтения (с маппингом в сущности или DTO), DML-операций и даже может использовать процедуры для Insert/Update/Delete начиная с EF Core 7.
1. Вызов SELECT-процедуры с маппингом в сущность
// Хранимая процедура SQL Server:
// CREATE PROCEDURE GetProductsByCategory @CategoryId INT
// AS SELECT * FROM Products WHERE CategoryId = @CategoryId
var categoryId = 5;
var products = await context.Products
.FromSqlRaw("EXEC GetProductsByCategory @p0", categoryId)
.AsNoTracking()
.ToListAsync();
2. Маппинг в keyless DTO (не DbSet)
Если процедура возвращает колонки, не совпадающие с сущностью, используйте Keyless Entity:
public class ProductSummary
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int OrderCount { get; set; }
}
// В DbContext:
modelBuilder.Entity<ProductSummary>().HasNoKey().ToView(null);
// Вызов:
var summaries = await context.Set<ProductSummary>()
.FromSqlRaw("EXEC GetProductSummaries")
.ToListAsync();
3. Скалярный результат через SqlQuery (EF Core 7+)
var totalRevenue = await context.Database
.SqlQuery<decimal>($"EXEC GetTotalRevenue {DateTime.UtcNow.Year}")
.FirstAsync();
4. DML-процедура без результата
var productId = 42;
var newPrice = 99.99m;
await context.Database.ExecuteSqlRawAsync(
"EXEC UpdateProductPrice @p0, @p1",
productId, newPrice);
5. Хранимые процедуры для CRUD (EF Core 7+)
EF Core 7 позволяет связать Insert/Update/Delete с конкретными процедурами через Fluent API:
modelBuilder.Entity<Product>()
.InsertUsingStoredProcedure(
"Product_Insert",
spBuilder => spBuilder
.HasParameter(p => p.Name)
.HasParameter(p => p.Price)
.HasResultColumn(p => p.Id))
.UpdateUsingStoredProcedure(
"Product_Update",
spBuilder => spBuilder
.HasOriginalValueParameter(p => p.Id)
.HasParameter(p => p.Name)
.HasParameter(p => p.Price))
.DeleteUsingStoredProcedure(
"Product_Delete",
spBuilder => spBuilder
.HasOriginalValueParameter(p => p.Id));
После этого обычный context.Products.Add(product); await context.SaveChangesAsync(); вызовет Product_Insert автоматически.
6. Передача параметров через SqlParameter
var param = new SqlParameter("@CategoryId", SqlDbType.Int) { Value = 5 };
var products = await context.Products
.FromSqlRaw("EXEC GetProductsByCategory @CategoryId", param)
.ToListAsync();
Подводные камни
- FromSqlRaw с процедурой нельзя дополнять
.Where()или.OrderBy()— EF не может обернуть EXEC в подзапрос. Фильтрация должна быть внутри процедуры. - Keyless entities не отслеживаются ChangeTracker — нельзя вызывать
Update()на них. - Имена параметров чувствительны к провайдеру:
@p0для SQL Server, позиционные параметры могут отличаться в PostgreSQL. - Процедуры с OUTPUT-параметрами требуют явного
SqlParameterсDirection = ParameterDirection.Outputи чтения значения после выполнения. - EF Core 7 CUD-процедуры не поддерживают все сценарии (например, оптимистичную блокировку через RowVersion) — проверяйте документацию для вашей версии.
- Миграции не отслеживают тела хранимых процедур — изменения в процедурах нужно управлять вручную или через
migrationBuilder.Sql(). - При использовании
AsNoTracking()с процедурой ChangeTracker не активен — изменения через процедуру не отразятся в уже загруженных сущностях контекста.
Common mistakes
- Путать stored procedures и raw SQL boundary с похожим механизмом из другой версии или платформы.
- Игнорировать runtime-границы Entity Framework: lifecycle, DI scope, SQL translation, UI thread или platform API.
- Не обсуждать null/empty/error cases и поведение под нагрузкой.
What the interviewer is testing
- Кандидат объясняет stored procedures и raw SQL boundary на конкретном примере, а не только определением.
- Указывает последствия для производительности, тестируемости и поддержки.
- Различает документированное поведение текущего стека и устаревшие практики.