Entity FrameworkSeniorTechnical

Как использовать 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 на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics