|
| 1 | +# SGuard.DataAnnotations |
| 2 | + |
| 3 | + |
| 4 | +[](https://www.nuget.org/packages/SGuard.DataAnnotations/) |
| 5 | +[](https://www.nuget.org/packages/SGuard.DataAnnotations/) |
| 6 | +[](https://codecov.io/gh/selcukgural/SGuard.DataAnnotations) |
| 7 | + |
| 8 | + |
| 9 | +**SGuard.DataAnnotations** provides localized and extensible DataAnnotations support for .NET, including: |
| 10 | +- Localizable validation attributes (with robust fallback and custom error handling) |
| 11 | +- Collection, conditional, and property comparison validators are not found in standard DataAnnotations |
| 12 | +- Guard pattern (`Is.*` for boolean return, `ThrowIf.*` for exception-throwing) for model validation |
| 13 | +- Seamless integration with DataAnnotations and SGuard's fail-fast/callback philosophy |
| 14 | +- Well-tested and extensible for real-world application scenarios |
| 15 | + |
| 16 | +> **Note:** For fluent validation/guard support, see the upcoming [`SGuard.FluentValidation`](https://github.com/selcukgural/SGuard.FluentValidation) package. |
| 17 | +> |
| 18 | +> **Important:** If you want to implement custom callback, fail-fast, or chainable guard logic, you should also review the [SGuard core project](https://github.com/selcukgural/SGuard), which provides advanced guard and callback APIs used by this library. |
| 19 | +
|
| 20 | +--- |
| 21 | + |
| 22 | +## Table of Contents |
| 23 | + |
| 24 | +- [Installation](#installation) |
| 25 | +- [Features](#features) |
| 26 | +- [Supported Languages](#supported-languages) |
| 27 | +- [Supported Attributes](#supported-attributes) |
| 28 | + - [String & Common Validators](#string--common-validators) |
| 29 | + - [Collection Validators](#collection-validators) |
| 30 | + - [Conditional Validators](#conditional-validators) |
| 31 | + - [Comparison Validators](#comparison-validators) |
| 32 | +- [Guard Pattern API](#guard-pattern-api) |
| 33 | + - [Is.* Methods](#is-methods) |
| 34 | + - [ThrowIf.* Methods](#throwif-methods) |
| 35 | +- [Localization & Fallback](#localization--fallback) |
| 36 | +- [Extending SGuard.DataAnnotations](#extending-sguarddataannotations) |
| 37 | +- [Minimal API Example (Real World)](#minimal-api-example-real-world) |
| 38 | +- [Advanced Topics](#advanced-topics) |
| 39 | + - [Error Handling](#error-handling) |
| 40 | +- [FAQ / Tips](#faq--tips) |
| 41 | +- [Contributing](#contributing) |
| 42 | +- [License](#license) |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## Installation |
| 47 | + |
| 48 | +Install via NuGet: |
| 49 | + |
| 50 | +```bash |
| 51 | +dotnet add package SGuard.DataAnnotations |
| 52 | +``` |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## Features |
| 57 | + |
| 58 | +- **Localized error messages** via resource files (`.resx`), with fallback to default or custom messages. |
| 59 | +- **Advanced collection and conditional validation** (min/max count, required-if, required collection, collection element validation, etc.). |
| 60 | +- **Comparison attributes** for property-to-property or value-to-value checks (greater than, less than, between, compare, etc.). |
| 61 | +- **Full DataAnnotations compatibility**—use SGuard attributes anywhere a standard attribute is accepted. |
| 62 | +- **Guard pattern API** (`Is`/`ThrowIf`) for easy imperative validation and exception/callback handling. |
| 63 | + |
| 64 | +--- |
| 65 | + |
| 66 | +## Supported Languages |
| 67 | + |
| 68 | +SGuard.DataAnnotations ships with built-in resource support for the following languages: |
| 69 | + |
| 70 | +| Language | Culture Code | Localized Resource File | |
| 71 | +|-------------------|--------------|---------------------------------| |
| 72 | +| English (default) | `en` | `SGuardDataAnnotations.resx` | |
| 73 | +| Turkish | `tr` | `SGuardDataAnnotations.tr.resx` | |
| 74 | +| German | `de` | `SGuardDataAnnotations.de.resx` | |
| 75 | +| French | `fr` | `SGuardDataAnnotations.fr.resx` | |
| 76 | +| Russian | `ru` | `SGuardDataAnnotations.ru.resx` | |
| 77 | +| Japanese | `ja` | `SGuardDataAnnotations.ja.resx` | |
| 78 | +| Hindi | `hi` | `SGuardDataAnnotations.hi.resx` | |
| 79 | + |
| 80 | +> **Note:** |
| 81 | +> - If the current UI culture is not found, SGuard will fallback to English or to the fallback message if provided. |
| 82 | +> - You can add your own resource files to support additional languages. |
| 83 | +> - [How to add your own language?](#how-to-add-a-custom-language) |
| 84 | +--- |
| 85 | + |
| 86 | +## Supported Attributes |
| 87 | + |
| 88 | +### String & Common Validators |
| 89 | + |
| 90 | +| Attribute | Purpose | Supported Types | Example Usage | |
| 91 | +|------------------------------------|--------------------------------------|----------------------------------|-----------------------------------------------------------------------------------------------------------------------| |
| 92 | +| `SGuardRequiredAttribute` | Required field (localized) | Any | `[SGuardRequired(typeof(Resources.SGuardDataAnnotations), "Username_Required")]` | |
| 93 | +| `SGuardMinLengthAttribute` | Minimum string length | `string`, `array`, `ICollection` | `[SGuardMinLength(5, typeof(Resources.SGuardDataAnnotations), "Username_MinLength")]` | |
| 94 | +| `SGuardMaxLengthAttribute` | Maximum string length | `string`, `array`, `ICollection` | `[SGuardMaxLength(20, typeof(Resources.SGuardDataAnnotations), "Username_MaxLength")]` | |
| 95 | +| `SGuardStringLengthAttribute` | Min/max string length | `string` | `[SGuardStringLength(12, typeof(Resources.SGuardDataAnnotations), "Username_MaxLength")]` | |
| 96 | +| `SGuardRegularExpressionAttribute` | Regex pattern | `string` | `[SGuardRegularExpression("^[a-zA-Z0-9_]+$", typeof(Resources.SGuardDataAnnotations), "Username_InvalidCharacters")]` | |
| 97 | +| `SGuardEmailAddressAttribute` | Email format | `string` | `[SGuardEmailAddress(typeof(Resources.SGuardDataAnnotations), "Email_InvalidFormat")]` | |
| 98 | +| `SGuardPhoneAttribute` | Phone format | `string` | `[SGuardPhone(typeof(Resources.SGuardDataAnnotations), "Profile_Phone_Invalid")]` | |
| 99 | +| `SGuardUrlAttribute` | URL format | `string` | `[SGuardUrl(typeof(Resources.SGuardDataAnnotations), "Common_Url_Invalid")]` | |
| 100 | +| `SGuardCreditCardAttribute` | Credit card format | `string` | `[SGuardCreditCard(typeof(Resources.SGuardDataAnnotations), "Common_CreditCard_Invalid")]` | |
| 101 | +| `SGuardRangeAttribute` | Value must be within a numeric range | `int`, `double` | `[SGuardRange(1, 10, typeof(Resources.SGuardDataAnnotations), "Common_Range")]` | |
| 102 | + |
| 103 | +### Collection Validators |
| 104 | + |
| 105 | +| Attribute | Purpose | Supported Types | Example Usage | |
| 106 | +|---------------------------------------|-----------------------------------------------------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| |
| 107 | +| `SGuardRequiredCollectionAttribute` | Collection must not be null/empty | `IEnumerable`, arrays, etc. | `[SGuardRequiredCollection(typeof(Resources.SGuardDataAnnotations), "Common_Collection_Required")]` | |
| 108 | +| `SGuardMinCountAttribute` | Minimum item count in collection | `IEnumerable`, arrays, etc. | `[SGuardMinCount(2, typeof(Resources.SGuardDataAnnotations), "Common_Collection_MinCount")]` | |
| 109 | +| `SGuardMaxCountAttribute` | Maximum item count in collection | `IEnumerable`, arrays, etc. | `[SGuardMaxCount(5, typeof(Resources.SGuardDataAnnotations), "Common_Collection_MaxCount")]` | |
| 110 | +| `SGuardCollectionItemsMatchAttribute` | Each item must match one/more validators (e.g. regex, required) | `IEnumerable`, arrays, etc. | `[SGuardCollectionItemsMatch(typeof(EmailAddressAttribute), typeof(Resources.SGuardDataAnnotations), "Email_InvalidFormat", AggregateAllErrors = true)]` | |
| 111 | + |
| 112 | +**Details:** |
| 113 | +- `SGuardCollectionItemsMatchAttribute` can take multiple validators and will apply them to each item. |
| 114 | +- `AggregateAllErrors` (default: `false`): If `true`, collects all errors; if `false`, returns on first failure. |
| 115 | +- Supported on any `IEnumerable` (e.g., `List<T>`, arrays, custom collections). |
| 116 | + |
| 117 | +### Conditional Validators |
| 118 | + |
| 119 | +| Attribute | Purpose | Supported Types | Example Usage | |
| 120 | +|-----------------------------|-------------------------------------------------|-----------------|-----------------------------------------------------------------------------------------------------| |
| 121 | +| `SGuardRequiredIfAttribute` | Field required if another property equals value | Any | `[SGuardRequiredIf("Country", "USA", typeof(Resources.SGuardDataAnnotations), "Address_Required")]` | |
| 122 | + |
| 123 | +### Comparison Validators |
| 124 | + |
| 125 | +| Attribute | Purpose | Supported Types | Example Usage | |
| 126 | +|------------------------------|----------------------------------------------|-------------------|----------------------------------------------------------------------------------------------------------| |
| 127 | +| `SGuardCompareAttribute` | Values must be equal (like CompareAttribute) | Any | `[SGuardCompare("Password", typeof(Resources.SGuardDataAnnotations), "Password_Mismatch")]` | |
| 128 | +| `SGuardBetweenAttribute` | Value must be between two properties | IComparable types | `[SGuardBetween("Min", "Max", true, typeof(Resources.SGuardDataAnnotations), "Common_Between")]` | |
| 129 | +| `SGuardGreaterThanAttribute` | Value must be greater than another property | IComparable types | `[SGuardGreaterThan("MinAge", typeof(Resources.SGuardDataAnnotations), "Profile_BirthDate_MinimumAge")]` | |
| 130 | +| `SGuardLessThanAttribute` | Value must be less than another property | IComparable types | `[SGuardLessThan("MaxAge", typeof(Resources.SGuardDataAnnotations), "Profile_BirthDate_MaximumAge")]` | |
| 131 | + |
| 132 | +**Supported types:** `int`, `double`, `decimal`, `DateTime`, `string`, etc. (anything implementing `IComparable`) |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## Guard Pattern API |
| 137 | + |
| 138 | +**SGuard.DataAnnotations** provides two imperative APIs for runtime validation, following the SGuard pattern. |
| 139 | + |
| 140 | +### Is.* Methods |
| 141 | + |
| 142 | +- **Purpose:** Return `bool` for validation checks (never throw). |
| 143 | +- **Callback:** Optional `SGuardCallback` invoked with `GuardOutcome.Success`/`Failure`. |
| 144 | + For advanced callback usage, see the [SGuard project documentation](https://github.com/selcukgural/SGuard). |
| 145 | +- **Example:** |
| 146 | + ```csharp |
| 147 | + if (Is.DataAnnotationsValid(model)) |
| 148 | + { |
| 149 | + // model is valid |
| 150 | + } |
| 151 | + ``` |
| 152 | + |
| 153 | +- **With callback:** |
| 154 | + ```csharp |
| 155 | + bool valid = Is.DataAnnotationsValid(model, callback: outcome => |
| 156 | + { |
| 157 | + if (outcome == GuardOutcome.Failure) |
| 158 | + Console.WriteLine("Validation failed!"); |
| 159 | + }); |
| 160 | + ``` |
| 161 | + |
| 162 | +- **Get all validation errors:** |
| 163 | + ```csharp |
| 164 | + bool valid = Is.DataAnnotationsValid(model, out var results); |
| 165 | + foreach (var err in results) |
| 166 | + Console.WriteLine($"{string.Join(", ", err.MemberNames)}: {err.ErrorMessage}"); |
| 167 | + ``` |
| 168 | + |
| 169 | +### ThrowIf.* Methods |
| 170 | + |
| 171 | +- **Purpose:** Throw exception if validation fails. |
| 172 | +- **Callback:** Invoked before throw (`GuardOutcome.Failure`) or on pass (`GuardOutcome.Success`). |
| 173 | +- **Example:** |
| 174 | + ```csharp |
| 175 | + ThrowIf.DataAnnotationsInValid(model); |
| 176 | + // Throws DataAnnotationsException if model is invalid. |
| 177 | + ``` |
| 178 | + |
| 179 | +- **Custom exception:** |
| 180 | + ```csharp |
| 181 | + ThrowIf.DataAnnotationsInValid<ArgumentException>(model, new ArgumentException("Custom error!")); |
| 182 | + ``` |
| 183 | + |
| 184 | +- **Custom exception with constructor args:** |
| 185 | + ```csharp |
| 186 | + ThrowIf.DataAnnotationsInValid<ArgumentException>(model, new object[] { "Custom error!" }); |
| 187 | + ``` |
| 188 | + |
| 189 | +#### Exception Details |
| 190 | + |
| 191 | +- Throws `DataAnnotationsException` by default, which contains all validation errors. |
| 192 | +- Extract errors (see also [`SGuard.DataAnnotations.Extensions`](./SGuard.DataAnnotations/src/Extensions/DataAnnotationsExceptionExtensions.cs)): |
| 193 | + ```csharp |
| 194 | + catch (DataAnnotationsException ex) |
| 195 | + { |
| 196 | + if (ex.TryGetValidationErrors(out var errors)) |
| 197 | + { |
| 198 | + foreach (var err in errors) |
| 199 | + Console.WriteLine($"{string.Join(", ", err.Members)}: {err.Message}"); |
| 200 | + } |
| 201 | + } |
| 202 | + ``` |
| 203 | + |
| 204 | +--- |
| 205 | + |
| 206 | +## Localization & Fallback |
| 207 | + |
| 208 | +- All SGuard attributes support: |
| 209 | + - `ErrorMessageResourceType` and `ErrorMessageResourceName` (standard .NET resource workflow) |
| 210 | + - `FallbackResourceName`: Used if the main resource key is missing. |
| 211 | + - `FallbackMessage`: Used if both resource keys are missing. |
| 212 | +- **Culture:** Error messages are localized to the current `UICulture`. |
| 213 | +- **Example:** |
| 214 | + ```csharp |
| 215 | + [SGuardMinLength(3, typeof(Resources.SGuardDataAnnotations), "Username_MinLength", |
| 216 | + FallbackResourceName = "Common_MinLength", FallbackMessage = "Min length required.")] |
| 217 | + public string Username { get; set; } |
| 218 | + ``` |
| 219 | + |
| 220 | +### How to add a custom language? |
| 221 | + |
| 222 | +1. Copy `SGuardDataAnnotations.resx` and rename to e.g. `SGuardDataAnnotations.es.resx` for Spanish. |
| 223 | +2. Translate all keys/values. |
| 224 | +3. Rebuild and set your thread/UI culture accordingly. |
| 225 | + |
| 226 | +--- |
| 227 | + |
| 228 | +## Extending SGuard.DataAnnotations |
| 229 | + |
| 230 | +Want to add your own fully localized validation attribute? |
| 231 | +Inherit from `SGuardValidationAttributeBase` and implement `IsValid`: |
| 232 | + |
| 233 | +```csharp |
| 234 | +public class MyCustomLocalizedAttribute : SGuardValidationAttributeBase |
| 235 | +{ |
| 236 | + public MyCustomLocalizedAttribute(Type resourceType, string resourceName) |
| 237 | + : base(resourceType, resourceName) {} |
| 238 | + |
| 239 | + protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) |
| 240 | + { |
| 241 | + // Your logic here |
| 242 | + if (value == null) |
| 243 | + return new ValidationResult(FormatErrorMessage(validationContext.DisplayName)); |
| 244 | + return ValidationResult.Success; |
| 245 | + } |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## Minimal API Example (Real World) |
| 252 | + |
| 253 | +Here's how you use SGuard.DataAnnotations in an ASP.NET Core Minimal API: |
| 254 | + |
| 255 | +```csharp |
| 256 | +using Microsoft.AspNetCore.Builder; |
| 257 | +using Microsoft.AspNetCore.Http; |
| 258 | +using SGuard.DataAnnotations; |
| 259 | + |
| 260 | +var builder = WebApplication.CreateBuilder(args); |
| 261 | +var app = builder.Build(); |
| 262 | + |
| 263 | +app.MapPost("/register", (UserRegistration model) => |
| 264 | +{ |
| 265 | + if (!Is.DataAnnotationsValid(model, out var errors)) |
| 266 | + return Results.BadRequest(errors.Select(e => new { e.MemberNames, e.ErrorMessage })); |
| 267 | + |
| 268 | + // If valid, continue |
| 269 | + return Results.Ok("Registration successful!"); |
| 270 | +}); |
| 271 | + |
| 272 | +app.Run(); |
| 273 | + |
| 274 | +public class UserRegistration |
| 275 | +{ |
| 276 | + [SGuardRequired(typeof(Resources.SGuardDataAnnotations), "Username_Required")] |
| 277 | + [SGuardMinLength(3, typeof(Resources.SGuardDataAnnotations), "Username_MinLength")] |
| 278 | + [SGuardMaxLength(20, typeof(Resources.SGuardDataAnnotations), "Username_MaxLength")] |
| 279 | + public string Username { get; set; } |
| 280 | + |
| 281 | + [SGuardRequired(typeof(Resources.SGuardDataAnnotations), "Email_Required")] |
| 282 | + [SGuardEmailAddress(typeof(Resources.SGuardDataAnnotations), "Email_InvalidFormat")] |
| 283 | + public string Email { get; set; } |
| 284 | + |
| 285 | + [SGuardRequired(typeof(Resources.SGuardDataAnnotations), "Password_Required")] |
| 286 | + [SGuardStringLength(100, typeof(Resources.SGuardDataAnnotations), "Password_MaxLength")] |
| 287 | + public string Password { get; set; } |
| 288 | + |
| 289 | + [SGuardCompare("Password", typeof(Resources.SGuardDataAnnotations), "Password_Mismatch")] |
| 290 | + public string ConfirmPassword { get; set; } |
| 291 | + |
| 292 | + [SGuardRequiredCollection(typeof(Resources.SGuardDataAnnotations), "Profile_Phone_Required")] |
| 293 | + [SGuardCollectionItemsMatch(typeof(SGuardPhoneAttribute), typeof(Resources.SGuardDataAnnotations), "Profile_Phone_Invalid", AggregateAllErrors = true)] |
| 294 | + public List<string> PhoneNumbers { get; set; } |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | +--- |
| 299 | + |
| 300 | +## Advanced Topics |
| 301 | + |
| 302 | +### Error Handling |
| 303 | + |
| 304 | +- **Guard methods**: Return `bool` or throw, never both. |
| 305 | +- **Attributes**: Always return `ValidationResult`, never throw. |
| 306 | +- **All exceptions**: Contain full error detail, member names, and support for extraction/extension. |
| 307 | +- **For advanced fail-fast, callback, or custom guard usage:** |
| 308 | + See [SGuard project documentation](https://github.com/selcukgural/SGuard). |
| 309 | + |
| 310 | +--- |
| 311 | + |
| 312 | +## FAQ / Tips |
| 313 | + |
| 314 | +**Q:** _Can I use SGuard attributes in ASP.NET Core, Blazor, WinForms, etc.?_ |
| 315 | +**A:** Yes, SGuard attributes implement the standard DataAnnotations contract. |
| 316 | + |
| 317 | +**Q:** _What happens if a resource key is missing?_ |
| 318 | +**A:** The attribute will use `FallbackResourceName` if provided, otherwise `FallbackMessage`, otherwise `[ResourceKey]`. |
| 319 | + |
| 320 | +**Q:** _How do I validate a collection’s items?_ |
| 321 | +**A:** Use `[SGuardCollectionItemsMatch(...)]`. See [Collection Validators](#collection-validators). |
| 322 | + |
| 323 | +**Q:** _Can I chain SGuard and standard DataAnnotations attributes?_ |
| 324 | +**A:** Yes, you can stack any combination on your model. |
| 325 | + |
| 326 | +**Q:** _Will SGuard.DataAnnotations work with FluentValidation?_ |
| 327 | +**A:** Yes, as long as you use DataAnnotations integration. For a full fluent API, see [`SGuard.FluentValidation`](https://github.com/selcukgural/SGuard.FluentValidation). |
| 328 | + |
| 329 | +**Q:** _How do I quickly test everything?_ |
| 330 | +**A:** |
| 331 | +1. Run all tests (requires .NET SDK): |
| 332 | + ```bash |
| 333 | + dotnet test |
| 334 | + ``` |
| 335 | +2. For a quick validation in your app, call: |
| 336 | + ```csharp |
| 337 | + if (!Is.DataAnnotationsValid(model, out var results)) |
| 338 | + // handle errors, see 'results' |
| 339 | + ``` |
| 340 | +
|
| 341 | +--- |
| 342 | +
|
| 343 | +## Contributing |
| 344 | +
|
| 345 | +Pull requests, issues, and suggestions are very welcome! |
| 346 | +See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. |
| 347 | +
|
| 348 | +--- |
| 349 | +
|
| 350 | +## License |
| 351 | +
|
| 352 | +MIT License. See [LICENSE](LICENSE). |
| 353 | +
|
| 354 | +--- |
| 355 | +
|
| 356 | +## See Also |
| 357 | +
|
| 358 | +- [SGuard (core)](https://github.com/selcukgural/SGuard) |
| 359 | +- [SGuard.FluentValidation (upcoming)](https://github.com/selcukgural/SGuard.FluentValidation) |
| 360 | +
|
| 361 | +--- |
0 commit comments