Core primitives for explicit, railway-oriented error handling in .NET (net8.0/net9.0): Result, Error, Option, guard clauses, object mapping, password hashing, and common extensions.
dotnet add package DuonDevKit.Coreusing DuonDevKit.Core.Errors;
using DuonDevKit.Core.Results;
Result<int> Parse(string input)
{
if (!int.TryParse(input, out var value))
return Error.Validation("VAL001", "Not a valid number.");
return Result.Success(value);
}
string message = Parse("42").Match(
onSuccess: value => $"Parsed: {value}",
onFailure: error => $"Failed: {error.Message}");Use Match, Map, and Bind to avoid accessing Result<T>.Value until success is known. MapAsync, BindAsync, and EnsureAsync support the same flow for Task<Result<T>> and short-circuit on failure.
Result<string> result = Parse("42")
.Ensure(value => value > 0, Error.Validation("VAL002", "Must be positive."))
.Map(value => value.ToString());Error has factory helpers including Validation, Business, NotFound, Conflict, Unauthorized, Forbidden, and Unexpected. Call error.ToHttpStatusCode() when an HTTP status code is needed.
Most Core types are used directly and need no service registration. Register mapping implementations only when using the mapping APIs:
using DuonDevKit.Core.Mapping;
services.AddDuonDevKitMappers(typeof(Program).Assembly);The scan fails at startup when more than one mapper handles the same source/destination pair.
Use Option<T> when a value may be absent but absence is not an error.
Option<User> user = users.TryGetValue(id, out var value) ? value : Option<User>.None;
Result<User> requiredUser = user.ToResult(Error.NotFound("USER001", "User not found."));Guards return a failed Result rather than throw for expected validation failures.
Result validation = Result.Combine(
Guard.Against.NullOrEmpty(name, nameof(name)),
Guard.Against.NegativeOrZero(quantity, nameof(quantity)));Create explicit IMapper<TSource, TDestination> or IUpdateMapper<TSource, TDestination> implementations and register an assembly with services.AddDuonDevKitMappers(...).
IPasswordHasher and Pbkdf2PasswordHasher provide PBKDF2-HMAC-SHA256 password hashing with a random salt.
IPasswordHasher hasher = new Pbkdf2PasswordHasher();
string hash = hasher.Hash(password);
bool valid = hasher.Verify(password, hash);StartOfX/EndOfX helpers (Day, Week, Month, Year) for DateTime and DateTimeOffset. StartOfX returns midnight of the first day in the period; EndOfX returns the last tick of the last day (23:59:59.9999999), so a range check like date >= StartOfMonth && date <= EndOfMonth includes the entire last day.
using DuonDevKit.Core.Extensions;
DateTime today = DateTime.UtcNow;
DateTime firstOfMonth = today.StartOfMonth();
DateTime lastInstantOfMonth = today.EndOfMonth();
DateTime mondayOfThisWeek = today.StartOfWeek(); // defaults to MondayComparison helpers: IsBetween (bounds given in either order, inclusive defaults to true), IsSameDay, IsWeekend/IsWeekday, and the "now"-relative IsToday/IsInPast/IsInFuture (Kind-aware for DateTime; always unambiguous for DateTimeOffset, since it carries its own UTC instant).
bool inRange = order.PlacedAt.IsBetween(promotion.StartsAt, promotion.EndsAt);
bool overdue = invoice.DueAt.IsInPast();Time zone conversion: prefer the DateTimeOffset overload of ToTimeZone — it's always unambiguous. The DateTime overload requires a known Kind (throws for Unspecified); ToUtcFrom treats a bare DateTime as wall-clock time in a given zone and throws instead of guessing through a DST gap or overlap.
TimeZoneInfo newYork = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
DateTimeOffset localTime = DateTimeOffset.UtcNow.ToTimeZone(newYork);
DateTime utcFromWallClock = businessOpeningTime.ToUtcFrom(newYork); // throws on a DST gap/overlapDateTime overloads preserve Kind; DateTimeOffset overloads preserve Offset.
DataAnnotationsValidator runs System.ComponentModel.DataAnnotations validation ([Required],
[MaxLength], [Range], a custom IValidatableObject, etc.) against any object and converts the
outcome to a Result — no third-party dependency. Reach for DuonDevKit.Validation's FluentValidation
integration instead when rules need to be conditional, compare properties against each other, or call
out to a database/service.
using DuonDevKit.Core.Validation;
Result validation = DataAnnotationsValidator.Validate(request);
if (validation.IsFailure)
return validation.ToApiResult(); // DuonDevKit.AspNetCoreEvery violation is joined into one message ("PropertyName: ErrorMessage; ...") — for an HTTP endpoint
that needs a field-level { "PropertyName": ["message"] } response body instead, see
DuonDevKit.AspNetCore's WithDuonDevKitValidation<T>() Minimal API filter.