Fluent, declarative validation for .NET — composed at runtime, not baked into your models.
DynamicVNET lets you describe how a POCO should be validated through a Fluent API, then compiles that description into an executable validator at runtime. No attributes on your models required — internally, it wraps the same System.ComponentModel.DataAnnotations engine .NET already ships with, so behavior stays familiar while the authoring model stays flexible.
Reach for it when:
- The model isn't yours to decorate — it lives in a third-party or black-box
.dll. - One type needs several rule sets depending on context, e.g.
Createvs.Update. - Validation is a domain concern, not something that belongs baked into a data class.
- Rules depend on other state on the object — conditional, branching validation.
DynamicVNET has shipped over 18,000 downloads on NuGet.
| Fluent API | Chainable, expression-based rules: builder.Required(x => x.Email).EmailAddress(). |
| Branching | Scope rules to a condition, and nest conditions inside conditions. |
| Nested members | Validate deep paths like x => x.Token.TokenNumber transparently. |
| Duplicate-safe | Registering the same rule for the same member twice is a no-op. |
| Strongly-typed validators | Derive from BaseValidator<T> for a reusable, injectable validator class. |
| Fail-fast or collect-all | Stop at the first failure, or gather every one. |
| Validation profiles | Register multiple named rule sets for one type via ProfileValidator. |
| Escape hatch | Arbitrary Func<T, bool> logic via Predicate when built-ins fall short. |
| Structured results | Validate() returns per-rule detail, not just a boolean. |
# Package Manager Console
Install-Package DynamicVNET -Version 1.4.1# .NET CLI
dotnet add package DynamicVNET --version 1.4.1<!-- PackageReference -->
<PackageReference Include="DynamicVNET" Version="1.4.1" />Targets .NET 5.0, .NET Standard 2.0, and .NET Framework 4.6.1 — usable from modern and legacy projects alike.
flowchart LR
A[POCO Model] --> B[ValidatorFactory.Create]
B --> C[Fluent Rule Builder]
C --> D[Rule Set]
D --> E[Validator]
F[Instance to check] --> E
E -->|IsValid| G[bool]
E -->|Validate| H[IEnumerable<ValidationRuleResult>]
The models used throughout this document:
public class Employee
{
public string Name { get; set; }
public Token TokenNumber { get; set; }
public string Email { get; set; }
}
public class Token
{
public string Number { get; set; }
}A validator, built and run in a few lines:
Employee emp = new Employee
{
Name = "Jhon",
TokenNumber = new Token { Number = "2312412312341" },
Email = "jhon.sim@gmail.com"
};
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.StringLen(x => x.Name, 4)
.EmailAddress(x => x.Email)
.Required(x => x.TokenNumber.Number);
});
bool isValid = validator.IsValid(emp);Each example below is self-contained and builds on the Employee / Token models above.
Focus a member once with .For(...), then stack rules against it without repeating the selector.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.For(x => x.Email)
.Required()
.EmailAddress();
builder.For(x => x.Name)
.Required()
.StringLen(50, min: 2);
});Selectors walk into child objects freely, and duplicate registrations are ignored automatically — safe when composing rule sets from multiple sources.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.Required(x => x.TokenNumber.Number)
.Required(x => x.TokenNumber.Number); // duplicate, silently ignored
});Scope a group of rules to a condition evaluated against the instance being validated.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.Branch(x => x.Name.Contains("Jhon"), x =>
{
x.MaxLen(m => m.TokenNumber.Number, 15);
});
});Branches compose — a condition can enclose further conditions, each with its own rule set.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.Required(x => x.TokenNumber.Number)
.Branch(x => x.Name.Contains("resul"), x =>
{
x.Required(y => y.Email)
.StringLen(y => y.Email, 2)
.Branch(n => n.Name.Length >= 4, n =>
{
n.MaxLen(s => s.TokenNumber.Number, length: 4);
});
})
.Branch(x => x.Email.Contains("aa"), x =>
{
x.Required(y => y.Name)
.StringLen(y => y.Name, 5)
.StringLen(y => y.TokenNumber.Number, 9);
});
});A failed rule inside a branch surfaces on NestedResults of that branch's ValidationRuleResult, so you can trace why a conditional block failed.
Drop to arbitrary logic whenever a built-in rule doesn't fit.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.Predicate(x => x.Email.EndsWith("@company.com"), "Email must be a company address");
});Range, GreaterThan, and LessThan are overloaded for int, double, decimal, float, and byte.
public class Order
{
public int Quantity { get; set; }
public decimal Total { get; set; }
}
var validator = ValidatorFactory.Create<Order>(builder =>
{
builder.Range(x => x.Quantity, 1, 100)
.GreaterThan(x => x.Total, 0m, "Total must be a positive amount");
});By default every rule runs and every failure is collected. Set failFirst: true to short-circuit on the first invalid rule — useful for cheap gate checks.
var validator = ValidatorFactory.Create<Employee>(builder =>
{
builder.Required(x => x.Name)
.Required(x => x.Email)
.EmailAddress(x => x.Email);
}, failFirst: true);For a validator you want to reuse, inject, or unit test in isolation, derive from BaseValidator<T>.
public class EmployeeValidator : BaseValidator<Employee>
{
protected override void Configure(ITypeRuleMarker<Employee> builder)
{
builder.For(x => x.Name)
.Required();
builder.Branch(x => x.Name.Contains("Jhon"), x =>
{
x.MaxLen(m => m.TokenNumber.Number, 15);
})
.For(x => x.Email)
.Required()
.EmailAddress();
builder.Required(x => x.TokenNumber.Number);
}
}var empValidator = new EmployeeValidator();
bool result = empValidator.IsValid(emp);Override FailFirst on the class to opt a whole validator into fail-fast mode:
public class FastEmployeeValidator : BaseValidator<Employee>
{
public override bool FailFirst => true;
protected override void Configure(ITypeRuleMarker<Employee> builder)
{
builder.Required(x => x.Name).Required(x => x.Email);
}
}Register several named rule sets for the same type and pick one at call time — ideal for Create vs. Update style scenarios.
var profiles = new ProfileValidator();
profiles.AddByProfile<Employee>("Create", builder =>
{
builder.Required(x => x.Name).Required(x => x.Email);
});
profiles.AddByProfile<Employee>("Update", builder =>
{
builder.EmailAddress(x => x.Email);
}, failFirst: true);
bool isValidOnCreate = profiles.IsValid("Create", emp);Validate(instance) returns one ValidationRuleResult per evaluated rule.
IEnumerable<ValidationRuleResult> results = validator.Validate(emp);
foreach (var result in results.Where(r => !r.IsValid))
{
Console.WriteLine($"{result.MemberName} failed {result.ValidationName}: {result.ErrorMessage}");
}Each result exposes:
MemberName/ValidationName— what was checked, and by which rule.IsValid— the outcome.ErrorMessage/ErrorInfo— failure detail, as aValidationException.NestedResults/HasNestedResults— failures bubbled up from an innerBranch.
Every method below is available both directly on ITypeRuleMarker<T> (builder.Required(x => x.Email)) and, after focusing a member with .For(...), as a member-less chain (builder.For(x => x.Email).Required()).
| Method | Applies to | Description |
|---|---|---|
Required |
any | Ensures the member is non-null / non-empty. |
Null / NotNull |
reference types | Asserts the member is (or isn't) null. |
StringLen |
string |
Validates a string falls within a min/max length. |
MaxLen |
string |
Enforces a maximum length. |
RegularExp |
string |
Validates against an arbitrary regex pattern. |
EmailAddress |
string |
Validates well-formed email addresses. |
Url |
string |
Validates the value is a URL. |
Range |
numeric | Validates a value falls within [min, max]. |
GreaterThan / LessThan |
int, double, decimal, float, byte |
Numeric comparisons. |
Predicate |
any | Arbitrary custom logic via Func<T, bool>. |
Branch |
any | Applies a nested rule set only when a condition holds. |
The fluent API records rules onto a RuleMarker<T>, which produces an immutable rule set. A Validator<T> runs that set against an instance through a pluggable Applier:
DefaultApplierexecutes every rule and collects every result.FailFirstApplier(used whenfailFirst: true) stops at the first failure.
Straightforward rules — Required, StringLen, MaxLen, Range, and the regex-based rules — are adapted directly onto System.ComponentModel.DataAnnotations attributes, keeping behavior consistent with the .NET validation ecosystem. Predicate and Branch exist as the escape hatch for everything attributes can't express.
DynamicVNET.Lib/ Core library (net5.0 / netstandard2.0 / net461)
DynamicVNET.Lib.Unit.Tests/ xUnit unit tests
DynamicVNET.Lib.Integration.Tests/ xUnit integration tests (end-to-end validator scenarios)
DynamicVNET.Lib.Benchmarks/ BenchmarkDotNet performance benchmarks
Issues and pull requests are welcome at github.com/rasulhsn/DynamicVNET. For a new validation rule or API shape, opening an issue first helps keep the design aligned before you invest time in a PR.
Released under the MIT License.
DynamicVNET · Copyright © 2018–2024 Rasul Huseynov