Functional-first SQL Server toegang voor moderne .NET-applicaties.
SharpFunctional.MSSQL is een .NET 10 / C# 14 library die combineert:
- Entity Framework Core gebruiksgemak
- Dapper performance
- Ingebouwde functionele types (
Option<T>,Seq<T>,Fin<T>) — zero externe dependencies - No-exception API-oppervlak voor verwachte foutpaden
Deze package helpt je SQL Server data-access te bouwen met:
- expliciete success/failure flows
- composable async operaties
- transaction-safe uitvoering
- structured logging
- ingebouwde retry/timeout configuratie
- OpenTelemetry tracing hooks
- server-side paginatie met navigatie-metadata
- specification pattern voor herbruikbare queries
- batch insert/update/delete operaties
IAsyncEnumerable<T>streaming voor grote datasets- circuit breaker resilience pattern
De 3.0.1 en 3.0.2 releases zijn volledig backwards compatible en focussen op verbeteringen in resilience en observability.
Toegevoegd:
RetryJitterModeinSqlExecutionOptions(Nonestandaard,Fullopt-in)- optionele
ActivityEnricherdelegate inSqlExecutionOptionsvoor custom OpenTelemetryActivityverrijking CircuitBreakerSnapshotimmutable diagnostisch modelCircuitBreaker.GetSnapshot()voor thread-safe inspectie van state/counters/timing
Aangepast (non-breaking):
- emitted activities in
FunctionalMsSqlDb,DapperFunctionalDbenEfFunctionalDbondersteunen nu optionele verrijking - logging in
FunctionalMsSqlDbgebruikt nu source-generatedLoggerMessagemethodes om allocaties te verlagen en diagnostiek-throughput te verbeteren - deze logging-optimalisatie kwam uit de community, na een LinkedIn snippet code review verzoek van Ewart Nijburg (Principal .NET & Azure Architect)
- fouten in enrichers worden defensief afgehandeld en breken data access operaties niet
- testdekking uitgebreid voor de nieuwe resilience/telemetry scenario's
De LanguageExt.Core dependency is volledig verwijderd.
Alle functionele types (Option<T>, Fin<T>, Seq<T>, Unit, Error) zijn nu ingebouwde lichtgewicht readonly struct-implementaties in de SharpFunctional.MsSql.Functional namespace — speciaal gebouwd voor deze library.
| Wat veranderd | Vóór (v1/v2) | Na (v3) |
|---|---|---|
| Functionele types | LanguageExt.Core (4.4.9, >200 types) |
Ingebouwd: 5 types + Prelude |
| Externe dependencies | LanguageExt + transitieve deps | Zero functionele deps |
| Import | using LanguageExt; |
using SharpFunctional.MsSql.Functional; |
| API-oppervlak | Identiek | Identiek — drop-in vervanging |
Migratie vanaf v2:
// Vervang:
using LanguageExt;
using LanguageExt.Common;
using static LanguageExt.Prelude;
// Door:
using SharpFunctional.MsSql.Functional;
using static SharpFunctional.MsSql.Functional.Prelude;Alle typenamen (Option<T>, Fin<T>, Seq<T>, Error.New(), FinSucc(), FinFail(), toSeq()) blijven hetzelfde.
Grote feature-toevoegingen bovenop de v1-basis:
Nieuwe EF Core operaties:
FindPaginatedAsync<T>— server-side paginatie metQueryResults<T>(totaal pagina's, navigatie-metadata,Mapprojectie)FindAsync<T>(IQuerySpecification<T>)— specification pattern met filter, include, sortering en paginatieInsertBatchAsync<T>/UpdateBatchAsync<T>/DeleteBatchAsync<T>— configureerbare batch-operatiesStreamAsync<T>—IAsyncEnumerable<T>streaming voor grote datasets
Nieuwe Dapper operatie:
ExecuteStoredProcPaginatedAsync<T>— gepagineerde stored procedure resultaten viaQueryMultipleAsync
Nieuwe gemeenschappelijke types:
QueryResults<T>— immutable paginatie-recordIQuerySpecification<T>/QuerySpecification<T>— composable query specificationsCircuitBreaker— thread-safe circuit breaker pattern (Closed→Open→HalfOpen)
Nieuwe diagnostiek:
- 8 nieuwe OpenTelemetry tags (
entity_type,batch_size,item_count,page_number,page_size,duration_ms,correlation_id,circuit_state) - EF Core activity tracing voor alle nieuwe methoden
C# 14 modernisering:
- Primary constructors op alle kernklassen
- C# 14
Lockclass (vervangtobjectlocks) - Collection expressions overal
- Zero breaking changes — alles additief
Fundament van de functionele SQL Server access library:
FunctionalMsSqlDbfacade met EF Core + Dapper backendsEfFunctionalDb— 9 functionele CRUD-operatiesDapperFunctionalDb— 5 functionele query/stored proc operaties- Transaction support (
InTransactionAsync,InTransactionMapAsync) SqlExecutionOptionsmet retry/timeout configuratie- Transient SQL foutdetectie
- OpenTelemetry
ActivitySourceintegratie - DI registratie via
ServiceCollectionExtensions - Volledige xUnit v3 testsuite
Option<T>voor optionele waardenSeq<T>voor queryresultaten (gebaseerd opImmutableArray<T>)Fin<T>voor success/failure met foutcontextUnitals void-vervangingErrorvoor gestructureerde foutrepresentatie
GetByIdAsync<T, TId>FindOneAsync<T>QueryAsync<T>AddAsync<T>SaveAsync<T>DeleteByIdAsync<T, TId>CountAsync<T>AnyAsync<T>- expliciete
WithTracking()modus FindPaginatedAsync<T>— server-side paginatie metQueryResults<T>FindAsync<T>(IQuerySpecification<T>)— specification pattern queriesInsertBatchAsync<T>— configureerbare batch insertsUpdateBatchAsync<T>— batch updates met ondersteuning voor detached entitiesDeleteBatchAsync<T>— predicate-gebaseerde batch deletesStreamAsync<T>—IAsyncEnumerable<T>streaming voor grote datasets
QueryAsync<T>QuerySingleAsync<T>ExecuteStoredProcAsync<T>ExecuteStoredProcSingleAsync<T>ExecuteStoredProcNonQueryAsyncExecuteStoredProcPaginatedAsync<T>— gepagineerde stored procedure resultaten viaQueryMultipleAsync
InTransactionAsync<T>voor commit/rollback flowsInTransactionMapAsync<TIn, TOut>voor transactionele mapping- ondersteuning voor zowel EF- als Dapper-backend
CancellationTokenop alle publieke async API's- cancel-propagatie door EF, Dapper, retries en helper-extensies
SqlExecutionOptionsvoor:- command timeout
- max retries
- exponential backoff
- transient SQL detectie (timeouts, deadlocks, service busy/unavailable)
CircuitBreakerpattern:- thread-safe state machine (
Closed→Open→HalfOpen) - configureerbare failure threshold, open duration en half-open success threshold
- functionele
ExecuteAsync<T>dieFin<T>retourneert
- thread-safe state machine (
ILoggerhooks voor lifecycle/failure/retry diagnostiek- source-generated
LoggerMessagelogging inFunctionalMsSqlDbom allocaties in package-diagnostiek te beperken - OpenTelemetry support via
ActivitySource:- source name:
SharpFunctional.MsSql - transaction activities
- Dapper operation activities (
dapper.query.seq,dapper.query.single,dapper.storedproc.*) - EF Core operation activities (
ef.find.paginated,ef.find.spec,ef.batch.insert,ef.batch.update,ef.batch.delete) - retry events en gestandaardiseerde tags (
backend,operation,success,retry.attempt) - uitgebreide tags:
entity_type,batch_size,item_count,page_number,page_size,duration_ms,correlation_id,circuit_state
- source name:
FunctionalMsSqlDb- root facade en transaction orchestrationEfFunctionalDb- functionele EF Core operaties (CRUD, paginatie, batch, streaming, specification)DapperFunctionalDb- functionele Dapper operaties (queries, stored procedures, paginatie)TransactionExtensions- transaction mapping helpersFunctionalExtensions- async functionele compositie (Bind,Map)SqlExecutionOptions- timeout/retry policy configuratieSharpFunctionalMsSqlDiagnostics- tracing constants enActivitySourceCircuitBreaker- thread-safe circuit breaker pattern voor database-operatiesCircuitBreakerOptions- circuit breaker configuratie (thresholds, durations)QueryResults<T>- gepagineerd queryresultaat met navigatie-metadataIQuerySpecification<T>/QuerySpecification<T>- herbruikbare, composable query specificationsOption<T>/Fin<T>/Seq<T>/Unit/Error- zero-dependency functionele types (vervangt LanguageExt)Prelude- statische helpers (FinFail,FinSucc,toSeq,Optional,unit)ServiceCollectionExtensions- DI registratie helpersFunctionalMsSqlDbOptions- opties voor DI configuratie
dotnet add package SharpFunctional.MSSQLSharpFunctional.MSSQL integreert met Microsoft.Extensions.DependencyInjection via IOptions<FunctionalMsSqlDbOptions>.
FunctionalMsSqlDb wordt geregistreard als scoped (één instantie per request/scope).
// AppDbContext moet al geregistreerd zijn
services.AddDbContext<AppDbContext>(opts => opts.UseSqlServer(connectionString));
services.AddFunctionalMsSqlEf<AppDbContext>(opts =>
{
opts.ExecutionOptions = new SqlExecutionOptions(commandTimeoutSeconds: 60);
});services.AddFunctionalMsSqlDapper(connectionString, opts =>
{
opts.ExecutionOptions = new SqlExecutionOptions(
commandTimeoutSeconds: 30,
maxRetryCount: 3,
baseRetryDelay: TimeSpan.FromMilliseconds(200));
});services.AddDbContext<AppDbContext>(opts => opts.UseSqlServer(connectionString));
// Connection string direct meegeven
services.AddFunctionalMsSql<AppDbContext>(connectionString, opts =>
{
opts.ExecutionOptions = new SqlExecutionOptions(commandTimeoutSeconds: 60, maxRetryCount: 3);
});
// Of alles via de opties delegate
services.AddFunctionalMsSql<AppDbContext>(opts =>
{
opts.ConnectionString = connectionString;
opts.ExecutionOptions = new SqlExecutionOptions(commandTimeoutSeconds: 60);
});public class UserService(FunctionalMsSqlDb db)
{
public async Task<Option<User>> GetUserAsync(int id, CancellationToken ct)
=> await db.Ef().GetByIdAsync<User, int>(id, ct);
public async Task<Seq<OrderDto>> GetOrdersAsync(int userId, CancellationToken ct)
=> await db.Dapper().QueryAsync<OrderDto>(
"SELECT * FROM Orders WHERE UserId = @UserId",
new { UserId = userId },
ct);
}using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharpFunctional.MsSql;
using SharpFunctional.MsSql.Common;
var options = new DbContextOptionsBuilder<MyDbContext>()
.UseSqlServer(connectionString)
.Options;
await using var dbContext = new MyDbContext(options);
await using var connection = new SqlConnection(connectionString);
var executionOptions = new SqlExecutionOptions(
commandTimeoutSeconds: 30,
maxRetryCount: 2,
baseRetryDelay: TimeSpan.FromMilliseconds(100),
maxRetryDelay: TimeSpan.FromSeconds(2));
var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<FunctionalMsSqlDb>();
var db = new FunctionalMsSqlDb(
dbContext: dbContext,
connection: connection,
executionOptions: executionOptions,
logger: logger);var user = await db.Ef().GetByIdAsync<User, int>(42, cancellationToken);
user.IfSome(u => Console.WriteLine($"Found: {u.Name}"));
user.IfNone(() => Console.WriteLine("User not found"));var rows = await db.Dapper().QueryAsync<UserDto>(
"SELECT Id, Name FROM Users WHERE IsActive = @IsActive",
new { IsActive = true },
cancellationToken);using static SharpFunctional.MsSql.Functional.Prelude;
var result = await db.InTransactionAsync(async txDb =>
{
var add = await txDb.Ef().AddAsync(new User { Name = "Ada" }, cancellationToken);
if (add.IsFail) return FinFail<string>(Error.New("Add failed"));
await dbContext.SaveChangesAsync(cancellationToken);
return FinSucc("committed");
}, cancellationToken);var page = await db.Ef().FindPaginatedAsync<User>(
u => u.IsActive,
pageNumber: 2,
pageSize: 25,
cancellationToken);
page.Match(
Succ: results =>
{
Console.WriteLine($"Pagina {results.PageNumber}/{results.TotalPages} ({results.TotalCount} totaal)");
foreach (var user in results.Items)
Console.WriteLine(user.Name);
},
Fail: error => Console.WriteLine(error));var spec = new QuerySpecification<Order>(o => o.Total > 1000)
.SetOrderByDescending(o => o.OrderDate)
.SetSkip(50)
.SetTake(25);
var orders = await db.Ef().FindAsync(spec, cancellationToken);
orders.IfSome(list => Console.WriteLine($"Gevonden: {list.Count} orders"));// Batch insert
var newUsers = Enumerable.Range(1, 500).Select(i => new User { Name = $"User{i}" });
var inserted = await db.Ef().InsertBatchAsync(newUsers, batchSize: 100, cancellationToken);
// Batch update
var updated = await db.Ef().WithTracking().UpdateBatchAsync(modifiedUsers, batchSize: 100, cancellationToken);
// Batch delete
var deleted = await db.Ef().DeleteBatchAsync<User>(u => u.IsActive == false, batchSize: 200, cancellationToken);await foreach (var user in db.Ef().StreamAsync<User>(u => u.IsActive, cancellationToken))
{
await ProcessUserAsync(user, cancellationToken);
}var options = new CircuitBreakerOptions
{
FailureThreshold = 5,
OpenDuration = TimeSpan.FromSeconds(30),
SuccessThresholdInHalfOpen = 2
};
var breaker = new CircuitBreaker(options);
var result = await breaker.ExecuteAsync(
async ct => await db.Ef().GetByIdAsync<User, int>(42, ct),
cancellationToken);
// result is Fin<Option<User>> — controleer breaker status
Console.WriteLine($"Circuit status: {breaker.State}");var page = await db.Dapper().ExecuteStoredProcPaginatedAsync<OrderDto>(
"usp_GetOrders",
new { StatusId = 1, PageNumber = 1, PageSize = 50 },
cancellationToken);
page.Match(
Succ: results => Console.WriteLine($"Pagina {results.PageNumber} van {results.TotalPages}"),
Fail: error => Console.WriteLine(error));Gebruik de volgende source name in je tracer-configuratie:
SharpFunctional.MsSql
Geëmitteerde telemetry bevat:
- transaction activities (EF en Dapper)
- Dapper operation activities (
dapper.query.seq,dapper.query.single,dapper.storedproc.*) - EF Core operation activities (
ef.find.paginated,ef.find.spec,ef.batch.insert,ef.batch.update,ef.batch.delete) - retry events en gestandaardiseerde tags (
backend,operation,success,retry.attempt) - uitgebreide diagnostische tags:
entity_type— het entity CLR-typenaambatch_size— batchgrootte voor bulkoperatiesitem_count— aantal beïnvloede itemspage_number/page_size— paginatieparametersduration_ms— operatieduur in millisecondencorrelation_id— koppelt gerelateerde operatiescircuit_state— circuit breaker status
dotnet restore
dotnet build SharpFunctional.MSSQL.slnx -c Releasedotnet pack src/SharpFunctional.MSSQL/SharpFunctional.MSSQL.csproj -c Release -o ./artifacts/packagesGenereert:
.nupkg.snupkg
dotnet test tests/SharpFunctional.MSSQL.TestsDe testsuite gebruikt xUnit v3 en bevat LocalDB-gebaseerde integratietests.
src/— library broncodetests/— xUnit v3 testsuite (150+ tests)examples/— uitvoerbare voorbeeldapplicaties:SharpFunctional.MSSQL.Example— uitgebreide console-app (16 secties met CRUD, aggregaten, functionele chaining, Dapper, transacties, paginatie, specification pattern, batch operaties, streaming, circuit breaker en DI)SharpFunctional.MSSQL.DI.Example— dependency injection voorbeeld metProductServicedat alle drie registratie-overloads, paginatie, specification queries, batch inserts, streaming en circuit breaker demonstreert
docs/— aanvullende documentatie.github/— CI/CD en repo-automatiseringCHANGELOG.md— versiegeschiedenis
Twee uitvoerbare console-applicaties demonstreren alle features van de library:
Een uitgebreide walkthrough met 16 secties:
| Sectie | Feature |
|---|---|
| 1–2 | Database setup en seed data (klanten, producten, orders) |
| 3 | EF Core queries — GetByIdAsync, FindOneAsync, QueryAsync |
| 4 | Aggregaten — CountAsync, AnyAsync |
| 5 | Functionele chaining — Option → Seq, Option → Option, Seq → Seq |
| 6 | Dapper queries — raw SQL, enkel resultaat, joins |
| 7–8 | Transacties en InTransactionMapAsync |
| 9 | Verwijderen en verifiëren |
| 10 | Gepagineerde queries — FindPaginatedAsync met QueryResults<T>.Map |
| 11 | Specification pattern — QuerySpecification<T> met sortering en skip/take |
| 12 | Batch operaties — InsertBatchAsync, UpdateBatchAsync, DeleteBatchAsync |
| 13 | Streaming — StreamAsync<T> met await foreach |
| 14 | Circuit breaker — succes, trip naar Open, afwijzing, reset |
| 15–16 | Eindoverzicht en DI container demo |
Demonstreert FunctionalMsSqlDb registratie en gebruik via constructor injection:
- Alle drie registratie-overloads: alleen EF, alleen Dapper, EF + Dapper gecombineerd
ProductServicemet methoden voor:GetByIdAsync,GetByCategoryAsync,CountInStockAsync,GetSummariesAsync,GetPaginatedAsync,GetBySpecificationAsync,BatchInsertAsync,StreamAllAsync,AddProductAsync- Circuit breaker integratie die service-aanroepen omhult
# Voer het volledige voorbeeld uit (vereist SQL Server LocalDB)
cd examples/SharpFunctional.MSSQL.Example
dotnet run
# Voer het DI voorbeeld uit
cd examples/SharpFunctional.MSSQL.DI.Example
dotnet run- Maak en push een semantic version tag (bijvoorbeeld
v0.1.0). - GitHub Actions bouwt en pakt de library.
- De release-workflow publiceert naar NuGet.org met
NUGET_API_KEY.
MIT. Zie LICENSE.
Issues en pull requests zijn welkom.
