EntityFrameworkCore.InterfaceSets 1.3.0

dotnet add package EntityFrameworkCore.InterfaceSets --version 1.3.0
                    
NuGet\Install-Package EntityFrameworkCore.InterfaceSets -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EntityFrameworkCore.InterfaceSets" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EntityFrameworkCore.InterfaceSets" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="EntityFrameworkCore.InterfaceSets" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EntityFrameworkCore.InterfaceSets --version 1.3.0
                    
#r "nuget: EntityFrameworkCore.InterfaceSets, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EntityFrameworkCore.InterfaceSets@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EntityFrameworkCore.InterfaceSets&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=EntityFrameworkCore.InterfaceSets&version=1.3.0
                    
Install as a Cake Tool

EntityFrameworkCore.InterfaceSets

A library for Entity Framework Core that enables querying entities through shared interfaces, allowing you to work with entities from different class hierarchies that implement a common interface.

Overview

EntityFrameworkCore.InterfaceSets extends EF Core with the ability to query across multiple entity types that share a common interface. This is particularly useful for implementing cross-cutting concerns like soft deletion, archiving, auditing, or multi-tenancy.

Instead of writing repetitive queries for each entity type, you can query all entities that implement a specific interface as if they were a single collection.

Installation

dotnet add package EntityFrameworkCore.InterfaceSets

Quick Start

1. Define your interface

public interface ISoftDeletable
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

2. Implement the interface on your entities

public class Order : ISoftDeletable
{
    public int Id { get; set; }
    public string OrderNumber { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

public class Product : ISoftDeletable
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

3. Query by interface

// Count all soft-deleted entities across all entity types
var deletedCount = context.InterfaceSet<ISoftDeletable>()
    .Where(x => x.IsDeleted)
    .Count();

// Get all archived entities
var archivedItems = context.InterfaceSet<IArchivable>()
    .Where(x => x.IsArchived)
    .ToList();

// Restore all soft-deleted items older than 30 days
var cutoffDate = DateTime.UtcNow.AddDays(-30);
var oldDeletedItems = context.InterfaceSet<ISoftDeletable>()
    .Where(x => x.IsDeleted && x.DeletedAt < cutoffDate)
    .ToList();

foreach (var item in oldDeletedItems)
{
    item.IsDeleted = false;
    item.DeletedAt = null;
}
await context.SaveChangesAsync();

Supported Operations

Synchronous Operations

  • Count() / LongCount()
  • First() / FirstOrDefault()
  • Single() / SingleOrDefault()
  • Any()
  • ToList()
  • Where() with LINQ expressions
  • OfType<T>() for filtering by specific entity types

Asynchronous Operations

  • CountAsync() / LongCountAsync()
  • FirstAsync() / FirstOrDefaultAsync()
  • SingleAsync() / SingleOrDefaultAsync()
  • AnyAsync()
  • ToListAsync()

All operations support method chaining and standard LINQ query syntax.

How It Works

When you call InterfaceSet<TInterface>(), the library:

  1. Discovers all entity types in your DbContext that implement the interface
  2. Rewrites your LINQ expressions to target the concrete entity types
  3. Executes separate queries against each entity type's DbSet
  4. Aggregates the results based on the operation (e.g., sums counts, concatenates lists)

For example, InterfaceSet<ISoftDeletable>().Count() internally executes:

context.Orders.Where(rewrittenExpression).Count() +
context.Products.Where(rewrittenExpression).Count() +
// ... for all ISoftDeletable entity types

Limitations

1. No Direct Modifications

You cannot use InterfaceSet for direct updates or deletes:

// This will NOT work
context.InterfaceSet<ISoftDeletable>().ExecuteDelete(); // Not supported

Instead, materialize the entities first:

var items = context.InterfaceSet<ISoftDeletable>().ToList();
foreach (var item in items)
{
    item.IsDeleted = true;
}
context.SaveChanges();

2. Limited JOIN Support

Joins across interface sets are not supported. You can only query and filter within a single interface set.

3. Navigation Properties

Navigation properties defined in interfaces are supported with some limitations:

What Works:

  • Filtering by navigation property foreign keys
  • Filtering by navigation property object properties (e.g., x.CreatedBy.Username == "admin")
public interface IAuditable
{
    int? CreatedByUserId { get; set; }
    AuditUser? CreatedBy { get; set; }
    DateTime CreatedAt { get; set; }
}

// Filter by foreign key - works perfectly
var itemsCreatedByUser = context.InterfaceSet<IAuditable>()
    .Where(x => x.CreatedByUserId == userId)
    .ToList();

// Filter by navigation property - also works!
var itemsByAdmin = context.InterfaceSet<IAuditable>()
    .Where(x => x.CreatedBy.Username == "admin")
    .ToList();

What Doesn't Work:

  • .Include() - Navigation properties will NOT be loaded even if you use .Include(x => x.CreatedBy)
  • Navigation properties in returned entities will always be null unless you manually load them afterward

The navigation properties must be properly configured in EF Core's OnModelCreating for each entity type.

4. Performance Considerations

  • Each query generates N separate SQL queries (one per entity type implementing the interface)

5. Sorting Limitations

Sorting with OrderBy() does not work as expected, as it only orders per dbSet, resulting in an unordered total sequence.
To implement proper ordering, the OrderBy expression would need to be extracted and evaluated inMemory, i am happy to accept PRs for this feature.

Advanced Usage

Filtering by Specific Entity Types

Use OfType<T>() to query only specific entity types:

// Only query Orders, not all ISoftDeletable entities
var deletedOrders = context.InterfaceSet<ISoftDeletable>()
    .OfType<Order>()
    .Where(x => x.IsDeleted)
    .ToList();

Complex Filters

var recentlyArchived = context.InterfaceSet<IArchivable>()
    .Where(x => x.IsArchived && x.ArchivedAt > DateTime.UtcNow.AddDays(-7))
    .ToList();

Async Operations

var count = await context.InterfaceSet<ISoftDeletable>()
    .Where(x => !x.IsDeleted)
    .CountAsync();

var items = await context.InterfaceSet<IArchivable>()
    .ToListAsync();

Extensibility

The library uses an auto-discovery pattern for operation handlers. To add support for custom operations, create a handler implementing IOperationHandler<TResult>:

public class CustomOperationHandler<T> : BaseOperationHandler<T>
{
    public override bool CanHandle(string operationName, Expression expression)
    {
        return operationName == "MyCustomOperation";
    }

    public override T Execute(Expression expression, IEnumerable<IQueryable> dbSets, Type interfaceType)
    {
        // Your implementation
    }

    public override Task<T> ExecuteAsync(Expression expression, IEnumerable<IQueryable> dbSets,
        Type interfaceType, CancellationToken cancellationToken)
    {
        // Your async implementation
    }
}

Handlers are automatically discovered and registered at runtime.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 79 1/20/2026
1.2.1 91 1/19/2026
1.2.0 98 1/18/2026
1.1.0 92 1/4/2026
1.0.1 86 1/4/2026
1.0.0 88 1/4/2026

Performance improvements and fix for interfaces on basetypes not in the model