DEV Community

Cover image for Calling Views, Stored Procedures and Functions in EF Core
Anton Martyniuk
Anton Martyniuk

Posted on • Originally published at antondevtips.com on

Calling Views, Stored Procedures and Functions in EF Core

While EF Core provides a robust API for interacting with the database, there are scenarios where you need to call existing database views, stored procedures, or functions.
In this blog post, we will explore how to call views, stored procedures, and functions using EF Core.

On my website: antondevtips.com I share .NET and Architecture best practises.
Subscribe to become a better developer.
Download source code for this blog post for free.

How To Call Database Views in EF Core

A database view is a virtual table that contains data from one or multiple database tables.
Unlike a physical table, a view does not store data itself.
It contains a set of predefined SQL queries to fetch data from the database.

Let's explore an application that stores Customers, Products, Orders, and OrderDetails in the Postgres database.
Here is what our database looks like:

CREATE TABLE customers (
    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    name VARCHAR(255),
    phone VARCHAR(20),
    email VARCHAR(255),
    is_active BOOLEAN
);

CREATE TABLE products (
    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    name VARCHAR(255),
    price DECIMAL
);

CREATE TABLE orders (
    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    customer_id INT,
    date DATE,
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

CREATE TABLE order_details (
    id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    order_id INT,
    product_id INT,
    quantity INT,
    price DECIMAL(10, 2),
    FOREIGN KEY (order_id) REFERENCES orders(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);
Enter fullscreen mode Exit fullscreen mode

And we have a EF Core DbContext that maps to this database:

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : DbContext(options)
{
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Our database has a view that returns total sales per each Customer:

CREATE VIEW IF NOT EXISTS total_sales_per_customer AS
SELECT customer.name, SUM(order_details.quantity * order_details.price) AS total_sales
FROM customers customer
         JOIN orders "order" ON customer.id = "order".customer_id
         JOIN order_details order_details ON "order".id = order_details.order_id
GROUP BY customer.name
ORDER BY total_sales desc;
Enter fullscreen mode Exit fullscreen mode

In this Postgres database, you can call the view to get the results:

select * from total_sales_per_customer;
Enter fullscreen mode Exit fullscreen mode

To call a view in EF Core, you need to map it to a model class and add to the DbContext as keyless entity:

public class TotalSalesPerCustomer
{
    public string Name { get; set; }
    public decimal TotalSales { get; set; }
}

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : DbContext(options)
{
    public DbSet<TotalSalesPerCustomer> TotalSalesPerCustomers { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<TotalSalesPerCustomer>()
            .HasNoKey()
            .ToView("total_sales_per_customer");
    }
}
Enter fullscreen mode Exit fullscreen mode

We map entity as HasNoKey because we are calling the database view that doesn't have a primary key.
In the ToView method you need to specify the name of the database view.

And here is how you can call the view in the minimal API endpoint:

app.MapGet("/api/total-sales", async (ApplicationDbContext dbContext) =>
{
    var totalSales = await dbContext.TotalSalesPerCustomers
        .ToListAsync();

    return Results.Ok(totalSales);
});
Enter fullscreen mode Exit fullscreen mode

As a result, you will receive something like this:

[
    {
        "name": "Virginia Champlin",
        "totalSales": 153486.75
    },
    {
        "name": "Mary Kessler",
        "totalSales": 132898.65
    },
    {
        "name": "Louis Stark",
        "totalSales": 112785.54
    },
    {
        "name": "Kirk Renner",
        "totalSales": 104335.26
    },
    {
        "name": "Jan Keebler",
        "totalSales": 78566.76
    }
]
Enter fullscreen mode Exit fullscreen mode

How To Call Stored Procedures in EF Core

A stored procedure in SQL is a group of SQL queries that can be saved and reused multiple times.
Unlike a database view, stored procedure executes SQL statements that don't return data.

Let's assume that our database has the following stored procedure to update price in the order_details price:

CREATE OR REPLACE PROCEDURE update_order_details_price(
    orderDetailId INT,
    newPrice DECIMAL
)
LANGUAGE plpgsql
AS $$
BEGIN
    UPDATE order_details
    SET price = newPrice
    WHERE id = orderDetailId;
    COMMIT;
END;
$$;
Enter fullscreen mode Exit fullscreen mode

EF Core allows you to call stored procedures directly.

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : DbContext(options)
{
    public async Task UpdateOrderDetailPriceAsync(int orderDetailId, decimal newPrice)
    {
        await Database.ExecuteSqlAsync($"CALL update_order_details_price({orderDetailId}, {newPrice})");
    }
}
Enter fullscreen mode Exit fullscreen mode

Despite using interpolated string, ExecuteSqlAsync method is safe as the string is actually a FormattableString.

If you don't want to use string interpolation, you can use ExecuteSqlRawAsync method and pass arguments:

public async Task UpdateOrderDetailPriceAsync(int orderDetailId, decimal newPrice)
{
    await Database.ExecuteSqlRawAsync("CALL update_order_details_price({0}, {1})", orderDetailId, newPrice);
}
Enter fullscreen mode Exit fullscreen mode

Now, let's define a POST minimal API endpoint to update the price:

public record UpdateRequest(int OrderDetailId, decimal NewPrice);

app.MapPost("/update-order-detail-price",
    async ([FromBody] UpdateRequest request, ApplicationDbContext context) =>
{
    await context.UpdateOrderDetailPriceAsync(request.OrderDetailId, request.NewPrice);
    return Results.Ok();
});
Enter fullscreen mode Exit fullscreen mode

How To Call Database Functions in EF Core

A function in SQL is a group of SQL queries that can be saved and reused multiple times.
It is similar to stored procedure but return data.

A function can be one of the following types:

  • table-valued function
  • scalar function.

How To Call Table-Valued Function in EF Core

Table-Valued function is a function that returns data of a table type.

Let's assume that our database has the following function that returns total sales of the customer:

CREATE OR REPLACE FUNCTION get_customer_total_sales(customerId INT)
RETURNS TABLE (
    name VARCHAR,
    total_sales DECIMAL
) AS $$
BEGIN
    RETURN QUERY
    SELECT c.name, SUM(od.quantity * od.price) AS total_sales
    FROM customers c
    JOIN orders o ON c.id = o.customer_id
    JOIN order_details od ON o.id = od.order_id
    WHERE c.id = customerId
    GROUP BY c.name;
END;
$$ LANGUAGE plpgsql;
Enter fullscreen mode Exit fullscreen mode

You can map this function by using the HasDbFunction method in the DbContext:

public class CustomerTotalSales
{
    public string Name { get; set; }
    public decimal TotalSales { get; set; }
}

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDbFunction(() => GetCustomerTotalSales(default))
            .HasName("get_customer_total_sales");
    }

    public IQueryable<CustomerTotalSales> GetCustomerTotalSales(int customerId) =>
        FromExpression(() => GetCustomerTotalSales(customerId));
}
Enter fullscreen mode Exit fullscreen mode

Here we create a public function, that returns a SQL expression, by wrapping GetCustomerTotalSales method using FromExpression method from the base DbContext class.
Then we map it to the database function "get_customer_total_sales" using the HasDbFunction method.

Now we can call this function inside a minimal API endpoint:

app.MapGet("/get-customer-total-sales/{customerId}",
    async (int customerId, ApplicationDbContext context) =>
{
    var customerTotalSales = await context.GetCustomerTotalSales(customerId)
        .ToListAsync();

    return Results.Ok(customerTotalSales);
});
Enter fullscreen mode Exit fullscreen mode

When calling this endpoint, you'll get the following response:

[
  {
    "name": "Kirk Renner",
    "totalSales": 34778.42
  }
]
Enter fullscreen mode Exit fullscreen mode

How To Call Scalar Function in EF Core

Scalar function is a function that returns a single value.

Let's assume that our database has the following function that returns total order detail price:

CREATE OR REPLACE FUNCTION get_order_detail_price(productId INT)
RETURNS DECIMAL AS $$
BEGIN
    RETURN (SELECT SUM(price * quantity) FROM order_details WHERE product_id = productId);
END;
$$ LANGUAGE plpgsql;
Enter fullscreen mode Exit fullscreen mode

This function returns a sum of all order details of the particular product.

We can map it in EF Core using the HasDbFunction method:

public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
    : DbContext(options)
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDbFunction(
            typeof(ApplicationDbContext).GetMethod(nameof(GetOrderDetailPrice), 
            new[] { typeof(int) })!
        ).HasName("get_order_detail_price");
    }

    public static decimal GetOrderDetailPrice(int productId)
        => throw new NotImplementedException();
}
Enter fullscreen mode Exit fullscreen mode

Just never mind throw new NotImplementedException(); as this method won't be called directly and is proxied by EF Core to call the actual database function.

Now you can use this function inside a LINQ query to get products that have total order details more than 20 000:

app.MapGet("/get-expensive-products-by-orders",
    async (ApplicationDbContext context) =>
{
    var expensiveProducts = await context.Products
        .Where(x => ApplicationDbContext.GetOrderDetailPrice(x.Id) > 20_000m)
        .ToListAsync();

    return Results.Ok(expensiveProducts);
});
Enter fullscreen mode Exit fullscreen mode

When calling this endpoint, you'll get the following response:

[
  {
    "id": 7,
    "name": "Fantastic Soft Pizza",
    "price": 60.46,
    "orderDetails": []
  },
  {
    "id": 9,
    "name": "Handmade Wooden Shoes",
    "price": 893.77,
    "orderDetails": []
  },
  {
    "id": 10,
    "name": "Awesome Soft Bike",
    "price": 256.69,
    "orderDetails": []
  }
]
Enter fullscreen mode Exit fullscreen mode

On my website: antondevtips.com I share .NET and Architecture best practises.
Subscribe to become a better developer.
Download source code for this blog post for free.

Top comments (0)