ProphetsWay.EFTools 3.0.0

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

ProphetsWay.EFTools

Write your Data Access Layer as declarations. EFTools is a set of abstract Entity Framework Core base classes that implement the CRUD, paging, soft-delete, ordering, transaction, ownership and disposal plumbing behind a ProphetsWay.BaseDataAccess Data Access Layer — so the only code you write is the code your own contract adds.

Build Status NuGet License: MIT

Which version this document describes

This README documents the 3.0.0 line, which is not released. There is no 3.0.0 package on nuget.org and you cannot install it today. The published package is 2.2.0 — a different library, built on Entity Framework 6 and EF Core, targeting net461 through net90, with a class surface 3.0.0 removes. Nothing below applies to it. If you are on 2.2.x, read CHANGELOG.md before you plan an upgrade; the migration is real work and there are no compatibility shims.

The 2.2.x classes are gone from the source, not deprecated in place. See Names removed in 3.0.0 for what they were and what took over.


Why EFTools

Write an Entity Framework Data Access Layer by hand and you write the same forty lines per entity, seven times. Read untracked; order stably or your paging silently overlaps; copy the argument so a rollback can actually reverse an update; stamp created, updated and deleted dates; detach in a finally so a failed write does not flush on the next SaveChanges; translate the identifier lookup into a parameterized predicate rather than a SQL literal. Every one of those is easy to get subtly wrong, and none of them is your domain.

EFTools already wrote them. You pick the base class matching what your Data Access Object actually promises, pass it the context, and stop.

Here is a complete Data Access Object from the reference implementation in this repository:

internal class JobDao : BaseGetAllDao<Job, int>, IJobDao
{
	public JobDao(DbContext context) : base(context) { }
}

That class has working Get, Insert, Update, Delete and GetAll — untracked reads, copy-on-write, stable ordering, parameterized key lookup — and one line of body.

Highlights

  • A Data Access Object costs a constructor. Of the seven in this repository's proving ground, one writes nothing beyond it, four write exactly one member of their own, and none writes more than three.
  • Soft delete is free, and it is not bypassable. BaseSoftDao expresses every difference as an override, never a new, so a soft Data Access Object reached through a BaseDao-typed reference still soft-deletes instead of destroying the row.
  • Keys are not restricted to int / long / Guid. TKey carries no constraint at all, so a string key or an int? key is legal.
  • Keyless entities have a real home. Join tables and composite natural keys get a family whose Root* members implement no capability interface — deriving from one commits you to publishing nothing.
  • Disposal and transactions are specified, not improvised. Dispose is sealed, idempotent, never throws, and rolls an open transaction back; every other member throws ObjectDisposedException afterward; the caller states who owns the context.
  • The library names no database provider. Nothing in its C# calls UseSqlServer. You build the options; you pick the provider. One caveat, stated honestly: provider neutrality.

Install

3.0.0 is unreleased. To use the library described here, build it from source — see Building & Testing Locally.

The package that exists on nuget.org today is the 2.2.x line, which this document does not describe:

dotnet add package ProphetsWay.EFTools --version 2.2.0
Install-Package ProphetsWay.EFTools -Version 2.2.0

What 3.0.0 will target

Target framework net10.0 — and only net10.0
Entity Framework Core 10.0.11. Entity Framework 6 is not supported
Contracts ProphetsWay.BaseDataAccess 3.2.0

The single target is deliberate rather than an oversight — see Architecture & Design Decisions. If you are on .NET Framework, .NET 8 or .NET 9, stay on the 2.2.x line.


Quick Start

Three files: a context, a Data Access Object, and a Data Access Layer that hands them the context.

1 — Derive your context

BaseEFContext selects no provider and adds no members. Whoever builds the options names the database.

using Microsoft.EntityFrameworkCore;
using ProphetsWay.EFTools;

public class ExampleContext : BaseEFContext
{
	public ExampleContext(DbContextOptions<ExampleContext> options) : base(options) { }

	public DbSet<Company> Companies { get; set; }
	public DbSet<Job> Jobs { get; set; }
	public DbSet<User> Users { get; set; }

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.Entity<User>().HasOne(x => x.Company).WithMany().HasForeignKey("CompanyId");
		modelBuilder.Entity<User>().HasOne(x => x.Job).WithMany().HasForeignKey("JobId");

		modelBuilder.Entity<Company>().ToTable("Companies");
		modelBuilder.Entity<Job>().ToTable("Jobs");
		modelBuilder.Entity<User>().ToTable("Users");
	}
}

Deriving from BaseEFContext is optional. BaseEFDataAccess<TContext> constrains its context to DbContext, so a context you already have — one registered in a dependency-injection container, for instance — needs no re-parenting.

2 — Derive each Data Access Object from the family matching its capability

using Microsoft.EntityFrameworkCore;
using ProphetsWay.EFTools;

internal class UserDao : BaseDao<User, int>, IUserDao
{
	public UserDao(DbContext context) : base(context) { }

	public void CustomUserFunctionality(User user)
	{
		user.Whatever = "custom functionality triggered";
		Update(user);
	}
}

3 — Derive your Data Access Layer, and say who owns the context

using Microsoft.EntityFrameworkCore;
using ProphetsWay.EFTools;

public class ExampleDataAccess : BaseEFDataAccess<ExampleContext>, IExampleDataAccess
{
	private readonly IUserDao _userDao;

	//The provider is named here, in your own file. A PostgreSQL consumer writes UseNpgsql instead.
	public ExampleDataAccess(string connectionString)
		: this(new ExampleContext(new DbContextOptionsBuilder<ExampleContext>()
			.UseSqlServer(connectionString)
			.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
			.Options), ContextOwnership.Owned)
	{ }

	//A context someone else built and will dispose - a DI container, or a test with its own database.
	public ExampleDataAccess(ExampleContext context)
		: this(context, ContextOwnership.Borrowed)
	{ }

	private ExampleDataAccess(ExampleContext context, ContextOwnership ownership)
		: base(context, ownership)
	{
		_userDao = new UserDao(Context);
	}

	public User Get(User item)
	{
		ThrowIfDisposed();
		return _userDao.Get(item);
	}

	public void Insert(User item)
	{
		ThrowIfDisposed();
		_userDao.Insert(item);
	}

	public int Update(User item)
	{
		ThrowIfDisposed();
		return _userDao.Update(item);
	}

	public int Delete(User item)
	{
		ThrowIfDisposed();
		return _userDao.Delete(item);
	}
}

What you get

Every CRUD call on User works. Reads come back as untracked snapshots. Writes read your argument rather than adopting it. Insert assigns the store-generated identifier back onto your instance once the row has committed. The layer disposes the context it built, and refuses every call once disposed.

using (var dal = new ExampleDataAccess(connectionString))
{
	dal.Insert(user);                    //user.Id now holds the stored identifier
	var fetched = dal.Get<User>(user.Id);
}

Get<T>(id) comes from ProphetsWay.BaseDataAccess's reflection dispatcher, which BaseEFDataAccess derives from and overrides. No type parameter on your layer confers it.


Core Concepts

The entity is the argument, and that is the point

Every CRUD method takes the entity as its argument — Get(User item), not GetUser(int id). It looks redundant until you see what it buys: your Data Access Layer publishes one method name per operation, and the argument type selects which Data Access Object handles it. Insert(company) and Insert(user) are the same name on the same interface, and nothing in your business layer names a table, a Data Access Object or a store.

For Get and Delete, only the identifier property of the instance you pass needs a value.

Get does not promise to return the instance you passed. Use the return value — it is a fresh untracked snapshot, never your argument and never the store's own tracked object.

Base* publishes; Root* does not

The prefix is load-bearing in this library, and the two are not interchangeable:

  • Base* implements at least one ProphetsWay.BaseDataAccess capability interface. Deriving from it means your Data Access Object answers that interface, whether or not your own contract mentions it.
  • Root* is plumbing that implements no capability interface. Deriving from it commits you to nothing; your own interface declares exactly the subset you support.

That distinction is why a join table's Data Access Object can refuse to publish Get and have the refusal hold structurally rather than by convention.

The key is resolved by name, and its type is open

BaseDao<TEntity, TKey> finds the identifier property by name — {TypeName}Id first, then Id — and validates it in the constructor, so a mis-wired entity fails when you build the layer rather than on first use. The property must be a public instance property of type TKey carrying a set accessor of any visibility.

An explicit IBaseIdEntity<T> implementation does not satisfy this. It compiles to a non-public, interface-qualified property that neither lookup finds, and the constructor throws DataAccessConventionException.

TKey carries no constraint, so string and int? are legal keys alongside int, long and Guid.

The layer owns the context, the transaction and disposal

Data Access Objects have no transaction members in 3.0.0. TransactionStart, TransactionCommit and TransactionRollBack live on BaseEFDataAccess<TContext>, because a transaction in EF Core lives on the context and every Data Access Object on a layer shares one.

  • One transaction per instance, and they do not nest — a second TransactionStart throws InvalidOperationException.
  • A failed commit leaves nothing open and discards its writes. Do not follow one with TransactionRollBack; there is nothing left to roll back and that call throws.
  • TransactionStart never inspects Context.Database.CurrentTransaction, so it can never check-and-skip. On a Borrowed context whose owner already began a transaction directly, EF Core's own exception propagates unwrapped and that outer transaction is left in force.

Disposal is a specified contract, not a convenience. Dispose is sealed, idempotent and never throws; it rolls an open transaction back; it runs your DisposeCore() hook; and it disposes the context only under ContextOwnership.Owned. All ten other members throw ObjectDisposedException once it has run.

One obligation is yours. Nothing intercepts calls to members your layer declares. Call the protected ThrowIfDisposed() as the first statement of every per-entity forwarder and every custom method you write — as the Quick Start layer above does — or a disposed Borrowed layer will silently serve them from a context that is still alive.

Ownership is stated, never defaulted

public enum ContextOwnership { Borrowed = 0, Owned = 1 }

There is no default to fall into, and the constructor rejects anything that is neither value with ArgumentOutOfRangeException. A defaulted ownership is the shape that produces a silent double-dispose in a dependency-injection host and a silent leak in a hand-built one, depending only on which way the default happened to point.


API Reference

Thirteen public declarations — twelve abstract classes and one enum. All in namespace ProphetsWay.EFTools, with no sub-namespaces.

The context and the layer

Type Member Purpose
BaseEFContext : DbContext protected BaseEFContext(DbContextOptions) Its only member. Optional base for your context; selects no provider
BaseEFDataAccess<TContext> protected BaseEFDataAccess(TContext, ContextOwnership) Its only constructor. Captures a built context and who disposes it — no query, no connection, no transaction
protected TContext Context The context every Data Access Object on the layer shares, typed so you construct them without a cast
protected ContextOwnership Ownership What you told it
protected bool IsDisposed true from the first statement of Dispose onward
protected void ThrowIfDisposed() Call this first in every member you declare
public override void TransactionStart() Begins; throws if one is already open
public override void TransactionCommit() Commits and clears state; a failure discards the writes
public override void TransactionRollBack() Rolls back and clears state
public sealed override void Dispose() Idempotent, non-throwing; rolls back; disposes the context only when Owned
protected virtual void DisposeCore() Your hook. Must not throw
Get<T>(object id), GetAll<T>(), GetPaged<T>(int, int), GetCount<T>(), Insert<T>, Update<T>, Delete<T> Dispatcher overrides inherited from ProphetsWay.BaseDataAccess, each guarded
ContextOwnership (enum) Borrowed = 0, Owned = 1 No default value

BaseEFDataAccess<TContext> is declared : BaseDataAccess.BaseDataAccess, IBaseDataAccess where TContext : DbContext.

The keyed families — where TEntity : class, IBaseIdEntity<TKey>

Type Base Adds
BaseDao<TEntity, TKey> — IBaseDao<TEntity> — Get, Insert, Update, Delete
BaseGetAllDao<TEntity, TKey> BaseDao IBaseGetAllDao<TEntity> — publishes GetAll
BasePagedDao<TEntity, TKey> BaseDao IBasePagedDao<TEntity> — publishes GetPaged and GetCount

Each takes protected BaseXxxDao(DbContext context). BaseDao<TEntity, TKey> carries:

Kind Members
Public TEntity? Get(TEntity item), void Insert(TEntity item), int Update(TEntity item), int Delete(TEntity item), IList<TEntity> GetAll(TEntity? item), IList<TEntity> GetPaged(TEntity? item, int skip, int take), int GetCount(TEntity? item)
Protected state DbContext Context, DbSet<TEntity> Dataset
Protected seams TrackForWrite, ApplyUpdateValues, GetKey, MatchRow, KeyEquals, KeySelector, ApplyReadFilter, ApplyIncludes, ApplyStableOrder

The item argument on GetAll, GetPaged and GetCount is a type selector only, and it is null when the call arrives through the dispatcher. Never read it.

Get is annotated TEntity?, and "not found" is what that expresses. The whole library is compiled under #nullable enable, and ProphetsWay.BaseDataAccess 3.2.0 annotates IBaseDao<T> to match — so a call through the interface now binds Company? where it used to bind an unannotated Company. If you compile with nullable warnings on, expect a CS8602 at any site that dereferences a Get result without checking it. It is a new warning, not a break: the change is source- and binary-compatible, and the warning is pointing at a null you could always have received.

The keyed soft-delete families — where TEntity : class, IBaseSoftIdEntity<TKey>

Type Base Adds
BaseSoftDao<TEntity, TKey> BaseDao<TEntity, TKey> Soft Insert / Update / Delete, the created, updated and deleted timestamps, and a DeletedDate == null read filter
BaseSoftGetAllDao<TEntity, TKey> BaseSoftDao IBaseGetAllDao<TEntity>
BaseSoftPagedDao<TEntity, TKey> BaseSoftDao IBasePagedDao<TEntity>

Two timestamp hooks are overridable: protected virtual DateTime GetCurrentTimestamp() and protected virtual DateTime NormalizeRetrievedTimestamp(DateTime value) — the latter restores the DateTimeKind a relational provider does not persist.

ApplyReadFilter adds DeletedDate == null and nothing else, so GetAll, GetPaged and GetCount agree with one another while the unfiltered Get still finds a deleted row.

The keyless families

Type Base Implements For
RootNonIdDao<TEntity> — nothing TEntity : class, IBaseEntity. Plumbing that commits you to no interface
BaseNonIdDao<TEntity> RootNonIdDao IBaseDao<TEntity> The same, when you do want the full shape. Adds virtual Get and Update
RootSoftNonIdDao<TEntity> RootNonIdDao nothing TEntity : class, IBaseSoftEntity. Keyless soft delete
BaseSoftNonIdDao<TEntity> RootSoftNonIdDao IBaseDao<TEntity> Keyless soft delete with the full shape

RootNonIdDao<TEntity> publishes Insert, Delete, GetAll, GetPaged and GetCount, and exposes GetCore, UpdateCore, TrackForWrite, ApplyStableOrder, ApplyReadFilter, ApplyIncludes and ApplyUpdateValues as protected seams. Two members deserve attention:

  • protected abstract Expression<Func<TEntity, bool>> MatchRow(TEntity item) — the predicate identifying a single row. It replaces the entire identifier apparatus, and it is the only abstract member in the library. It is translated to SQL, not evaluated in memory.
  • ApplyStableOrder throws NotSupportedException until you override it. GetCore and UpdateCore order nothing and are unaffected, so a write-only join Data Access Object is fully functional with MatchRow alone.

Common Scenarios

Every sample below is taken from ProphetsWay.Example.DataAccess.EF, the working implementation in this repository.

A paged Data Access Object with one method of its own

protected DbSet<TEntity> Dataset is available on the keyed and keyless families alike, so a custom method composes over it directly.

using Microsoft.EntityFrameworkCore;
using ProphetsWay.EFTools;
using System.Linq;

internal class CompanyDao : BasePagedDao<Company, int>, ICompanyDao
{
	public CompanyDao(DbContext context) : base(context) { }

	public Company GetCustomCompanyFunction(int id)
	{
		return Dataset.OrderBy(x => x.Id).Skip(id % GetCount(null)).First();
	}
}

Soft delete, and reaching a row the read filter hides

DepartmentDao derives from BaseSoftPagedDao<Department, int> and satisfies eighteen of its nineteen contract rules without overriding anything. The nineteenth — Restore — belongs to its own interface and cannot be expressed through Update, which deliberately preserves the stored DeletedDate.

TrackForWrite is the seam that makes it possible: it starts from the raw Dataset, which is what keeps ApplyReadFilter off the query. That filter excludes exactly the rows Restore exists to reach, so a Restore composed on top of it would return 0 for every department it was ever asked about.

public int Restore(Department item)
{
	if (item == null)
		throw new ArgumentNullException(nameof(item));

	var stored = TrackForWrite(item);

	if (stored == null)
		return 0;

	try
	{
		if (!stored.DeletedDate.HasValue)
			return 0;

		stored.DeletedDate = null;
		Context.SaveChanges();
		item.DeletedDate = null;

		return 1;
	}
	finally
	{
		Context.Entry(stored).State = EntityState.Detached;
	}
}

The finally is not optional. stored is tracked from the moment TrackForWrite returns, so every exit owes the detach — the success path, a throwing SaveChanges, and the early return that writes nothing. One left behind carries a pending change that the next write on the shared context would flush.

A keyless join table

CompanyResource has no identifier at all; its identity is the (CompanyId, ResourceId) pair. Its interface does not inherit IBaseDao<T>, so the Data Access Object derives from RootNonIdDao<CompanyResource>, which publishes no Get. Deriving from BaseNonIdDao<CompanyResource> instead would publish one and remove that guarantee silently, with every behavioral test still green.

using Microsoft.EntityFrameworkCore;
using ProphetsWay.EFTools;
using System;
using System.Linq;
using System.Linq.Expressions;

internal class CompanyResourceDao : RootNonIdDao<CompanyResource>, ICompanyResourceDao
{
	public CompanyResourceDao(DbContext context) : base(context) { }

	protected override Expression<Func<CompanyResource, bool>> MatchRow(CompanyResource item)
	{
		return x => x.CompanyId == item.CompanyId && x.ResourceId == item.ResourceId;
	}

	protected override IOrderedQueryable<CompanyResource> ApplyStableOrder(IQueryable<CompanyResource> query)
	{
		return query.OrderBy(x => x.CompanyId).ThenBy(x => x.ResourceId);
	}

	//This Data Access Object's own contract: a pair the store already holds is a silent no-op, not an error.
	public override void Insert(CompanyResource item)
	{
		if (item == null)
			throw new ArgumentNullException(nameof(item));

		if (Dataset.IgnoreQueryFilters().AsNoTracking().Any(MatchRow(item)))
			return;

		base.Insert(item);
	}
}

IgnoreQueryFilters() is required for the same reason every MatchRow-located path in the library calls it: a consumer-declared global query filter would otherwise hide the stored row from the check, the guard would fall through, and the base member would hit the primary key.

Map the composite key in your context:

modelBuilder.Entity<CompanyResource>().HasKey(x => new { x.CompanyId, x.ResourceId });
modelBuilder.Entity<CompanyResource>().ToTable("CompanyResources");

Borrowing a context from a dependency-injection container

Illustrative — not currently present in the repo.

services.AddDbContext<ExampleContext>(options => options.UseNpgsql(connectionString));
services.AddScoped<IExampleDataAccess>(sp => new ExampleDataAccess(sp.GetRequiredService<ExampleContext>()));

The ExampleDataAccess(ExampleContext) constructor this calls is real, and it passes ContextOwnership.Borrowed, so the layer never disposes a context the container owns. The library itself imposes no dependency-injection framework and references none.


Architecture & Design Decisions

Entity Framework Core only

2.2.0 shipped two implementations chosen by target framework — EF 6.5.1 on .NET Framework and EF Core on .NET 8 and 9 — and the two did not behave alike. Most visibly, the EF6 Update was an upsert that would store a row that did not exist, while the EF Core one requires the row to be there already. One package did not expose one contract. The EF6 branch is removed outright; 2.2.x remains the answer for .NET Framework.

One target framework

net10.0, alone. This library is a thin layer over EF Core, and carrying runtimes EF Core itself no longer serves cost more than it returned. This is a deliberate, ratified exception to the wider ProphetsWay convention of shipping netstandard2.0 beside a current LTS — taken here because the dependency makes that extra reach illusory.

No compatibility wrappers

None exist and none are planned. The 3.0.0 break is real, and the migration is stated plainly in CHANGELOG.md. A shim that let 2.2.x source compile against 3.0.0 semantics would be worse than the break, because the two lines differ behaviorally and not merely structurally.

The key type is open, and the key value is parameterized

The 2.2.x design closed the key over three value types through a where TIdType : struct constraint, which is why it needed three namespaces. Dropping the constraint collapses eighteen classes into six and makes a string or int? key expressible for the first time.

The key value is carried into the expression tree by reference, through a private carrier field, so the provider parameterizes it. Expression.Constant over the key itself would be emitted as a SQL literal and give a distinct query plan per key value.

Soft delete overrides; it never shadows

Every soft-delete difference is an override. Had any been new, a soft Data Access Object upcast to BaseDao<TEntity, TKey> — which is what happens whenever a layer holds its Data Access Objects behind their interfaces — would hard-delete. That is a data-loss bug no test written against the interface would catch.

Keyed and keyless are unrelated inheritance branches

They share no base class, deliberately: a keyless entity has no identifier to make optional. What they do share is behavior, and that lives in two internal static helpers — EntityGraph for the copy-for-store walk, the inverse-navigation cleanup and the finally detach, and SoftTimestamps for the stamping policy. Duplication of declarations across the two branches is structural and accepted; duplication of logic is not.

Provider neutrality — done in code, not yet in packaging

The library's C# names no provider. UseSqlServer and UseInMemoryDatabase appear nowhere in it. You build the DbContextOptions, so you choose SQL Server, PostgreSQL, SQLite or in-memory — and 3.0.0 removed the BaseEFContext(string) constructor that used to make that choice for you.

The packaging does not match yet, and you should know before you plan a restore. ProphetsWay.EFTools.csproj still carries PackageReference entries for Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.InMemory, so a consumer on PostgreSQL still transitively restores two providers they will never use. Removing a transitive reference is itself a breaking change, so it is scheduled to land inside 3.0.0 rather than after it. Tracked as FR 7 in docs/feature-requests.md.

Names removed in 3.0.0

The 2.2.x class surface was deleted outright rather than deprecated in place, so nothing below is in the source, and nothing below has a shim:

  • the eighteen key-specific closures in ProphetsWay.EFTools.Guid, .Int and .Long — those three namespaces no longer exist, so a using on one stops resolving and the file fails at the top rather than at the class
  • the RootBaseDao<T, TIdType> and RootBaseSoftDao<T, TIdType> bridges beneath them, whose where TIdType : struct constraint is the one the open key exists to drop
  • LegacyBaseNonIdDao<T>, LegacyBaseSoftNonIdDao<T> and LegacyRootNonIdDao<T> — development-branch scaffolding that lived for a handful of commits so the original names could be taken by the types that replace them. If an alpha or beta cut showed you one, it is not part of 3.0.0 and there is nothing to migrate onto

Each of the eighteen closures has a direct replacement, formed by moving the key type out of the namespace and into a second type argument — Int.BaseDao<T> becomes BaseDao<T, int>, and so on through all of them. The full mapping is in CHANGELOG.md; it is not repeated here. The two bridges have no equivalent — derive instead from whichever open-key family matches what your Data Access Object interface publishes.

One name survives the deletion with a different meaning: 2.2.x had an internal RootNonIdDao<T> engine, and 3.0.0 has a public RootNonIdDao<TEntity> extension point. They are different types that happen to share a name — different visibility, different members, and a MatchRow contract the old one never had. The internal one was not promoted; it was deleted and its body absorbed into the bases it served.

Nothing transitional ships. The library is the thirteen public declarations listed in the API Reference plus two internal static helpers, in one flat namespace, and it contains no conditionally compiled code at all.

Further reading

Document Contents
docs/api-contract.md The specification of the 3.0.0 surface, member by member
docs/repo-profile.md Evidence-based inventory — API surface, dependencies, packaging audit, test facts
docs/purpose-and-scope.md What this library is for, and the owner decisions behind it
docs/feature-requests.md The durable record of what was considered and deliberately not built

Building & Testing Locally

The reference implementation lives partly in a git submodule, so clone recursively:

git clone --recurse-submodules https://github.com/ProphetManX/ProphetsWay.EFTools.git
cd ProphetsWay.EFTools
dotnet restore
dotnet build

If you already cloned without it, run git submodule update --init --recursive.

The solution contains seven projects — three owned here (ProphetsWay.EFTools, ProphetsWay.Example.DataAccess.EF, ProphetsWay.EFTools.Tests) and four from the ProphetsWay.Example submodule.

Current SQL Server and Azure test runs

Follow docs/azure-sql-test-execution.md. Local SQL Server defaults to Disposable; Azure requires EFTOOLS_SQLSERVER_STORE_LIFECYCLE=Reusable. Set EFTOOLS_PROVIDER=SqlServer explicitly and supply EFTOOLS_SQLSERVER_CONNECTION_STRING securely as the base connection. Set EFTOOLS_SQLSERVER_SCRATCH_DATABASE_1 and EFTOOLS_SQLSERVER_SCRATCH_DATABASE_2 to two distinct, pre-existing, dedicated scratch databases matching ^EFToolsScratch_[A-Za-z0-9_]{1,113}$. Those two configured names are the reset allowlist. Keep settings private and session-local; do not commit or log connection values. Run only one test process against a scratch pair at a time: the lease pool coordinates callers within that process. Start with two empty scratch databases; do not publish the Example DACPAC to either. The fixtures create their own models after reset.

Destructive reset: reusable leases drop every user foreign key and table before and after use, destroying their contents. Never point scratch settings at a database containing anything you need. Reusable mode does not create or drop databases, or run migrations.

The fixed ProphetsWay.Example database is separate and never scratch-reset. The owner reports its Example DACPAC and synthetic seed data already applied. Before another authorized live run, validate the existing schema, connection and authentication; do not automatically republish the DACPAC. No commit is needed to run tests.

For a separately approved trial, configure the settings above and run from the repository root:

dotnet test ProphetsWay.EFTools.Tests/ProphetsWay.EFTools.Tests.csproj -f net10.0 --filter "Execution!=LocalPhysicalLifecycle"

The filter excludes the six local physical-lifecycle checks, not all Guard=Seam checks. Keep those physical checks in the unfiltered local Disposable run; their prior 6/6 result is separate historical evidence, not part of the filtered trial total.

Owner-run filtered trial, 2026-09-08: the retained TRX reconciliation confirms 369 included executions, all passing, zero failures and zero skips, including all 14 provider-selection-exempt comparison cases. The owner reports 99.4 seconds for the trial and a successful build in 103.5 seconds. See the dated result for the evidence boundaries. Azure endpoint and authentication were not independently verified. Gate 2, FR 19, and release remain pending owner review; this passing milestone is not certification or release sign-off. The two owner-specific defaults were removed from the fixture template. The owner prefers history removal but accepts leaving prior history intact and accepts the fixture source as-is. Those decisions do not independently verify live infrastructure or clear the infrastructure review gate.

Running the tests

dotnet test

SQL Server is the default test provider. TestStore.cs owns connection configuration: localhost, integrated security and TrustServerCertificate=True by default, with an optional EFTOOLS_SQLSERVER_CONNECTION_STRING override. Constants.cs names the fixed ProphetsWay.Example database and delegates configuration to TestStore. Its schema comes from the submodule's ProphetsWay.Example.Database project; the existing baseline is prepared separately and is not scratch-reset. CI policy is unchanged: app-variables.yml sets LocalTestsOnly: 'yes' and the pipeline skips the suite entirely.

Direct fixtures also use TestStore and follow its provider selection. Set EFTOOLS_PROVIDER=Sqlite in the test process for SQLite in-memory checks without SQL Server; leaving it unset selects SQL Server. The following trait filter selects the keyless and insert areas, not a provider:

dotnet test --filter "Area=Keyless|Area=Insert"

How the conformance suite works

The interesting half of the suite is not written here. ProphetsWay.Example.Tests — in the submodule — tests IExampleDataAccess without naming any implementation. This repository adapts those classes with one-line subclasses (public class EFUserDaoTests : UserDaoTests { }) and repoints them at the Entity Framework implementation through a single [ModuleInitializer] in TestSeam.cs.

The same test bodies also run against an in-memory implementation upstream. That is the argument for the whole paradigm: swap the Data Access Layer, change nothing else, and the tests still pass.

The ProphetsWay.Example submodule is a pinned pointer to another repository. Never edit files under ProphetsWay.Example/ from here — change them upstream and advance the pointer.


Contributing

Issues and pull requests are welcome at github.com/ProphetManX/ProphetsWay.EFTools.

Two things to know before you open one:

  • The 3.0.0 line is open and breaking. The 2.2.x surface is already deleted; additions to the twelve-class surface that replaced it are weighed against docs/api-contract.md first.
  • Versions are set by hand in app-variables.yml, by the owner alone. Do not bump them in a pull request.

Versioning

We use SemVer for versioning. For the versions available, see the tags on this repository.

Changelog

See CHANGELOG.md. The 3.0.0 entry is the migration guide — read it before upgrading from 2.2.x.

Authors

See also the list of contributors who participated in this project.

License

This project is licensed under the MIT License - see the LICENSE file for details.

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
3.0.0 110 9/12/2026
3.0.0-518.Beta 75 9/12/2026
3.0.0-518.Alpha 65 9/12/2026
3.0.0-514.Alpha 92 8/25/2026
3.0.0-513.Alpha 73 8/24/2026
3.0.0-512.Alpha 78 8/23/2026
3.0.0-510.Alpha 83 8/23/2026
3.0.0-509.Beta 71 8/23/2026
3.0.0-509.Alpha 68 8/23/2026
3.0.0-508.Alpha 76 8/23/2026
3.0.0-502.Alpha 76 8/23/2026
2.2.0 289 5/2/2025
2.2.0-471.Beta 171 5/2/2025
2.2.0-471.Alpha 154 5/2/2025
2.1.1 239 5/1/2025
2.1.1-465.Beta 169 5/1/2025
2.1.1-465.Alpha 177 5/1/2025
2.1.0-461.Alpha 162 5/1/2025
2.0.0-445.Beta 762 8/15/2022
2.0.0-445.Alpha 250 8/15/2022
Loading failed

# v3.0.0
Not yet published — nuget.org still serves 2.2.0 as the latest version of this package.  Everything below is a
change against that published 2.2.0 package.

This is a rewrite of the library on top of Entity Framework Core alone, and it breaks nearly every consumer.
Read the whole entry before upgrading.  There are no compatibility wrappers and none are planned; if the
migration is not worth your afternoon, 2.2.x remains installable and is unchanged — but read the next section
before you decide, because *unchanged* includes four defects this release fixes.

### If you are on 2.2.0: four defects you are exposed to, and three of them are silent
The published 2.2.0 package carries four defects.  **Three of the four fail silently** — no exception, no
failed build, no log line, just wrong data — so never having noticed one is not evidence you are unaffected.
Each has its own ```Fixed:``` section further down; this is the short list, so that a reader who is not
upgrading does not have to read the rest of the entry to find them.

- **A leaked context and a leaked connection per Data Access Layer instance.**  ```BaseEFDataAccess```
 constructed its own ```DbContext``` and never disposed it, and there was no ```Dispose``` for you to call
 either.  This is the one that is not silent forever: it surfaces as connection-pool exhaustion under load,
 and it is attributable once you go looking.
- **Commit and rollback that silently do nothing.**  The keyless Data Access Object base began a transaction
 only when none was already open, then reached the transaction it never stored through ```?.``` when
 committing and rolling back.  Both calls returned normally having done nothing at all.
- **```Update``` that un-deletes a soft-deleted row.**  Whole-object replacement wiped the stored
 ```DeletedDate```, so updating a soft-deleted row brought it back into every read.  A second ```Delete```
 also moved the original timestamp forward and returned ```1``` rather than ```0```.
- **Reads and writes that hand back the store's own tracked object.**  An edit you made and never submitted
 was written to the database on the next ```SaveChanges``` anywhere on that layer.

**No 2.2.1 will be cut.**  These are documented rather than patched — a deliberate decision taken because the
2.2.x consumer base is judged to be effectively empty, not an oversight — and the upgrade to 3.0.0 is the
remedy on offer.  **That remedy is not available to everyone.**  This release targets ```net10.0``` and
nothing else, so if you are on ```net48```, ```net8.0``` or ```net9.0``` you cannot take it at all, and what
follows is a description of what you are running rather than an upgrade path.  For that case the mitigations
live in your own code: there is no ```Dispose``` in 2.2.0 to call, so create as few Data Access Layer
instances as you can and keep them short-lived; drive transactions through ```Context.Database``` yourself
rather than through a Data Access Object's ```EnsureBeginTransaction```/```EnsureTransactionCommit```/
```EnsureTransactionRollback``` members; never pass a soft-deleted entity to ```Update```; and configure
```QueryTrackingBehavior.NoTracking``` on the context you hand in, which closes the ```Get``` half of the
fourth defect though not the ```Insert``` half.

### Entity Framework 6 support is gone
The 2.2.0 package shipped two implementations chosen by target framework — EntityFramework 6.5.1 on the .NET
Framework targets and EF Core on .NET 8 and 9 — and the two did not behave alike.  Most visibly, the EF6
```Update``` was an upsert that would store a row that did not exist, while the EF Core one requires the row to
be there already.  The EF6 branch has been removed outright.  If you were relying on the upsert, you now need an
explicit ```Insert```.

### Target frameworks collapse to net10.0
```net461```, ```net471```, ```net48```, ```net80``` and ```net90``` are all gone and ```net10.0``` is the only
target.  This is deliberate rather than incidental — this library is a thin layer over EF Core, and carrying
runtimes EF Core itself no longer serves was costing more than it returned.  If you are on .NET Framework, .NET 8
or .NET 9 you cannot restore this version and should remain on the 2.2.x line.  Entity Framework Core moves from
9.0.4 to 10.0.11 alongside it.

### ProphetsWay.BaseDataAccess moves from 2.5.0 to 3.2.0
Three changes in that dependency reach you through this one.  ```IBaseDataAccess``` now extends ```IDisposable```,
so every Data Access Layer has a ```Dispose``` and callers are expected to use one.  Exceptions thrown by your
own Data Access Layer methods are no longer wrapped in ```TargetInvocationException``` on their way out of the
dispatcher — if you were catching that wrapper and unwrapping ```InnerException```, catch the real exception type
instead.  And 3.2.0 annotates that package's contracts for nullable reference types, which changes what the
compiler can tell you rather than what runs.

The annotation that reaches an ordinary caller is on ```IBaseDao<T>```, whose ```Get``` every earlier version of
that package left null-oblivious:

```c#
// 2.5.0 and 3.1.0 — null-oblivious
T Get(T item);

// 3.2.0
T? Get(T item);
```
Nothing about the behavior is new — a ```Get``` for a row that is not there has always come back ```null``` — but
a call through your own ```ICompanyDao : IBaseDao<Company>``` now binds ```Company?``` instead of an oblivious
```Company```, so a project with nullable enabled may see a ```CS8602``` where it previously saw nothing.  That is
a new warning and not a break: no signature changed, nothing you have compiled needs rebuilding, and a project
without nullable enabled sees none of it.  If you implement ```IBaseDao<T>``` yourself rather than deriving from a
base in this library, read that package's own 3.2.0 entry as well — ```GetAll```, ```GetPaged``` and
```GetCount``` gained an annotated parameter, and an implementation declaring a plain ```T``` there is now
stricter than the interface allows and will report ```CS8767```.

Taking 3.2.0 is also what let the last of this library's warning suppressions go.  Ten
```#pragma warning disable CS8766``` pairs across eight files were holding down a diagnostic raised because the
bases here declared the nullable ```Get``` return their documentation described while the interface they
implemented could not say the same thing.  Both sides agree now, so the suppressions are deleted rather than
replaced and no ```#pragma``` of any kind is left in the library — the compiler verifies what it was previously
being asked to ignore.

### BaseNonIdDao and BaseSoftNonIdDao keep their names but are different types
This is the change most likely to be missed, because these two names do not disappear.  They still resolve, so
the upgrade reads like a small refactor rather than a replacement.  In 2.2.0 these were:

```c#
BaseNonIdDao<T> : IBaseDao<T> where T: class, IBaseEntity
BaseSoftNonIdDao<T> : BaseNonIdDao<T>, IBaseDao<T> where T : class, IBaseSoftEntity
```
They are now built on a separate plumbing class, and the members underneath you are not the members that were
there before:

```c#
BaseNonIdDao<TEntity> : RootNonIdDao<TEntity>, IBaseDao<TEntity>
BaseSoftNonIdDao<TEntity> : RootSoftNonIdDao<TEntity>, IBaseDao<TEntity>
```
```Get``` and ```Update``` were abstract and are now virtual with working defaults, so an existing override
keeps compiling while the code it overrides has changed underneath it.  ```Insert``` and ```Delete``` were
concrete and non-virtual and are now inherited virtual members with new implementations.  ```Context``` and
```Dataset``` were public and are now protected.  The public ```EnsureBeginTransaction```,
```EnsureTransactionCommit``` and ```EnsureTransactionRollback``` members are no longer on this surface.  In
their place ```RootNonIdDao``` declares one new abstract member, ```MatchRow```, which returns the predicate
identifying a single row — so your derived class will at least fail to compile until you supply it, which is the
one piece of this the compiler will catch for you.

The migration is to rewrite the class onto the new keyless family described below, supplying ```MatchRow``` and
dropping any overrides that no longer correspond to anything.  No wrapper preserving the old shape ships in this
release, so there is nothing to fall back to and no reason to delay the rewrite.

### BaseEFDataAccess takes one type parameter and is constructed differently
The published line documented ```BaseEFDataAccess<TContextType, TIdType>```.  That type no longer exists.  The
identifier type parameter was never used for anything the class did, and it is gone:

```c#
BaseEFDataAccess<TContext> : BaseDataAccess.BaseDataAccess, IBaseDataAccess where TContext : DbContext
```
The class is now abstract, and its context constraint has been relaxed from ```BaseEFContext``` to
```DbContext```, so any context you already have will do.  The protected ```Context``` property is typed
```TContext``` rather than ```DbContext```, which means your derived layer sees its own context type without a
cast.

Both public constructors are gone.  ```BaseEFDataAccess(string connectionString)``` and
```BaseEFDataAccess(DbContextOptions options)``` are replaced by a single protected constructor that takes a
built context and a statement of who owns it:

```c#
protected BaseEFDataAccess(TContext context, ContextOwnership ownership)
```
A derived layer that called ```: base(connectionString)``` must now build the context itself — or accept one
from its caller — and say whether it is responsible for disposing it.

### BaseEFContext no longer takes a connection string, and no longer picks a provider
```BaseEFContext(string connectionString)``` has been removed; the ```DbContextOptions``` constructor is the
only one left.  On the EF Core side that removed constructor called ```UseSqlServer``` for you, which meant the
library chose your database provider.  It no longer does — whoever builds the options names the provider.  This
also removes the v2.0.0 behavior where omitting a connection string produced an in-memory context; build the
options with the provider you want, in-memory included — though as the next section describes, the in-memory
provider is no longer one this package hands you.  Deriving from ```BaseEFContext``` is now optional, since
```BaseEFDataAccess<TContext>``` accepts any ```DbContext```.

### The package no longer brings a database provider with it
Installing 2.2.0 also installed ```Microsoft.EntityFrameworkCore.SqlServer``` and
```Microsoft.EntityFrameworkCore.InMemory```, because both were ```PackageReference```s of the library itself.
A consumer on PostgreSQL, MySQL or SQLite restored two providers they were never going to call, and carried them
into every build and every deployment.  Both are gone from this package.
```Microsoft.EntityFrameworkCore``` 10.0.11 stays, because that is the dependency the library genuinely has.

Measured across the change, the dependency closure of a project taking this package goes from
```EntityFrameworkCore```, ```EntityFrameworkCore.SqlServer``` and ```EntityFrameworkCore.InMemory``` at the top
level — with ```EntityFrameworkCore.Relational``` and ```Microsoft.SqlServer.Server``` behind them — to
```EntityFrameworkCore``` alone, behind which sit only ```EntityFrameworkCore.Abstractions``` and
```EntityFrameworkCore.Analyzers```.  No provider of any kind, and no relational assembly, remains anywhere in
the closure.

**This is a breaking change if you were calling a provider you never referenced yourself.**  A line such as
```options.UseSqlServer(connectionString)``` compiled under 2.2.0 without
```Microsoft.EntityFrameworkCore.SqlServer``` ever appearing in your project file, because this package supplied
it for you.  That line will now fail to compile.  The remedy is one line in your own project file:

```xml
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.11" />
```
Name whichever provider you actually use — ```Npgsql.EntityFrameworkCore.PostgreSQL```,
```Pomelo.EntityFrameworkCore.MySql```, ```Microsoft.EntityFrameworkCore.Sqlite```, or
```Microsoft.EntityFrameworkCore.InMemory``` if what you wanted it for was tests.  Nothing else about the call
changes: no signature moved, no type was removed, and the code you already wrote keeps working the moment the
package it depends on is declared where it belongs.

This is the packaging catching up to the code rather than a change of behavior, and it is worth being precise
about which.  The library has named no provider in code since the ```BaseEFContext``` change described above —
there is no ```UseSqlServer``` or ```UseInMemoryDatabase``` call anywhere in it, and ```BaseEFContext``` exposes
a single ```protected BaseEFContext(DbContextOptions)``` constructor that names nothing.  What lagged behind was
the package manifest, which went on shipping two providers the code had already stopped using.

It lands in 3.0.0 because it cannot land anywhere later.  Removing a dependency changes what restores for
everyone who has already taken the package, so doing this after 3.0.0 shipped would have cost a 4.0.0 on its own.
Provider neutrality is the shape this library was meant to have, and this release is the last opportunity to
reach it without spending another major version to get there.

### The Guid, Int and Long namespaces have been removed
```ProphetsWay.EFTools.Guid```, ```ProphetsWay.EFTools.Int``` and ```ProphetsWay.EFTools.Long``` are gone, and
with them the 18 key-specific base classes they held — six in each namespace, being ```BaseDao```,
```BaseGetAllDao```, ```BasePagedDao``` and their three soft-delete counterparts.  This is likely to be the first
error you see, and it is a blunter one than the rest of this release: ```using ProphetsWay.EFTools.Int;``` no
longer resolves, so the file stops compiling at the top rather than at the class.

Every one of the eighteen has a direct replacement in the open-key families described below, formed by moving the
key type out of the namespace and into a second type argument:

```c#
// 2.2.x
using ProphetsWay.EFTools.Int;
public class UserDao : BasePagedDao<User>, IUserDao { /* … */ }

// 3.0.0
using ProphetsWay.EFTools;
public class UserDao : BasePagedDao<User, int>, IUserDao { /* … */ }
```
```Int.BaseDao<T>``` becomes ```BaseDao<T, int>```, ```Guid.BaseSoftPagedDao<T>``` becomes
```BaseSoftPagedDao<T, Guid>```, ```Long.BaseGetAllDao<T>``` becomes ```BaseGetAllDao<T, long>```, and so on
through all eighteen.  Constructor signatures are unchanged, so for most Data Access Objects the edit is the
```using``` line and one type argument.  The library now lives entirely in the single ```ProphetsWay.EFTools```
namespace.

One collision disappears with them.  ```ProphetsWay.EFTools.Guid``` shadowed ```System.Guid``` in any file that
imported it, which is why a ```Guid``` key sometimes had to be written out as ```System.Guid``` to compile at
all.  That no longer happens.

The two bridge classes underneath the closures, ```RootBaseDao<T, TIdType>``` and
```RootBaseSoftDao<T, TIdType>```, have been removed alongside them.  Both were marked
```[EditorBrowsable(Never)]``` and were not meant to be named directly; if you did name one, there is no 3.0.0
equivalent, and the replacement is to derive from whichever open-key family matches the capabilities your Data
Access Object interface publishes.

### New: keyed Data Access Object families with an open key type
The key-specific bases required a value-type key, through a ```where TIdType : struct``` constraint on the
bridge they closed over.  The new generic families drop it:

```c#
BaseDao<TEntity, TKey> : IBaseDao<TEntity> where TEntity : class, IBaseIdEntity<TKey>
BaseGetAllDao<TEntity, TKey> : BaseDao<TEntity, TKey>, IBaseGetAllDao<TEntity>
BasePagedDao<TEntity, TKey> : BaseDao<TEntity, TKey>, IBasePagedDao<TEntity>
```
```TKey``` carries no constraint at all, so a ```string``` key or a ```Nullable<int>``` key is now legal where
before it was not expressible.  ```BaseSoftDao<TEntity, TKey>```, ```BaseSoftGetAllDao<TEntity, TKey>``` and
```BaseSoftPagedDao<TEntity, TKey>``` are the soft-delete counterparts and manage the created, updated and
deleted timestamps as the 2.x soft classes did.

### New: keyless Data Access Object families
An entity with no identifier — a join table, or one whose only key is a composite natural key — previously had
to be presented through ```IBaseDao<T>``` whether its contract promised that or not.  Four types replace that
arrangement, and they split the plumbing from what you publish:

```c#
RootNonIdDao<TEntity> where TEntity : class, IBaseEntity
BaseNonIdDao<TEntity> : RootNonIdDao<TEntity>, IBaseDao<TEntity>
RootSoftNonIdDao<TEntity> : RootNonIdDao<TEntity> where TEntity : class, IBaseSoftEntity
BaseSoftNonIdDao<TEntity> : RootSoftNonIdDao<TEntity>, IBaseDao<TEntity>
```
The ```Root``` classes declare no capability interface, so a Data Access Object deriving from one publishes only
the members it chooses.  The ```Base``` classes add ```IBaseDao<TEntity>``` and no members, for the case where
you do want the full interface.  Reads are filtered, ordered and paged through overridable seams —
```ApplyReadFilter```, ```ApplyStableOrder```, ```ApplyIncludes``` — and the row a write targets is identified by
your ```MatchRow``` predicate, which is translated to SQL rather than evaluated in memory.

### New: ContextOwnership, and a real disposal contract
```ContextOwnership``` is a two-value enum, ```Borrowed``` and ```Owned```, with **no default value you can fall
into** — the constructor makes you say which one applies.  A layer told ```Owned``` disposes the context along
with itself; one told ```Borrowed``` never touches it, because someone else will.

```c#
public enum ContextOwnership { Borrowed = 0, Owned = 1 }
```
```BaseEFDataAccess``` implements the disposal contract that ```ProphetsWay.BaseDataAccess``` 3.x specifies.
```Dispose``` is sealed, idempotent and does not throw; it rolls back a transaction still open at the time; and
it disposes the context only when it owns it.  Every other member throws ```ObjectDisposedException``` once the
layer has been disposed.  Override ```DisposeCore``` to release anything your own layer created.

### Fixed: a Data Access Layer no longer leaks its context and its connection
In 2.2.0 both of ```BaseEFDataAccess```' constructors built the context themselves —
```Context = (DbContext)Activator.CreateInstance(typeof(TContextType), new object[] { connectionString });```
— and **nothing ever disposed it.**  There was nowhere to: ```IBaseDataAccess``` did not extend
```IDisposable``` in that release, the class declared no ```Dispose```, no ```IDisposable``` and no finalizer,
and so a conscientious caller writing ```using``` around a Data Access Layer instance could not compile it.
Every instance you constructed left a ```DbContext```, and the database connection behind it, to be collected
whenever the garbage collector got to it.

This is the one of the four that eventually announces itself.  A long-running or high-throughput application
reaches connection-pool exhaustion and starts timing out on acquisition, and the cause is findable once you
suspect it — but a low-traffic application can run for a very long time without arriving there, which is why
it shipped.

In 3.0.0 the layer is disposable and the disposal is specified rather than incidental: ```Dispose``` is
```sealed```, idempotent and does not throw, it rolls back a transaction still open at the time, and it
disposes the context **only when it owns it** — which is now a thing the constructor makes you state, through
the ```ContextOwnership``` argument described above.  A layer handed a container-managed context leaves it
alone; a layer that was given one to own releases it.

### Fixed: commit and rollback no longer silently do nothing
This is the most dangerous of the four, because there is no observable difference between it working and it
not.  In 2.2.0 the keyless Data Access Object base began a transaction only when the context did not already
carry one:

```c#
// 2.2.0
if (Context.Database.CurrentTransaction == null)
_transaction = Context.Database.BeginTransaction();
```
When a transaction **was** already open — an outer unit of work, or another Data Access Object on the same
shared context that got there first — ```_transaction``` stayed ```null```.  The matching commit and rollback
then both reached it through ```?.```, so **both returned normally having done nothing.**  No exception, no
return value indicating a skip, nothing in a log.  A Data Access Object that believed it had committed its
work had not committed anything, and a rollback written to reverse a failed write did not run — the write was
left to whatever the outer transaction decided later.

In 3.0.0 transactions belong to the Data Access Layer and not to a Data Access Object.  ```TransactionStart```,
```TransactionCommit``` and ```TransactionRollBack``` live on ```BaseEFDataAccess<TContext>```, no Data Access
Object base in the library carries a transaction member of any kind, and the library never consults
```CurrentTransaction``` to decide whether to begin one — so the check-and-skip that caused this cannot be
expressed.  Every misuse now throws instead of returning quietly: a second ```TransactionStart``` throws
```InvalidOperationException``` because transactions do not nest, and a commit or a rollback with nothing open
throws for the same reason.  If you hand the layer a ```Borrowed``` context whose owner has already begun a
transaction directly, Entity Framework Core's own exception propagates unwrapped and that outer transaction is
left in force — this layer will not silently enroll itself in someone else's transaction.

### Fixed: Update no longer un-deletes a soft-deleted row
In 2.2.0 the soft-delete base implemented ```Update``` as whole-object replacement —
```entry.CurrentValues.SetValues(item)``` over every mapped column, ```DeletedDate``` included.  An
```Update``` carrying an instance that was fetched before the delete, or built fresh from a form or a message,
wrote ```null``` over the stored ```DeletedDate``` and **the row came back to life**, reappearing in every
```GetAll```, every ```GetPaged``` and every count.  Nothing threw and no row count looked wrong.

Two more halves of the same defect sat beside it.  ```Delete``` stamped ```DeletedDate``` unconditionally
without asking whether the row was live, so a **second** delete of an already-deleted row overwrote the
original timestamp with a later one and returned ```1``` rather than ```0``` — meaning any retention window,
audit trail or "deleted before" query reading that column was answering from a timestamp that had quietly
moved.  And ```Insert``` stamped ```CreatedDate``` without clearing the other two, so a caller reusing an
instance carried stale timestamps into a brand-new row — including, in the worst case, a ```DeletedDate```,
storing a row that was invisible to every read the moment it was written.

In 3.0.0 all three are corrected together.  ```Update``` preserves the stored ```CreatedDate``` and
```DeletedDate``` and ignores whatever the incoming instance carries for them, so an update can neither
rewrite history nor soft-delete a row behind ```Delete```'s back; updating a deleted row is allowed and leaves
it deleted.  ```Delete``` checks liveness first — an already-deleted or absent row returns ```0```, changes
nothing, and **never refreshes an existing ```DeletedDate```**, so the stamp always reports when the row was
actually deleted and a second call is idempotent rather than an error.  ```Insert``` forces
```UpdatedDate``` and ```DeletedDate``` to ```null``` whatever the caller assigned.  All of them are declared
```override``` rather than ```new```, so a soft Data Access Object reached through a
```BaseDao<TEntity, TKey>```-typed reference still soft-deletes instead of quietly hard-deleting.

### Fixed: a read or a write no longer hands you the store's own object
In 2.2.0 ```Insert``` was ```Dataset.Add(item); Context.SaveChanges();```.  ```SaveChanges``` moves the entity
from ```Added``` to ```Unchanged``` and **leaves it in the change tracker**, so the instance you passed in
became the layer's instance.  ```Get``` was ```Dataset.Where(i => i.Id == item.Id).SingleOrDefault()``` with
no ```AsNoTracking``` and no projection, so it handed back the store's own tracked object rather than a copy
of it.  Entity Framework Core tracks by default and nothing in the library changed that, so unless you had
configured ```QueryTrackingBehavior.NoTracking``` on the context yourself, **an edit you made to a retrieved
entity — or to something hanging off its navigation properties — and never submitted was written to the
database on the next ```SaveChanges``` anywhere on that layer.**  It is silent, and unlike the leaked
connection the damage lands in your data rather than in your process.

In 3.0.0 ```Get```, ```GetAll``` and ```GetPaged``` read ```AsNoTracking``` and return fresh untracked
instances — never the argument you passed and never the store's object.  ```Insert``` gives the store a copy,
attaches everything reachable through your navigation properties as ```Unchanged``` so related rows are read
and never written, and writes back only the identifier the row ended up carrying, once the save has returned.
Every write detaches what it tracked before it returns.

**What stops happening, which is the part to read even if none of the above sounds familiar.**  This is the
only one of the four whose fix takes something away, and a 2.2.0 application can be depending on it without
anyone having decided to.  A stray edit made through an entity you retrieved **no longer reaches the
database** — if some part of your code has been relying on a change persisting without an explicit
```Update```, that write is now silently dropped where it used to be silently applied, and it is the one item
in this release that can change what your stored data looks like without changing a line of your own code.
And ```Update``` against a row that does not exist now returns ```0```, where 2.2.0 threw
```InvalidOperationException``` out of ```Single``` — so a ```catch``` you wrote around that call **stops
firing**, and a return value you may never have checked becomes the only signal that nothing was written.
Check it.

### Fixed: a failed Insert no longer leaves timestamps or an identifier on your instance
A soft entity's ```Insert``` stamped the created and updated timestamps onto the caller's instance before
attempting the write, so a ```SaveChanges``` that threw left you holding an instance that claimed to describe a
stored row when nothing had been stored.  The subtler half of the same defect survived a successful save: the
keyed insert assigns the identifier, clears inverse navigations and detaches, all after the row is committed, and
anything throwing in those steps left a real database identifier sitting beside a creation date naming no row.
Writes now stamp a copy and write back to your instance only once the save has returned, so the state you are
holding and the state of the database agree in both cases.

### Fixed: TargetException on a committed write
Clearing inverse navigations read a collection navigation as though it were a single principal and reflected onto
the collection object itself, raising ```TargetException``` one statement after the row had already been
committed.  The call appeared to fail while the write had in fact succeeded.  Collection and reference
navigations are now handled distinctly.

### The whole surface, and a note on prerelease builds
Nothing transitional ships.  The library is twelve public classes and one enum: ```BaseEFContext``` and
```BaseEFDataAccess<TContext>```; the keyed ```BaseDao```, ```BaseGetAllDao``` and ```BasePagedDao```; their
soft-delete counterparts ```BaseSoftDao```, ```BaseSoftGetAllDao``` and ```BaseSoftPagedDao```; the keyless
```RootNonIdDao```, ```BaseNonIdDao```, ```RootSoftNonIdDao``` and ```BaseSoftNonIdDao```; and
```ContextOwnership```.  All of them are in the ```ProphetsWay.EFTools``` namespace, and there is no longer any
conditionally compiled code in the library at all.

If you tried an alpha or beta package cut while this line was still in development, you may have seen three
```Legacy```-prefixed keyless classes, or the key-typed namespaces described above, sitting alongside the new
families.  Those were scaffolding internal to the development branch — the older classes were renamed for a
handful of commits so the new ones could take their names — and **none of them is part of 3.0.0**.  There is no
```LegacyBaseNonIdDao<T>``` to migrate onto.  Plan on the generic families.