AiTech.Anvil 1.1.0

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

AiTech.Anvil

Enterprise application foundation for .NET 10 (data providers: EF Core 10 + MongoDB) — async repository + unit of work, automatic soft‑delete, centralized audit, atomic transactions, connection‑string encryption, and ASP.NET Core integration.

AiTech.Anvil is a small, opinionated foundation for building layered enterprise applications. It provides the cross‑cutting plumbing (data access, transactions, auditing, security, web tracing) so your application code stays focused on domain and business logic.

Packages

Package What it gives you
AiTech.Anvil Provider-neutral foundation: entities, async IRepository + IUnitOfWork contracts, neutral commit exceptions (AnvilCommitException/AnvilConcurrencyException/AnvilDuplicateKeyException), optimistic concurrency (IConcurrencyTracked + SetExpectedVersion), BaseAppService (atomic transactions), AES connection‑string encryption, PredicateBuilder, in‑process event bus, typed HTTP client, persistence seeding, AddAnvilCore
AiTech.Anvil.Data.EntityFrameworkCore EF Core provider for the Anvil data contracts: BaseDbContext (automatic soft‑delete filters + provider‑agnostic optimistic concurrency — SQL Server rowversion, application‑rotated token on every other provider), EF repository + unit of work, audit interceptor, declared relational escape hatches (IEfRepository, IEfUnitOfWork, IQueryService), AddAnvilEntityFrameworkCore<TDbContext> — the only package that references Microsoft.EntityFrameworkCore
AiTech.Anvil.Data.MongoDB MongoDB provider (native MongoDB.Driver) for the Anvil data contracts: buffered-write repository + unit of work, sessions/transactions, audit, provider-rotated optimistic concurrency, declared document escape hatch (IMongoQueryService), AddAnvilMongoDB — the only package that references MongoDB.Driver
AiTech.Anvil.Console Testable IConsole, interactive ConsoleMenu, connection‑string encryption / AES key‑generation command
AiTech.Anvil.AspNetCore [TraceOnErrorFile] action filter (writes a self‑contained error log), HttpContext‑based current user, minimal client detection + ApiUtils, AddAnvilAspNetCore
AiTech.Anvil.Messaging.RabbitMQ RabbitMQ transport for the cross‑process integration bus (topic exchange + per‑service queue, publisher + hosted consumer), AddAnvilRabbitMq — the only package that references RabbitMQ.Client

All packages target net10.0, ship XML docs + symbols (snupkg), and build warning‑free.

Install

dotnet add package AiTech.Anvil

Then add what you need — each one pulls the core in automatically:

dotnet add package AiTech.Anvil.Data.EntityFrameworkCore   # EF Core provider
dotnet add package AiTech.Anvil.Data.MongoDB               # MongoDB provider
dotnet add package AiTech.Anvil.AspNetCore                 # ASP.NET Core integration
dotnet add package AiTech.Anvil.Console                    # console helpers
dotnet add package AiTech.Anvil.Messaging.RabbitMQ         # integration bus transport

Pick one data provider per application: AddAnvilEntityFrameworkCore<TDbContext> and AddAnvilMongoDB both register the same IRepository/IUnitOfWork contracts.


Quick start

1. Define entities

using AiTech.Anvil.Entities;

// String/GUID key + audit fields (CreationTime/CreatedBy/LastUpdateTime/LastUpdateBy) + soft delete (IsDeleted)
public sealed class Product : ModernRichEntityBase
{
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

Base classes: EntityBase<TKey> (struct key), ModernEntityBase (string key), RichEntityBase<TKey> / ModernRichEntityBase (add audit + soft delete via IRichEntity).

2. Define your DbContext

using AiTech.Anvil.Data.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : BaseDbContext(options)
{
    public DbSet<Product> Products => Set<Product>();
}

BaseDbContext automatically applies a global query filter e => !EF.Property<bool>(e, "IsDeleted") to every IRichEntity. If you override OnModelCreating, call base.OnModelCreating(modelBuilder).

3. Register the pipeline

using AiTech.Anvil.Data.EntityFrameworkCore;

builder.Services.AddAnvilEntityFrameworkCore<AppDbContext>(
    builder.Configuration,
    (options, connectionString) => options.UseSqlServer(connectionString)); // you pick the EF provider

AddAnvilEntityFrameworkCore registers: the DbContext (with the resolved connection string + audit interceptor), IRepository<> + IEfRepository<>, IUnitOfWork + IEfUnitOfWork, IQueryService<TDbContext>, the crypto helper, and a default current‑user accessor.

4. Use it from an application service

using AiTech.Anvil.Data;
using AiTech.Anvil.Services;

public sealed class CatalogService(IUnitOfWork uow, IRepository<Product> products)
    : BaseAppService(uow)
{
    public Task<IList<Product>> SearchAsync(string term) =>
        products.FetchAsync(p => p.Name.Contains(term), take: 50, sort: p => p.Name);

    public Task<IList<ValidationResult>> CreateAsync(Product product) =>
        SaveEntityAsync(product, products).AsTask(); // validates, tracks, commits in one transaction
}

Reads are async (GetSingleAsync, FetchAsync, FetchWithProjectionAsync, CountAsync, AnyAsync). Writes (Save, Delete, HardDelete) only touch the change tracker — the DB write happens at the unit‑of‑work commit.

Atomic multi‑table operations

Wrap multiple writes in a single ExecuteInTransactionAsync; only the outermost call commits (one SaveChanges), and any exception rolls back everything:

public Task TransferAsync(Product a, Product b) =>
    ExecuteInTransactionAsync(async () =>
    {
        products.Save(a);
        products.Save(b);
        await Task.CompletedTask; // one commit, atomic across both rows
    });

Soft vs hard delete

  • Delete(entity) → soft delete for IRichEntity (sets IsDeleted, hidden by the global filter); physical otherwise.
  • HardDelete(entity) → always physical.
  • Query() / QueryIgnoringFilters("...") → relational escape hatches on IEfRepository<T> (e.g. to read soft‑deleted rows); injecting IEfRepository declares that the consumer is bound to a relational provider.

Provider-agnostic discipline

The neutral contract (IRepository<T>, IUnitOfWork) is implemented by both the relational (EF Core) and document (MongoDB) providers. An app that only injects the neutral contract can swap providers via DI registration — AddAnvilMongoDB(configuration, o => o.DatabaseName = "...") in place of AddAnvilEntityFrameworkCore<TDbContext>(...). To stay swappable:

  • No custom methods inside predicates (r => r.IsExpired()) — no LINQ provider can translate them (EF already rejects these); fetch first, filter in memory.
  • No StringComparison/culture overloads in predicates — use the ToLower() pattern instead.
  • No navigation-property traversal in predicates — use scalar FK columns (g.AudienceId == id).
  • Cross-entity reads (joins/aggregations) are not part of the neutral contract: define a query interface in your application layer and implement it per provider in your infrastructure layer.
  • Injecting IEfRepository<T>, IEfUnitOfWork or IQueryService<TDbContext> declares a relational binding — legitimate, but that consumer is no longer provider-neutral.
  • Injecting IMongoQueryService declares a document-store binding — the symmetric Mongo escape hatch.
  • MongoDB swappability constraints: use a string key (ModernEntityBase) — Mongo has no auto-increment, so saving a new struct-keyed entity throws NotSupportedException. Multi-document transactions require a replica set (single-node is fine for dev). DateTime.UtcNow inside a predicate is evaluated client-side by the driver (EF translates it server-side) — a negligible clock difference.

Connection‑string encryption

AddAnvilEntityFrameworkCore resolves the connection string by name: it prefers ConnectionStrings:{name}Encrypted (AES‑decrypted) and falls back to ConnectionStrings:{name} in clear text.

// appsettings.json
{
  "ConnectionStrings": {
    "DefaultEncrypted": "<base64-ciphertext>"
  }
}

⚠️ Key storage: the AES key (Crypto:KeyBase64) must live outside appsettings.json — in an environment variable, user‑secrets, or a Key Vault. Storing the key next to the ciphertext defeats the purpose. AES‑GCM here provides authenticated encryption (confidentiality + integrity): a tampered ciphertext fails to decrypt.

Generate a key and encrypt a string with the AiTech.Anvil.Console package:

using AiTech.Anvil.Console;
using AiTech.Anvil.Console.Commands;

var console = new SystemConsole();
var key = ConnectionStringCryptoCommand.GenerateKey(console);                 // prints a base64 AES‑256 key
ConnectionStringCryptoCommand.EncryptConnectionString(console, key, "Server=...;"); // prints the ciphertext

ASP.NET Core integration

using AiTech.Anvil.Data.EntityFrameworkCore;
using AiTech.Anvil.AspNetCore.DependencyInjection;

builder.Services.AddAnvilEntityFrameworkCore<AppDbContext>(builder.Configuration, (o, cs) => o.UseSqlServer(cs));
builder.Services.AddAnvilAspNetCore(); // HttpContext-based current user (so audit captures the request user)

Error tracing to file

Apply [TraceOnErrorFile] to a controller or action. When the action throws, a self‑contained log file is written under App_Data/traced-errors/<date>/ containing the request (id, authenticated user, verb, controller/action, JSON parameters), the response, the duration and the full stack trace:

using AiTech.Anvil.AspNetCore.Filters;

[ApiController, Route("api/orders")]
[TraceOnErrorFile] // optional: [TraceOnErrorFile(TargetFolder = "logs/errors")]
public sealed class OrdersController : ControllerBase { /* ... */ }

Client helpers: ApiUtils.GetClientIpAddress(HttpContext) and ApiUtils.GetClientInfo(HttpContext) (best‑effort browser/OS from the User‑Agent).


In‑process events & typed HTTP client (core)

// Events — handlers run in sequence; exceptions are aggregated (one bad handler doesn't stop the others)
services.AddAnvilEventBus();
services.AddScoped<IEventHandler<OrderPlaced>, SendConfirmationEmail>();
await eventBus.PublishAsync(new OrderPlaced(orderId));

// Typed HTTP client — wraps the outcome; never throws on non-2xx OR transport failures (inspect IsSuccess)
services.AddAnvilHttpClient(c => c.BaseAddress = new Uri("https://api.example.com/"));
// For resilience, install Microsoft.Extensions.Http.Resilience in your app and chain it on the
// returned builder (kept out of core to stay dependency-light; retries non-idempotent calls too):
// services.AddAnvilHttpClient(c => ...).AddStandardResilienceHandler();
var result = await httpClient.GetAsync<Order>("/orders/1",
    configureRequest: req => req.Headers.Authorization = new("Bearer", token));   // optional per-request hook
if (result.IsSuccess)               { /* use result.Data */ }
else if (result.IsTransportFailure) { /* network/timeout/circuit — result.Exception */ }
else                                { /* HTTP error — result.StatusCode / result.ErrorContent */ }

// Build query strings safely (encodes, skips null values):
var url = "/orders" + QueryString.Build(("status", "open"), ("q", search));

Integration bus (cross‑process)

The in‑process event bus above dispatches within one application. The integration bus carries messages between services. Its abstraction lives in the core AiTech.Anvil package and is transport‑agnostic — your application code never references a broker. Swap or upgrade the transport (today: RabbitMQ) without touching a single application project.

1. Define a message and a handler (core — no broker dependency)

using AiTech.Anvil.Messaging;

// The topic is the routing key. Without [IntegrationMessage], it defaults to the type's full name.
[IntegrationMessage("orders.placed")]
public sealed record OrderPlaced(string OrderId, decimal Total) : IIntegrationMessage;

public sealed class OnOrderPlaced(IEmailSender email) : IIntegrationEventHandler<OrderPlaced>
{
    public Task HandleAsync(OrderPlaced message, CancellationToken ct = default) =>
        email.SendAsync($"Order {message.OrderId} received", ct);
}

2. Register the bus + handlers

using AiTech.Anvil.Messaging;

services.AddIntegrationHandler<OrderPlaced, OnOrderPlaced>(); // register handlers (transport-independent)

Then pick a transport. RabbitMQ (separate package, binds RabbitMq from configuration):

using AiTech.Anvil.Messaging.RabbitMQ;

services.AddAnvilRabbitMq(builder.Configuration); // publisher + hosted consumer (BackgroundService)
// appsettings.json
{
  "RabbitMq": {
    "Host": "localhost",
    "Port": 5672,
    "UserName": "guest",
    "Password": "guest",
    "ExchangeName": "aitech.Anvil", // topic exchange (default)
    "QueueName": "orders-service",  // this service's durable queue
    "PrefetchCount": 10,
    "DeadLetterEnabled": true,   // retry then dead-letter failed messages (default)
    "MaxRetries": 3,             // attempts before parking in the dead-letter queue
    "RetryDelay": "00:00:05"     // delay between retries (retry-queue TTL)
  }
}

For unit tests (or a modular monolith) register the in‑memory transport instead — same abstraction, no broker:

services.AddAnvilInMemoryBus();

3. Publish

public sealed class CheckoutService(IIntegrationBus bus)
{
    public Task CompleteAsync(string orderId, decimal total) =>
        bus.PublishAsync(new OrderPlaced(orderId, total));
}

The publisher resolves the topic, publishes to the topic exchange with the topic as the routing key; the hosted consumer binds this service's queue, dispatches each message to all registered handlers for its topic, and acks on success. Handler exceptions propagate with their original type and stack trace and trigger the dead-letter/retry flow below.

Dead-letter & retry. A failed message is never silently dropped. The consumer retries it up to MaxRetries times (default 3), each retry delayed by RetryDelay (default 5s) via an auto-declared retry-queue (<queue>.retry); once retries are exhausted the message is parked in an auto-declared dead-letter queue (<queue>.dlq, fed by the fanout exchange <queue>.dlx) carrying x-retry-count / x-death-reason / x-exception-message headers. The retry republish is broker-confirmed before the original delivery is acked, so messages are not lost. Dead-lettering is ON by default; set "DeadLetterEnabled": false to opt out (failed messages are then nacked without requeue, i.e. dropped). Because RetryDelay is baked into the retry-queue declaration, changing it later requires deleting the existing <queue>.retry queue.

Why a separate transport package: confining RabbitMQ.Client to AiTech.Anvil.Messaging.RabbitMQ means a broker version bump, an API signature change, or a deprecated client release is isolated to one package — no application project is ever tied to RabbitMQ.


Known limitations

  • Save cannot insert an entity with a caller‑assigned key. Convention: a new entity has an empty/null Id and Save assigns a GUID; a non‑empty Id is treated as an update. To insert with a known key, add via the DbContext/DbSet directly.
  • Paging requires a stable sort key. FetchAsync takes a single OrderBy; paging over a non‑unique column is non‑deterministic — sort on a unique key (or add a tiebreaker via IQueryService).
  • AES‑GCM provides authenticated encryption (confidentiality + integrity; a tampered ciphertext fails to decrypt) — suitable for protecting a connection string at rest with a securely managed key.
  • ApiUtils trusts X‑Forwarded‑For as‑is (no proxy allow‑list) — review for security‑sensitive deployments.
  • TraceAttribute is not a public extension point (its trace models are internal); use the supplied [TraceOnErrorFile].

Building

dotnet build -warnaserror   # 0 warnings
dotnet test                 # all projects

Releases are not built from this repository. It deliberately carries no packaging configuration — no Directory.Build.props, no version, no pack script. A separate repo injects the version (from a git tag), licence, README and symbol settings at pack time. This repo has one job: its green test run is what authorises a release.

Integration tests: the OAuth API ships an end‑to‑end suite (AiTech.OAuth.Api.IntegrationTests) that drives real HTTP through WebApplicationFactory against a local .\SqlExpress throwaway database with real RS256 token issuance. Like the Mongo/RabbitMQ suites, it auto‑skips when the server is unavailable, so dotnet test stays green without local infra. The Chimera resource API ships a complementary suite (AiTech.Chimera.Api.IntegrationTests) covering a JWT validation matrix in isolation plus a real OAuth handshake; the handshake tests auto‑skip when SQL Express is unavailable. The OAuth primitives — password hashing (PasswordHasher), secure token generation/hashing (SecureTokenGenerator), RS256 JWT issuance/validation (RsaJwtTokenProvider), the strong‑password policy (StrongPasswordAttribute), and the generic paged search helper (OAuthAppServiceBase) — are covered by a dependency‑free unit suite (AiTech.OAuth.UnitTests) that always runs: no SQL or broker required.

License

MIT © 2026 Andrea Iridio

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 (5)

Showing the top 5 NuGet packages that depend on AiTech.Anvil:

Package Downloads
AiTech.Anvil.Console

Console support for AiTech.Anvil: testable IConsole, interactive ConsoleMenu, connection-string encryption and AES key-generation commands.

AiTech.Anvil.Data.EntityFrameworkCore

Entity Framework Core provider for the AiTech.Anvil data contracts: BaseDbContext with automatic soft-delete filters, EF repository and unit of work, audit interceptor, declared relational escape hatches (IEfRepository, IEfUnitOfWork, IQueryService).

AiTech.Anvil.AspNetCore

ASP.NET Core integration for AiTech.Anvil: error-trace filters, HttpContext current user, client detection.

AiTech.Anvil.Data.MongoDB

MongoDB provider for the AiTech.Anvil data contracts: native-driver repository and unit of work with buffered writes, sessions/transactions, audit, optimistic concurrency, and a declared escape hatch (IMongoQueryService).

AiTech.Anvil.Messaging.RabbitMQ

RabbitMQ transport for the AiTech.Anvil integration bus.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 179 7/18/2026
1.0.0 170 7/16/2026