Project · EF Core companions

EF read-scope and cursor companion

Eight independent BCL checks for supplied read scope, deletion policy and composite cursors; no authorization, EF translation or database emulation.

All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.

Companion.csproj

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net9.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>

Program.cs

Console.WriteLine(ReadChecks.Run() + " read checks passed");

Domain/ReadRules.cs

namespace SharpRune.EfCompanion;
public sealed record Row(string Tenant, int SortKey, int Id, bool Deleted);
public sealed record Cursor(int SortKey, int Id);
public static class ReadRules
{
    public static int[] Page(IEnumerable<Row> rows, string trustedTenant, bool includeDeleted, Cursor? after, int size)
    {
        if (string.IsNullOrWhiteSpace(trustedTenant)) throw new ArgumentException("Trusted scope required", nameof(trustedTenant));
        if (size is < 1 or > 100) throw new ArgumentOutOfRangeException(nameof(size));
        return rows.Where(x => x.Tenant == trustedTenant && (includeDeleted || !x.Deleted))
            .Where(x => after is null || x.SortKey > after.SortKey || x.SortKey == after.SortKey && x.Id > after.Id)
            .OrderBy(x => x.SortKey).ThenBy(x => x.Id).Take(size).Select(x => x.Id).ToArray();
    }
}

Checks/ReadChecks.cs

using SharpRune.EfCompanion;
public static class ReadChecks
{
    public static int Run()
    {
        var rows = new[] { new Row("A", 10, 1, false), new Row("A", 10, 2, true), new Row("A", 10, 3, false), new Row("A", 20, 4, false), new Row("B", 5, 5, false) };
        int count = 0;
        void Check(bool value, string name) { if (!value) throw new InvalidOperationException(name); count++; }
        string Join(int[] values) => string.Join(",", values);
        Check(Join(ReadRules.Page(rows, "A", false, null, 2)) == "1,3", "first page ties");
        Check(Join(ReadRules.Page(rows, "A", false, new Cursor(10, 3), 2)) == "4", "exclusive next page");
        Check(Join(ReadRules.Page(rows, "A", true, null, 3)) == "1,2,3", "include deleted scope");
        Check(Join(ReadRules.Page(rows, "B", false, null, 2)) == "5", "separate supplied scope");
        Check(ReadRules.Page(rows, "Unknown", false, null, 2).Length == 0, "empty matching set");
        bool invalid = false; try { ReadRules.Page(rows, "", false, null, 2); } catch (ArgumentException) { invalid = true; }
        Check(invalid, "missing scope rejected");
        invalid = false; try { ReadRules.Page(rows, "A", false, null, 0); } catch (ArgumentOutOfRangeException) { invalid = true; }
        Check(invalid, "zero size rejected");
        invalid = false; try { ReadRules.Page(rows, "A", false, null, 101); } catch (ArgumentOutOfRangeException) { invalid = true; }
        Check(invalid, "oversized page rejected");
        return count;
    }
}