Validot is a performance-first, compact library for advanced model validation. Using a simple declarative fluent interface, it efficiently handles classes, structs, nested members, collections, nullables, plus any relation or combination of them. It also supports translations, custom logic extensions with tests, and DI containers.
## Announcement! Validot is archived!
Validot has been my public pet project since 2020, a proof-of-concept turned into a standalone, fully-featured NuGet library. Its core focus is on performance and a low-allocation approach. On release day, Validot was 2.5 times faster while consuming 8 times less memory than the industry's gold standard: FluentValidation! I pushed dotnet memory performance and optimizations to their limits. And I'm proud of my work.
Given my daily responsibilities and other coding projects, I have to confess that it seems improbable I'll have time to keep working on Validot.
I appreciate all the contributors, and especially, I extend my gratitude to Jeremy Skinner for his work on FluentValidation. I genuinely believe I played a role in the open source community by motivating Jeremy to enhance FluentValidation's performance.
## Quickstart
Add the Validot nuget package to your project using dotnet CLI:
```
dotnet add package Validot
```
All the features are accessible after referencing single namespace:
``` csharp
using Validot;
```
And you're good to go! At first, create a specification for your model with the fluent api.
``` csharp
Specification specification = _ => _
.Member(m => m.Email, m => m
.Email()
.WithExtraCode("ERR_EMAIL")
.And()
.MaxLength(100)
)
.Member(m => m.Name, m => m
.Optional()
.And()
.LengthBetween(8, 100)
.And()
.Rule(name => name.All(char.IsLetterOrDigit))
.WithMessage("Must contain only letter or digits")
)
.And()
.Rule(m => m.Age >= 18 || m.Name != null)
.WithPath("Name")
.WithMessage("Required for underaged user")
.WithExtraCode("ERR_NAME");
```
The next step is to create a [validator](../docs/DOCUMENTATION.md#validator). As its name stands - it validates objects according to the [specification](../docs/DOCUMENTATION.md#specification). It's also thread-safe so you can seamlessly register it as a singleton in your DI container.
``` csharp
var validator = Validator.Factory.Create(specification);
```
Validate the object!
``` csharp
var model = new UserModel(email: "inv@lidv@lue", age: 14);
var result = validator.Validate(model);
```
The [result](../docs/DOCUMENTATION.md#result) object contains all information about the [errors](../docs/DOCUMENTATION.md#error-output). Without retriggering the validation process, you can extract the desired form of an output.
``` csharp
result.AnyErrors; // bool flag:
// true
result.MessageMap["Email"] // collection of messages for "Email":
// [ "Must be a valid email address" ]
result.Codes; // collection of all the codes from the model:
// [ "ERR_EMAIL", "ERR_NAME" ]
result.ToString(); // compact printing of codes and messages:
// ERR_EMAIL, ERR_NAME
//
// Email: Must be a valid email address
// Name: Required for underaged user
```
* [See this example's real code](../tests/Validot.Tests.Functional/Readme/QuickStartFuncTests.cs)
## Features
### Advanced fluent API, inline
No more obligatory if-ology around input models or separate classes wrapping just validation logic. Write [specifications](../docs/DOCUMENTATION.md#specification) inline with simple, human-readable [fluent API](../docs/DOCUMENTATION.md#fluent0api). Native support for properties and fields, structs and classes, [nullables](../docs/DOCUMENTATION.md#asnullable), [collections](../docs/DOCUMENTATION.md#ascollection), [nested members](../docs/DOCUMENTATION.md#member), and possible combinations.
``` csharp
Specification nameSpecification = s => s
.LengthBetween(5, 50)
.SingleLine()
.Rule(name => name.All(char.IsLetterOrDigit));
Specification emailSpecification = s => s
.Email()
.And()
.Rule(email => email.All(char.IsLower))
.WithMessage("Must contain only lower case characters");
Specification userSpecification = s => s
.Member(m => m.Name, nameSpecification)
.WithMessage("Must comply with name rules")
.And()
.Member(m => m.PrimaryEmail, emailSpecification)
.And()
.Member(m => m.AlternativeEmails, m => m
.Optional()
.And()
.MaxCollectionSize(3)
.WithMessage("Must not contain more than 3 addresses")
.And()
.AsCollection(emailSpecification)
)
.And()
.Rule(user => {
return user.PrimaryEmail is null || user.AlternativeEmails?.Contains(user.PrimaryEmail) == false;
})
.WithMessage("Alternative emails must not contain the primary email address");
```
* [Blog post about constructing specifications in Validot](https://lenar.dev/posts/crafting-model-specifications-using-validot)
* [Guide through Validot's fluent API](../docs/DOCUMENTATION.md#fluent-api)
* [If you prefer the approach of having a separate class for just validation logic, it's also fully supported](../docs/DOCUMENTATION.md#specification-holder)
### Validators
Compact, highly optimized, and thread-safe objects to handle the validation.
``` csharp
Specification bookSpecification = s => s
.Optional()
.Member(m => m.AuthorEmail, m => m.Optional().Email())
.Member(m => m.Title, m => m.NotEmpty().LengthBetween(1, 100))
.Member(m => m.Price, m => m.NonNegative());
var bookValidator = Validator.Factory.Create(bookSpecification);
services.AddSingleton>(bookValidator);
```
``` csharp
var bookModel = new BookModel() { AuthorEmail = "inv@lid_em@il", Price = 10 };
bookValidator.IsValid(bookModel);
// false
bookValidator.Validate(bookModel).ToString();
// AuthorEmail: Must be a valid email address
// Title: Required
bookValidator.Validate(bookModel, failFast: true).ToString();
// AuthorEmail: Must be a valid email address
bookValidator.Template.ToString(); // Template contains all of the possible errors:
// AuthorEmail: Must be a valid email address
// Title: Required
// Title: Must not be empty
// Title: Must be between 1 and 100 characters in length
// Price: Must not be negative
```
* [What Validator is and how it works](../docs/DOCUMENTATION.md#validator)
* [More about template and how to use it](../docs/DOCUMENTATION.md#template)
### Results
Whatever you want. [Error flag](../docs/DOCUMENTATION.md#anyerrors), compact [list of codes](../docs/DOCUMENTATION.md#codes), or detailed maps of [messages](../docs/DOCUMENTATION.md#messagemap) and [codes](../docs/DOCUMENTATION.md#codemap). With sugar on top: friendly [ToString() printing](../docs/DOCUMENTATION.md#tostring) that contains everything, nicely formatted.
``` csharp
var validationResult = validator.Validate(signUpModel);
if (validationResult.AnyErrors)
{
// check if a specific code has been recorded for Email property:
if (validationResult.CodeMap["Email"].Contains("DOMAIN_BANNED"))
{
_actions.NotifyAboutDomainBanned(signUpModel.Email);
}
var errorsPrinting = validationResult.ToString();
// save all messages and codes printing into the logs
_logger.LogError("Errors in incoming SignUpModel: {errors}", errorsPrinting);
// return all error codes to the frontend
return new SignUpActionResult
{
Success = false,
ErrorCodes = validationResult.Codes,
};
}
```
* [Validation result types](../docs/DOCUMENTATION.md#result)
### Rules
Tons of [rules available out of the box](../docs/DOCUMENTATION.md#rules). Plus, an easy way to [define your own](../docs/DOCUMENTATION.md#custom-rules) with the full support of Validot internal features like [formattable message arguments](../docs/DOCUMENTATION.md#message-arguments).
``` csharp
public static IRuleOut ExactLinesCount(this IRuleIn @this, int count)
{
return @this.RuleTemplate(
value => value.Split(Environment.NewLine).Length == count,
"Must contain exactly {count} lines",
Arg.Number("count", count)
);
}
```
``` csharp
.ExactLinesCount(4)
// Must contain exactly 4 lines
.ExactLinesCount(4).WithMessage("Required lines count: {count}")
// Required lines count: 4
.ExactLinesCount(4).WithMessage("Required lines count: {count|format=000.00|culture=pl-PL}")
// Required lines count: 004,00
```
* [List of built-in rules](../docs/DOCUMENTATION.md#rules)
* [Writing custom rules](../docs/DOCUMENTATION.md#custom-rules)
* [Message arguments](../docs/DOCUMENTATION.md#message-arguments)
### Translations
Pass errors directly to the end-users in the language of your application.
``` csharp
Specification specification = s => s
.Member(m => m.PrimaryEmail, m => m.Email())
.Member(m => m.Name, m => m.LengthBetween(3, 50));
var validator = Validator.Factory.Create(specification, settings => settings.WithPolishTranslation());
var model = new UserModel() { PrimaryEmail = "in@lid_em@il", Name = "X" };
var result = validator.Validate(model);
result.ToString();
// Email: Must be a valid email address
// Name: Must be between 3 and 50 characters in length
result.ToString(translationName: "Polish");
// Email: Musi być poprawnym adresem email
// Name: Musi być długości pomiędzy 3 a 50 znaków
```
At the moment Validot delivers the following translations out of the box: [Polish](../docs/DOCUMENTATION.md#withpolishtranslation), [Spanish](../docs/DOCUMENTATION.md#withspanishtranslation), [Russian](../docs/DOCUMENTATION.md#withrussiantranslation), [Portuguese](../docs/DOCUMENTATION.md#withportuguesetranslation) and [German](../docs/DOCUMENTATION.md#withgermantranslation).
* [How translations work](../docs/DOCUMENTATION.md#translations)
* [Custom translation](../docs/DOCUMENTATION.md#custom-translation)
* [How to selectively override built-in error messages](../docs/DOCUMENTATION.md#overriding-messages)
### Dependency injection
Although Validot doesn't contain direct support for the dependency injection containers (because it aims to rely solely on the .NET Standard 2.0), it includes helpers that can be used with any DI/IoC system.
For example, if you're working with ASP.NET Core and looking for an easy way to register all of your validators with a single call (something like `services.AddValidators()`), wrap your specifications in the [specification holders](../docs/DOCUMENTATION.md#specification-holder), and use the following snippet:
``` csharp
public void ConfigureServices(IServiceCollection services)
{
// ... registering other dependencies ...
// Registering Validot's validators from the current domain's loaded assemblies
var holderAssemblies = AppDomain.CurrentDomain.GetAssemblies();
var holders = Validator.Factory.FetchHolders(holderAssemblies)
.GroupBy(h => h.SpecifiedType)
.Select(s => new
{
ValidatorType = s.First().ValidatorType,
ValidatorInstance = s.First().CreateValidator()
});
foreach (var holder in holders)
{
services.AddSingleton(holder.ValidatorType, holder.ValidatorInstance);
}
// ... registering other dependencies ...
}
```
* [What specification holders are and how to create them](../docs/DOCUMENTATION.md#specification-holder)
* [Fetching specification holders from assemblies](../docs/DOCUMENTATION.md#fetching-holders)
* [Writing the fully-featured `AddValidators` extension step-by-step](../docs/DOCUMENTATION.md#dependency-injection)
## Validot vs FluentValidation
A short statement to start with - [@JeremySkinner](https://twitter.com/JeremySkinner)'s [FluentValidation](https://fluentvalidation.net/) is an excellent piece of work and has been a huge inspiration for this project. True, you can call Validot a direct competitor, but it differs in some fundamental decisions, and lot of attention has been focused on entirely different aspects. If - after reading this section - you think you can bear another approach, api and [limitations](#fluentValidations-features-that-validot-is-missing), at least give Validot a try. You might be positively surprised. Otherwise, FluentValidation is a good, safe choice, as Validot is certainly less hackable, and achieving some particular goals might be either difficult or impossible.
### Validot is faster and consumes less memory
This document shows oversimplified results of [BenchmarkDotNet](https://benchmarkdotnet.org/) execution, but the intention is to present the general trend only. To have truly reliable numbers, I highly encourage you to [run the benchmarks yourself](../docs/DOCUMENTATION.md#benchmarks).
There are three data sets, 10k models each; `ManyErrors` (every model has many errors), `HalfErrors` (circa 60% have errors, the rest are valid), `NoErrors` (all are valid) and the rules reflect each other as much as technically possible. I did my best to make sure that the tests are just and adequate, but I'm a human being and I make mistakes. Really, if you spot errors [in the code](https://github.com/bartoszlenar/Validot/tree/cdca31a2588bf801288ef73e8ca50bfd33be8049/tests/Validot.Benchmarks), framework usage, applied methodology... or if you can provide any counterexample proving that Validot struggles with some particular scenarios - I'd be very very very happy to accept a PR and/or discuss it on [GitHub Issues](https://github.com/bartoszlenar/Validot/issues).
To the point; the statement in the header is true, but it doesn't come for free. Wherever possible and justified, Validot chooses performance and less allocations over [flexibility and extra features](#fluentvalidations-features-that-validot-is-missing). Fine with that kind of trade-off? Good, because the validation process in Validot might be **~1.6x faster while consuming ~4.7x less memory** (in the most representational, `Validate` tests using `HalfErrors` data set). Especially when it comes to memory consumption, Validot could be even 13.3x better than FluentValidation (`IsValid` tests with `HalfErrors` data set) . What's the secret? Read my blog post: [Validot's performance explained](https://lenar.dev/posts/validots-performance-explained).
| Test | Data set | Library | Mean [ms] | Allocated [MB] |
| - | - | - | -: | -: |
| Validate | `ManyErrors` | FluentValidation | `703.83` | `453` |
| Validate | `ManyErrors` | Validot | `307.04` | `173` |
| FailFast | `ManyErrors` | FluentValidation | `21.63` | `21` |
| FailFast | `ManyErrors` | Validot | `16.76` | `32` |
| Validate | `HalfErrors` | FluentValidation | `563.92` | `362` |
| Validate | `HalfErrors` | Validot | `271.62` | `81` |
| FailFast | `HalfErrors` | FluentValidation | `374.90` | `249` |
| FailFast | `HalfErrors` | Validot | `173.41` | `62` |
| Validate | `NoErrors` | FluentValidation | `559.77` | `354` |
| Validate | `NoErrors` | Validot | `260.99` | `75` |
* [Validate benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated.
* [FailFast benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated, the process stops on the first error.
FluentValidation's `IsValid` is a property that wraps a simple check whether the validation result contains errors or not. Validot has [AnyErrors](../docs/DOCUMENTATION.md#anyerrors) that acts the same way, and [IsValid](../docs/DOCUMENTATION.md#isvalid) is a special mode that doesn't care about anything else but the first rule predicate that fails. If the mission is only to verify the incoming model whether it complies with the rules (discarding all of the details), this approach proves to be better up to one order of magnitude:
| Test | Data set | Library | Mean [ms] | Allocated [MB] |
| - | - | - | -: | -: |
| IsValid | `ManyErrors` | FluentValidation | `20.91` | `21` |
| IsValid | `ManyErrors` | Validot | `8.21` | `6` |
| IsValid | `HalfErrors` | FluentValidation | `367.59` | `249` |
| IsValid | `HalfErrors` | Validot | `106.77` | `20` |
| IsValid | `NoErrors` | FluentValidation | `513.12` | `354` |
| IsValid | `NoErrors` | Validot | `136.22` | `24` |
* [IsValid benchmark](../tests/Validot.Benchmarks/Comparisons/ValidationBenchmark.cs) - objects are validated, but only to check if they are valid or not.
Combining these two methods in most cases could be quite beneficial. At first, [IsValid](../docs/DOCUMENTATION.md#isvalid) quickly verifies the object, and if it contains errors - only then [Validate](../docs/DOCUMENTATION.md#validate) is executed to report the details. Of course in some extreme cases (megabyte-size data? millions of items in the collection? dozens of nested levels with loops in reference graphs?) traversing through the object twice could neglect the profit. Still, for the regular web api input validation, it will undoubtedly serve its purpose:
``` csharp
if (!validator.IsValid(model))
{
errorMessages = validator.Validate(model).ToString();
}
```
| Test | Data set | Library | Mean [ms] | Allocated [MB] |
| - | - | - | -: | -: |
| Reporting | `ManyErrors` | FluentValidation | `768.00` | `464` |
| Reporting | `ManyErrors` | Validot | `379.50` | `294` |
| Reporting | `HalfErrors` | FluentValidation | `592.50` | `363` |
| Reporting | `HalfErrors` | Validot | `294.60` | `76` |
* [Reporting benchmark](../tests/Validot.Benchmarks/Comparisons/ReportingBenchmark.cs):
* FluentValidation validates model, and `ToString()` is called if errors are detected.
* Validot processes the model twice - at first, with its special mode, [IsValid](../docs/DOCUMENTATION.md#isvalid). Secondly - in case of errors detected - with the standard method, gathering all errors and printing them with `ToString()`.
Benchmarks environment: Validot 2.3.0, FluentValidation 11.2.0, .NET 6.0.7, i7-9750H (2.60GHz, 1 CPU, 12 logical and 6 physical cores), X64 RyuJIT, macOS Monterey.
### Validot handles nulls on its own
In Validot, null is a special case [handled by the core engine](../docs/DOCUMENTATION.md#null-policy). You don't need to secure the validation logic from null as your predicate will never receive it.
``` csharp
Member(m => m.LastName, m => m
.Rule(lastName => lastName.Length < 50) // 'lastName' is never null
.Rule(lastName => lastName.All(char.IsLetter)) // 'lastName' is never null
)
```
### Validot treats null as an error by default
All values are marked as required by default. In the above example, if `LastName` member were null, the validation process would exit `LastName` scope immediately only with this single error message:
```
LastName: Required
```
The content of the message is, of course, [customizable](../docs/DOCUMENTATION.md#withmessage).
If null should be allowed, place [Optional](../docs/DOCUMENTATION.md#optional) command at the beginning:
``` csharp
Member(m => m.LastName, m => m
.Optional()
.Rule(lastName => lastName.Length < 50) // 'lastName' is never null
.Rule(lastName => lastName.All(char.IsLetter)) // 'lastName' is never null
)
```
Again, no rule predicate is triggered. Also, null `LastName` member doesn't result with errors.
* [Read more about how Validot handles nulls](../docs/DOCUMENTATION.md#null-policy)
### Validot's Validator is immutable
Once [validator](../docs/DOCUMENTATION.md#validator) instance is created, you can't modify its internal state or [settings](../docs/DOCUMENTATION.md#settings). If you need the process to fail fast (FluentValidation's `CascadeMode.Stop`), use the flag:
``` csharp
validator.Validate(model, failFast: true);
```
### FluentValidation's features that Validot is missing
Features that might be in the scope and are technically possible to implement in the future:
* failing fast only in a single scope ([discuss it on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/5))
* validated value in the error message ([discuss it on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/6))
Features that are very unlikely to be in the scope as they contradict the project's principles, and/or would have a very negative impact on performance, and/or are impossible to implement:
* Full integration with ASP.NET or other frameworks:
* Validot tries to remain a single-purpose library, depending only on .NET Standard 2.0. Thus all integrations need to be done individually.
* However, Validot delivers [FetchHolders method](../docs/DOCUMENTATION.md#fetching-holders) that makes such integrations possible to wrap within a few lines of code. The quick example is in the [Dependency Injection section of this readme file](#dependency-injection), more advanced solution with explanation is contained [in the documentation](../docs/DOCUMENTATION.md#dependency-injection).
* Access to any stateful context in the rule condition predicate:
* It implicates a lack of support for dynamic message content and/or amount.
* Callbacks:
* Please react on [failure/success](../docs/DOCUMENTATION.md#anyerrors) after getting [validation result](../docs/DOCUMENTATION.md#result).
* Pre-validation:
* All cases can be handled by additional validation and a proper if-else.
* Also, the problem of the root being null doesn't exist in Validot (it's a regular case, [covered entirely with fluent api](../docs/DOCUMENTATION.md#presence-commands))
* Rule sets
* workaround; multiple [validators](../docs/DOCUMENTATION.md#validator) for different parts of the object.
* `await`/`async` support
* only support for large collections is planned ([more details on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/2))
* severities ([more details on GitHub Issues](https://github.com/bartoszlenar/Validot/issues/4))
* workaround: multiple [validators](../docs/DOCUMENTATION.md#validator) for error groups with different severities.
## Project info
### Requirements
Validot is a dotnet class library targeting .NET Standard 2.0. There are no extra dependencies.
Please check the [official Microsoft document](https://learn.microsoft.com/en-us/dotnet/standard/net-standard?tabs=net-standard-2-0) that lists all the platforms that can use it on.
### Versioning
[Semantic versioning](https://semver.org/) is being used very strictly. The major version is updated only when there is a breaking change, no matter how small it might be (e.g., adding extra method to the public interface). On the other hand, a huge pack of new features will bump the minor version only.
Before every major version update, at least one preview version is published.
### Reliability
Unit tests coverage hits 100% very close, and it can be detaily verified on [codecov.io](https://codecov.io/gh/bartoszlenar/Validot/branch/main).
Before publishing, each release is tested on the ["latest" version](https://help.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#supported-runners-and-hardware-resources) of the following operating systems:
* macOS
* Ubuntu
* Windows Server
using the upcoming, the current and all also the supported [LTS versions](https://dotnet.microsoft.com/platform/support/policy/dotnet-core) of the underlying frameworks:
* .NET 8.0
* .NET 6.0
* .NET Framework 4.8 (Windows 2019 only)
### Performance
Benchmarks exist in the form of [the console app project](https://github.com/bartoszlenar/Validot/tree/5219a8da7cc20cd5b9c5c49dd5c0940e829f6fe9/tests/Validot.Benchmarks) based on [BenchmarkDotNet](https://github.com/dotnet/BenchmarkDotNet). Also, you can trigger performance tests [from the build script](../docs/DOCUMENTATION.md#benchmarks).
### Documentation
The documentation is hosted alongside the source code, in the git repository, as a single markdown file: [DOCUMENTATION.md](./../docs/DOCUMENTATION.md).
Code examples from the documentation live as [functional tests](https://github.com/bartoszlenar/Validot/tree/132842cff7c5097c1cad8e762df094e74bb6038c/tests/Validot.Tests.Functional).
### Development
The entire project ([source code](https://github.com/bartoszlenar/Validot), [issue tracker](https://github.com/bartoszlenar/Validot/issues), [documentation](../docs/DOCUMENTATION.md), and [CI workflows](https://github.com/bartoszlenar/Validot/actions)) is hosted here on github.com.
Any contribution is more than welcome. If you'd like to help, please don't forget to check out the [CONTRIBUTING](./../docs/CONTRIBUTING.md) file and [issues page](https://github.com/bartoszlenar/Validot/issues).
### Licencing
Validot uses the [MIT license](../LICENSE). Long story short; you are more than welcome to use it anywhere you like, completely free of charge and without oppressive obligations.
================================================
FILE: .github/pull_request_template.md
================================================
## Issue
Related issue: # (required - if the issue doesn't exist, please create it first)
## Type of changes
- [ ] Documentation update or other changes not related to the code.
- [ ] Bug fix (non-breaking change which fixes an issue).
- [ ] New feature (non-breaking change which adds functionality).
- [ ] Breaking change (fix or feature that would cause existing functionality to change).
## Description
* Please provide the PR description in points.
* Include as many details as you can
* What code areas are affected?
* Any impact on performance?
* Any problems with passing unit tests?
``` csharp
// If applicable, please provide any code that demonstrates the changes (a new functionality in action, or a proof that the bug is fixed)
```
## Tests
* Please list in points all the details about testing the content of this PR
* Any new/altered unit/functional tests?
* Any new benchmarks?
* Any new unit/functional tests? Any altered tests?
================================================
FILE: .github/workflows/CI.yml
================================================
name: CI
on:
push:
branches-ignore:
- "wip/**"
pull_request:
branches-ignore:
- "wip/**"
release:
types: [published]
jobs:
tests:
strategy:
matrix:
os: [macos-latest, windows-latest, ubuntu-latest]
dotnet: ["8.0.x"]
runs-on: ${{ matrix.os }}
name: Test on ${{ matrix.os }} using dotnet ${{ matrix.dotnet }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Setup dotnet
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
${{ matrix.dotnet }}
- name: Init workflow variables
run: pwsh .github/workflows/Init-WorkflowVariables.ps1
- name: Compile
run: pwsh build.ps1 --target compile --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }}
- name: Tests
run: pwsh build.ps1 --target tests --skip --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }}
- name: Upload artifact; details of failed tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: Validot.${{ env.VALIDOT_VERSION }}.${{ matrix.dotnet }}.${{ matrix.os }}.testresults
path: artifacts/tests/Validot.${{ env.VALIDOT_VERSION }}.testresults
tests_netframework:
strategy:
matrix:
os: [windows-2019]
dotnet: [net48]
runs-on: ${{ matrix.os }}
name: Test on ${{ matrix.os }} using dotnet ${{ matrix.dotnet }}
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Setup base dotnet sdk
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Init workflow variables
run: pwsh .github/workflows/Init-WorkflowVariables.ps1
- name: Compile
run: pwsh build.ps1 --target compile --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }}
- name: Tests
run: pwsh build.ps1 --target tests --skip --dotnet ${{ matrix.dotnet }} --configuration release --version ${{ env.VALIDOT_VERSION }}
- name: Upload artifact; details of failed tests
uses: actions/upload-artifact@v4
if: failure()
with:
name: Validot.${{ env.VALIDOT_VERSION }}.${{ matrix.dotnet }}.${{ matrix.os }}.testresults
path: artifacts/tests/Validot.${{ env.VALIDOT_VERSION }}.testresults
code_coverage:
needs: [tests, tests_netframework]
if: github.event_name == 'release' || github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
name: Code coverage
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Setup base dotnet sdk
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
6.0.x
- name: Init workflow variables
run: pwsh .github/workflows/Init-WorkflowVariables.ps1
- name: Analyze code coverage
run: pwsh build.ps1 --target codecoveragereport --dotnet 6.0.x --configuration debug --version ${{ env.VALIDOT_VERSION }}
- name: Upload artifact; code coverage data
uses: actions/upload-artifact@v4
with:
name: Validot.${{ env.VALIDOT_VERSION }}.opencover.xml
path: artifacts/coverage/Validot.${{ env.VALIDOT_VERSION }}.opencover.xml
- name: Upload artifact; code coverage summary
uses: actions/upload-artifact@v4
with:
name: Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json
path: artifacts/coverage_reports/Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json
- name: Upload artifact; code coverage report
if: github.event_name == 'release'
uses: actions/upload-artifact@v4
with:
name: Validot.${{ env.VALIDOT_VERSION }}.coverage_report
path: artifacts/coverage_reports/Validot.${{ env.VALIDOT_VERSION }}.coverage_report
nuget_package:
needs: [tests, tests_netframework]
if: github.event_name == 'release'
runs-on: ubuntu-latest
name: NuGet package
steps:
- name: Checking out code
uses: actions/checkout@v2
- name: Setup base dotnet sdk
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Init workflow variables
run: pwsh .github/workflows/Init-WorkflowVariables.ps1
- name: Create package
run: pwsh build.ps1 --target nugetpackage --dotnet 8.0.x --configuration release --commitsha ${{ github.sha }} --version ${{ env.VALIDOT_VERSION }}
- name: Upload artifact; nuget package
uses: actions/upload-artifact@v4
with:
name: Validot.${{ env.VALIDOT_VERSION }}.nupkg
path: artifacts/nuget/${{ env.VALIDOT_VERSION }}/Validot.${{ env.VALIDOT_VERSION }}.nupkg
- name: Upload artifact; nuget package symbols
uses: actions/upload-artifact@v4
with:
name: Validot.${{ env.VALIDOT_VERSION }}.snupkg
path: artifacts/nuget/${{ env.VALIDOT_VERSION }}/Validot.${{ env.VALIDOT_VERSION }}.snupkg
- name: Publish nuget package
run: pwsh build.ps1 --target publishnugetpackage --skip --dotnet 8.0.x --configuration release --version ${{ env.VALIDOT_VERSION }} --commitsha ${{ github.sha }} --nugetapikey ${{ secrets.NUGET_API_KEY }}
release_assets:
needs: [code_coverage, nuget_package]
if: github.event_name == 'release'
runs-on: ubuntu-latest
name: Upload release assets
steps:
- name: Checking out code
uses: actions/checkout@v2
- name: Init workflow variables
run: pwsh .github/workflows/Init-WorkflowVariables.ps1
- name: Download artifact; nuget package
uses: actions/download-artifact@v4.1.7
with:
name: Validot.${{ env.VALIDOT_VERSION }}.nupkg
path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget
- name: Download artifact; nuget package symbols
uses: actions/download-artifact@v4.1.7
with:
name: Validot.${{ env.VALIDOT_VERSION }}.snupkg
path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget
- name: Download artifact; code coverage data
uses: actions/download-artifact@v4.1.7
with:
name: Validot.${{ env.VALIDOT_VERSION }}.opencover.xml
path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage/data
- name: Download artifact; code coverage summary
uses: actions/download-artifact@v4.1.7
with:
name: Validot.${{ env.VALIDOT_VERSION }}.coverage_summary.json
path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage
- name: Download artifact; code coverage report
uses: actions/download-artifact@v4.1.7
with:
name: Validot.${{ env.VALIDOT_VERSION }}.coverage_report
path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage/report
- name: Zip artifacts
run: |
cd artifacts
zip -rX Validot.${{ env.VALIDOT_VERSION }}.nuget.zip Validot.${{ env.VALIDOT_VERSION }}.nuget
zip -rX Validot.${{ env.VALIDOT_VERSION }}.coverage.zip Validot.${{ env.VALIDOT_VERSION }}.coverage
- name: Upload release asset; nuget package with symbols
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.nuget.zip
asset_name: Validot.${{ env.VALIDOT_VERSION }}.nuget.zip
asset_content_type: application/zip
- name: Upload release asset; code coverage data and reports
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ github.event.release.upload_url }}
asset_path: artifacts/Validot.${{ env.VALIDOT_VERSION }}.coverage.zip
asset_name: Validot.${{ env.VALIDOT_VERSION }}.coverage.zip
asset_content_type: application/zip
================================================
FILE: .github/workflows/Init-WorkflowVariables.ps1
================================================
$commitShortSha = $env:GITHUB_SHA.Substring(0, 7)
if ($env:GITHUB_EVENT_NAME.Equals("release")) {
$tag = $env:GITHUB_REF.Substring("refs/tags/".Length).TrimStart('v');
if ($tag -match "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$")
{
$version = $tag
}
else
{
Write-Error "Tag contains invalid semver: $tag" -ErrorAction Stop
}
}
else {
$version = $commitShortSha
}
"VALIDOT_VERSION=$version" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VALIDOT_COMMIT=$commitShortSha" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"VALIDOT_CI=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
================================================
FILE: .gitignore
================================================
# Created by https://www.gitignore.io/api/csharp
# Edit at https://www.gitignore.io/?templates=csharp
### Csharp ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# End of https://www.gitignore.io/api/csharp
# Visual Studio Code
.vscode/
# Tools
tools/
# jetBrains Rider
.idea/
# MacOS
.DS_Store
================================================
FILE: .nuke/build.schema.json
================================================
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/build",
"title": "Build Schema",
"definitions": {
"build": {
"type": "object",
"properties": {
"AllowWarnings": {
"type": "boolean",
"description": "Allow warnings"
},
"BenchmarksFilter": {
"type": "string",
"description": "Benchmark filter. If empty, all benchmarks will be run"
},
"CodeCovApiKey": {
"type": "string",
"description": "CodeCov API key, allows to publish code coverage"
},
"CommitSha": {
"type": "string",
"description": "Commit SHA"
},
"Configuration": {
"type": "string",
"description": "Configuration to build. 'Debug' (default) or 'Release'",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
},
"DotNet": {
"type": "string",
"description": "dotnet framework id or SDK version (if SDK version is provided, the highest framework available is selected). Default value is 'netcoreapp3.1'"
},
"FullBenchmark": {
"type": "boolean",
"description": "If true, BenchmarkDotNet will run full (time consuming, but more accurate) jobs"
},
"Help": {
"type": "boolean",
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
"NuGetApi": {
"type": "string",
"description": "NuGet API. Where to publish NuGet package. Default value is 'https://api.nuget.org/v3/index.json'"
},
"NuGetApiKey": {
"type": "string",
"description": "NuGet API key, allows to publish NuGet package"
},
"Partition": {
"type": "string",
"description": "Partition to use on CI"
},
"Plan": {
"type": "boolean",
"description": "Shows the execution plan (HTML)"
},
"Profile": {
"type": "array",
"description": "Defines the profiles to load",
"items": {
"type": "string"
}
},
"Root": {
"type": "string",
"description": "Root directory during build execution"
},
"Skip": {
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"AddTranslation",
"Benchmarks",
"Clean",
"CodeCoverage",
"CodeCoverageReport",
"Compile",
"CompileProject",
"CompileTests",
"NugetPackage",
"PublishCodeCoverage",
"PublishNugetPackage",
"Reset",
"Restore",
"Tests"
]
}
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"AddTranslation",
"Benchmarks",
"Clean",
"CodeCoverage",
"CodeCoverageReport",
"Compile",
"CompileProject",
"CompileTests",
"NugetPackage",
"PublishCodeCoverage",
"PublishNugetPackage",
"Reset",
"Restore",
"Tests"
]
}
},
"TranslationName": {
"type": "string",
"description": "(only for target AddTranslation) Translation name"
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
},
"Version": {
"type": "string",
"description": "Version. Default value is '0.0.0-timestamp'"
}
}
}
}
}
================================================
FILE: .nuke/parameters.json
================================================
{
"$schema": "./build.schema.json",
"Solution": "Validot.sln"
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020-2021 Bartosz Lenar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Validot.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{013FC1C3-9AFF-43CE-915C-6BF4FA32FE80}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot", "src\Validot\Validot.csproj", "{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Tests.Unit", "tests\Validot.Tests.Unit\Validot.Tests.Unit.csproj", "{6F71A1C1-319A-492F-B930-0FF4FB599918}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Tests.Functional", "tests\Validot.Tests.Functional\Validot.Tests.Functional.csproj", "{E5AC95A3-1613-433C-A318-9A19B24BF73F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.Benchmarks", "tests\Validot.Benchmarks\Validot.Benchmarks.csproj", "{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Validot.MemoryLeak", "tests\Validot.MemoryLeak\Validot.MemoryLeak.csproj", "{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{4BDB92C9-0BA6-4740-A177-18E80611976A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AssemblyWithHolders", "tests\AssemblyWithHolders\AssemblyWithHolders.csproj", "{971FACBC-AF7F-4897-A081-947C6F2A864A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4BDB92C9-0BA6-4740-A177-18E80611976A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4BDB92C9-0BA6-4740-A177-18E80611976A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x64.ActiveCfg = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x64.Build.0 = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x86.ActiveCfg = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Debug|x86.Build.0 = Debug|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|Any CPU.Build.0 = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x64.ActiveCfg = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x64.Build.0 = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x86.ActiveCfg = Release|Any CPU
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22}.Release|x86.Build.0 = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x64.ActiveCfg = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x64.Build.0 = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x86.ActiveCfg = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Debug|x86.Build.0 = Debug|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|Any CPU.Build.0 = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x64.ActiveCfg = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x64.Build.0 = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x86.ActiveCfg = Release|Any CPU
{6F71A1C1-319A-492F-B930-0FF4FB599918}.Release|x86.Build.0 = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x64.Build.0 = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Debug|x86.Build.0 = Debug|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|Any CPU.Build.0 = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x64.ActiveCfg = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x64.Build.0 = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x86.ActiveCfg = Release|Any CPU
{E5AC95A3-1613-433C-A318-9A19B24BF73F}.Release|x86.Build.0 = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x64.ActiveCfg = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x64.Build.0 = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x86.ActiveCfg = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Debug|x86.Build.0 = Debug|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|Any CPU.Build.0 = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x64.ActiveCfg = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x64.Build.0 = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x86.ActiveCfg = Release|Any CPU
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08}.Release|x86.Build.0 = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x64.ActiveCfg = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x64.Build.0 = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x86.ActiveCfg = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Debug|x86.Build.0 = Debug|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|Any CPU.Build.0 = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x64.ActiveCfg = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x64.Build.0 = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x86.ActiveCfg = Release|Any CPU
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE}.Release|x86.Build.0 = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x64.ActiveCfg = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x64.Build.0 = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x86.ActiveCfg = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Debug|x86.Build.0 = Debug|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|Any CPU.Build.0 = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x64.ActiveCfg = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x64.Build.0 = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x86.ActiveCfg = Release|Any CPU
{971FACBC-AF7F-4897-A081-947C6F2A864A}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{BFD540CD-1648-4E7B-B5C5-B6EC0C6D4D22} = {013FC1C3-9AFF-43CE-915C-6BF4FA32FE80}
{6F71A1C1-319A-492F-B930-0FF4FB599918} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}
{E5AC95A3-1613-433C-A318-9A19B24BF73F} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}
{D9D2BCCB-1D86-42A3-87F8-9FBA4057BD08} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}
{1030576B-DAC1-46C1-A6E3-6E809B5ED0EE} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}
{971FACBC-AF7F-4897-A081-947C6F2A864A} = {1A57BE44-6C94-4A7C-A2B5-EE6CDB6E9176}
EndGlobalSection
EndGlobal
================================================
FILE: build/.editorconfig
================================================
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning
dotnet_diagnostic.CA1707.severity = none
dotnet_diagnostic.CA2211.severity = none
dotnet_diagnostic.CA1050.severity = none
dotnet_diagnostic.CA1847.severity = none
dotnet_diagnostic.CA1845.severity = none
dotnet_diagnostic.IDE0022.severity = none
dotnet_diagnostic.IDE0051.severity = none
dotnet_diagnostic.IDE0058.severity = none
================================================
FILE: build/Build.cs
================================================
#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'.
#pragma warning disable IDE0057 // Use range operator
#pragma warning disable CA1852 // sealed class
#pragma warning disable CA1865 // Use char overload
using System;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Nuke.Common;
using Nuke.Common.CI;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
[ShutdownDotNetAfterServerBuild]
class Build : NukeBuild
{
static readonly Regex SemVerRegex = new Regex(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$", RegexOptions.Compiled);
static readonly Regex TargetFrameworkRegex = new Regex(@".+<\/TargetFramework>", RegexOptions.Compiled);
static readonly DateTimeOffset BuildTime = DateTimeOffset.UtcNow;
static readonly string DefaultFrameworkId = "net8.0";
public static int Main() => Execute(x => x.Compile);
[Parameter("Configuration to build. 'Debug' (default) or 'Release'.")]
readonly Configuration Configuration = Configuration.Debug;
[Parameter("dotnet framework id or SDK version (if SDK version is provided, the highest framework available is selected). Default value is 'netcoreapp3.1'")]
string DotNet;
[Parameter("Version. Default value is '0.0.0-timestamp'")]
string Version;
[Parameter("NuGet API. Where to publish NuGet package. Default value is 'https://api.nuget.org/v3/index.json'")]
readonly string NuGetApi = "https://api.nuget.org/v3/index.json";
[Parameter("NuGet API key, allows to publish NuGet package.")]
readonly string NuGetApiKey;
[Parameter("CodeCov API key, allows to publish code coverage.")]
readonly string CodeCovApiKey;
[Parameter("Commit SHA")]
readonly string CommitSha;
[Parameter("If true, BenchmarkDotNet will run full (time consuming, but more accurate) jobs.")]
readonly bool FullBenchmark;
[Parameter("Benchmark filter. If empty, all benchmarks will be run.")]
readonly string BenchmarksFilter;
[Parameter("Allow warnings")]
readonly bool AllowWarnings;
[Parameter("(only for target AddTranslation) Translation name")]
readonly string TranslationName;
[Solution]
readonly Solution Solution;
AbsolutePath SourceDirectory => RootDirectory / "src";
AbsolutePath TestsDirectory => RootDirectory / "tests";
AbsolutePath ToolsPath => RootDirectory / "tools";
AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts";
AbsolutePath TestsResultsDirectory => ArtifactsDirectory / "tests";
AbsolutePath CodeCoverageDirectory => ArtifactsDirectory / "coverage";
AbsolutePath CodeCoverageReportsDirectory => ArtifactsDirectory / "coverage_reports";
AbsolutePath BenchmarksDirectory => ArtifactsDirectory / "benchmarks";
AbsolutePath NuGetDirectory => ArtifactsDirectory / "nuget";
protected override void OnBuildInitialized()
{
base.OnBuildCreated();
DotNet = GetFramework(DotNet);
Logger.Info($"DotNet: {DotNet}");
Version = GetVersion(Version);
Logger.Info($"Version: {Version}");
Logger.Info($"NuGetApi: {NuGetApi ?? "MISSING"}");
Logger.Info($"Configuration: {Configuration}");
Logger.Info($"CommitSha: {CommitSha ?? "MISSING"}");
Logger.Info($"AllowWarnings: {AllowWarnings}");
Logger.Info($"FullBenchmark: {FullBenchmark}");
Logger.Info($"BenchmarkFilter: {FullBenchmark}");
var nuGetApiKeyPresence = (NuGetApiKey is null) ? "MISSING" : "present";
Logger.Info($"NuGetApiKey: {nuGetApiKeyPresence}");
var codeCovApiKeyPresence = (CodeCovApiKey is null) ? "MISSING" : "present";
Logger.Info($"CodeCovApiKey: {codeCovApiKeyPresence}");
SetFrameworkInTests(DotNet);
SetVersionInAssemblyInfo(Version, CommitSha);
}
protected override void OnBuildFinished()
{
ResetFrameworkInTests();
ResetVersionInAssemblyInfo();
base.OnBuildFinished();
}
Target AddTranslation => _ => _
.Requires(() => TranslationName)
.Executes(() =>
{
CreateFromTemplate(SourceDirectory / "Validot" / "Translations" / "_Template");
CreateFromTemplate(TestsDirectory / "Validot.Tests.Unit" / "Translations" / "_Template");
void CreateFromTemplate(AbsolutePath templatePath)
{
CopyDirectoryRecursively(templatePath, templatePath.Parent / TranslationName);
var files = new DirectoryInfo(templatePath.Parent / TranslationName).GetFiles();
foreach (var file in files)
{
var finalFilePath = file.FullName.Replace("_Template", TranslationName).Replace(".txt", string.Empty);
RenameFile(file.FullName, finalFilePath);
File.WriteAllText(finalFilePath, File.ReadAllText(finalFilePath).Replace("_Template", TranslationName));
}
}
});
Target Reset => _ => _
.Executes(() =>
{
EnsureCleanDirectory(TemporaryDirectory);
EnsureCleanDirectory(ArtifactsDirectory);
EnsureCleanDirectory(ToolsPath);
ResetFrameworkInTests();
})
.Triggers(Clean);
Target Clean => _ => _
.Before(Restore)
.Executes(() =>
{
SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
TestsDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory);
});
Target Restore => _ => _
.Executes(() =>
{
DotNetRestore(_ => _
.SetProjectFile(Solution));
});
Target CompileProject => _ => _
.DependsOn(Clean, Restore)
.Executes(() =>
{
DotNetBuild(c => c
.EnableNoRestore()
.SetTreatWarningsAsErrors(!AllowWarnings)
.SetProjectFile(SourceDirectory / "Validot/Validot.csproj")
.SetConfiguration(Configuration)
.SetFramework("netstandard2.0")
);
});
Target CompileTests => _ => _
.DependsOn(Clean, Restore)
.After(CompileProject)
.Executes(() =>
{
var testsProjects = new[]
{
TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj",
TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj"
};
foreach (var testProject in testsProjects)
{
DotNetBuild(c => c
.EnableNoRestore()
.SetTreatWarningsAsErrors(!AllowWarnings)
.SetProjectFile(testProject)
.SetConfiguration(Configuration)
.SetFramework(DotNet)
.AddProperty("DisableSourceLink", "1")
);
}
});
Target Compile => _ => _
.DependsOn(CompileProject, CompileTests);
Target Tests => _ => _
.DependsOn(CompileTests)
.Executes(() =>
{
DotNetTest(p => p
.EnableNoBuild()
.SetConfiguration(Configuration)
.SetProjectFile(TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj")
.SetFramework(DotNet)
.SetLoggers($"junit;LogFilePath={TestsResultsDirectory / $"Validot.{Version}.testresults" / $"Validot.{Version}.unit.junit"}")
);
DotNetTest(p => p
.EnableNoBuild()
.SetConfiguration(Configuration)
.SetProjectFile(TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj")
.SetFramework(DotNet)
.SetLoggers($"junit;LogFilePath={TestsResultsDirectory / $"Validot.{Version}.testresults" / $"Validot.{Version}.functional.junit"}")
);
});
Target CodeCoverage => _ => _
.DependsOn(CompileTests)
.Requires(() => Configuration == Configuration.Debug)
.Executes(() =>
{
var reportFile = CodeCoverageDirectory / $"Validot.{Version}.opencover.xml";
DotNetTest(p => p
.EnableNoBuild()
.SetProjectFile(TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj")
.SetConfiguration(Configuration.Debug)
.SetFramework(DotNet)
.AddProperty("CollectCoverage", "true")
.AddProperty("CoverletOutput", reportFile)
.AddProperty("CoverletOutputFormat", "opencover")
.AddProperty("DisableSourceLink", "1")
);
Logger.Info($"CodeCoverage opencover format file location: {reportFile} ({new FileInfo(reportFile).Length} bytes)");
});
Target CodeCoverageReport => _ => _
.DependsOn(CodeCoverage)
.Requires(() => Configuration == Configuration.Debug)
.Executes(() =>
{
var toolPath = InstallAndGetToolPath("dotnet-reportgenerator-globaltool", "4.8.1", "ReportGenerator.dll", "net5.0");
var toolParameters = new[]
{
$"-reports:{CodeCoverageDirectory / $"Validot.{Version}.opencover.xml"}",
$"-reporttypes:HtmlInline_AzurePipelines;JsonSummary",
$"-targetdir:{CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_report"}",
$"-historydir:{CodeCoverageReportsDirectory / "_history"}",
$"-title:Validot unit tests code coverage report",
$"-tag:v{Version}" + (CommitSha is null ? "" : $"+{CommitSha}"),
};
ExecuteTool(toolPath, string.Join(" ", toolParameters.Select(p => $"\"{p}\"")));
File.Move(CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_report/Summary.json", CodeCoverageReportsDirectory / $"Validot.{Version}.coverage_summary.json");
});
Target Benchmarks => _ => _
.DependsOn(Clean)
.Executes(() =>
{
var benchmarksPath = BenchmarksDirectory / $"Validot.{Version}.benchmarks";
var jobShort = FullBenchmark ? string.Empty : "--job short";
var filter = BenchmarksFilter is null ? "*" : BenchmarksFilter;
DotNetRun(p => p
.SetProjectFile(TestsDirectory / "Validot.Benchmarks/Validot.Benchmarks.csproj")
.SetConfiguration(Configuration.Release)
.SetProcessArgumentConfigurator(a => a
.Add("--")
.Add($"--artifacts {benchmarksPath} {jobShort}")
.Add("--exporters GitHub StackOverflow JSON HTML")
.Add($"--filter {filter}")
)
);
});
Target NugetPackage => _ => _
.DependsOn(Compile)
.Requires(() => Configuration == Configuration.Release)
.Executes(() =>
{
DotNetPack(p => p
.EnableNoBuild()
.SetConfiguration(Configuration.Release)
.SetProject(SourceDirectory / "Validot/Validot.csproj")
.SetVersion(Version)
.SetOutputDirectory(NuGetDirectory / Version)
);
});
Target PublishNugetPackage => _ => _
.DependsOn(NugetPackage)
.Requires(() => NuGetApiKey != null)
.Requires(() => Configuration == Configuration.Release)
.Executes(() =>
{
DotNetNuGetPush(p => p
.SetSource(NuGetApi)
.SetApiKey(NuGetApiKey)
.SetTargetPath(NuGetDirectory / Version / $"Validot.{Version}.nupkg")
);
});
Target PublishCodeCoverage => _ => _
.DependsOn(CodeCoverage)
.Requires(() => CodeCovApiKey != null)
.Requires(() => Configuration == Configuration.Debug)
.Executes(() =>
{
var reportFile = CodeCoverageDirectory / $"Validot.{Version}.opencover.xml";
var toolPath = InstallAndGetToolPath("codecov.tool", "1.13.0", "codecov.dll", "net5.0");
var toolParameters = new[]
{
$"--sha {CommitSha}",
$"--file {reportFile}",
$"--token {CodeCovApiKey}",
$"--required"
};
ExecuteTool(toolPath, string.Join(" ", toolParameters));
});
void SetFrameworkInTests(string framework)
{
var testsCsprojs = new[]
{
TestsDirectory / "Validot.Tests.Unit/Validot.Tests.Unit.csproj",
TestsDirectory / "Validot.Tests.Functional/Validot.Tests.Functional.csproj",
TestsDirectory / "Validot.Benchmarks/Validot.Benchmarks.csproj",
};
foreach (var csproj in testsCsprojs)
{
SetFrameworkInCsProj(framework, csproj);
}
}
void SetFrameworkInCsProj(string framework, string csProjPath)
{
Logger.Info($"Setting framework {framework} in {csProjPath}");
var content = TargetFrameworkRegex.Replace(File.ReadAllText(csProjPath), $"{framework}");
File.WriteAllText(csProjPath, content);
}
void SetVersionInAssemblyInfo(string version, string commitSha)
{
var assemblyVersion = "0.0.0.0";
var assemblyFileVersion = "0.0.0.0";
if (SemVerRegex.IsMatch(version))
{
assemblyVersion = version.Substring(0, version.IndexOf(".", StringComparison.InvariantCulture)) + ".0.0.0";
assemblyFileVersion = version.Contains("-", StringComparison.InvariantCulture)
? version.Substring(0, version.IndexOf("-", StringComparison.InvariantCulture)) + ".0"
: version + ".0";
}
Logger.Info("Setting AssemblyVersion: " + assemblyVersion);
Logger.Info("Setting AssemblyFileVersion: " + assemblyFileVersion);
var assemblyInfoPath = SourceDirectory / "Validot/Properties/AssemblyInfo.cs";
var assemblyInfoLines = File.ReadAllLines(assemblyInfoPath);
var autogeneratedPostfix = "// this line is autogenerated by the build script";
for (var i = 0; i < assemblyInfoLines.Length; ++i)
{
if (assemblyInfoLines[i].Contains("AssemblyVersion", StringComparison.InvariantCulture))
{
assemblyInfoLines[i] = $"[assembly: System.Reflection.AssemblyVersion(\"{assemblyVersion}\")] {autogeneratedPostfix}";
}
else if (assemblyInfoLines[i].Contains("AssemblyFileVersion", StringComparison.InvariantCulture))
{
assemblyInfoLines[i] = $"[assembly: System.Reflection.AssemblyFileVersion(\"{assemblyFileVersion}\")] {autogeneratedPostfix}";
}
}
File.WriteAllLines(assemblyInfoPath, assemblyInfoLines);
}
void ResetVersionInAssemblyInfo() => SetVersionInAssemblyInfo("0.0.0", null);
void ResetFrameworkInTests() => SetFrameworkInTests(DefaultFrameworkId);
string GetFramework(string dotnet)
{
if (string.IsNullOrWhiteSpace(dotnet))
{
Logger.Warn("DotNet: parameter not provided");
return DefaultFrameworkId;
}
if (char.IsDigit(dotnet.First()))
{
Logger.Info($"DotNet parameter recognized as SDK version: " + dotnet);
if (dotnet.StartsWith("2.1.", StringComparison.Ordinal))
{
return "netcoreapp2.1";
}
if (dotnet.StartsWith("3.1.", StringComparison.Ordinal))
{
return "netcoreapp3.1";
}
if (dotnet.StartsWith("5.0.", StringComparison.Ordinal))
{
return "net5.0";
}
if (dotnet.StartsWith("6.0.", StringComparison.Ordinal))
{
return "net6.0";
}
if (dotnet.StartsWith("7.0.", StringComparison.Ordinal))
{
return "net7.0";
}
if (dotnet.StartsWith("8.0.", StringComparison.Ordinal))
{
return "net8.0";
}
Logger.Warn("Unrecognized dotnet SDK version: " + dotnet);
return dotnet;
}
if (dotnet.StartsWith("netcoreapp", StringComparison.Ordinal) && dotnet["netcoreapp".Length..].All(c => char.IsDigit(c) || c == '.'))
{
Logger.Info("DotNet parameter recognized as .NET Core target: " + DotNet);
return dotnet;
}
if (dotnet.StartsWith("net", StringComparison.Ordinal) && DotNet["net".Length..].All(char.IsDigit))
{
Logger.Info("DotNet parameter recognized as .NET Framework target: " + dotnet);
return dotnet;
}
Logger.Warn("Unrecognized dotnet framework id: " + dotnet);
return dotnet;
}
string GetVersion(string version)
{
if (version is null)
{
Logger.Warn("Version: not provided.");
return $"0.0.0-{BuildTime.DayOfYear}{BuildTime.ToString("HHmmss", CultureInfo.InvariantCulture)}";
}
return version;
}
void ExecuteTool(string toolPath, string parameters)
{
ProcessTasks.StartProcess(ToolPathResolver.GetPathExecutable("dotnet"), toolPath + " -- " + parameters).AssertZeroExitCode();
}
string InstallAndGetToolPath(string name, string version, string executableFileName, string framework = null)
{
var frameworkPart = framework is null ? $" (framework {framework})" : string.Empty;
var toolStamp = $"{name} {version}{frameworkPart}, executable file: {executableFileName}";
Logger.Info($"Looking for tool: {toolStamp}");
var toolPath = ResolveToolPath();
if (toolPath is null)
{
DotNetToolInstall(c => c
.SetPackageName(name)
.SetVersion(version)
.SetToolInstallationPath(ToolsPath)
.SetGlobal(false));
}
toolPath = ResolveToolPath();
if (toolPath is null)
{
Logger.Error($"Unable to find tool path: {name} {version} {executableFileName} {framework}");
}
return toolPath;
AbsolutePath ResolveToolPath()
{
var frameworkPart = framework != null ? (framework + "/**/") : string.Empty;
Serilog.Log.Debug($"Looking for tool in {ToolsPath} using glob pattern: **/{name}/{version}/**/{frameworkPart}{executableFileName}");
var files = ToolsPath.GlobFiles($"**/{name}/{version}/**/{frameworkPart}{executableFileName}");
if (files.Count > 1)
{
foreach (var file in files)
{
Serilog.Log.Warning($"Found tool candidate: {file}");
}
var toolPath = files.First();
Serilog.Log.Warning($"Found many tool candidates, so proceeding with the first one: {toolPath}");
return toolPath;
}
return files.FirstOrDefault();
}
}
}
================================================
FILE: build/Configuration.cs
================================================
using System;
using System.ComponentModel;
using System.Linq;
using Nuke.Common.Tooling;
[TypeConverter(typeof(TypeConverter))]
public class Configuration : Enumeration
{
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
public static Configuration Release = new Configuration { Value = nameof(Release) };
public static implicit operator string(Configuration configuration)
{
return configuration.Value;
}
}
================================================
FILE: build/_build.csproj
================================================
Exenet8.0CS0649;CS0169....1
================================================
FILE: build/_build.csproj.DotSettings
================================================
DO_NOT_SHOWDO_NOT_SHOWDO_NOT_SHOWDO_NOT_SHOWImplicitImplicitExpressionBody0NEXT_LINETrueFalse120IF_OWNER_IS_SINGLE_LINEWRAP_IF_LONGFalse<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />TrueTrueTrueTrueTrueTrueTrueTrueTrue
================================================
FILE: build.cmd
================================================
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*
================================================
FILE: build.ps1
================================================
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetChannel = "STS"
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_NOLOGO = 1
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
$env:PATH = "$DotNetDirectory;$env:PATH"
}
Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
& $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null
& $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
}
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
================================================
FILE: build.sh
================================================
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="STS"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_NOLOGO=1
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
export PATH="$DOTNET_DIRECTORY:$PATH"
fi
echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then
"$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true
"$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true
fi
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
================================================
FILE: docs/CHANGELOG.md
================================================
# Changelog
All notable changes to the [Validot project](https://github.com/bartoszlenar/Validot) will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.5.0] - 2025-02-01
### Added
- Chinese translation. [#15](https://github.com/bartoszlenar/Validot/issues/15) [#37](https://github.com/bartoszlenar/Validot/pull/37)
## [2.5.0] - 2024-05-24
### Added
- Added `AsDictionary` command. [#26](https://github.com/bartoszlenar/Validot/issues/26)
## [2.4.1] - 2023-03-16
### Fixed
- Fixed invalid placeholders in messages. [#32](https://github.com/bartoszlenar/Validot/issues/32)
## [2.4.0] - 2022-10-01
### Added
- Added `AsType` command. [#4](https://github.com/bartoszlenar/Validot/issues/24)
### Fixed
- Inline XML documentation for `AsConverted`.
## [2.3.0] - 2022-08-13
### Added
- Added `AsConverted` command. [#3](https://github.com/bartoszlenar/Validot/issues/3)
### Removed
- Official support for not supported dotnet versions (.NET Core 2.1, .NET 5.0).
## [2.2.0] - 2021-11-05
### Added
- German translation (along with `WithGermanTranslation` extension to the settings builder). [#12](https://github.com/bartoszlenar/Validot/issues/12)
- Portuguese translation (along with `WithPortugueseTranslation` extension to the settings builder). [#13](https://github.com/bartoszlenar/Validot/issues/13)
### Fixed
- Fix to Spanish translation in `Times.BeforeOrEqualTo` message key. [#20](https://github.com/bartoszlenar/Validot/pull/20/commits/6a68dcdc17589f3c9bd524bc2266238b5245ff50)
- Minor performance fixes and code improvements. [#21](https://github.com/bartoszlenar/Validot/pulls/21) [#22](https://github.com/bartoszlenar/Validot/pulls/22)
## [2.1.0] - 2021-06-07
### Added
- Spanish translation (along with `WithSpanishTranslation` extension to the settings builder). [#11](https://github.com/bartoszlenar/Validot/issues/11)
- Russian translation (along with `WithRussianTranslation` extension to the settings builder). [#14](https://github.com/bartoszlenar/Validot/issues/14)
- Translation template with script. To add a new translation all you need to do is call the build script, e.g. to add Korean, execute `pwsh build.ps1 --target AddTranslation --translationName Korean` (you can use `bash build.sh` instead of `pwsh build.ps1`) and the template with phrases will be created at `src/Validot/Translations/Korean` (plus unit tests in their own location). The only thing that is left to do is to enter translated phrases into the dictionary and make a PR!
- A preview version of .NET 6 in the CI pipeline for all unit and functional tests.
## [2.0.0] - 2021-02-01
### Added
- `FetchHolders` method in the factory that helps [fetching specification holders](DOCUMENTATION.md#fetching-holders) from the assemblies and delivers a handy way to create the validators and [register them in the dependency injection containers](DOCUMENTATION.md#dependency-injection). [#10](https://github.com/bartoszlenar/Validot/issues/10)
- Method in the factory that accepts the settings (in form of `IValidatorSettings`) directly, so the settings (e.g. from another validator) could be reused. This method compensates the lack of validator's public constructor.
- [Settings holders](DOCUMENTATION.md#settings-holder) (`ISettingsHolder` interface), a mechanism similar to specification holders. This feature compensates the lack of `ITranslationHolder`.
### Fixed
- Fixed inline XML code documentation, so it's visible from IDEs when referencing a nuget package.
### Changed
- Introduced `IValidatorSettings` as a public interface for read-only access to the `ValidatorSettings` instance. `ValidatorSettings` class isn't public anymore, and validator's `Settings` property is now of type `IValidatorSettings`. This is a breaking change.
- Renamed `ReferenceLoopProtection` flag to `ReferenceLoopProtectionEnabled`. This is a breaking change.
- `IsValid` method uses a dedicated validation context that effectively doubles the speed of the operation
- Ported all test projects to .NET 5.
- Replaced ruleset-based code style rules with editorconfig and `Microsoft.CodeAnalysis.CSharp.CodeStyle` roslyn analyzers.
- Renamed `master` git branch to `main`.
### Removed
- Validator's public constructor. Please use the factory to create validators. If you want to reuse the settings, factory has new method that accepts `IValidatorSettings` instance. This is a breaking change.
- Translation holders (`ITranslationHolder` interface). You can easily replace them with the newly introduced settings holders (`ISettingsHolder` interface). This is a breaking change.
- CapacityInfo feature. It wasn't publicly available anyway and ultimately didn't prove to boost up the performance.
## [1.2.0] - 2020-11-04
### Added
- `And` - a fluent API method that [helps to visually separate the rules](DOCUMENTATION.md#And) within the specification. [#9](https://github.com/bartoszlenar/Validot/issues/9)
- Inline documentation (XML comments)
## [1.1.0] - 2020-09-01
### Added
- Email rule now operates in two modes: ComplexRegex (which covers the previous, regex-based behavior, and is still set as default) and DataAnnotationsCompatible (compatible with the dotnet's [EmailAddressAttribute](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.emailaddressattribute?view=netcore-3.1)).
- Title case support for the name argument, so name `SuperImportantValue123` when inserted with `{_name|format=titleCase}` is converted into `Super Important Value 123`. [#1](https://github.com/bartoszlenar/Validot/issues/1)
## [1.0.0] - 2020-06-23
First stable and public release. The reference point for all further changes.
================================================
FILE: docs/CONTRIBUTING.md
================================================
# Contributing
If you're reading this file, it means that you are - more or less - interested in contributing to the project. Before anything else, I'd like to say - thank you! It really means a lot to me that you consider this project to be worth your time.
## Flow
1. Let's have a discussion first!
- It's really important that you visit the project's [Issues page](https://github.com/bartoszlenar/Validot/issues) and check if there is already an ongoing discussion around the same (or similar) idea.
- Let's have a short chat about a feature that you have in mind (or a bug that you found), _before_ any actual code work. Validot is tweaked performance-wise and has a very sensitive codebase and for this reason I prefer to assign new features to myself by default. Of course, it's not a rule, but double please, let's have a chat about new features and breaking changes before you dedicate your time to Validot.
2. Fork and code!
- How to build, run unit and functional tests? How to analyse code coverage a execute benchmarks? It's all covered in the [documentation](./DOCUMENTATION.md#Development).
- Please get familiar with Validot's [project principles](#project-principles), [git](#git) and [code standards](#code-standards)
- Provide changes in the documentation if needed.
3. Raise the PR!
## Project principles
- Validot is not the ultimate solution to all validation scenarios and cases in the world. Let's keep it compact and simple, focused on a single problem.
- Validot should not have any other dependencies than .NET Standard 2.0.
- Validot - unless absolutely necessary - should not sacrifice performance for extra features.
- Validot follows [semantic versioning](https://semver.org/) very strictly, no matter how annoying it could be.
## Code standards
- Be aware that the code needs to compile and pass the tests on all of the LTS versions of .NET, under all supported OSes.
- The CI system will let you know if your PR fails.
- Please ensure that your code is covered with unit and functional tests.
- Don't hesitate to ask - I'm more than happy to help you with everything!
- CI system verifies the code style as well.
- If your change is related with some core validation mechanism, please run the benchmarks to ensure it isn't affecting the performance.
## Git
- The commits should follow the pattern of short notes in the past tense:
- `Added Hungarian translation`
- `Fixed IsValid bug #XXX`
- Ideally, PR has a single commit with all changes, but that's not a requirement. As long as the each commit has logical sense and complies with all the rules - it compiles, passes tests, contains the related documentation changes - then it's fine.
## Translations
- If you can help with expanding the list of built-it translations, that would be great! There is a build script helper there for you:
- Type `pwsh build.ps1 --target AddTranslation --translationName Gibberlish` (of course, plese replace `Gibberlish` with your language name).
- Navigate into `src/Validot/Translations/Gibberlish/GibberlishTranslation.cs` and replace the English phrases with their proper translations.
- The script prepares everything, including `AddGibberlishTranslation()` settings extension and automatic unit tests. All you need to do next is to raise a PR.
- You can replace `pwsh build.ps1` with `sh build.sh` or even execute windows command `bash.cmd`. It's all the same.
================================================
FILE: docs/DOCUMENTATION.md
================================================
# Documentation
## Table of contents
- [Documentation](#documentation)
- [Table of contents](#table-of-contents)
- [Introduction](#introduction)
- [Specification](#specification)
- [Scope commands](#scope-commands)
- [Parameter commands](#parameter-commands)
- [Presence commands](#presence-commands)
- [Error output](#error-output)
- [Message](#message)
- [Code](#code)
- [Path](#path)
- [Fluent api](#fluent-api)
- [Rule](#rule)
- [RuleTemplate](#ruletemplate)
- [Member](#member)
- [AsModel](#asmodel)
- [AsCollection](#ascollection)
- [AsNullable](#asnullable)
- [AsConverted](#asconverted)
- [AsType](#astype)
- [AsDictionary](#asdictionary)
- [WithCondition](#withcondition)
- [WithPath](#withpath)
- [WithMessage](#withmessage)
- [WithExtraMessage](#withextramessage)
- [WithCode](#withcode)
- [WithExtraCode](#withextracode)
- [Optional](#optional)
- [Required](#required)
- [Forbidden](#forbidden)
- [And](#and)
- [Null policy](#null-policy)
- [Reference loop](#reference-loop)
- [Validator](#validator)
- [Validate](#validate)
- [IsValid](#isvalid)
- [Factory](#factory)
- [Specification holder](#specification-holder)
- [Settings holder](#settings-holder)
- [Reusing settings](#reusing-settings)
- [Fetching holders](#fetching-holders)
- [Dependency injection](#dependency-injection)
- [Settings](#settings)
- [WithReferenceLoopProtection](#withreferenceloopprotection)
- [WithTranslation](#withtranslation)
- [Template](#template)
- [Result](#result)
- [AnyErrors](#anyerrors)
- [Paths](#paths)
- [Codes](#codes)
- [CodeMap](#codemap)
- [MessageMap](#messagemap)
- [GetTranslatedMessageMap](#gettranslatedmessagemap)
- [TranslationNames](#translationnames)
- [ToString](#tostring)
- [Rules](#rules)
- [Global rules](#global-rules)
- [Bool rules](#bool-rules)
- [Char rules](#char-rules)
- [Collections rules](#collections-rules)
- [Numbers rules](#numbers-rules)
- [Texts rules](#texts-rules)
- [Times rules](#times-rules)
- [Guid rules](#guid-rules)
- [TimeSpan rules](#timespan-rules)
- [Custom rules](#custom-rules)
- [Message arguments](#message-arguments)
- [Enum argument](#enum-argument)
- [Guid argument](#guid-argument)
- [Number argument](#number-argument)
- [Text argument](#text-argument)
- [Time argument](#time-argument)
- [Translation argument](#translation-argument)
- [Type argument](#type-argument)
- [Path argument](#path-argument)
- [Name argument](#name-argument)
- [Translations](#translations)
- [Built-in translations](#built-in-translations)
- [WithPolishTranslation](#withpolishtranslation)
- [WithSpanishTranslation](#withspanishtranslation)
- [WithRussianTranslation](#withrussiantranslation)
- [WithPortugueseTranslation](#withportuguesetranslation)
- [WithGermanTranslation](#withgermantranslation)
- [Overriding messages](#overriding-messages)
- [Custom translation](#custom-translation)
- [Development](#development)
- [Build](#build)
- [Tests](#tests)
- [Benchmarks](#benchmarks)
## Introduction
- This documentation is written in short points.
- Sometimes a point contains a subpoint.
- Occasionally, a point could have a source code following it.
- It's for demonstration, and the code is also commented in italic font.
- Most code examples in this documentation are using the following set of models:
``` csharp
public class BookModel
{
public string Title { get; set; }
public IEnumerable Authors { get; set; }
public IEnumerable Languages { get; set; }
public int YearOfFirstAnnouncement { get; set; }
public int? YearOfPublication { get; set; }
public PublisherModel Publisher { get; set; }
public bool IsSelfPublished { get; set; }
}
public class AuthorModel
{
public string Name { get; set; }
public string Email { get; set; }
}
public class PublisherModel
{
public string CompanyId { get; set; }
public string Name { get; set; }
}
public enum Language
{
English,
Polish
}
```
_Comments are usually placed below the code sample, but that's not the rock-solid principle. The important thing is that they are related to the preceding point, while the next point starts the new thing._
- Vast majority of the code snippets live as functional tests in the [separate project](../tests/Validot.Tests.Functional/).
---
## Specification
- Specification is an expression that uses [fluent api](#fluent-api) to describe all conditions of a valid object.
- Technically, [specification is a generic delegate](../src/Validot/Specification/Specification.cs), and in most cases, you'll see it in the form of a lambda function.
- If you prefer the approach of wrapping validation logic into a separate class, use the [specification holder](#specification-holder).
- Specification - considered purely as a C# function - is executed by the [validator](#validator) during its construction (directly or through the [factory](#factory)).
- However the validation logic (that specification contains in the form of predicates) is triggered only when [validator](#validator) calls [Validate](#validate) method.
- Fluent api consist of commands called in so-called method chain:
``` csharp
Specification yearSpecification = m => m
.GreaterThan(-10000)
.NotEqualTo(0).WithMessage("There is no such year as 0")
.LessThan(3000);
```
_Above; four chained commands: `GreaterThan`, `NotEqualTo`, `WithMessage`, `LessThan`. All of them - the entire specification - is the single scope that validates value of type `int`._
- Logically, specification consist of scopes. And the scope could be explained as:
- Set of commands that describe validation rules for the same value.
- This value is often referred to in this documentation as "scope value".
- If the value is null, scope acts according to the [null policy](#null-policy).
``` csharp
Specification yearSpecification = s => s
.GreaterThan(-10000)
.NotEqualTo(0).WithMessage("There is no such year as 0")
.LessThan(3000);
Specification bookSpecification = s => s
.Member(m => m.YearOfFirstAnnouncement, yearSpecification)
.Member(m => m.YearOfPublication, m => m
.Positive()
)
.Rule(m => m.YearOfPublication == m.YearOfFirstAnnouncement).WithMessage("Same year in both places is invalid");
```
_Above; `yearSpecification` contains four commands in its scope, all validating the value of type `int`._
_Next one, `bookSpecification`, is more complex. Let's analyse it:_
_First [Member](#member) command steps into the `BookModel`'s member of type `int` named `YearOfFirstAnnouncement` and in its scope validates the value using the `yearSpecification` defined earlier._
_Second [Member](#member) command opens scope that validates `YearOfPublication`; this scope contains single rule, `Positive`. Also, according to the [null policy](#null-policy), it requires the nullable member `YearOfPublication` to have a value._
_The last [scope command](#scope-commands), [Rule](#rule) contains a piece of logic for `BookModel` and [parameter command](#parameter-commands) [WithMessage](#withmessage) defines the error message if the predicate fails._
- You can also say that specification is a scope. A "root level" scope.
- All commands and their logic are related to a single value (of type `T` in `Specification`).
- The [null policy](#null-policy) is followed here as well.
- Commands that validate parts of the model are using... specification to describe the scope rules.
- Even the root scope behaves as it was placed in [AsModel](#asmodel) command.
- There are three types of commands:
- [Scope commands](#scope-commands) - contain validation logic and produce [error output](#error-output).
- [Parameter commands](#paramter-commands) - changes the behavior of the preceding [scope command](#scope-commands).
- [Presence commands](#presence-commands) - sets the scope behavior in case of null value.
---
### Scope commands
- Scope command is a command that validates the model by:
- executing the validation logic directly:
- [Rule](#rule) - executes a custom predicate.
- [RuleTemplate](#ruletemplate) and all of the [built-in rules](#rules) - executes a predefined piece of logic.
- executing the validation logic wrapped in another [specification](#specification), in the way dependent on the scope value type:
- [Member](#member) - executes specification on the model's member.
- [AsModel](#asmodel) - executes specification on the model.
- [AsCollection](#ascollection) - executes specification on each item of the collection type model.
- [AsNullable](#asnullable) - executes specification on the value of the nullable type model.
``` csharp
Specification authorSpecification = m => m
.Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100))
.Member(m => m.Email, m => m.Email())
.Rule(m => m.Email != m.Name);
```
_In the above code you can see the specification containing only scope commands._
- Scope command produces [error output](#error-output) if - by any bit of a validation logic - the scope value is considered as invalid.
- How is "scope" term related with scope command?
- Good to read; [Specification](#specification) - also tries to describe what is a scope.
- All scope commands (except for [Rule](#rule) and [RuleTemplate](#ruletemplate)) validate the value by executing a specification (which is a scope).
- [Rule](#rule) and [RuleTemplate](#ruletemplate) are slightly different. They contain the most atomic part of validation logic - a predicate. They are still [scope commands](#scope-commands), because:
- They determine if the value is valid or not. The only difference is that they execute the logic directly instead of wrapped within another scope.
- They produce [error output](#error-output) in case of validation error.
---
### Parameter commands
- Parameter command is a command that affects (parametrizes) the closest [scope command](#scope-commands) placed before it.
- [WithCondition](#withcondition) - sets execution condition.
- [WithPath](#withpath) - sets the path for the [error output](#error-output).
- [WithMessage](#withmessage) - overwrites the entire [error output](#error-output) with a single message.
- [WithExtraMessage](#withextramessage) - appends a single message to the [error output](#error-output).
- [WithCode](#withcode) - overwrites the entire [error output](#error-output) with a single code.
- [WithExtraCode](#withextracode) - appends a single code to the [error output](#error-output).
- Parameter commands have their order strictly defined and enforced by the language constructs.
- So you might notice that some commands are not available from certain places.
- Example: [AsNullable](#asnullable) can't be called in the scope that validates `int`.
- Example: [WithCode](#withcode) can't be called after [WithMessage](#withmessage), because that doesn't make much sense (double overwrite...).
- To know what other commands are allowed to be placed before/after, read the section about the particular command.
- It doesn't matter how many parameter commands are defined in the row - they are all related to the closest preceding [scope command](#scope-command) (or [presence command](#presence-commands)).
- All the parameter commands start with `With...`, so it's easy to group them visually:
``` csharp
Specification authorSpecification = s => s
.Member(m => m.Name, m => m.NotWhiteSpace().MaxLength(100))
.WithCondition(m => !string.IsNullOrEmpty(m.Name))
.WithPath("AuthorName")
.WithCode("AUTHOR_NAME_ERROR")
.Member(m => m.Email, m => m.Email())
.WithMessage("Invalid email!")
.WithExtraCode("EMAIL_ERROR")
.Rule(m => m.Email != m.Name)
.WithCondition(m => m.Email != null && m.Name != null)
.WithPath("Email")
.WithMessage("Name can't be same as Email");
```
_Above, you can see that the first [Member](#member) command is configured with the following parameters commands: [WithCondition](#withcondition), [WithPath](#withpath) and [WithCode](#withcode)._
_The second [Member](#member) command is configured with [WithMessage](#withmessage), and [WithExtraCode](#withextracode) commands._
_The third scope command - [Rule](#rule) - is configured with [WithCondition](#withcondition), [WithPath](#withpath), and [WithMessage](#withmessage) commands_
---
### Presence commands
- Presence command is the command that defines the behavior of the entire scope in case of null scope value:
- [Required](#required) - scope value must not be null.
- if no presence command exists in the scope, this behavior is set implicitly, by default.
- [Forbidden](#forbidden) - scope value must be null.
- [Optional](#optional) - scope value can be null.
- Value gets validated normally if it isn't null, but nothing happens if it is.
- Only one presence command is allowed within the scope.
- Presence command needs to be the first command in the scope.
- Presence commands produce [error output](#error-output) that can be modified with some of the [parameter commands](#parameter-commands).
- Not all of them, because e.g. you can't change their [path](#path) or set an [execution condition](#withcondition).
- Good to read: [Handling nulls](#null-policy) - details about the null value validation strategy.
``` csharp
Specification authorSpecification = m => m
.Optional()
.Member(m => m.Name, m => m
.Optional()
.NotWhiteSpace()
.MaxLength(100)
)
.Member(m => m.Email, m => m
.Required().WithMessage("Email is obligatory.")
.Email()
)
.Rule(m => m.Email != m.Name);
```
_In the example above the entire model is allowed to be null. Similarly - `Name` member. `Email` is required, but the error output will contain a custom message (`Email is obligatory.`) in case of null._
---
### Error output
- Error output is everything that is returned from the scope if - according to the internal logic - the scope value is invalid.
- Therefore, the absence of error output means that the value is valid.
- Error output can contain:
- [Error messages](#message) - human-readable messages explaining what went wrong.
- [Error codes](#code) - flags that help to organize the logic around specific errors.
- Both. There are no limitations around that. The error output can contain only messages, only codes, or a mix.
- The validation process assigns every error output to the [path](#path) where it was produced.
- The [path](#path) shows the location where the error occurred.
- Sometimes this documentation refers to this action as "saving error output _under the path_"
- Good to read:
- [Result](#result) - how to get the error output from the validation process.
- [Path](#path) - how the paths are constructed.
---
#### Message
- Messages are primarily targeted to humans.
- Use case; logs and the details about invalid models incoming from the frontend.
- Use case; rest api returning messages that frontend shows in the pop up.
- [Error output](#error-output) can contain one or more error messages.
- Good to read:
- [Translations](#translations) - how to translate a message or [overwrite](#overriding-messages) the default one.
- [Message arguments](#message-arguments) - how to use message arguments.
- [MessageMap](#messagemap) - how to read messages from the [validation result](#result).
- Message can be set using [WithMessage](#withmessage), [WithExtraMessage](#withmessage), and [RuleTemplate](#ruletemplate) commands.
``` csharp
Specification yearSpecification = s => s
.Rule(year => year > -300)
.WithMessage("Minimum year is 300 B.C.")
.WithExtraMessage("Ancient history date is invalid.")
.Rule(year => year != 0)
.WithMessage("The year 0 is invalid.")
.WithExtraMessage("There is no such year as 0.")
.Rule(year => year < 10000)
.WithMessage("Maximum year is 10000 A.D.");
var validator = Validator.Factory.Create(yearSpecification);
var result = validator.Validate(-500);
result.MessageMap[""][0] // Minimum year is 300 B.C.
result.MessageMap[""][1] // Ancient history date is invalid.
validator.ToString();
// Minimum year is 300 B.C.
// Ancient history date is invalid.
```
_In the above code, [MessageMap](#messagemap) holds the messages assigned to their paths. Empty string as a path means that the error is recorded for the root model._
- Printing returned by [ToString](#tostring) method includes the path before each message.
``` csharp
Specification yearSpecification = s => s
.Rule(year => year > -300)
.WithMessage("Minimum year is 300 B.C.")
.WithExtraMessage("Ancient history date is invalid.")
.Rule(year => year != 0)
.WithMessage("The year 0 is invalid.")
.WithExtraMessage("There is no such year as 0.")
.Rule(year => year < 10000)
.WithMessage("Maximum year is 10000 A.D.");
Specification bookSpecification = s => s
.Member(m => m.YearOfFirstAnnouncement, yearSpecification)
.Member(m => m.YearOfPublication, m => m.AsNullable(yearSpecification))
.Rule(m => m.YearOfFirstAnnouncement <= m.YearOfPublication)
.WithCondition(m => m.YearOfPublication.HasValue)
.WithMessage("Year of publication must be after the year of first announcement");
var validator = Validator.Factory.Create(bookSpecification);
var book = new BookModel() { YearOfFirstAnnouncement = 0, YearOfPublication = -100 };
var result = validator.Validate(book);
result.MessageMap[""][0]; // Year of publication must be after the year of first announcement
result.MessageMap["YearOfFirstAnnouncement"][0]; // "The year 0 is invalid.
result.MessageMap["YearOfFirstAnnouncement"][1]; // There is no such year as 0.
result.ToString();
// Year of publication must be after the year of first announcement
// YearOfFirstAnnouncement: The year 0 is invalid.
// YearOfFirstAnnouncement: There is no such year as 0.
```
---
#### Code
- Codes are primarily for the parsers and interpreters - they should be short flags, easy to process.
- Code cannot contain white space characters.
- Good to read:
- [CodeMap](#codemap) - how to read codes from the validation result.
- [Codes](#codes) - a quick list of all codes from the result.
``` csharp
Specification yearSpecification = s => s
.Rule(year => year > -300)
.WithCode("MAX_YEAR")
.Rule(year => year != 0)
.WithCode("ZERO_YEAR")
.WithExtraCode("INVALID_VALUE")
.Rule(year => year < 10000)
.WithCode("MIN_YEAR");
var validator = Validator.Factory.Create(yearSpecification);
var result = validator.Validate(0);
result.Codes; // [ "ZERO_YEAR", "INVALID_VALUE" ]
result.CodeMap[""][0]; // [ "ZERO_YEAR" ]
result.CodeMap[""][1]; // [ "INVALID_VALUE" ]
result.ToString();
// ZERO_YEAR, INVALID_VALUE
```
_In the above example, [CodeMap](#codemap) acts similarly to [MessageMap](#messagemap). Also, for your convenience, [Codes](#codes) holds all the error codes in one place. [ToString()](#tostring) called on the result prints error codes, coma separated, in the first line._
---
### Path
- Path is a string that shows the way of reaching the value that is invalid.
- "The way" means which members need to be traversed through in order to reach the particular value.
- Example; `Author.Email` path describes the value of `Email` that is inside `Author`.
- Path contains segments, and each one stands for one member that the validation context needs to enter in order to reach the value.
- Path segments are separated with `.` (dot character).
- [Member](#member), which is the way of stepping into the nested level uses the member's name as a segment.
``` csharp
model.Member.NestedMember.MoreNestedMember.Email = "invalid_email_value";
var result = validator.Validate(model);
result.MessageMap["Member.NestedMember.MoreNestedMember.Email"][0]; // Must be a valid email address
result.ToString();
// Member.NestedMember.MoreNestedMember.Email: Must be a valid email address
```
- When it comes to collections (validated with [AsCollection](#ascollection), n-th (counting from zero) item is considered as the member named `#n`.
``` csharp
model.MemberCollection[0].NestedMember.MoreNestedMemberCollection[23].Email = "invalid_email_value";
var result = validator.Validate(model);
result.MessageMap["MemberCollection[0].NestedMember.MoreNestedMemberCollection[23].Email"][0]; // Must be a valid email address
result.ToString();
// MemberCollection[0].NestedMember.MoreNestedMemberCollection[23]: Must be a valid email address
```
_Above, `MemberCollection.#0.NestedMember.MoreNestedMemberCollection.#23.Email:` is the path that leads through 1st item of `MemberCollection` and 24th item of `MoreNestedMemberCollection`._
- You are free to modify the path of every error output using [WithPath](#withpath).
---
### Fluent api
- The order the commands in the specification is strictly enforced by the language constructs. Invalid order means compilation error.
---
#### Rule
- `Rule` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `Rule` defines a single, atomic bit of validation logic with a predicate that accepts the scope value and returns:
- `true`, if the scope value is valid.
- `false`, if the scope value in invalid.
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m.Rule(isAgeValid);
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.IsValid(12); // true
ageValidator.IsValid(20); // false
ageValidator.Validate(32).ToString();
// Error
```
- If the predicate returns `false`, the `Rule` scope returns [error output](#error-output).
- The default error output of `Rule` command is a single [message](#message) key `Global.Error`
- Default English translation for it is just `Error`.
- It can be altered with [WithMessage](#withmessage) command.
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m.Rule(isAgeValid).WithMessage("The age is invalid");
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// The age is invalid
```
_This is just a regular usage of [WithMessage](#withmessage) command that overwrites the entire [error output](#error-output) of the preceding [scope command](#scope-commands) (in this case - `Rule`)._
- `Rule` can be used to validate dependencies between the scope object's members.
- If the [error output](#error-output) of such validation should be placed in the member scope rather than its parent, use [WithPath](#withpath) command.
``` csharp
Specification bookSpecification = m => m
.Rule(book => book.IsSelfPublished == (book.Publisher is null)).WithMessage("Book must have a publisher or be self-published.");
var bookValidator = Validator.Factory.Create(bookSpecification);
bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = new PublisherModel() }).ToString();
// Book must have a publisher or be self-published.
bookValidator.Validate(new BookModel() { IsSelfPublished = true, Publisher = null }).AnyErrors; // false
```
- The value received in the predicate as an argument is never null.
- All null-checks on it are redundant, no matter what code analysis has to say about it.
- Although the received value is never null, its members could be!
``` csharp
Specification publisherSpecification = m => m
.Rule(publisher =>
{
if (publisher.Title.Contains(publisher.CompanyId))
{
return false;
}
return true;
});
var validator = Validator.Factory.Create(publisherSpecification);
validator.Validate(new PublisherModel()); // throws NullReferenceException
```
_In the above example, `publisher` argument is never null, but `Title` and `CompanyId` could be, thus it's high a risk of `NullReferenceException`._
- All unhandled exceptions are bubbled up to the surface and can be caught from `Validate` method.
- Exceptions are unmodified and are not wrapped.
``` csharp
var verySpecialException = new VerySpecialException();
Specification bookSpecification = m => m.Rule(book => throw verySpecialException);
var bookValidator = Validator.Factory.Create(bookSpecification);
try
{
bookValidator.Validate(new BookModel());
}
catch(VerySpecialException exception)
{
object.ReferenceEquals(exception, verySpecialException); // true
}
```
- After processing the [Specification](#specification), the [validator](#validator) stores the predicate in its internals.
- This is the very reason to be double-cautious when "capturing" variables in the predicate function as you're risking memory leak. Especially when the [validator](#validator) is registered as a singleton in a DI container.
---
#### RuleTemplate
- `RuleTemplate` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `RuleTemplate` is a special version of [Rule](#rule).
- All of the details described in the [Rule](#rule) section also apply to `RuleTemplate`.
- The purpose of `RuleTemplate` is to deliver a convenient foundation for predefined, reusable rules.
- All [built-in rules](#rules) use `RuleTemplate` under the hood. There are no exceptions, hacks, or special cases.
- So if you decide to write your own [custom rules](#custom-rules), you're using the exact same api that the Validot uses.
- Technically, there is nothing wrong in placing `RuleTemplate` in the specification directly, but it's not considered as a good practice.
- You should rather limit the usage of `RuleTemplate` to its purpose; [custom rules](#custom-rules).
- `RuleTemplate` accepts three parameters:
- `Predicate` - predicate that tells if the value is valid or not (exactly the same meaning as in [Rule](#rule)).
- `message` - error message content. Required.
- `args` - a collection of [arguments](#message-arguments) that can be used in the message content. Optional.
- `message` sets the single [error message](#message) that will be in the [error output](#error-output) if the predicate returns `false`.
- So the result is the same as when using `Rule` followed by `WithMessage`. Below example presents that:
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification1 = m => m.Rule(isAgeValid).WithMessage("The age is invalid");
Specification ageSpecification2 = m => m.RuleTemplate(isAgeValid, "The age is invalid");
var ageValidator1 = Validator.Factory.Create(ageSpecification1);
var ageValidator2 = Validator.Factory.Create(ageSpecification2);
ageValidator1.Validate(32).ToString();
// The age is invalid
ageValidator2.Validate(32).ToString();
// The age is invalid
```
_The above code presents that there is no difference between the basic usage of [Rule](#rule) and [RuleTemplate](#ruletemplate)._
- `args` parameter is optional, and it's a collection of [arguments](#message-arguments) that can be used in placeholders within the error message.
- Each argument needs to be created with `Arg` static factory
- Ok, technically it doesn't _need_ to be created by the factory, but it's highly recommended as implementing `IArg` yourself could be difficult and more support for it is planned, but not in the very nearly future.
- Factory contains helper methods to create arguments related with enums, types, texts, numbers, and guids.
- When creating an argument, factory needs:
- `name` - needs to be unique across the collection of arguments.
- it's the base part of the placeholder: `{name}`
- value - value that the message can use
- `Arg.Number("minimum", 123)` - creates a number argument named `minimum` with `int` value of `123`
- `Arg.Text("title", "Star Wars")` - creates text argument named `title` with `string` value of `"Star Wars"`
- Good to read: [Message arguments](#message-arguments) - how to use arguments in messages
- Placeholders in the [error message](#message) will be replaced with the value of the related argument.
- Name must be the same
- Placeholder needs follow the pattern: `{argumentName}`
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m
.RuleTemplate(isAgeValid, "Age must be between {minAge} and {maxAge}", Arg.Number("minAge", 0), Arg.Number("maxAge", 18));
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// Age must be between 0 and 18
```
- Optionally, placeholders can contain additional parameters:
- Good to read: [Message arguments](#message-arguments)
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m
.RuleTemplate(
isAgeValid,
"Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}",
Arg.Number("minAge", 0),
Arg.Number("maxAge", 18)
);
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// Age must be between 0.00 and 18,00
```
_Notice that the format follows dotnet [custom numeric format strings](https://docs.microsoft.com/en-us/dotnet/standard/base-types/custom-numeric-format-strings). The `maxAge` argument also has a different culture set (`pl-PL`, so `,` as a divider instead of `.`)._
- Not all arguments need to be used.
- One argument can be used more than once in the same message.
- If there is any error (like invalid name of the argument or parameter), no exception is thrown in the code, but the string, unformatted, goes directly to the error output.
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m
.RuleTemplate(
isAgeValid,
"Age must be between {minAge|format=0.00} and {maximumAge|format=0.00|culture=pl-PL}",
Arg.Number("minAge", 0),
Arg.Number("maxAge", 18)
);
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// "Age must be between 0.00 and {maximumAge|format=0.00|culture=pl-PL}"
```
_In the above example, `maximumAge` is invalid argument name (`maxAge` would be OK in this case) and therefore - the placeholder stays as it is._
- `RuleTemplate` exposes its arguments to all [messages](#message) in its [error output](#error-output).
- Each message can contain only a subset of arguments.
- Each message is free to use any formatting it wants.
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m
.RuleTemplate(
isAgeValid,
"Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}",
Arg.Number("minAge", 0),
Arg.Number("maxAge", 18)
)
.WithExtraMessage("Must be more than {minAge}")
.WithExtraMessage("Must be below {maxAge|format=0.00}! {maxAge}!");
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// Age must be between 0.00 and 18,00
// Must be more than 0
// Must be below 18.00! 18!
```
- Arguments passed to `RuleTemplate` are also available in [WithMessage](#withmessage) and [WithExtraMessage](#withextramessage).
``` csharp
Predicate isAgeValid = age => (age >= 0) && (age < 18);
Specification ageSpecification = m => m
.RuleTemplate(
isAgeValid,
"Age must be between {minAge|format=0.00} and {maxAge|format=0.00|culture=pl-PL}",
Arg.Number("minAge", 0),
Arg.Number("maxAge", 18)
)
.WithMessage("Only {minAge}-{maxAge}!");
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// Only 0-18!
```
- Because all the [built-in rules](#rules) are based on `RuleTemplate`, this is the magic behind altering their error message and still having access to the arguments.
``` csharp
Specification ageSpecification = m => m.Between(min: 0, max: 18).WithMessage("Only {min}-{max|format=0.00}!");
var ageValidator = Validator.Factory.Create(ageSpecification);
ageValidator.Validate(32).ToString();
// Only 0-18!
```
_In the above example, `Between` is a built-in rule for `int` type values that exposes `min` and `max` parameters to be used in the error messages._
- Good to read:
- [Message arguments](#message-arguments) - everything about the available arguments, their types, and parameters.
- [Custom rules](#custom-rules) - how to create a custom rule, step by step.
- [Rules](#rules) - the detailed list of all arguments available in each of the built-in rule.
---
#### Member
- `Member` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `Member` executes a specification upon a scope object's member.
- `Member` command accepts:
- member selector - a lambda expression pointing at a scope object's member.
- specification - [specification](#specification) to be executed upon the selected member.
- Member selector serves two purposes:
- It points at the member that will be validated with the passed [specification](#specification).
- So technically it determines type `T` in `Specification` that `Member` accepts as a second parameter.
- It defines the nested path under which the entire [error output](#error-output) from the passed [specification](#specification) will be saved.
- By default, if the member selector is `m => m.Author`, the [error output](#error-output) will be saved under the path `Author` (as a next segment).
``` csharp
Specification nameSpecification = s => s
.Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!");
var nameValidator = Validator.Factory.Create(nameSpecification);
nameValidator.Validate("Adam !!!").ToString();
// Must consist of letters only!
// Must not contain whitespace!
```
_In the above example, you can see specification and validation of a string value. Let's use this exact specification inside `Member` command and observe how the entire output is saved under a nested path:_
``` csharp
Specification publisherSpecification = s => s
.Member(m => m.Name, nameSpecification);
var publisherValidator = Validator.Factory.Create(publisherSpecification);
var publisher = new PublisherModel()
{
Name = "Adam !!!"
};
publisherValidator.Validate(publisher).ToString();
// Name: Must consist of letters only!
// Name: Must not contain whitespace!
```
_Let's add one more level:_
``` csharp
Specification bookSpecification = s => s
.Member(m => m.Publisher, publisherSpecification);
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
Publisher = new PublisherModel()
{
Name = "Adam !!!"
}
};
authorValidator.Validate(book).ToString();
// Publisher.Name: Must consist of letters only!
// Publisher.Name: Must not contain whitespace!
```
- Whether to define a [specification](#specification) upfront and pass it to the `Member` command or define everything inline - it's totally up to you. It doesn't make any difference.
- The only thing that is affected is the source code readability.
- However, in some particular situations, reusing predefined specifications could lead to having an infinite reference loop in the object. This topic is covered in [Reference loop](#reference-loop) section.
``` csharp
Specification bookSpecification = s => s
.Member(m => m.Publisher, m => m
.Member(m1 => m1.Name, m1 => m1
.Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!")
)
);
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
Publisher = new PublisherModel()
{
Name = "Adam !!!"
};
};
authorValidator.Validate(book).ToString();
// Publisher.Name: Must consist of letters only!
// Publisher.Name: Must not contain whitespace!
```
- Selected member can be only one level from the scope object!
- No language construct prevents you from stepping into more nested levels (so no compilation errors), but then, during runtime, [validator](#validator) throws the exception from its constructor (or [factory](#factory)).
- This behavior is very likely to be updated in the future versions, so such selectors might be allowed someday... but not now.
``` csharp
Specification bookSpecification = s => s
.Member(m => m.Publisher.Name, nameSpecification);
Validator.Factory.Create(bookSpecification); // throws exception
```
_In the above example, the exception is thrown because member selector goes two levels down (`Publisher.Name`). Please remember that one level down is allowed (just `Publisher` would be totally OK)._
- Selected member can be either property or variable.
- It can't be a function.
- Type of selected member doesn't matter (can be a reference type, value type, string, enum, or whatever...).
- The default path for the [error output](#error-output) (determined by the member selector) can be altered using [WithPath](#withpath) command.
- If the selected member contains null, the member scope is still executed and the [error output](#error-output) entirely depends on the [specification](#specification).
- It means that null member is not anything special. It's a normal situation, and the behavior relies on the passed [specification](#specification), its [presence commands](#presence-commands), and the [null handling strategy](#null-policy).
``` csharp
Specification publisherSpecification = s => s
.Member(m => m.Name, m => m
.Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!")
);
Specification publisherSpecificationRequired = s => s
.Member(m => m.Name, m => m
.Required().WithMessage("Must be filled in!")
.Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!")
);
Specification publisherSpecificationOptional = s => s
.Member(m => m.Name, m => m
.Optional()
.Rule(name => name.All(char.IsLetter)).WithMessage("Must consist of letters only!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!")
);
var publisherValidator = Validator.Factory.Create(publisherSpecification);
var publisherValidatorRequired = Validator.Factory.Create(publisherSpecificationRequired);
var publisherValidatorOptional = Validator.Factory.Create(publisherSpecificationOptional);
var publisher = new PublisherModel()
{
Name = null
};
publisherValidator.Validate(publisher).ToString();
// Name: Required
publisherValidatorRequired.Validate(publisher).ToString();
// Name: Must be filled in!
publisherValidatorOptional.Validate(publisher).AnyErrors; // false
```
_Without any [presence command](#presence-commands) in `publisherSpecification`, the default behavior is to require the scope value to be non-null. The [error message](#message) can be customized (`publisherSpecificationRequired`) with [Required](#required) command followed by [WithMessage](#withmessage)._
_If the specification starts with `Optional`, no error is returned from the member scope._
---
#### AsModel
- `AsModel` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `AsModel` executes a specification upon the scope value.
- `AsModel` command accepts only one argument; a specification `Specification`, where `T` is the current scope type.
- Technically `AsModel` executes specification in the same scope that it lives itself.
- So you can say it's like [Member](#member) command, but it doesn't step into any member.
``` csharp
Specification emailSpecification = s => s
.Rule(email => email.Contains('@')).WithMessage("Must contain @ character!");
Specification emailAsModelSpecification = s => s
.AsModel(emailSpecification);
var emailValidator = Validator.Factory.Create(emailSpecification);
var emailAsModelValidator = Validator.Factory.Create(emailAsModelSpecification);
emailValidator.Validate("invalid email").ToString();
// Must contain @ character!
emailAsModelValidator.Validate("invalid email").ToString();
// Must contain @ character!
```
_In the above code you can see that it doesn't matter whether specification is used directly or through `AsModel` - the validation logic is the same and the [error output](#error-output) is saved under the same [path](#path)._
``` csharp
Specification emailSpecification = s => s
.Rule(email => email.Contains('@')).WithMessage("Must contain @ character!");
Specification emailNestedAsModelSpecification = s => s
.AsModel(s1 => s1
.AsModel(s2 => s2
.AsModel(emailSpecification)
)
);
var emailValidator = Validator.Factory.Create(emailSpecification);
var emailNestedAsModelValidator = Validator.Factory.Create(emailNestedAsModelSpecification);
emailValidator.Validate("invalid email").ToString();
// Must contain @ character!
emailAsModelValidator.Validate("invalid email").ToString();
// Must contain @ character!
```
_The above example presents that even several levels of nested `AsModel` commands don't make any difference._
- `AsModel` can be used to execute many independent [specifications](#specification) on the same value.
- Effectively, it's like merging [specifications](#specification) into one.
``` csharp
Specification atRequiredSpecification = s => s
.Rule(text => text.Contains('@')).WithMessage("Must contain @ character!");
Specification allLettersLowerCaseSpecification = s => s
.Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!");
Specification lengthSpecification = s => s
.Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters")
.Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters");
Specification emailSpecification = s => s
.AsModel(atRequiredSpecification)
.AsModel(allLettersLowerCaseSpecification)
.AsModel(lengthSpecification);
var emailValidator = Validator.Factory.Create(emailSpecification);
emailValidator.Validate("Email").ToString();
// Must contain @ character!
// All letters need to be lower case!
// Must be longer than 5 characters
```
_In the above example, you can see how three separate [specifications](#specification) are - practically - combined into one._
- `AsModel` can be used to mix predefined specifications with inline rules.
- Thanks to this, you might "modify" the presence rule in the predefined specification.
``` csharp
Specification atRequiredSpecification = s => s
.Rule(text => text.Contains('@')).WithMessage("Must contain @ character!");
Specification allLettersLowerCaseSpecification = s => s
.Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!");
Specification emailSpecification = s => s
.Optional()
.AsModel(atRequiredSpecification)
.AsModel(allLettersLowerCaseSpecification)
.Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters")
.Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters");
var emailValidator = Validator.Factory.Create(emailSpecification);
emailValidator.Validate("Email").ToString();
// Must contain @ character!
// All letters need to be lower case!
// Must be longer than 5 characters
emailValidator.Validate(null).AnyErrors; // false
```
_The example above shows that predefined [specification](#specification) can be expanded with more rules (`AsModel` and subsequent [Rule](#rule) commands)._
_Also, you can observe the interesting behavior that can be described as [presence rule](#presence-commands) alteration. Please notice that `emailSpecification` starts with [Optional](#optional) command that makes the entire model optional (null is allowed) and no error is returned even though both `atRequiredSpecification` and `allLettersLowerCaseSpecification` require model to be not null. Of course, technically it is NOT a modification of their presence settings, but the specification execution would never reach them. Why? The scope value is null, and the scope presence rule `Optional` allows this. And in case of null, as always, no further validation is performed in the scope. Not a big deal, but the example gives an overview of how to play with fluent-api bits to "modify" presence rule._
_Naturally, this works the other way around. Below a short demo of how to make a model required while only using specification that allows the model to be null:_
``` csharp
Specification emailOptionalSpecification = s => s
.Optional()
.Rule(text => text.Contains('@')).WithMessage("Must contain @ character!");
Specification emailSpecification = s => s
.AsModel(emailOptionalSpecification);
var emailOptionalValidator = Validator.Factory.Create(emailOptionalSpecification);
var emailValidator = Validator.Factory.Create(emailSpecification);
emailOptionalValidator.Validate(null).AnyErrors; // false
emailOptionalValidator.Validate("Email").ToString();
// Must contain @ character!
emailValidator.Validate(null).ToString();
// Required
emailValidator.Validate("Email").ToString();
// Must contain @ character!
```
_As you can notice, null passed to `emailOptionalValidator` doesn't produce any validation errors (and it's okay, because the specification allows that with `Optional` command). Having the same specification in `AsModel` effectively changes this behavior. True, null passed to `AsModel` would not return any error output, but null never gets there. The root scope (`emailSpecification`) doesn't allow nulls and it terminates the validation before reaching `AsModel`._
- `AsModel` can be very helpful if you want to bundle many commands and want a single [error message](#message) if any of them indicates validation error.
- Saying that, `AsModel` can wrap the entire [specification](#specification) and return single [error message](#message) out of it.
- This is just a regular usage of [WithMessage](#withmessage) command and applies to all [scope commands](#scope-commands), not only `AsModel`. It's mentioned here only to present this very specific use case. For more details, please read the [WithMessage](#withmessage) section.
``` csharp
Specification emailSpecification = s => s
.Rule(text => text.Contains('@')).WithMessage("Must contain @ character!")
.Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c))).WithMessage("All letters need to be lower case!")
.Rule(text => text.Length > 5).WithMessage("Must be longer than 5 characters")
.Rule(text => text.Length < 20).WithMessage("Must be shorter than 20 characters");
Specification emailWrapperSpecification = s => s
.AsModel(emailSpecification).WithMessage("This value is invalid as email address");
var emailValidator = Validator.Factory.Create(emailSpecification);
var emailWrapperValidator = Validator.Factory.Create(emailWrapperSpecification);
emailValidator.Validate("Email").ToString();
// Must contain @ character!
// All letters need to be lower case!
// Must be longer than 5 characters
emailWrapperValidator.Validate("Email").ToString();
// This value is invalid as email address
```
_Above, `emailSpecification` contains multiple rules and - similarly - can have several [messages](#message) in its [error output](#error-output). When wrapped within `AsModel` followed by `WithMessage` command, any validation failure results with just a single error message._
_The advantage of this combination is even more visible when you define [specification](#specification) inline and skip all of the error messages attached to the rules - they won't ever be in the output anyway._
``` csharp
Specification emailSpecification = s => s
.AsModel(s1 => s1
.Rule(text => text.Contains('@'))
.Rule(text => !text.Any(c => !char.IsLetter(c) || char.IsLower(c)))
.Rule(text => text.Length > 5)
.Rule(text => text.Length < 20)
).WithMessage("This value is invalid as email address");
var emailValidator = Validator.Factory.Create(emailSpecification);
emailValidator.Validate("Email").ToString();
// This value is invalid as email address
```
---
#### AsCollection
- `AsCollection` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `AsCollection` command has two generic type parameters: `AsCollection`, where:
- `TItem` - is a type of the single item in the collection.
- `T` - is derived from `IEnumerable`.
- `AsCollection` has dedicated versions for some dotnet native collections, so you don't need to specify a pair of `IEnumerable` and `TItem` while dealing with:
- `T[]`
- `IEnumerable`
- `ICollection`
- `IReadOnlyCollection`
- `IList`
- `IReadOnlyList`
- `List`
- `AsCollection` accepts one parameter; item [specification](#specification) `Specification`.
- `AsCollection` executes the passed [specification](#specification) upon each item in the collection.
- Internally, getting the items out of the collection is done using `foreach` loop.
- Validation doesn't materialize the collection. Elements are picked up using enumerator (as in standard `foreach` loop).
- So it might get very tricky when you implement IEnumerable yourself; there is no protection against an infinite stream of objects coming from the enumerator, etc.
- Items are validated one after another, sequentially.
- Support for async collection validation is coming in the future releases.
- [Error output](#error-output) from the n-th item in the collection is saved under the path `#n`.
- The counting starts from zero (the first item in the collection is `0` and its [error output](#error-output) will be saved under `#0`).
- Validation uses the standard `foreach` loop over the collection, so "n-th item" really means "n-th item received from enumerator".
- For some types, the results won't be deterministic, simple because the collection itself doesn't guarantee to keep the order. It might happen that the error output saved under path `#1` next time will be saved under `#13`. This could be a problem for custom collections or some particular use cases, like instance of `HashSet` that gets modified between the two validations. But it will never happen for e.g. array or `List`.
``` csharp
Specification evenNumberSpecification = s => s
.Rule(number => (number % 2) == 0).WithMessage("Number must be even");
Specification specification = s => s
.AsCollection(evenNumberSpecification);
var validator = Validator.Factory.Create(specification);
var numbers = new[] { 1, 2, 3, 4, 5 };
validator.Validate(numbers).ToString();
// #0: Number must be even
// #2: Number must be even
// #4: Number must be even
```
_`AsCollection` is able to automatically resolve the type parameters for array. In this case, `AsCollection` is `AsCollection` under the hood._
- `AsCollection` makes sense only if the type validated in the scope is a collection
- Well... technically, that's not entirely true, because the only requirement is that it implements `IEnumerable` interface.
- Code completion tools (IntelliSense, Omnisharp, etc.) will show `AsCollection` as always available, but once inserted you'll need to define `T` and `TItem`, so effectively - `AsCollection` works only for collections.
_Let's consider a custom class holding two collections:_
``` csharp
class NumberCollection : IEnumerable, IEnumerable
{
public IEnumerable Ints { get; set; }
public IEnumerable Doubles { get; set; }
IEnumerator IEnumerable.GetEnumerator() => Doubles.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Ints.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this).GetEnumerator();
}
```
_You can use `AsCollection` to validate an object as a collection of any type; as long as you are able to specify both generic type parameters:_
``` csharp
Specification evenNumberSpecification = s => s
.Rule(number => (number % 2) == 0).WithMessage("Number must be even");
Specification smallDecimalSpecification = s => s
.Rule(number => Math.Floor(number) < 0.5).WithMessage("Decimal part must be below 0.5");
Specification specification = s => s
.AsCollection(evenNumberSpecification)
.AsCollection(smallDecimalSpecification);
var validator = Validator.Factory.Create(specification);
var numberCollection = new NumberCollection()
{
Ints = new [] { 1, 2, 3, 4, 5 },
Doubles = new [] { 1.1, 2.8, 3.3, 4.6, 5.9 }
}
validator.Validate(numberCollection).ToString();
// #0: Number must be even
// #1: Decimal part must be below 0.5
// #2: Number must be even
// #3: Decimal part must be below 0.5
// #4: Number must be even
// #4: Decimal part must be below 0.5
```
_Above, `AsCollection` command triggers validation of `NumberCollection` as a collection of `int` and `double` items, each with their own [specification](#specification)._
- `AsCollection` doesn't treat the null item as anything special. The behavior is described by the passed [specification](#specification).
- `AsCollection` is like [Member](#member) command, but the member selector is pointing at the collection items and the path is dynamic.
``` csharp
Specification authorSpecification = s => s
.Member(m => m.Email, m => m
.Rule(email => email.Contains('@')).WithMessage("Must contain @ character!")
);
Specification bookSpecification = s => s
.Member(m => m.Authors, m => m.AsCollection(authorSpecification));
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
Authors = new[]
{
null,
new AuthorModel() { Email = "foo@bar" },
new AuthorModel() { Email = null },
null,
new AuthorModel() { Email = "InvalidEmail" },
null,
}
};
bookValidator.Validate(book).ToString();
// Authors.#0: Required
// Authors.#2.Email: Required
// Authors.#3: Required
// Authors.#4.Email: Must contain @ character!
// Authors.#5: Required
```
_In the code above you can see that null items in the collection result with the default [error message](#message). This is because `authorSpecification` doesn't allow nulls._
_Let's change this and see what happens:_
``` csharp
Specification authorSpecification = s => s
.Optional()
.Member(m => m.Email, m => m
.Rule(email => email.Contains('@')).WithMessage("Must contain @ character!")
);
Specification bookSpecification = s => s
.Member(m => m.Authors, m => m.AsCollection(authorSpecification));
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
Authors = new[]
{
null,
new AuthorModel() { Email = "foo@bar" },
new AuthorModel() { Email = null },
null,
new AuthorModel() { Email = "InvalidEmail" },
null,
}
};
validator.Validate(book).ToString();
// Authors.#2.Email: Required
// Authors.#4.Email: Must contain @ character!
```
_Above, `authorSpecification` starts with [Optional](#optional) command, and therefore null items in the collection are allowed._
- `AsCollection` validates the collection items, but the collection itself (as an object) can be normally validated in its own scope normally, as any other value.
- One of the widespread use cases is to verify the collection size:
``` csharp
Specification authorSpecification = s => s
.Optional()
.Member(m => m.Email, m => m
.Rule(email => email.Contains('@')).WithMessage("Must contain @ character!")
);
Specification bookSpecification = s => s
.Member(m => m.Authors, m => m
.AsCollection(authorSpecification)
.Rule(authors => authors.Count() <= 5).WithMessage("Book can have max 5 authors.")
);
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
Authors = new[]
{
null,
new AuthorModel() { Email = "foo@bar" },
new AuthorModel() { Email = null },
null,
new AuthorModel() { Email = "InvalidEmail" },
null,
}
};
bookValidator.Validate(book).ToString();
// Authors.#2.Email: Required
// Authors.#4: Must contain @ character!
// Authors: Book can have max 5 authors.
```
---
#### AsNullable
- `AsNullable` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `AsNullable` "unwraps" the nullable value and provides the way to validate it with a [specification](#specification).
- `AsNullable` accepts a single parameter; `Specification`, where `T` is a value type wrapped in `Nullable` (`T?`).
- Null value never reaches `AsNullable`, exactly as [handling nulls policy](#null-policy) states.
- The passed [specification](#specification) describes `T` that is a value type, so [Optional](#optional) command is not even available.
- Null must be handled one level higher (in the [specification](#specification) that contains `AsNullable`).
``` csharp
Specification numberSpecification = s => s
.Rule(number => number < 10).WithMessage("Number must be less than 10");
Specification nullableSpecification = s => s
.AsNullable(numberSpecification);
var validator = Validator.Factory.Create(nullableSpecification);
validator.Validate(5).AnyErrors; // false
validator.Validate(15).ToString();
// Number must be less than 10
validator.Validate(null).ToString();
// Required
```
_In the above code, `Validate` method accepts `int?`. You can observe that the value is unwrapped by `AsNullable` and validated with `numberSpecification` (that describes just `int`)._
_If the nullable value is null, it is stopped at the level of `nullableSpecification`, which doesn't allow nulls. Of course, you can change this behavior:_
``` csharp
Specification numberSpecification = s => s
.Rule(number => number < 10).WithMessage("Number must be less than 10");
Specification nullableSpecification = s => s
.Optional()
.AsNullable(numberSpecification);
var validator = Validator.Factory.Create(nullableSpecification);
validator.Validate(5).AnyErrors; // false
validator.Validate(null).AnyErrors; // false
validator.Validate(15).ToString();
// Number must be less than 10
```
_Now, `nullableSpecification` starts with [Optional](#optional) command, and therefore - null doesn't result with an error. On the other hand - if nullable has a value, it is passed and validated with `numberSpecification`._
- [Every built-in rule](#rules) for a value type has an extra variant for the nullable of this type.
- So you don't need to provide `AsNullable` in the most popular and simple cases.
``` csharp
Specification numberSpecification = s => s.GreaterThan(0).LessThan(10);
Specification nullableSpecification = s => s.GreaterThan(0).LessThan(10);
var numberValidator = Validator.Factory.Create(numberSpecification);
var nullableValidator = Validator.Factory.Create(nullableSpecification);
numberValidator.Validate(5).AnyErrors; // false
nullableValidator.Validate(5).AnyErrors; // false
numberValidator.Validate(15).ToString();
// Must be less than 10
nullableValidator.Validate(15).ToString();
// Must be less than 10
```
_In the above code, `GreaterThan` and `LessThan` can be applied to both `Specification` and `Specification`. Technically, they are two separate rules with same names. The consistency of their inner logic is verified by the unit tests._
- `AsNullable` can be handy when you have two versions of the same type (nullable and non-nullable) that can be validated with the same specification.
``` csharp
Specification yearSpecification = s => s
.Rule(year => year >= -3000).WithMessage("Minimum year is 3000 B.C.")
.Rule(year => year <= 3000).WithMessage("Maximum year is 3000 A.D.");
Specification bookSpecification = s => s
.Member(m => m.YearOfFirstAnnouncement, yearSpecification)
.Member(m => m.YearOfPublication, m => m
.Optional()
.AsNullable(yearSpecification)
);
var bookValidator = Validator.Factory.Create(bookSpecification);
var book = new BookModel()
{
YearOfFirstAnnouncement = -4000,
YearOfPublication = 4000
};
bookValidator.Validate(book).ToString()
// YearOfFirstAnnouncement: Minimum year is 3000 B.C.
// YearOfPublication: Maximum year is 3000 A.D.
```
_Above the example how two members - nullable `YearOfPublication` and non-nullable `YearOfFirstAnnouncement` - can be validated with the same specification `yearSpecification`._
---
#### AsConverted
- `AsConverted` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `AsConverted` validates the value as a different value.
- It could be a value of the same, or of a different type.
- The type of the specification is determined by the converter's output.
- `AsConverted` accepts:
- A conversion function (of type `System.Converter`) that takes the current scope value and outputs the new value.
- A specification for type `TOutput` used to validate the converted value.
- `AsConverted` executes the delivered specification within the same scope (so all errors are saved on the same level)
- So technically, it could be considered as [AsModel](#asmodel), but with a conversion method that's executed upon the scope value before the futher validation.
_Below; a snippet presenting how to sanitize the value (for whatever reason that could be an actual case) before validating it with the predefined specification._
``` csharp
Specification nameSpecification = s => s
.Rule(name => char.IsUpper(name.First())).WithMessage("Must start with a capital letter!")
.Rule(name => !name.Any(char.IsWhiteSpace)).WithMessage("Must not contain whitespace!");
Converter sanitizeName = firstName => firstName.Trim();
Specification nameValueSpecification = s => s
.AsConverted(sanitizeName, nameSpecification);
var nameValidator = Validator.Factory.Create(nameValueSpecification);
nameValidator.Validate("Bartosz").AnyErrors; // false
nameValidator.Validate(" Bartosz ").AnyErrors; // false
nameValidator.Validate(" bartosz ").ToString();
// Must start with a capital letter!
nameValidator.Validate(" Bart osz ").ToString();
// Must not contain whitespace!
```
_Of course, type can be different. It's the converter's output that determines the specification. Also, both arguments could be delivered inline:_
``` csharp
Specification authorSpecification = s => s
.Member(m => m.Name, m => m.AsConverted(
name => name.Length,
nameLength => nameLength.Rule(l => l % 2 == 0).WithMessage("Characters amount must be even"))
);
var nameValidator = Validator.Factory.Create(authorSpecification);
var author = new AuthorModel()
{
Name = "Bartosz"
};
nameValidator.Validate(author).ToString();
// Name: Characters amount must be even
```
- The [template](#template) will contain all errors from the delivered specification, which could lead to misleading case in which the "Required" error is listed as a possible outcome for a value type.
- This happens when a value type is converted to a reference type.
- If you want to "fix" te template, add [Optional](#optional) at the beginning in the converted value's specification.
``` csharp
Specification specification1 = s => s
.AsConverted(
v => v.ToString(CultureInfo.InvariantCulture),
c => c.MaxLength(10).WithMessage("Number must be max 5 digits length")
);
Validator.Factory.Create(specification1).Template.ToString();
// Required
// Number must be max 5 digits length
Specification specification2 = s => s
.AsConverted(
v => v.ToString(CultureInfo.InvariantCulture),
c => c.Optional().MaxLength(10).WithMessage("Number must be max 5 digits length")
);
Validator.Factory.Create(specification2).Template.ToString();
// Number must be max 5 digits length
```
---
#### AsType
- `AsType` is a [scope command](#scope-commands).
- Can be placed after:
- any command except [Forbidden](#forbidden).
- Can be followed by:
- any of the [scope commands](#scope-commands).
- any of the [parameter commands](#parameter-commands).
- `AsType` validates the value as if it was of a different type.
- If the value can be cast into the target type (using `is`/`as` operators), the validation proceeds with the given specifiction.
- If the value can't be cast (`is` check returns false), nothing happens. No error output is recorded and the validation continues with the subsequent commands.
- `AsType` accepts:
- A specification for type `TTarget` used to validate the cast value.
- `AsType` executes the delivered specification within the same scope (so all errors are saved on the same level)
- So technically `.AsType(targetTypeSpecification)`, it could be considered as a shortcut for [AsConverted](#asmodel) command combined with [WithCondition](#withcondition): `.AsConverted(v => v as TargetType, targetTypeSpecification).WithCondition(v => v is TargetType)`.
_Let's use the classic inheritance example, like: `Animal -> Mammal -> Elephant`_:
``` csharp
class Animal
{
public int AnimalId { get; set; }
}
class Mammal : Animal
{
public int MammalId { get; set; }
}
class Elephant : Mammal
{
public int ElephantId { get; set; }
}
```
_Contructing validator for the class at the bottom of the inheritance graph (`Elephant` in this case), you can use `AsType` and apply specifiction of any of its ancestors_:
``` csharp
Specification idSpecification = s => s.NonZero();
Specification animalSpecification = s => s
.Member(m => m.AnimalId, idSpecification);
Specification elephantSpecification = s => s
.Member(m => m.ElephantId, idSpecification)
.AsType(animalSpecification);
var elephantValidator = Validator.Factory.Create(elephantSpecification);
elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors; // false
elephantValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString();
// ElephantId: Must not be zero
elephantValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString();
// AnimalId: Must not be zero
```
_It works also in opposite direction. You can create a validator for the ancestor type and use descendants' specifications:_
``` csharp
Specification idSpecification = s => s.NonZero();
Specification elephantSpecification = s => s
.Member(m => m.ElephantId, idSpecification);
Specification animalSpecification = s => s
.Member(m => m.AnimalId, idSpecification)
.AsType(elephantSpecification);
var animalValidator = Validator.Factory.Create(animalSpecification);
animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 10 }).AnyErrors; // false
animalValidator.Validate(new Elephant() { ElephantId = 0, AnimalId = 10 }).ToString();
// ElephantId: Must not be zero
animalValidator.Validate(new Elephant() { ElephantId = 10, AnimalId = 0 }).ToString();
// AnimalId: Must not be zero
```
_`AsType` executes only if the type can be cast (`value is TTargetType` is true), so you can use specifiction of unrelated types if for whatever reason you need something that works like a validation hub. Notice that you can construct the specification inline as well (but it's handy to do it with a constructor notation so the compiler can pick up the types from it):_
``` csharp
Specification