MagicCSharp.Data 1.0.2

dotnet add package MagicCSharp.Data --version 1.0.2
                    
NuGet\Install-Package MagicCSharp.Data -Version 1.0.2
                    
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="MagicCSharp.Data" Version="1.0.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MagicCSharp.Data" Version="1.0.2" />
                    
Directory.Packages.props
<PackageReference Include="MagicCSharp.Data" />
                    
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 MagicCSharp.Data --version 1.0.2
                    
#r "nuget: MagicCSharp.Data, 1.0.2"
                    
#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 MagicCSharp.Data@1.0.2
                    
#: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=MagicCSharp.Data&version=1.0.2
                    
Install as a Cake Addin
#tool nuget:?package=MagicCSharp.Data&version=1.0.2
                    
Install as a Cake Tool

MagicCSharp.Data

Repository contracts for a domain that should not know how it is stored. IRepository<TEntity, TKey, TEdit, TFilter>, three opt-in capabilities, the pagination types, and the LINQ helpers that turn a filter into a query. No persistence library — a domain project referencing this gets interfaces and nothing else.

Add it to the project that holds your entities and repository interfaces. The Entity Framework implementation is MagicCSharp.Data.EntityFramework, with PostgreSQL wiring in MagicCSharp.Data.Postgres — or implement the same interfaces over whatever store you have.

dotnet add package MagicCSharp.Data

An entity is three records

What you can write, what comes back, and what you can query by:

public record Order : OrderEdit, IIdEntity
{
    public required long Id { get; init; }
    public required DateTimeOffset Created { get; init; }
    public required DateTimeOffset Updated { get; init; }
}

public record OrderEdit
{
    public required long CustomerId { get; init; }
    public required decimal Total { get; init; }
    public required OrderStatus Status { get; init; }
}

public class OrderFilter
{
    public long? CustomerId { get; init; }
    public OrderStatus? Status { get; init; }
    public ComparableRange<DateTimeOffset>? Created { get; init; }
}

Order derives from OrderEdit, so an entity is accepted wherever an edit is, and "these fields can be written" is a compile-time fact rather than a convention. Id, Created and Updated are set by the system and cannot be handed in on an edit. Entities keyed by an unguessable string implement IKeyEntity instead of IIdEntity.

The interface says what the entity supports

public interface IOrdersRepository :
    IRepository<Order, long, OrderEdit, OrderFilter>,
    IPaginatedRepository<Order, OrderFilter>;

TKey is long for a Snowflake id or string for a key. Pagination, soft delete and search are deliberately not on IRepository: a repository opts in by also implementing IPaginatedRepository, ISoftDeleteRepository or ISearchRepository, so a caller can see from the interface which of them the entity supports, and a small lookup table does not carry a paging method it will never need.

That interface is everything the domain sees. A use case takes IOrdersRepository; the class behind it lives in the data project and the domain never references it.

What IRepository gives you

Member
Count(filter) Without loading rows
GetKeys(filter) Just the keys, when that is all the caller needs
Get(filter) The entities matching
Get(keys) By several keys; missing ones are skipped, so the result may be shorter
Get(key) One, or null
Create(edit) · Create(edits) One, re-read so navigation properties are populated; or several in one round trip, returning keys in order
Update(key, edit) · Update(edits) Apply an edit; or several in one round trip — nothing is written if any key is missing
Update(entity) · Update(entities) Write back something read, edited in memory and saved whole
Delete(key) · Delete(keys) · Delete(filter) Permanent. The filter form deletes in the database rather than loading first

A write to a missing key throws NotFoundException. The batch forms fail as a unit rather than half-applying. Deletes and batch updates return the number of rows affected.

var order = await ordersRepository.GetOrThrow(orderId);   // NotFoundIdException(orderId, "Order") if it is not there

Get(key) returns null, which is right for the caller that has something to do about it and wrong for the far more common one that does not. GetOrThrow makes reads agree with Update and Delete, and with MagicCSharp.AspNetCore the exception is a 404 — so an endpoint that fetches by id has no null branch.

One Get(filter), not a method per question

Almost every query is a filter. A method per question — GetByCustomer, GetPendingSince, GetLatestForCustomer — grows a repository interface without bound and puts the same WHERE clause in three places.

var pendingOrders = await ordersRepository.Get(new OrderFilter
{
    CustomerId = customerId,
    Status = OrderStatus.Pending,
    Created = new ComparableRange<DateTimeOffset> { Start = timeProvider.GetUtcNow().AddDays(-7) },
});

A filter property is nullable, and null means "do not narrow on this". Add a property when a caller needs to narrow on something new. In the implementation, these helpers turn each property into a predicate and skip it when it is null:

Helper Narrows on
ApplyNullableValueFilter(value, x => (long?)x.CustomerId) equality on a nullable struct
ApplyStringNullableValueFilter(value, x => x.Name, StringFilterOperation.Contains) Equals, Contains, StartsWith or EndsWith, case-insensitive
ApplyComparableRangeFilter(range, x => (DateTimeOffset?)x.Created) a ComparableRange<T>, with inclusive or exclusive ends
ApplyListFilter(values, x => (long?)x.Id) membership in a list
ApplyNavigationNullableFilter(values, x => x.Tags, tag => (long?)tag.Id) any related row's value being in a list
ApplyIsDeletedFilter(isDeleted, x => x.Deleted) null: every row; false: live only; true: deleted only

The selector casts to the nullable type so the helper can compare against a possibly-null column. An empty list deliberately matches nothing rather than everything: Ids = [] from a caller that computed "no orders" should return no orders.

Write a custom method when the filter genuinely cannot express the query — a join across several tables, a window function, an aggregation, raw SQL for performance. Those are the exceptions, and the interface should say so by having very few of them.

Pages

var page = await ordersRepository.Get(new PaginationRequest(pageSize: 20, page: 1), filter);

page.Items;        // this page
page.TotalCount;   // so a caller can render "page 3 of 12" without a second query
page.TotalPages;

PaginationRequest clamps to a minimum page of 1 and page size of 1, and defaults to 50 per page. new PaginationRequest(isDisabled: true) returns everything, for an export.

A port, not Entity Framework

The paved path is EF + Postgres. The port is not. DynamoDB for writes with Elasticsearch for queries can sit behind the same IOrdersRepository, and the use case that calls it does not change. What changes is the filter: on DynamoDB you can only add a property you can actually query — a key, an index — where Elasticsearch can be far wider. The filter is where you admit what a store can do, and adding a property to it is the moment to know which adapter you are stretching. That is not the AWS SDK leaking into a use case; it is engineers knowing their access patterns.

ISoftDeleteRepository<TEntity, TKey, TFilter> keeps the row and stamps Deleted, for anything a user can delete by mistake, anything an audit trail references, and anything a foreign key still points at. It coexists with Delete on purpose — soft delete for normal use, hard delete for a genuine purge. Soft-deleted rows still come back from queries unless the filter excludes them; do that in the repository's own filter application so callers cannot forget.

ISearchRepository<TKey> is free-text search without a search engine: one denormalized column of normalized keywords per row, which the caller refreshes with UpdateSearch(key, keywords) whenever a contributing value changes. SearchText does the normalization on both sides — lowercase, punctuation stripped, an optional synonym map — so "St." in the query finds "Street" in the data if the map says they are the same. A row matches when it contains every term, so more words narrow the result rather than widening it.

Implementations

The whole picture, and the optional repository layout: github.com/MagicDoorInc/MagicCSharp. MIT.

Product Compatible and additional computed target framework versions.
.NET 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 was computed.  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 (1)

Showing the top 1 NuGet packages that depend on MagicCSharp.Data:

Package Downloads
MagicCSharp.Data.EntityFramework

Entity Framework Core implementation of the MagicCSharp repository contracts. Base repositories for id- and key-keyed entities with batch writes, opt-in pagination, soft delete and search; the DAL base classes; and a DbContext that stores enums by name and normalizes timestamps to UTC. Provider-agnostic.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.2 233 9/24/2026
1.0.1 117 9/24/2026
1.0.0 119 9/23/2026
0.0.13 589 1/12/2026
0.0.12 138 1/12/2026
0.0.11 139 1/12/2026
0.0.9 146 1/12/2026
0.0.7 190 11/1/2025
0.0.6 161 11/1/2025
0.0.4 175 11/1/2025
0.0.2 172 11/1/2025