Nullean.Entities.EntityFrameworkCore 0.4.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Nullean.Entities.EntityFrameworkCore --version 0.4.0
                    
NuGet\Install-Package Nullean.Entities.EntityFrameworkCore -Version 0.4.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="Nullean.Entities.EntityFrameworkCore" Version="0.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Nullean.Entities.EntityFrameworkCore" Version="0.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Nullean.Entities.EntityFrameworkCore" />
                    
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 Nullean.Entities.EntityFrameworkCore --version 0.4.0
                    
#r "nuget: Nullean.Entities.EntityFrameworkCore, 0.4.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 Nullean.Entities.EntityFrameworkCore@0.4.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=Nullean.Entities.EntityFrameworkCore&version=0.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Nullean.Entities.EntityFrameworkCore&version=0.4.0
                    
Install as a Cake Tool

Nullean.Entities.EntityFrameworkCore

Nullean.Entities

NuGet

An entity that breaks its rules never reaches the database: SaveChanges throws first, and a status that moves backwards is caught too, because the before is the row EF loaded.

Without this package, an EF entity is changed in place, so Apply cannot see a "before", and each service has to remember to check the entity before saving. This package adds a SaveChangesInterceptor: on every SaveChanges and SaveChangesAsync it validates added entities and checks modified ones from EF's original values to their current state, and throws one ConstraintViolationException for every offending entity before anything is written.

It builds on Nullean.Entities and Microsoft.EntityFrameworkCore 10. It is optional and does not need the other integration packages.

Install

Targets net10.0 and net11.0, both on EF Core 10 (EF Core 11 is not released yet).

dotnet add package Nullean.Entities
dotnet add package Nullean.Entities.EntityFrameworkCore

Get going

Declare the rules on the entity class and register them where you configure the context:

using Microsoft.EntityFrameworkCore;
using Nullean.Entities;
using Nullean.Entities.EntityFrameworkCore;

services.AddDbContext<ShopContext>(options => options
	.UseSqlite(connection)
	.UseConstraints(c => c.Add<Order>()));    // or c.Add(OrderRules.All) for a companion's set

public enum OrderStatus { Draft, Placed, Shipped }

public sealed partial class Order
{
	public int Id { get; set; }
	public decimal Total { get; set; }
	public OrderStatus Status { get; set; }
	public int Version { get; set; }

	[Constraint]
	public static readonly Constraint<Order> TotalNotNegative = Constraints<Order>.Check(
		o => o.Total >= 0 ? [] : [Violation.Error(nameof(Total), "must not be negative")]);

	[Constraint]
	public static readonly Constraint<Order> StatusFlow = Constraints<Order>
		.Transitions(o => o.Status)
		.Allow(OrderStatus.Draft, OrderStatus.Placed)
		.Allow(OrderStatus.Placed, OrderStatus.Shipped);

	[Constraint]
	public static readonly Constraint<Order> VersionOnlyGrows = Constraints<Order>.Monotonic(o => o.Version, MonotonicDirection.NonDecreasing);
}

Then a change that breaks a rule throws, and the row stays as it was:

var order = await db.Orders.SingleAsync(o => o.Id == id);   // Placed, Version 3
order.Status = OrderStatus.Draft;
await db.SaveChangesAsync();
// ConstraintViolationException: SaveChanges rejected, constraints violated. Order (Modified): status-flow does not allow the transition from Placed to Draft.

The value: the rule is written once, on the entity, and every code path that saves it is covered, including the ones written next year. With Nullean.Entities.AspNetCore's AddConstraintProblemDetails(), the exception becomes a 400 ValidationProblem with no try/catch.

What you get

  • optionsBuilder.UseConstraints(c => ...) on DbContextOptionsBuilder and DbContextOptionsBuilder<TContext>: adds a ConstraintSaveChangesInterceptor.
  • EntityConstraints, the c above:
    • Add<T>() for an entity that declares its own rules ([Constraint] fields or [Constraints]), Add<T>(constraintSet) for any set.
    • Time: the TimeProvider the rules get (NotInFuture, NotOlderThan).
    • OnNotices: called with the entry and its warnings and hints when an entity saved; they never block.
    • Check(entry): the violations of one EntityEntry, for code that wants the same check without saving.
  • What runs: Validate for an Added entity, Check(original, current) for a Modified one, nothing for Deleted or Unchanged. A set registered for a base type also checks derived types.

Pitfalls and limits

  • The before is entry.OriginalValues.ToObject(): scalar properties only, no navigations. A sequential rule comparing a collection sees it empty before.
  • A rule over a collection sees what is loaded, not the table.
  • Adding or changing a child does not modify its parent, so the parent's rules do not run. Register rules for the child type too, or touch a property of the parent (a version column).
  • ExecuteUpdate, ExecuteDelete and raw SQL bypass the change tracker and are not checked. With AutoDetectChangesEnabled off, call DetectChanges first.
  • Rules are synchronous and pure: "is this email taken" is a query for your service, not a constraint.
  • Sets are registered on the interceptor, not read from IConstraintRegistry: that registry validates an object but cannot run the before-and-after check a modified entity needs.
  • Not marked trim or AOT compatible. The package itself builds without trim warnings, but EF Core's NativeAOT support is experimental.

Next

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.  net11.0 is compatible. 
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
0.5.0 46 11/12/2026
0.4.0 48 11/11/2026
0.3.0 90 11/4/2026