Skip to content

Commit 8290176

Browse files
committed
Update package files and prepare for release:
- Replace `PackageLicenseUrl` with `PackageLicenseExpression` in the project file. - Add LICENSE and README.md files for documentation and licensing. - Relocate and rename `icon.svg` to `assets/icon.svg` and update `PackageIcon` metadata accordingly. - Ensure assets (e.g., license, readme, and icon) are packed with the NuGet package.
1 parent 5b0b525 commit 8290176

5 files changed

Lines changed: 396 additions & 1 deletion

File tree

SGuard.DataAnnotations/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Selçuk Güral
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

SGuard.DataAnnotations/README.md

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
# SGuard.DataAnnotations
2+
3+
![CI](https://github.com/selcukgural/SGuard.DataAnnotations/actions/workflows/ci.yml/badge.svg)
4+
[![NuGet Version](https://img.shields.io/nuget/v/SGuard.DataAnnotations.svg?style=flat-square)](https://www.nuget.org/packages/SGuard.DataAnnotations/)
5+
[![NuGet Downloads](https://img.shields.io/nuget/dt/SGuard.DataAnnotations.svg?style=flat-square)](https://www.nuget.org/packages/SGuard.DataAnnotations/)
6+
[![Coverage](https://codecov.io/gh/selcukgural/SGuard.DataAnnotations/branch/main/graph/badge.svg)](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

Comments
 (0)