EasyAF 5.0
Performance Improvements
EasyAF 5.0 removes reflection and expression trees from the code your app runs on every request, every save, and every property assignment.68.7x faster audit checks
EntityManager checks an entity’s audit interfaces with pattern matching instead of a type dictionary and LINQ: 40.05 ns → 0.58 ns.30.6x faster property setters
Generated setters use
nameof instead of building an expression tree on every assignment: 874.4 ns → 28.6 ns, and zero allocations.6.0x faster deserialization
Deserializing runs every property’s setter, so the faster setters and the new serializer contract combine: 1,696.7 ns → 284.6 ns, 90.8% fewer allocations.
“Faster” is before ÷ after. “Fewer allocations” is (before − after) ÷ before. Every “before” is the EasyAF 4.x code, frozen in a
V4 namespace in the benchmark projects so both versions run side by side.
Creating and saving an entity, end to end
This is what your app does when it creates a record: create an entity, set its properties, and save it through the generated manager. Each save uses a new context and manager, the way a scoped request does, and writes to an in-memory database so disk and network time don’t hide the difference.
The individual improvements below are large, but Entity Framework does most of the work in a save. EasyAF 5.0 saves about 1 µs and 2 to 7 KB of allocations per save. On EF Core, that’s 6% of the whole save. EF6 spends about 3 ms per save, so the time saved is smaller than the run-to-run variation. With a real database, network time makes the share smaller again, but the allocations you save still reduce garbage collection on a busy server.
Audit interface detection in EntityManager
EntityManager checked which audit interfaces an entity implements by caching each type’s interfaces in a type dictionary, then searching them with LINQ. It now uses pattern matching (entity is ICreatedAuditable), which compiles to a single type check and works with Native AOT.
Checking all four audit interfaces on a Product, as ResetAuditProperties does:
EntityManager lifecycle methods
These run inside every insert, update, and reset. They combine the faster interface checks, the faster ID claim lookup, and the faster setters. Both flavors run the same scenario, with the database query skipped so only EasyAF’s code is measured.
Generated property setters
Generated setters now callSet(nameof(Name), ref _name, value) instead of Set(() => Name, ref _name, value). The old form built an expression tree on every assignment. nameof is a compile-time constant, so an assignment no longer allocates at all. Rebuild your project to regenerate your entities.
Setting 7 properties on a generated Product:
With a
PropertyChanged subscriber, such as a Blazor binding, each change also allocates a 24 B PropertyChangedEventArgs:
Serializing and deserializing entities
IgnoreAuditFields() removes the audit properties from System.Text.Json’s contract once per type, instead of using reflection on every call. The obsolete IgnoreAuditFieldsJsonConverterFactory now uses the same approach, so apps that haven’t migrated get almost the same gain.
Serializing one Product:
Deserializing one
Product, which also runs every property’s setter:
Serializing with
IgnoreAuditFields() is now faster than serializing the whole entity with plain System.Text.Json (186.8 ns, 696 B), because it writes four fewer properties.
Reading the user’s ID claim
ClaimsPrincipal.GetIdClaim() rebuilt the claim type string for every claim it checked, and allocated a closure, on every call. The claim type is now cached, and only rebuilt when you change the claim configuration.
Creating entities
DbObservableObject created an empty OriginalValues dictionary in every constructor, even though most entities are never change-tracked. The dictionary is now created on the first tracked change. Entity Framework constructs an entity for every row it reads, so this adds up on large queries.
Measured with BenchmarkDotNet 0.15.8 and its default job, on .NET 10.0.12, on an Intel Core Ultra 9 285K running Windows 11. The end-to-end benchmarks use Effort for EF6 and the EF Core InMemory provider. The benchmarks, and the EasyAF 4.x code they compare against, are in the
CloudNimble.EasyAF.Benchmarks.Core, .EF6, and .EFCore projects. Run one with dotnet run -c Release -f net10.0 --project src/CloudNimble.EasyAF.Benchmarks.Core. Times vary by machine; the ratios are what matter.Breaking changes
.NET 8 and .NET 9 are no longer supported
EasyAF 5.0 targets .NET 10 and .NET 11. Packages that also targetnetstandard2.0, net48, or net472 still do. Move your projects to .NET 10 or later before upgrading.
IgnoreAuditFieldsJsonConverterFactory is obsolete, and its output changed
Call IgnoreAuditFields() on your JsonSerializerOptions instead:
IgnoreAuditFields() now honor every System.Text.Json setting. That changes the JSON you send:
WhenWritingDefaultnow skips0,false, andGuid.Empty, not justnull. UseWhenWritingNullto keep writing them.- Naming policies and
[JsonPropertyName]now apply to entity properties. - Audit fields are also removed from related entities.
ICreatorTrackable<T> requires a non-nullable CreatedById
Code generation only applies ICreatorTrackable<T> when the CreatedById column is NOT NULL. If it allows nulls, the entity is still generated without the interface, and the build reports warning EASYAF005 at the property in your EDMX. To keep creator tracking, make CreatedById required on every table, and assign built-in or seed data to a system user ID that has zero permissions. See Table Design.
Source generator diagnostic IDs
EASYAF001 was used for three different messages. Each now has its own ID:
If you suppress
EASYAF001 in NoWarn or .editorconfig, check which of these messages you meant to suppress.
GenerateViews is now EasyAFGenerateViews
Rename the GenerateViews MSBuild property to EasyAFGenerateViews. The old name still works, but reports warning EASYAF008. If both are set, GenerateViews wins until you remove it.
Restier IModelBuilderExtensions renamed
The class is now EasyAF_Restier_IModelBuilderExtensions, to match the other EasyAF extension classes. Code that calls the methods as extensions is unaffected. Code that calls them through the class name must use the new name.
Improvements
dotnet easyaf new. Creates a complete EasyAF solution in one command: Core, Data, Business, Api (with OData MCP), MessageBus, and Tests projects, referenced the way EasyAF expects, withDirectory.Build.props,global.json, and an.slnx. Projects come from the .NET SDK’s own templates, so they match your installed SDK. Use--no-api,--no-messagebus, or--no-runtimeto leave projects out,--webjobto publish the message processor as an Azure WebJob, and-f net11.0to target .NET 11. Then rundotnet easyaf initto connect a database.intandlongIDs for audit fields.EntityManagernow fillsCreatedByIdandUpdatedByIdforGuid,int, andlongIDs, from the user’s ID claim. UseClaimsPrincipal.TryGetIdClaim(out int)orTryGetIdClaim(out long)to read those IDs yourself.- One less dependency.
CloudNimble.EasyAF.Businessno longer depends onBen.TypeDictionary.