Meridian.Sluice.EntityFrameworkCore 0.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Meridian.Sluice.EntityFrameworkCore --version 0.2.0
                    
NuGet\Install-Package Meridian.Sluice.EntityFrameworkCore -Version 0.2.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="Meridian.Sluice.EntityFrameworkCore" Version="0.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Meridian.Sluice.EntityFrameworkCore" Version="0.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Meridian.Sluice.EntityFrameworkCore" />
                    
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 Meridian.Sluice.EntityFrameworkCore --version 0.2.0
                    
#r "nuget: Meridian.Sluice.EntityFrameworkCore, 0.2.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 Meridian.Sluice.EntityFrameworkCore@0.2.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=Meridian.Sluice.EntityFrameworkCore&version=0.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Meridian.Sluice.EntityFrameworkCore&version=0.2.0
                    
Install as a Cake Tool

<p align="center"> <h1 align="center">Sluice</h1> <p align="center"> <a href="https://www.nuget.org/packages/Meridian.Sluice"><img src="https://img.shields.io/nuget/v/Meridian.Sluice?label=Meridian.Sluice" alt="NuGet" /></a> <a href="https://www.nuget.org/packages/Meridian.Sluice.EntityFrameworkCore"><img src="https://img.shields.io/nuget/v/Meridian.Sluice.EntityFrameworkCore?label=Meridian.Sluice.EntityFrameworkCore" alt="NuGet" /></a> <img src="https://img.shields.io/badge/license-MIT-blue" alt="License" /> </p> </p>

Your reads are your invalidation rules. Sluice is a cache for .NET that knows what each cached value was built from. Cache a user's dashboard, and Sluice notes that it read the user's row and the dark-mode flag. Toggle the flag and every dashboard that read it is thrown away. Change one user's greeting and only that user's dashboard goes. You never keep a list of cache keys to clear.

FusionCache does the actual caching: in memory, in Redis, and keeping several servers in sync. Sluice adds the "what did this read?" part. With EF Core, saving your changes clears the right entries by itself. With any other data source, you tell Sluice what changed after you write it.

The rules

Name each thing that can change, once.
A cached value records what it read while it's built.
A write clears the cached values that read what it changed.
Clearing waits until the write has committed.

Prerequisites

  • .NET 10.
  • FusionCache 2.9 or later. Sluice brings it in.
  • For Meridian.Sluice.EntityFrameworkCore: EF Core 10.0.12 or later on a relational provider. It's tested on PostgreSQL; the playground runs on SQLite.

Install

dotnet add package Meridian.Sluice
dotnet add package Meridian.Sluice.EntityFrameworkCore   # only if you use EF Core

Three steps in

The code below is the playground's: a dashboard for Alice, an admin, and Bob, a member. Both read their user row and a dark-mode flag. Only Alice reads a greeting, which comes from a service that isn't EF.

1. Declare what can change, and what you cache

builder.Services.AddSluice();
public sealed record UserId(string Value);
public sealed record Dashboard(string Name, bool DarkMode, string? Greeting);

public static class Dashboards
{
    // Something a cached value can depend on: one user's greeting.
    public static readonly Source<UserId> Greeting = new("greeting", id => id.Value);

    // Something cached: a user's dashboard, kept for five minutes unless invalidated.
    public static readonly Query<UserId, Dashboard> ForUser = new("dashboard", id => id.Value)
    {
        Duration = TimeSpan.FromMinutes(5),
    };
}

A Source names something that can change. Reads record it and writes clear it, and both use this one declaration, so they can't disagree on a name. A Query names something cached, with its key and value types, so the compiler checks every call. Sluice needs each key as text, and UserId isn't a string, so each declaration says how to turn it into one (id => id.Value). Strings, numbers, Guids and enums need nothing.

2. Cache a read

public ValueTask<Dashboard> GetDashboard(UserId userId, CancellationToken ct) =>
    cache.GetOrSetAsync(
        Dashboards.ForUser,
        userId,
        async (read, ct) =>
        {
            // EF Core: each read records the row it reads.
            var user = await read.Entity(db.Users, userId).SingleAsync(ct);
            var darkMode = await read.Entity(db.Flags, FlagId.DarkMode)
                .Select(flag => flag.Enabled)
                .SingleAsync(ct);

            // Any other source: record the dependency and fetch in one call.
            // Only admins read a greeting, so only admins depend on one.
            string? greeting = null;
            if (user.Role == Role.Admin)
            {
                greeting = await read.From(
                    Dashboards.Greeting.For(userId),
                    ct => greetings.GetAsync(userId, ct)
                );
            }

            return new Dashboard(user.Name, darkMode, greeting);
        },
        ct
    );

The lambda builds the dashboard when it isn't cached; Sluice calls it the compute. Each read call in it records what it read. read.From(dependency, fetch) works with any data source: it records the dependency and runs the fetch in one call, so you can't record one thing and fetch another. read.Entity is the EF Core version, covered below.

After both dashboards are cached, Alice's depends on her user row, the flag and her greeting. Bob's depends on his row and the flag: he isn't an admin, so his compute never read a greeting.

3. Invalidate once the write has committed

public async Task SetGreeting(UserId userId, string text)
{
    await greetings.SetAsync(userId, text);

    // Not EF, so say what changed, after the write.
    await cache.InvalidateAsync(Dashboards.Greeting.For(userId));
}

Only Alice's dashboard read Alice's greeting, so only hers is thrown away. Bob's stays cached. Invalidate after the write commits, never before. Otherwise another request could rebuild the dashboard from the old data in between and cache it again.

With EF Core

builder.Services.AddDbContext<AppDb>(
    (services, options) => options.UseSqlite(connectionString).UseSluice(services)
);

UseSluice connects the context to Sluice. Inside a compute, three helpers read EF data: read.Entity (one row by its key), read.Children (the rows under one parent) and read.All (every row). Each records what it reads and returns a query you can filter and project. Sluice works out the dependency from your EF model, so EF entities need no Source. Keys are your model's own types: the playground maps UserId with a value converter, so read.Entity(db.Users, userId) takes a UserId.

EF writes need no Sluice code:

var flag = await db.Flags.SingleAsync(flag => flag.Id == FlagId.DarkMode, ct);
flag.Enabled = !flag.Enabled;

// Once this commits, every dashboard that read the flag is evicted. No Sluice code needed.
await db.SaveChangesAsync(ct);

Saves through EF's change tracker just work. Once a save commits, Sluice clears the cached reads of every row it inserted, updated or deleted, and of the lists those rows were in or moved between. One case is broader: when a delete makes the database delete rows EF never loaded, or set their foreign key to null (ON DELETE CASCADE or SET NULL), Sluice can't tell which rows those were. It clears every cached read of those types instead. Database cascades has the details.

Sluice can't see writes that don't go through a UseSluice context: another service, a cron job, SQL run by hand, a trigger, a second context without UseSluice, ExecuteUpdate or raw SQL. After one of those, invalidate by hand, or the old value stays cached until it expires. Keep durations short for data written outside your app. For an EF set it's one line: await cache.InvalidateAsync(db.Users.AllDependencies());.

Inside a compute, an EF query that skips the helpers throws. Sluice wouldn't know what it read, so no write would ever clear the value it built.

Try it

dotnet run --project examples/playground

Open http://localhost:5319. Read both dashboards, then toggle dark mode or change Alice's greeting, and watch which dashboard recomputes. It runs on SQLite, so there's nothing else to start.

Also in the box

  • Typed declarations: which key types work, typed ids, keys with several parts or a tenant, and how long each query is cached
  • Invalidating: several things at once or a whole source, fallback values that still get cleared, and clearing everything
  • EF Core writes: foreign keys, database cascades, owned and inherited types, pooled contexts, and bulk updates
  • Strict reads: queries that would cache data Sluice can't track throw, and how to read views and raw SQL anyway
  • Transactions: EF transactions, TransactionScope and retries all clear the cache after the commit, not before
  • Several servers: sharing the cache through Redis, and a startup check for settings that would let cleared values come back

Documentation

Getting Started · Sources and Queries · EF Core · Configuration · How It Works · Limitations (what Sluice deliberately doesn't do)

Contributors: each package's rules, with the tests that prove them, are in src/Sluice/INVARIANTS.md and src/Sluice.EntityFrameworkCore/INVARIANTS.md.

License

MIT

Product Compatible and additional computed target framework versions.
.NET 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
0.3.0 56 10/9/2026
0.2.0 57 10/9/2026