Visitar URL original
GitHub - cedric56/Queryeables: One data-access API for .NET, local or remote: IEntityStore over Dommel, EF Core or a MagicOnion server, caches that follow the database, entity graphs and a source generator. · GitHub
Skip to content

About

One data-access API for .NET, local or remote: IEntityStore over Dommel, EF Core or a MagicOnion server, caches that follow the database, entity graphs and a source generator.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

94 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Queryeables

CI MIT License .NET 10 AOT analyzers clean Documentation

Queryeables

One data-access API for .NET, local or remote: your entities, their caches and their children, the same code everywhere.

A view model or a service asks for IPersonRepository or IEntityCache<Person, long>. It never knows whether the data comes from SQLite through Dommel, from SQL Server through EF Core, or from a server across the network.

What it is not: not an ORM (it runs on top of Dommel or EF Core, which you keep for complex queries), not a query builder or a query-string filter library, and not a GraphQL server. It is the layer above them: the same store API locally and remotely, in-memory caches that follow the database, parent-child graphs saved in one transaction, and change notifications between clients.

Status: early. Packages are not published on NuGet yet, and the API may still change. To try it now, build them from the sources into a local package source: three commands, in getting started.

dotnet new install Queryeables.Templates
dotnet new queryeables-console -n MyApp            # SQLite, a cache, a person and its contacts
dotnet new queryeables-client-server -n Shop       # the same, through a MagicOnion server

📖 Documentation: getting started, tutorial (local app, cache, graph, server, offline), guides, benchmarks, when to choose something else.

In one minute

[EntityCache]                                            // an in-memory cache that follows the database
public class Person
{
    public long Id { get; set; }
    public string Name { get; set; } = "";

    [EntityGraph(ParentNavigation = nameof(Contact.Owner))]   // saved, removed and loaded with the person
    public List<Contact>? Contacts { get; set; }          // null = not loaded: saving leaves the contacts alone
}

public class Contact
{
    public long Id { get; set; }
    public long PersonId { get; set; }                   // the foreign key, found by its name
    public Person? Owner { get; set; }
}

services.UseQueryeables(q => q
    .UseSqlite("Data Source=app.db")                     // the provider
    .AddGeneratedEntities());                            // what the attributes describe, written by the source generator
public sealed class PersonsViewModel(IPersonRepository persons, IEntityCache<Person, long> cache)
{
    public IReadOnlyCollection<Person> All => cache.Entities;                       // kept up to date by the database
    public Task<EntityResult> SaveAsync(Person person) => persons.SaveAsync(person);    // the person and its contacts, one transaction
}

Two attributes and one registration call. To use a remote server instead, the client changes one line: UseMagicOnion("https://server:5001") instead of UseSqlite(...). The sample runs this end to end on SQLite, then the same service as a remote client of an in-process server.

Start with your provider

Each provider brings the Queryeables package: the contracts, the remote-access core, the setup and the source generator. There is nothing else to add.

Data Package Setup
SQLite Queryeables.Sqlite q.UseSqlite("Data Source=app.db"), with change notifications
PostgreSQL Queryeables.PostgreSql q.UseNpgsql(connectionString, notifications: n => n.Watch<Person>(p => p.Id)), with change notifications
MongoDB Queryeables.MongoDB q.UseMongoDB(connectionString, "app", notifications: n => n.Watch<Person>()), with change streams
SQL Server, MySQL... Queryeables.Dommel q.UseDommel<SqlConnection>(connectionString)
Any EF Core provider Queryeables.EntityFramework q.UseEntityFramework(sp => new AppDbContext(options))
A Queryeables server Queryeables.MagicOnion.Client q.UseMagicOnion("https://server:5001")
Your own store Queryeables q.UseStores(sp => new MyStore(...))
dotnet add package Queryeables.Sqlite   # or .PostgreSql, .MongoDB, .Dommel, .EntityFramework, .MagicOnion.Client

What you get

  • One store API, IEntityStore: async CRUD, filters, paging, counting and transactions, identical for Dommel, EF Core and a remote server. Every implementation passes the same contract tests.
  • Entity graphs, [EntityGraph]: a parent saved, removed and loaded with its children, navigation properties set, related entities found in their caches. The generator writes I{Parent}Repository and {Parent}Graph, and a mistake in the attributes is a compile error (QRY001 to QRY009).
  • Caches that follow the database, [EntityCache]: row by row, bursts coalesced, incremental resynchronization after a reconnection.
  • Transactions that report their outcome: an EntityResult with an error kind (Validation, Conflict, NotFound, Forbidden...) instead of an exception.
  • Remote access without coupling: the remote core speaks to an IEntityTransport, and MagicOnion is one transport among others. No SQL from the client, a whitelist of filter operators, an access policy per type.
  • Offline mode, WithOfflineMode: a remote client keeps working on a local copy (IndexedDB in the browser, SQLite on desktop and mobile), is told when the server is back and when the data differs, and the user chooses when to synchronize. See offline mode and the two-client demo.
  • Per-user data: row-level security on the server, and ChangeUserAsync() on the client, which forgets the caches and the offline copy of the previous user before anything is loaded for the next.
  • Change notifications from SQLite or SQL Server, relayed to clients over MagicOnion or SignalR, without echoing a client's own changes.
  • Sync conventions: [LastModified], [SoftDelete], optimistic concurrency with [ConcurrencyCheck].
  • Traces and metrics through ActivitySource and Meter (Queryeables): remote calls, server requests, caches, changes, offline synchronizations. See observability.
  • Trimming and AOT analyzers clean on every library; the generator replaces reflection-based registration. Full Native AOT works for a client over MagicOnion, not over Dommel or EF Core: see Native AOT.

Local or remote, the same code

flowchart LR
    subgraph Client
        VM[IPersonRepository, caches] --> RS[RemoteEntityStore]
        M[IChangeFeed] --> VM
    end
    subgraph Server["Server (ASP.NET Core)"]
        D[EntityRequestDispatcher<br/>policy, validation] --> PS[IPersonRepository<br/>children saved]
        PS --> ST[IEntityStore]
        ST --> DB[(SQLite, SQL Server...)]
        DB -. changes .-> H[change hub]
    end
    RS -- MagicOnion --> D
    H -. MagicOnion or SignalR .-> M
Loading

Server: Queryeables.MagicOnion.Server.

builder.Services.UseQueryeables(q => q
    .UseSqlite("Data Source=server.db")
    .AddGeneratedEntities()
    .AddMagicOnionServer(server => server
        .Expose<Person>(EntityAccessPolicies.Authenticated)
        .Expose<Contact>(EntityAccessPolicies.Authenticated)));

Client: q.UseMagicOnion("https://server:5001").AddGeneratedEntities(). Keep the entities in a shared library: AddGeneratedEntities() registers them on both sides.

Queries

var page = await stores.Query<Person>()
    .Where(p => p.Name.StartsWith(search) || p.Id == id)
    .OrderBy(p => p.Name).ThenByDescending(p => p.Id)
    .Skip(50).Take(25)
    .Select(p => new PersonRow(p.Id, p.Name))
    .ToListAsync();

The same query locally and remotely: filtered, sorted and paged by the database with EF Core, by the server with a remote store. See stores and transactions.

Transactions and EntityResult

await using var store = stores.Create();      // IEntityStoreFactory, registered by the provider
EntityResult result = await store.RunInTransactionAsync(async s =>
{
    await s.SaveAsync(order, o => o.Id);       // insert when the key is 0, update otherwise
    await s.UpdateAsync(customer);
});

if (!result.Successful)
    Show(result.ErrorCode, result.Exception, result.Errors);

Exceptions inside the work and failed commits become a failed EntityResult; cancellation still throws. .OrThrow() turns a failure back into an EntityStoreException. Register an IEntityStoreFactory, never a store: a store is one connection, disposed by its user.

Is it for you?

Choose Queryeables for an application whose data is local today and may be shared through a server tomorrow, whose clients must see each other's changes, or that saves parent-child aggregates as one unit. Choose something else for an offline-first mobile app with large local databases (Datasync Community Toolkit), strong real-time consistency (ActualLab.Fusion), a complete application framework (ABP), or complex queries (EF Core or Dapper directly, next to Queryeables). The introduction compares them.

Packages

Package Contents
Queryeables Contracts, remote core, UseQueryeables, source generator. No database, no transport
Queryeables.Dommel Stores on Dommel: generated keys, optimistic concurrency, SQL logging
Queryeables.Sqlite Dommel on SQLite, with change notifications; SqliteOfflineStorage for the offline copy
Queryeables.PostgreSql Dommel on PostgreSQL, with change notifications
Queryeables.MongoDB Stores on MongoDB, with transactions and change streams
Queryeables.EntityFramework Stores on EF Core
Queryeables.SqlDependency SQL Server change notifications
Queryeables.MagicOnion MagicOnion messages and serializer, shared by client and server
Queryeables.MagicOnion.Client UseMagicOnion, change hub subscription
Queryeables.MagicOnion.Server AddMagicOnionServer, the MagicOnion service and change hub
Queryeables.Browser IndexedDbOfflineStorage: the offline copy in IndexedDB, for Blazor WebAssembly
Queryeables.SignalR, .SignalR.Server Change notifications over SignalR
Queryeables.Replication A durable server for the Routeables Remote Protocol (offline-first replication with merge): its log and documents in any store, projected to your entities
Queryeables.Templates dotnet new queryeables-console, queryeables-client-server

Security

  • Never expose EntityAccessPolicies.AllowAll on a public network. Configure ASP.NET Core authentication and use Authenticated, ReadOnly, or EntityAccessPolicies.From(request => ...).
  • Keep connection strings and certificate passwords out of the source code.
  • No SQL and no code goes over the wire: a filter is a list of conditions and computations from a fixed set, whose names the server binds to the queryable properties of the types it exposes; a filter through a relation reads the related type with its own access rules.
  • Keep each user to its own rows with a row filter: Expose<Order>(policy, rows: RowLevelSecurity.For<Order, long>(o => o.Id, user => o => o.TenantId == TenantOf(user))). Reads that are not paged stop at maxReadSize (10,000 by default).

Native AOT

Every library sets IsAotCompatible and builds without trimming or AOT warnings. That is a guarantee about Queryeables' own code, not about every provider at run time:

Setup Trimming Full Native AOT
Remote client, caches, graphs, offline mode Yes Yes: proven in CI by Queryeables.AotSmoke (in-process transport)
MagicOnion transport (UseMagicOnion) Yes With the MagicOnion and MessagePack source generators and [MessagePackObject] entities; not run in CI
Dommel / Dapper (UseSqlite, UseNpgsql, UseDommel) Yes No: Dapper emits IL at run time
EF Core (UseEntityFramework) Yes No: marked as requiring dynamic code, like EF Core itself
MongoDB (UseMongoDB) No No: the driver maps by reflection; marked like EF Core

See Native AOT.

Build and test

dotnet test Queryeables.slnx
dotnet run --project Samples/Queryeables.Sample
dotnet run --project Samples/Queryeables.OfflineDemo     # two clients, a network cut, a conflict
dotnet run -c Release --project Queryeables.Benchmarks -- --filter "*"
dotnet pack Queryeables.slnx -c Release -o ./artifacts -p:Version=0.1.0

See CONTRIBUTING.md for the rules of the code and the release steps, and CHANGELOG.md.

Design ideas come from the Datasync Community Toolkit, ActualLab.Fusion, ABP and Breeze: see CREDITS.md. Released under the MIT License.

About

One data-access API for .NET, local or remote: IEntityStore over Dommel, EF Core or a MagicOnion server, caches that follow the database, entity graphs and a source generator.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages