{"id":17182387,"url":"https://github.com/simoncropp/extendedfluentvalidation","last_synced_at":"2025-04-13T16:31:00.272Z","repository":{"id":42576079,"uuid":"400990404","full_name":"SimonCropp/ExtendedFluentValidation","owner":"SimonCropp","description":"Extends FluentValidation with some more opinionated rules.","archived":false,"fork":false,"pushed_at":"2025-04-09T00:42:46.000Z","size":520,"stargazers_count":16,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-12T01:21:33.368Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SimonCropp.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"license.txt","code_of_conduct":"code_of_conduct.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"SimonCropp"}},"created_at":"2021-08-29T08:31:38.000Z","updated_at":"2025-04-09T00:40:57.000Z","dependencies_parsed_at":"2023-12-25T01:36:42.245Z","dependency_job_id":"ebebaaf1-1a43-4a2a-803a-7078813fc586","html_url":"https://github.com/SimonCropp/ExtendedFluentValidation","commit_stats":{"total_commits":514,"total_committers":3,"mean_commits":"171.33333333333334","dds":"0.33852140077821014","last_synced_commit":"b3334c538cc78c0c930ddf41100b34348fd5a8cf"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FExtendedFluentValidation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FExtendedFluentValidation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FExtendedFluentValidation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SimonCropp%2FExtendedFluentValidation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SimonCropp","download_url":"https://codeload.github.com/SimonCropp/ExtendedFluentValidation/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248743715,"owners_count":21154725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T00:36:57.187Z","updated_at":"2025-04-13T16:31:00.263Z","avatar_url":"https://github.com/SimonCropp.png","language":"C#","funding_links":["https://github.com/sponsors/SimonCropp"],"categories":[],"sub_categories":[],"readme":"# \u003cimg src=\"/src/icon.png\" height=\"30px\"\u003e ExtendedFluentValidation\n\n[![Build status](https://ci.appveyor.com/api/projects/status/3lr9er83fo8mij5i?svg=true)](https://ci.appveyor.com/project/SimonCropp/ExtendedFluentValidation)\n[![NuGet Status](https://img.shields.io/nuget/v/ExtendedFluentValidation.svg)](https://www.nuget.org/packages/ExtendedFluentValidation/)\n\nExtends [FluentValidation](https://fluentvalidation.net/) with some more opinionated rules and extensions.\n\n**See [Milestones](../../milestones?state=closed) for release notes.**\n\n\n## Nuget\n\nhttps://nuget.org/packages/ExtendedFluentValidation/\n\n\n## Extra Rules\n\n\n### Nullability\n\nIt leverages nullability information to make all non-nullable reference properties to be required.\n\n\n### Dates\n\n`DateTime`, `DateTimeOffset`, and `DateOnly` cannot be `MinValue`.\n\n\n### Strings\n\nString cannot be `String.Empty` or only white-space. The logic being: if the absence of text is valid, then make the member nullable. This helps since nullable is a first class strong type feature, where \"string is empty or only white-space\" is a runtime check.\n\n\n### Guids\n\nGuids cannot be `Guid.Empty`.\n\n\n### Lists/Collections\n\nLists and Collection cannot be empty if `ValidatorConventions.ValidateEmptyLists()` is called in a module initializer. The logic being: if the absence of any values is valid, then make the member nullable. This helps since nullable is a first class strong type feature, where \"list contains no values\" is a runtime check.\n\n\n## Usage\n\nThere are two ways of applying the extended rules.\n\n\n### ExtendedValidator\n\nUsing a base class `ExtendedValidator`:\n\n\u003c!-- snippet: ExtendedValidatorUsage --\u003e\n\u003ca id='snippet-ExtendedValidatorUsage'\u003e\u003c/a\u003e\n```cs\nclass PersonValidatorFromBase :\n    ExtendedValidator\u003cPerson\u003e\n{\n    public PersonValidatorFromBase()\n    {\n        //TODO: add any extra rules\n    }\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/Tests.cs#L517-L528' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-ExtendedValidatorUsage' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### AddExtendedRules\n\nUsing an extension method `AddExtendedRules`:\n\n\u003c!-- snippet: AddExtendedRulesUsage --\u003e\n\u003ca id='snippet-AddExtendedRulesUsage'\u003e\u003c/a\u003e\n```cs\nclass PersonValidatorNonBase :\n    AbstractValidator\u003cPerson\u003e\n{\n    public PersonValidatorNonBase() =\u003e\n        this.AddExtendedRules();\n    //TODO: add any extra rules\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/Tests.cs#L530-L540' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-AddExtendedRulesUsage' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n### Equivalent\n\nThe above are equivalent to:\n\n\u003c!-- snippet: Person --\u003e\n\u003ca id='snippet-Person'\u003e\u003c/a\u003e\n```cs\npublic class Person\n{\n    public Guid Id { get; set; }\n    public string FirstName { get; set; }\n    public string? MiddleName { get; set; }\n    public string FamilyName { get; set; }\n    public DateTimeOffset Dob { get; set; }\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/Tests.cs#L502-L513' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-Person' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\u003c!-- snippet: Equivalent --\u003e\n\u003ca id='snippet-Equivalent'\u003e\u003c/a\u003e\n```cs\nclass PersonValidatorEquivalent :\n    AbstractValidator\u003cPerson\u003e\n{\n    public PersonValidatorEquivalent()\n    {\n        RuleFor(_ =\u003e _.Id)\n            .NotEqual(Guid.Empty);\n        RuleFor(_ =\u003e _.FirstName)\n            .NotEmpty();\n        RuleFor(_ =\u003e _.MiddleName)\n            .SetValidator(new NotWhiteSpaceValidator\u003cPerson\u003e());\n        RuleFor(_ =\u003e _.FamilyName)\n            .NotEmpty();\n        RuleFor(_ =\u003e _.Dob)\n            .NotEqual(DateTimeOffset.MinValue);\n    }\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/Tests.cs#L542-L562' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-Equivalent' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## Shared Rules\n\nGiven the following models:\n\n\u003c!-- snippet: SharedRulesModels --\u003e\n\u003ca id='snippet-SharedRulesModels'\u003e\u003c/a\u003e\n```cs\npublic interface IDbRecord\n{\n    public byte[] RowVersion { get; }\n    public Guid Id { get; }\n}\n\npublic class Person :\n    IDbRecord\n{\n    public Guid Id { get; set; }\n    public string Name { get; set; }\n    public byte[] RowVersion { get; set; }\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/SharedRuleTests.cs#L27-L43' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SharedRulesModels' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nIt is desirable to have the rules for `IDbRecord` defined separately, and not need to duplicate them for every implementing class. This can be done using shares rules.\n\nConfigure any shared rules at startup:\n\n\u003c!-- snippet: SharedRulesInit --\u003e\n\u003ca id='snippet-SharedRulesInit'\u003e\u003c/a\u003e\n```cs\n[ModuleInitializer]\npublic static void Init() =\u003e\n    ValidatorConventions.ValidatorFor\u003cIDbRecord\u003e()\n        .RuleFor(record =\u003e record.RowVersion)\n        .Must(rowVersion =\u003e rowVersion?.Length == 8)\n        .WithMessage(\"RowVersion must be 8 bytes\");\n```\n\u003csup\u003e\u003ca href='/src/Tests/SharedRuleTests.cs#L3-L12' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SharedRulesInit' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nThe `PersonValidator` used only the standard rules, so needs no constructor.\n\n\u003c!-- snippet: SharedRulesUsage --\u003e\n\u003ca id='snippet-SharedRulesUsage'\u003e\u003c/a\u003e\n```cs\nclass PersonValidator :\n    ExtendedValidator\u003cPerson\u003e;\n```\n\u003csup\u003e\u003ca href='/src/Tests/SharedRuleTests.cs#L45-L50' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SharedRulesUsage' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\nThe above is equivalent to:\n\n\u003c!-- snippet: SharedRulesEquivalent --\u003e\n\u003ca id='snippet-SharedRulesEquivalent'\u003e\u003c/a\u003e\n```cs\nclass PersonValidatorEquivalent :\n    AbstractValidator\u003cPerson\u003e\n{\n    public PersonValidatorEquivalent()\n    {\n        RuleFor(_ =\u003e _.Id)\n            .NotEqual(Guid.Empty);\n        RuleFor(_ =\u003e _.Name)\n            .NotEmpty();\n        RuleFor(_ =\u003e _.RowVersion)\n            .NotNull()\n            .Must(rowVersion =\u003e rowVersion?.Length == 8)\n            .WithMessage(\"RowVersion must be 8 bytes\");\n    }\n}\n```\n\u003csup\u003e\u003ca href='/src/Tests/SharedRuleTests.cs#L52-L70' title='Snippet source file'\u003esnippet source\u003c/a\u003e | \u003ca href='#snippet-SharedRulesEquivalent' title='Start of snippet'\u003eanchor\u003c/a\u003e\u003c/sup\u003e\n\u003c!-- endSnippet --\u003e\n\n\n## Icon\n\n[Pointed Star](https://thenounproject.com/term/pointed+star/802333/) designed by [Eliricon](https://thenounproject.com/mordarius/) from [The Noun Project](https://thenounproject.com).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fextendedfluentvalidation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimoncropp%2Fextendedfluentvalidation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimoncropp%2Fextendedfluentvalidation/lists"}