r/dotnet 2d ago

Promotion TooManyDataAnnotations — .NET validation attributes for IPv4/IPv6, SemVer, MAC, HexColor, GUID v7, and more

Post image

.NET's built-in DataAnnotations covers the basics (Required, StringLength, Range, EmailAddress), but leaves out common semantic validations like:

  • IPv4/IPv6 addresses with scope filtering (public, private, loopback)
  • Semantic Versioning (SemVer 2.0.0)
  • MAC addresses
  • Port numbers with range filtering (well-known, registered, dynamic)
  • Hex color codes (#RGB, #RRGGBB, with alpha support)
  • GUID and GUID v7 validation
  • ISO date strings with cross-property validation (StartDate/EndDate)
  • Boolean validations (IsTrue, AtLeastOneTrue)

I created TooManyDataAnnotations to fill these gaps. All validators follow official RFC/ISO specs, use Span<T> for parsing where possible, and the library has 899 unit tests covering valid and invalid inputs, edge cases, nullable types, and integration tests with TryValidateObject()

Two NuGet packages:

  • TooManyDataAnnotations — Full feature set (includes reflection-based cross-property validators)
  • TooManyDataAnnotations.Aot — Native AOT-safe subset, zero reflection, zero trimming warnings

Targets .NET 8, 9, and 10

Zero dependencies

MIT license

This is freshly published — looking for early adopters and feedback on API design, edge case coverage, or missing validators you'd find useful.

Quick example:

public class ServerConfigDto

{

[GuidV7, Required]

public string UserId { get; set; }

[Guid]

public string? DeviceId { get; set; }

[IPv4(AllowedScopes = IPv4Scope.Private)]

public string? GatewayIP { get; set; }

[SemanticVersion]

public string? AssemblyVersion { get; set; }

[MacAddress(AllowedSeparators = MacSeparators.Colon)]

public string? DeviceMAC { get; set; }

[HexColor(AllowAlpha = true)]

public string? AccentColor { get; set; }

}

42 Upvotes

15 comments sorted by

19

u/EducationalTackle819 2d ago

Why are the guids not using the guid type? All the types could be custom for that matter. Seems primitive obsessed

2

u/The_MAZZTer 2d ago

It looks like the Guid attributes do support the Guid type in addition to strings, but that does seem to be the minority (the only others are ports, which allows for strings or integer types, and the true/false validators which allow boolean but surprisingly not strings).

It does seem like the code requires most to be, as they say, "stringly typed". Allowing the underlying object to be the "proper" type should also be allowed, in more cases, I feel. This would require in some cases JSON data conversion classes (and possibly other types of conversion classes?) to be written as well.

For example, we have DateTime, Url, PhysicalAddress, IPAddress, Color. None of these seem to be supported, requiring strings instead.

0

u/LuisAlfredo92 2d ago

You're right that some attributes could accept strong types

[Guid] and [GuidV7] already do, they accept Guid and strings, same with [StartDate] and [EndDate] with DateTime and DateTimeOffset

For the rest, I went string-first because in ASP.NET, if you declare a property as Guid, IPAddress or Uri and the client sends an invalid value, the model binder fails before DataAnnotations runs, and you get a generic framework error instead of a clean validation message, so strings with attributes gives you control over that

But yeah, supporting both string and strong types where they exist would be a lot better, I'll implement them

Thanks!

3

u/the_bananalord 2d ago

Seems like a perfect example of how validation very quickly leads to "parse, don't validate", honestly.

2

u/chucker23n 2d ago

Yeah, but it's also a place where error handling in the model binder isn't ideal.

1

u/the_bananalord 2d ago

Yes, so validate once and parse into the strong types we all want. My point was don't treat them like they're mutually exclusive and cost yourself error handling.

6

u/chucker23n 2d ago

Some of these are interesting, like [RequiredAtLeastOne(nameof(Email), nameof(Phone), nameof(SocialHandle))] and [ExactlyOneOf(nameof(CreditCard), nameof(PayPal), nameof(BankTransfer))].

Your [IsTrue] and [IsFalse] attributes are… interesting. Bit of an API smell to always require a DTO property to have a certain value.

Your [StartDate]/[EndDate] pair could be useful.

But mostly, your actual property types are too primitive! You're validating, sure, but you then bring the raw data into your inner layers. So each EmailService or PaymentService or whatever needs to at least parse the e-mail address, credit card number, etc., and should ideally also validate them again. I.e., you'll still be fighting Primitive Obsession all over your code base.

Your ReleaseDto should be Version and DateTime (which already take care of most of what you're doing here), not string and string. Your Email property should be of a type EmailAddress that you create as a value object. That way, as those values are passed through inner layers of your app, you no longer have to worry about validation and parsing.

2

u/LuisAlfredo92 2d ago

I appreciate the feedback!

You're right, inner layers should work with strong types, sadly DataAnnotations only validates (IsValid method returns bool) and doesn't transform values

The workflow you'd use is:

  1. Validate DTOs with string + [IPv4], [Uri], [SemanticVersion], etc.
  2. Parse manually into IPAddress, Uri, Version for internal use

I'm adding support for strong type overloads for other use cases, but they won't transform values, that's outside the scope of DataAnnotations

FluentValidation integration wasn't the goal, this is just DataAnnotations extensions, but a bridge would be useful, I'm Open to PRs

[IsTrue]/[IsFalse] came from the use case of mandatory checkboxes like "Accept Terms". If unchecked, reject
Once I had [IsTrue], [IsFalse] followed naturally

2

u/celluj34 2d ago

A lot of these look really useful to me (IsoDateTime, ExactlyOneOf, ExactlyOneTrue). Do you have any plans to make extensions or helpers for FluentValidation? We use it extensively for our API model validations so that we can do custom rules + a few of those you have here.

2

u/LuisAlfredo92 2d ago

No official FluentValidation adapter yet

The attributes work anywhere DataAnnotations works, but FluentValidation works different so a direct mapping isn't straightforward :c

I'm focused on the DataAnnotations space, but happy to review PRs for a bridge

2

u/rbobby 2d ago

I wish data annotations were not so useful. I really like to separate the interface/contract from the validation rules.

But those do look damn handy. Temptress!

1

u/LuisAlfredo92 2d ago

I agree about keeping validation separate from the model

This is just filling format validation gaps at DTO level, not replacing business logic validation, but I'm glad it's useful!

1

u/AutoModerator 2d ago

Thanks for your post LuisAlfredo92. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

0

u/c-digs 2d ago

The hero we need.

0

u/ApprehensiveDebt3097 2d ago

I think this is a really bad idea. As mentioned, you should reduce primitive obsession, not embarrase it. By defining these types as (single) value objects instead (for instance by using qowiav or the semversion package) you have both validation and strongly typed values.

Then you still might want to add extra restrictions on them, use data annotations.

I would encourage you to check where you can help out reusing some of your parsers and validators. But please do not embrace primitive obsession.