Observe a DbSet
Once ordinary EF reads work, observation can refresh a result when the underlying table changes. Arc combines in-process SaveChanges interception with optional provider notifications. SQLite does not poll external writes.
Configure the host
Section titled “Configure the host”WithEntityFrameworkCore() registers observation services. For explicit registration in an existing Arc host, import Cratis.Arc.EntityFrameworkCore, Cratis.Arc.EntityFrameworkCore.Observe, Microsoft.EntityFrameworkCore, and Microsoft.Extensions.DependencyInjection, then use this startup fragment:
services.AddEntityFrameworkCoreObservation();services.AddDbContextWithConnectionString<OrdersDbContext>("Data Source=orders.db");Arc’s context helpers add the observation interceptor when those services are available. For ordinary EF registration, add .AddObservation(serviceProvider) in the two-argument options callback. The context must also be resolvable from a fresh scope: observation resolves its concrete type for initial and subsequent queries.
Keep the normal Arc registration and activation. Observe() accesses Arc’s initialized service provider and current query context; adding EF services alone is not a complete observation host.
Observe results
Section titled “Observe results”These are query-method fragments using your application’s context/entities. Here Customer.Id and customerId use the application’s CustomerId : ConceptAs<Guid> domain identity. Import Microsoft.EntityFrameworkCore and System.Reactive.Linq.
var orders = dbContext.Orders.Observe(order => order.IsPending);var customer = dbContext.Customers.ObserveSingle(customer => customer.Email == email);var byId = dbContext.Customers.ObserveById<Customer, CustomerId>(customerId);Observe() returns ISubject<IEnumerable<TEntity>>. ObserveSingle() and ObserveById() return ISubject<TEntity> and emit a value only when an entity exists; absence is not a null notification. ObserveById requires a public Id property. Collection observation uses single-key EF metadata where available, falling back to Id.
To customize loading, use the second callback:
var orders = dbContext.Orders.Observe( order => order.IsPending, configure: set => set.Include(order => order.Lines));Arc applies the filter and current query-context paging/sorting to the configured query. Initial querying is synchronous and can throw before a subject is returned. Later re-query errors are logged, not sent as OnError to subscribers.
Provider capabilities
Section titled “Provider capabilities”| Provider | In-process writes | External writes and prerequisites |
|---|---|---|
| SQLite | SaveChanges through contexts with Arc’s observation interceptor | No external notifier; StartListening is a no-op. No polling fallback. |
| PostgreSQL | Same interception path | LISTEN/NOTIFY; Arc attempts to create a function and table trigger. The database user needs suitable permissions, or an administrator must supply a compatible trigger/channel. |
| SQL Server | Same interception path | SqlDependency with Service Broker enabled and query-notification permissions/compatible queries. Arc attempts to enable Service Broker. No general polling fallback. |
A PostgreSQL listener can start even when trigger creation fails. That alone does not establish that external changes are observable. Notifier setup failures are logged and observation can continue with in-process notifications only. Monitor logs and verify an external write in a test database before relying on cross-process updates.
Notifications cause a query refresh; they are not a durable change log. Do not assume every intermediate state is delivered, that raw/bulk SQL passes through SaveChanges interception, or that changes to every included relationship independently trigger the root-table observation.
Lifetime and disposal
Section titled “Lifetime and disposal”Arc’s streaming transports dispose their observer subscriptions when clients disconnect; they do not complete the EF subject. EF producer cleanup is tied to subject completion, so returning a raw EF subject does not give its producer a disconnect-driven lifetime. An HTTP snapshot, including waitForFirstResult=true, does not close this ownership gap.
Before exposing EF observation through an observable query, establish an application-owned producer lifetime and test disconnects, concurrent clients, and shutdown. Do not complete a shared subject just because one client leaves: that would stop other clients too.
For a subject you own directly, retain both the subject and subscription and complete the subject when finished. Disposing the Rx subscription alone does not stop this EF observation implementation.
Lifecycle fragment inside an asynchronous method:
var subject = dbContext.Orders.Observe(order => order.IsPending);using var subscription = subject.Subscribe(orders => Console.WriteLine(orders.Count()));try{ await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);}finally{ subject.OnCompleted();}Completion initiates notifier/interceptor cleanup; it is not an awaitable guarantee that notifier disposal has finished. Test cleanup against concurrent notifications with your provider. This lifecycle differs from MongoDB collection observation, whose subject tracks subscriber disposal.
Scope and isolation
Section titled “Scope and isolation”Observation recreates scopes for reads. Arc’s pooled EF registrations keep the configured connection string; they do not infer per-tenant databases from the current tenant. Validate your application’s tenant strategy across initial reads, notification callbacks, and pooled reuse before using observation in a multi-tenant application.
Continue with read-only contexts and registration.