Skip to content

Quick Start ​

This guide walks you through a complete end-to-end example — from installing the NuGet packages to seeing the generated SQL.

Prerequisites ​

  • .NET 8 to .NET 10
  • EF Core 6 or later (any provider)

Step 1 — Install the Packages ​

Projectables is split into two NuGet packages:

PackagePurpose
EntityFrameworkCore.Projectables.Abstractions[Projectable] attribute + Roslyn source generator + code fixes
EntityFrameworkCore.ProjectablesEF Core runtime interceptor (UseProjectables())

In most single-project setups both packages go in the same project.

.NET CLI ​

bash
dotnet add package EntityFrameworkCore.Projectables.Abstractions
dotnet add package EntityFrameworkCore.Projectables

Package Manager Console ​

powershell
Install-Package EntityFrameworkCore.Projectables.Abstractions
Install-Package EntityFrameworkCore.Projectables

PackageReference (.csproj) ​

xml
<ItemGroup>
  <PackageReference Include="EntityFrameworkCore.Projectables.Abstractions" Version="*" />
  <PackageReference Include="EntityFrameworkCore.Projectables" Version="*" />
</ItemGroup>

Tip: Replace * with the latest stable version.

Step 2 — Define Your Entities ​

Add [Projectable] to any property or method whose body you want EF Core to translate to SQL:

csharp
using EntityFrameworkCore.Projectables;

public class User
{
    public int Id { get; set; }
    public string UserName { get; set; }
    public ICollection<Order> Orders { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public int UserId { get; set; }
    public DateTime CreatedDate { get; set; }
    public decimal TaxRate { get; set; }

    public User User { get; set; }
    public ICollection<OrderItem> Items { get; set; }

    // Mark computed properties with [Projectable]
    [Projectable] public decimal Subtotal => Items.Sum(item => item.Product.ListPrice * item.Quantity);
    [Projectable] public decimal Tax => Subtotal * TaxRate;
    [Projectable] public decimal GrandTotal => Subtotal + Tax;
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int Quantity { get; set; }
    public Product Product { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public decimal ListPrice { get; set; }
}

The source generator runs at compile time and emits a companion Expression<TDelegate> for each [Projectable] member — no runtime reflection.

Step 3 — Enable Projectables on Your DbContext ​

Call UseProjectables() when configuring your DbContextOptions. The extension method is in the Microsoft.EntityFrameworkCore namespace and is included in the EntityFrameworkCore.Projectables package.

With DI (AddDbContext) ​

csharp
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString)
           .UseProjectables());

With OnConfiguring ​

csharp
public class AppDbContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("your-connection-string")
            .UseProjectables();
    }
}

Step 4 — Use Projectable Members in Queries ​

Now you can use GrandTotal, Subtotal, and Tax directly in any LINQ query:

csharp
// In a Select projection
var orderSummaries = dbContext.Orders
    .Select(o => new {
        o.Id,
        o.Subtotal,
        o.Tax,
        o.GrandTotal
    })
    .ToList();

// In a Where clause
var highValueOrders = dbContext.Orders
    .Where(o => o.GrandTotal > 1000)
    .ToList();

// In an OrderBy
var sortedOrders = dbContext.Orders
    .OrderByDescending(o => o.GrandTotal)
    .ToList();

Step 5 — Check the Generated SQL ​

Use ToQueryString() to inspect the SQL EF Core generates:

csharp
var query = dbContext.Orders
    .Where(o => o.GrandTotal > 1000)
    .OrderByDescending(o => o.GrandTotal);

Console.WriteLine(query.ToQueryString());

The GrandTotal property composes Subtotal (which is also [Projectable]) — both are fully inlined into SQL:

sql
SELECT [o].[Id], [o].[UserId], [o].[CreatedDate], [o].[TaxRate]
FROM [Orders] AS [o]
WHERE (
    COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) +
    COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate]
) > 1000.0
ORDER BY (
    COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) +
    COALESCE(SUM([p].[ListPrice] * CAST([oi].[Quantity] AS decimal(18,2))), 0.0) * [o].[TaxRate]
) DESC

All computation happens in the database — no data is loaded into memory for filtering or sorting.

Adding Extension Methods ​

You can also define projectable extension methods — useful for logic that doesn't belong on the entity itself:

csharp
public static class UserExtensions
{
    [Projectable]
    public static Order GetMostRecentOrder(this User user, DateTime? cutoffDate = null) =>
        user.Orders
            .Where(x => cutoffDate == null || x.CreatedDate >= cutoffDate)
            .OrderByDescending(x => x.CreatedDate)
            .FirstOrDefault();
}

Use it in a query just like any regular method:

csharp
var result = dbContext.Users
    .Where(u => u.UserName == "Jon")
    .Select(u => new {
        GrandTotal = u.GetMostRecentOrder(DateTime.UtcNow.AddDays(-30)).GrandTotal
    })
    .FirstOrDefault();

Next Steps ​

Released under the MIT License.