Zeta.AspNetCore 0.1.9

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

Zeta

GitHub stars

codecov NuGet NuGet Downloads Build Build License: MIT

A composable, type-safe, async-first validation framework for .NET inspired by Zod.

var UserSchema = Z.Object<User>()
    .Field(u => u.Email, s => s.Email())
    .Field(u => u.Age, s => s.Min(18));

var result = await UserSchema.ValidateAsync(user);

result.Match(
    success: user => Console.WriteLine($"Valid: {user.Email}"),
    failure: errors => errors.ForEach(e => Console.WriteLine($"{e.Path}: {e.Message}"))
);

Features

  • Schema-first - Define validation as reusable schema objects
  • Async by default - Every rule can be async, no separate sync/async paths
  • Result pattern - No exceptions for validation failures
  • Composable - Schemas are values that can be reused and combined
  • Path-aware errors - Errors include location (user.address.street, items[0])
  • ASP.NET Core native - First-class support for Minimal APIs and Controllers

Installation

dotnet add package Zeta
dotnet add package Zeta.AspNetCore  # For ASP.NET Core integration

Schema Types

String

Z.String()
    .MinLength(3)
    .MaxLength(100)
    .Length(10)           // Exact length
    .Email()
    .Uuid()               // UUID/GUID format
    .Url()                // HTTP/HTTPS URLs
    .Uri()                // Any valid URI
    .Alphanumeric()       // Letters and numbers only
    .StartsWith("prefix")
    .EndsWith("suffix")
    .Contains("substring")
    .Regex(@"^[A-Z]")
    .NotEmpty()
    .Refine(s => s.StartsWith("A"), "Must start with A")

Numeric Types

Z.Int()
    .Min(0)
    .Max(100)
    .Refine(n => n % 2 == 0, "Must be even")

Z.Double()
    .Min(0.0)
    .Max(100.0)
    .Positive()
    .Negative()
    .Finite()

Z.Decimal()
    .Min(0m)
    .Max(1000m)
    .Positive()
    .Precision(2)       // Max 2 decimal places
    .MultipleOf(0.25m)  // Must be multiple of step

Date and Time

Z.DateTime()
    .Min(minDate)
    .Max(maxDate)
    .Past()             // Must be in the past
    .Future()           // Must be in the future
    .Between(min, max)
    .Weekday()          // Monday-Friday only
    .Weekend()          // Saturday-Sunday only
    .WithinDays(7)      // Within N days from now
    .MinAge(18)         // For birthdate validation
    .MaxAge(65)

Z.DateOnly()
    .Min(minDate).Max(maxDate).Past().Future()
    .Between(min, max).Weekday().Weekend()
    .MinAge(18).MaxAge(65)

Z.TimeOnly()
    .Min(minTime).Max(maxTime).Between(min, max)
    .BusinessHours()              // 9 AM - 5 PM (default)
    .BusinessHours(start, end)    // Custom hours
    .Morning()                    // 6 AM - 12 PM
    .Afternoon()                  // 12 PM - 6 PM
    .Evening()                    // 6 PM - 12 AM

Other Types

Z.Guid()
    .NotEmpty()         // Not Guid.Empty
    .Version(4)         // Specific UUID version (1, 2, 3, 4, or 5)

Z.Bool()
    .IsTrue()           // Must be true (e.g., terms accepted)
    .IsFalse()          // Must be false

Nullable (Optional Fields)

All schemas are required by default. Use .Nullable() to make fields optional:

Z.String().Nullable()           // Allows null strings
Z.Int().Nullable()              // Allows null ints (int?)
Z.Object<Address>().Nullable()  // Allows null objects

// In object schemas
Z.Object<User>()
    .Field(u => u.MiddleName, Z.String().Nullable())  // Optional field
    .Field(u => u.Age, Z.Int().Min(0).Nullable())     // Optional with validation

Object

Define field schemas inline with builder functions:

Z.Object<User>()
    .Field(u => u.Name, s => s.MinLength(2))
    .Field(u => u.Email, s => s.Email().MinLength(5))
    .Field(u => u.Age, s => s.Min(18).Max(100))
    .Field(u => u.Price, s => s.Positive().Precision(2))
    .Refine(u => u.Password != u.Email, "Password cannot be email");

// Supported for: string, int, double, decimal, bool, Guid, DateTime, DateOnly, TimeOnly

For Composability - Extract reusable schemas when needed across multiple objects:

var AddressSchema = Z.Object<Address>()
    .Field(a => a.Street, s => s.MinLength(3))
    .Field(a => a.ZipCode, s => s.Regex(@"^\d{5}$"));

Z.Object<User>()
    .Field(u => u.Name, s => s.MinLength(2))
    .Field(u => u.Address, AddressSchema);  // Reuse nested schema

Collections

Validate arrays and lists with .Each() for element validation:

// Simple element validation
Z.Collection<string>()
    .Each(s => s.Email())          // Validate each element
    .MinLength(1)                  // Collection-level validation
    .MaxLength(10)

Z.Collection<int>()
    .Each(n => n.Min(0).Max(100))
    .NotEmpty()

// In object schemas with fluent builders
Z.Object<User>()
    .Field(u => u.Tags, tags => tags
        .Each(s => s.MinLength(3).MaxLength(50))
        .MinLength(1).MaxLength(10));

// Complex nested objects - pass pre-built schema
var orderItemSchema = Z.Object<OrderItem>()
    .Field(i => i.ProductId, s => s)
    .Field(i => i.Quantity, s => s.Min(1));

Z.Collection(orderItemSchema)
    .MinLength(1);

// Errors include index path: "tags[0]", "items[2].quantity"

See the Collections guide for advanced patterns including context-aware validation.

Conditional Validation

Use .When() for dependent field validation:

Z.Object<Order>()
    .Field(o => o.PaymentMethod, Z.String())
    .Field(o => o.CardNumber, Z.String().Nullable())
    .Field(o => o.BankAccount, Z.String().Nullable())
    .When(
        o => o.PaymentMethod == "card",
        then: c => c.Require(o => o.CardNumber),    // CardNumber required when paying by card
        @else: c => c.Require(o => o.BankAccount)  // BankAccount required otherwise
    );

Use .Select() for inline schema building within conditionals:

Z.Object<User>()
    .Field(u => u.Password, Z.String().Nullable())
    .Field(u => u.RequiresStrongPassword, Z.Bool())
    .When(
        u => u.RequiresStrongPassword,
        then: c => c.Select(u => u.Password, s => s.MinLength(12).MaxLength(100))
    );

Result Pattern

Validation returns Result<T> instead of throwing exceptions:

var result = await schema.ValidateAsync(value);

// Pattern match
var output = result.Match(
    success: value => $"Valid: {value}",
    failure: errors => $"Invalid: {errors.Count} errors"
);

// Chain operations
var finalResult = await UserSchema.ValidateAsync(input)
    .Then(user => SaveUserAsync(user))
    .Map(saved => new UserResponse(saved.Id));

// Get value or throw
var user = result.GetOrThrow();

// Get value or default
var user = result.GetOrDefault(defaultUser);

ASP.NET Core Integration

Setup

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddZeta();

Minimal APIs

var UserSchema = Z.Object<User>()
    .Field(u => u.Email, s => s.Email())
    .Field(u => u.Name, s => s.MinLength(3));

app.MapPost("/users", (User user) => Results.Ok(user))
    .WithValidation(UserSchema);

Validation failures return 400 Bad Request with ValidationProblemDetails:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "Validation failed",
  "status": 400,
  "errors": {
    "email": ["Invalid email format"],
    "name": ["Must be at least 3 characters"]
  }
}

Controllers

Inject IZetaValidator for manual validation:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IZetaValidator _validator;

    private static readonly ISchema<User> UserSchema = Z.Object<User>()
        .Field(u => u.Name, s => s.MinLength(3))
        .Field(u => u.Email, s => s.Email());

    public UsersController(IZetaValidator validator)
    {
        _validator = validator;
    }

    [HttpPost]
    public async Task<IActionResult> Create(User user)
    {
        var result = await _validator.ValidateAsync(user, UserSchema);

        return result.ToActionResult(valid => Ok(new
        {
            Message = "User created",
            User = valid
        }));
    }
}

Result Extensions

// For Controllers - returns Ok or BadRequest
result.ToActionResult()
result.ToActionResult(v => CreatedAtAction(...))

// For Minimal APIs - returns Results.Ok or Results.ValidationProblem
result.ToResult()
result.ToResult(v => Results.Created(...))

Custom Rules

Inline Refinement

Z.String()
    .Refine(
        value => value.StartsWith("A"),
        message: "Must start with A",
        code: "starts_with_a"
    )

Custom Rule Class

public sealed class StartsWithUpperRule : IValidationRule<string>
{
    public ValidationError? Validate(string value, ValidationExecutionContext execution)
    {
        return char.IsUpper(value[0])
            ? null
            : new ValidationError(execution.Path, "starts_upper", "Must start with uppercase");
    }
}

// Usage
Z.String().Use(new StartsWithUpperRule())

Async Refinement

For async validation with context:

Z.String()
    .Email()
    .WithContext<UserContext>()
    .RefineAsync(
        async (email, ctx, ct) => !await _repo.EmailExistsAsync(email, ct),
        message: "Email already taken",
        code: "email_exists"
    )

See the Custom Rules guide for context-aware rules and advanced patterns.

Validation Context

For async data loading before validation (e.g., checking database):

// Define context
public record UserContext(bool EmailExists);

// Factory to load context
public class UserContextFactory : IValidationContextFactory<User, UserContext>
{
    private readonly IUserRepository _repo;

    public UserContextFactory(IUserRepository repo) => _repo = repo;

    public async Task<UserContext> CreateAsync(User input, IServiceProvider services, CancellationToken ct)
    {
        return new UserContext(EmailExists: await _repo.EmailExistsAsync(input.Email, ct));
    }
}

// Use in schema with .WithContext()
var UserSchema = Z.Object<User>()
    .Field(u => u.Name, s => s.MinLength(3))
    .WithContext<User, UserContext>()
    .Field(u => u.Email, s => s.Email())  // Fluent builders still work
    // For context-aware validation, use pre-built schemas
    .Field(u => u.Username,
        Z.String()
            .MinLength(3)
            .WithContext<UserContext>()
            .RefineAsync(async (username, ctx, ct) =>
                !await ctx.Repo.UsernameExistsAsync(username, ct),
                "Username already taken"));

// Register
builder.Services.AddZeta(typeof(Program).Assembly);

See the Validation Context guide for more details.

Error Model

public record ValidationError(
    string Path,     // "user.address.street" or "items[0]"
    string Code,     // "min_length", "email", "required"
    string Message   // "Must be at least 3 characters"
);

Standard Error Codes

Code Meaning
required Value is null/missing
min_length / max_length Length constraints
length Not exact length
min_value / max_value Value constraints
email / uuid / url / uri Format validation
alphanumeric Contains non-alphanumeric chars
starts_with / ends_with / contains String content
regex Pattern mismatch
precision / multiple_of Numeric constraints
positive / negative / finite Number signs
past / future / between / within_days Date constraints
weekday / weekend Day of week
min_age / max_age Age validation
business_hours / morning / afternoon / evening Time constraints
not_empty / version GUID validation
is_true / is_false Boolean constraints
custom_error Custom refinement failed

Benchmarks

Comparing Zeta against FluentValidation and DataAnnotations on .NET 10 (Apple M2 Pro).

Method Mean Allocated
FluentValidation 129.1 ns 600 B
FluentValidation (Async) 229.3 ns 672 B
Zeta 293.2 ns 152 B
Zeta (Invalid) 398.6 ns 904 B
DataAnnotations 617.8 ns 1,848 B
DataAnnotations (Invalid) 974.9 ns 2,672 B
FluentValidation (Invalid) 1,920.5 ns 7,728 B
FluentValidation (Invalid Async) 2,095.8 ns 7,800 B

Key findings:

  • Allocates 75% less memory than FluentValidation on valid input (152 B vs 600 B)
  • Allocates 8.5x less memory than FluentValidation on invalid input (904 B vs 7,728 B)
  • 4.8x faster than FluentValidation when validation fails (398 ns vs 1,920 ns)
  • 2.4x faster than DataAnnotations when validation fails (398 ns vs 974 ns)

Run benchmarks:

dotnet run --project benchmarks/Zeta.Benchmarks -c Release

Documentation

License

MIT

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.
  • net10.0

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.1.9 77 2/3/2026
0.1.8 84 1/29/2026
0.1.7 82 1/28/2026
0.1.6 84 1/27/2026
0.1.5 85 1/26/2026
0.1.4 90 1/24/2026
0.1.3 86 1/23/2026
0.1.2 82 1/23/2026