├── .github └── workflows │ ├── create-release.yml │ └── dotnet.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── LinkDotNet.ValidationExtensions.Tests ├── DateAttributes │ ├── FutureDateValidationAttributeTests.cs │ └── PastDateValidationAttributeTests.cs ├── Dynamic │ └── RequiredDynamicTests.cs ├── If │ ├── MaxLengthIfInverseTests.cs │ ├── MaxLengthIfTests.cs │ ├── MinLengthIfInverseTests.cs │ ├── MinLengthIfTests.cs │ ├── RequiredIfNotTests.cs │ └── RequiredIfTests.cs ├── LinkDotNet.ValidationExtensions.Tests.csproj └── RangeAttributes │ ├── DynamicRangeBuiltInTypesTests.cs │ ├── DynamicRangeTests.cs │ └── GenericDynamicRangeTests.cs ├── LinkDotNet.ValidationExtensions.sln ├── LinkDotNet.ValidationExtensions ├── DateAttributes │ ├── FutureDateValidationAttribute.cs │ └── PastDateValidationAttribute.cs ├── Dynamic │ └── RequiredDynamicAttribute.cs ├── Helpers │ └── ArgumentNullExceptionHelper.cs ├── If │ ├── IfHelper.cs │ ├── MaxIfAttribute.cs │ ├── MaxLengthIfAttribute.cs │ ├── MinIfAttribute.cs │ ├── MinLengthIfAttribute.cs │ ├── RangeIfAttribute.cs │ └── RequiredIfAttribute.cs ├── LinkDotNet.ValidationExtensions.csproj └── RangeAttributes │ ├── DynamicRangeAttribute.cs │ ├── GenericDynamicRangeAttribute.cs │ ├── MaxAttribute.cs │ └── MinAttribute.cs ├── Readme.md ├── Sample ├── Components │ ├── App.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── Pages │ │ ├── Home.razor │ │ └── Model.cs │ ├── Routes.razor │ └── _Imports.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Sample.csproj ├── appsettings.json └── wwwroot │ ├── app.css │ ├── favicon.png │ └── lib │ └── bootstrap │ └── dist │ ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.css.map │ ├── bootstrap-grid.min.css │ ├── bootstrap-grid.min.css.map │ ├── bootstrap-grid.rtl.css │ ├── bootstrap-grid.rtl.css.map │ ├── bootstrap-grid.rtl.min.css │ ├── bootstrap-grid.rtl.min.css.map │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.css.map │ ├── bootstrap-reboot.min.css │ ├── bootstrap-reboot.min.css.map │ ├── bootstrap-reboot.rtl.css │ ├── bootstrap-reboot.rtl.css.map │ ├── bootstrap-reboot.rtl.min.css │ ├── bootstrap-reboot.rtl.min.css.map │ ├── bootstrap-utilities.css │ ├── bootstrap-utilities.css.map │ ├── bootstrap-utilities.min.css │ ├── bootstrap-utilities.min.css.map │ ├── bootstrap-utilities.rtl.css │ ├── bootstrap-utilities.rtl.css.map │ ├── bootstrap-utilities.rtl.min.css │ ├── bootstrap-utilities.rtl.min.css.map │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ ├── bootstrap.rtl.css │ ├── bootstrap.rtl.css.map │ ├── bootstrap.rtl.min.css │ └── bootstrap.rtl.min.css.map │ └── js │ ├── bootstrap.bundle.js │ ├── bootstrap.bundle.js.map │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.esm.js │ ├── bootstrap.esm.js.map │ ├── bootstrap.esm.min.js │ ├── bootstrap.esm.min.js.map │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ └── bootstrap.min.js.map ├── stylecop.analyzers.ruleset └── stylecop.json /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create new Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | versionIncrement: 7 | description: 'The new version. For example: 1.1.0' 8 | required: true 9 | default: '' 10 | prerelease: 11 | description: 'Is this a pre-release?' 12 | type: boolean 13 | required: false 14 | default: false 15 | 16 | jobs: 17 | release: 18 | name: Publish new release 19 | runs-on: ubuntu-latest 20 | steps: 21 | 22 | - name: Checkout repository 23 | uses: actions/checkout@v3 24 | with: 25 | token: ${{ secrets.PAT }} 26 | persist-credentials: true 27 | fetch-depth: 0 28 | 29 | - name: Setup dotnet 30 | uses: actions/setup-dotnet@v3 31 | with: 32 | dotnet-version: | 33 | 5.0.x 34 | 6.0.x 35 | 7.0.x 36 | 8.0.x 37 | 9.0.x 38 | 39 | - name: Update CHANGELOG file 40 | uses: thomaseizinger/keep-a-changelog-new-release@1.3.0 41 | with: 42 | version: ${{ github.event.inputs.versionIncrement }} 43 | 44 | - name: Set git config 45 | run: | 46 | git config --local user.email "linkdotnet@action.com" 47 | git config --local user.name "LinkDotNet Bot" 48 | 49 | - name: Commit changes and push changes 50 | run: | 51 | git add CHANGELOG.md 52 | git commit -m "Update Changelog.md for ${{github.event.inputs.versionIncrement}} release" 53 | git push origin main 54 | 55 | - name: Create release on GitHub 56 | uses: thomaseizinger/create-release@1.0.0 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.PAT }} 59 | with: 60 | tag_name: v${{ github.event.inputs.versionIncrement }} 61 | target_commitish: ${{ env.RELEASE_COMMIT_HASH }} 62 | name: v${{ github.event.inputs.versionIncrement }} 63 | body: ${{ steps.changelog.outputs.changes }} 64 | draft: false 65 | prerelease: ${{ github.event.inputs.prerelease }} 66 | 67 | - name: Create release package 68 | run: | 69 | dotnet pack -c RELEASE -p:PackageVersion=${{ github.event.inputs.versionIncrement }} -o ${GITHUB_WORKSPACE}/packages /p:ContinuousIntegrationBuild=true --nologo --include-symbols -p:SymbolPackageFormat=snupkg 70 | 71 | - name: Upload to nuget 72 | run: | 73 | dotnet nuget push ${GITHUB_WORKSPACE}/packages/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 74 | dotnet nuget push ${GITHUB_WORKSPACE}/packages/*.snupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 75 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v3 17 | with: 18 | dotnet-version: | 19 | 5.0.x 20 | 6.0.x 21 | 7.0.x 22 | 8.0.x 23 | 9.0.x 24 | - name: Restore dependencies 25 | run: dotnet restore 26 | - name: Build 27 | run: dotnet build --no-restore -c Release 28 | - name: Test 29 | run: dotnet test -c Release --no-build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | 7 | appsettings.Development.json 8 | appsettings.Production.json 9 | 10 | *.[Pp]ublish.xml 11 | *.azurePubxml 12 | *.pubxml 13 | *.pubxml.user 14 | *.publishproj 15 | *.suo 16 | sitemap.xml 17 | robots.txt 18 | .vs/ 19 | .idea/ 20 | *.csproj.user 21 | *.dotsettings.user 22 | 23 | # Coverage files and reports 24 | /**/TestResults/* 25 | /**/coverage*.xml 26 | /CoverageReport/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to the **ValidationExtensions** will be documented in this file. The project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## [Unreleased] 6 | 7 | ### Added 8 | - net9.0 as target framework. 9 | 10 | ## [2.6.0] - 2023-07-07 11 | 12 | ### Fixed 13 | 14 | - Fixed issue with not indicating validation errors with a border around the control. By [Robelind](https://github.com/Robelind) 15 | - Fixed an issue where `RequiredIf` did not work with `null` values. By [Robelind](https://github.com/Robelind) 16 | 17 | ## [2.5.1] - 2023-04-16 18 | 19 | ### Added 20 | 21 | - Respect optional Error Message 22 | 23 | ### Fixed 24 | 25 | - Use wrong error sentence 26 | 27 | ## [2.5.0] - 2023-03-27 28 | 29 | ### Added 30 | 31 | - New attributes to check if a date is valid in the future or past. 32 | 33 | ## [2.4.0] - 2023-01-16 34 | 35 | - Added `DynamicRangeAttribute` which is the simplified and type-strong version of `DynamicRangeAttribute`. By [mhb164](https://github.com/mhb164) 36 | 37 | ## [2.3.2] - 2023-01-09 38 | 39 | ## [2.3.1] - 2023-01-09 40 | 41 | ### Added 42 | 43 | - Support for `netstandard2.0` and `net7.0` 44 | 45 | ### Added 46 | 47 | ## [2.1.0] 48 | 49 | ### Added 50 | 51 | - Added `RequiredDynamicAttribute` which can accept method to validate complicated requirement(s) and more readable code. By [mhb164](https://github.com/mhb164) 52 | 53 | ## [2.0.0] 54 | 55 | ### Added 56 | 57 | - Removed all `*IfNotAttribute` in favor of newly introducded `IsInverse` parameter. For example: `[RequiredIfNot(nameof(OtherProperty), false)]` --> `[RequiredIf(nameof(OtherProperty), false, isInverse: true)]` 58 | - Added `MinAttribute` which indicates that a number field has to have at least this value 59 | - Added `MinIfAttribute` which used `MinAttribute` if the condition is (not) met 60 | - Added `MaxAttribute` which indicates that a number field has to have at most this value 61 | - Added `MaxIfAttribute` which used `MaxAttribute` if the condition is (not) met 62 | 63 | ## [1.1.0] 64 | 65 | ### Added 66 | 67 | - Added `MinLengthRequiredIfAttribute` and `MinLengthRequiredIfNotAttribute` 68 | - Added `MaxLengthRequiredIfAttribute` and `MaxLengthRequiredIfNotAttribute` 69 | 70 | ## [1.0.0] 71 | 72 | - Initial Release 73 | 74 | [Unreleased]: https://github.com/linkdotnet/ValidationExtensions/compare/2.6.0...HEAD 75 | 76 | [2.6.0]: https://github.com/linkdotnet/ValidationExtensions/compare/2.5.1...2.6.0 77 | 78 | [2.5.1]: https://github.com/linkdotnet/ValidationExtensions/compare/2.5.0...2.5.1 79 | 80 | [2.5.0]: https://github.com/linkdotnet/ValidationExtensions/compare/2.4.0...2.5.0 81 | 82 | [2.4.0]: https://github.com/linkdotnet/ValidationExtensions/compare/2.3.2...2.4.0 83 | 84 | [2.3.2]: https://github.com/linkdotnet/ValidationExtensions/compare/2.3.1...2.3.2 85 | 86 | [2.3.1]: https://github.com/linkdotnet/ValidationExtensions/compare/138e2951b2d42584ed66e41e6f31e203e509b8ef...2.3.1 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Steven Giesel 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 | -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/DateAttributes/FutureDateValidationAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace LinkDotNet.ValidationExtensions.Tests.DateAttributes; 7 | 8 | public class FutureDateValidationAttributeTests 9 | { 10 | [Theory] 11 | [InlineData(true)] 12 | [InlineData(false)] 13 | public void IsValid_WithPastDate_ReturnsValidationError(bool useUtc) 14 | { 15 | // Arrange 16 | var validationAttribute = new FutureDateValidationAttribute { UseUtc = useUtc }; 17 | var pastDate = (useUtc ? DateTime.UtcNow : DateTime.Now).AddMinutes(-1); 18 | 19 | // Act 20 | var validationResult = validationAttribute.IsValid(pastDate); 21 | 22 | // Assert 23 | validationResult.Should().BeFalse(); 24 | } 25 | 26 | [Theory] 27 | [InlineData(true)] 28 | [InlineData(false)] 29 | public void IsValid_WithFutureDate_ReturnsSuccess(bool useUtc) 30 | { 31 | // Arrange 32 | var validationAttribute = new FutureDateValidationAttribute { UseUtc = useUtc }; 33 | var futureDate = (useUtc ? DateTime.UtcNow : DateTime.Now).AddMinutes(1); 34 | 35 | // Act 36 | var validationResult = validationAttribute.IsValid(futureDate); 37 | 38 | // Assert 39 | validationResult.Should().BeTrue(); 40 | } 41 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/DateAttributes/PastDateValidationAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace LinkDotNet.ValidationExtensions.Tests.DateAttributes; 7 | 8 | public class PastDateValidationAttributeTests 9 | { 10 | [Theory] 11 | [InlineData(true)] 12 | [InlineData(false)] 13 | public void IsValid_WithPastDate_ReturnsSuccess(bool useUtc) 14 | { 15 | // Arrange 16 | var validationAttribute = new PastDateValidationAttribute { UseUtc = useUtc }; 17 | var pastDate = (useUtc ? DateTime.UtcNow : DateTime.Now).AddMinutes(-1); 18 | 19 | // Act 20 | var validationResult = validationAttribute.IsValid(pastDate); 21 | 22 | // Assert 23 | validationResult.Should().BeTrue(); 24 | } 25 | 26 | [Theory] 27 | [InlineData(true)] 28 | [InlineData(false)] 29 | public void IsValid_WithFutureDate_ReturnsValidationError(bool useUtc) 30 | { 31 | // Arrange 32 | var validationAttribute = new PastDateValidationAttribute { UseUtc = useUtc }; 33 | var futureDate = (useUtc ? DateTime.UtcNow : DateTime.Now).AddMinutes(1); 34 | 35 | // Act 36 | var validationResult = validationAttribute.IsValid(futureDate); 37 | 38 | // Assert 39 | validationResult.Should().BeFalse(); 40 | } 41 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/Dynamic/RequiredDynamicTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace LinkDotNet.ValidationExtensions.Tests; 7 | 8 | public class RequiredDynamicTests 9 | { 10 | [Fact(DisplayName = "Should not be valid if mobile number is null")] 11 | public void ShouldNotBeValidIfMobileNumberIsNull() 12 | { 13 | var model = new Model(null, "John", "Wick", false, null); 14 | var context = new ValidationContext(model); 15 | var results = new List(); 16 | 17 | var isValid = Validator.TryValidateObject(model, context, results, true); 18 | 19 | isValid.Should().BeFalse(); 20 | } 21 | 22 | [Fact(DisplayName = "Should not be valid if firstname and surname is null")] 23 | public void ShouldNotBeValidIfFirstnameAndSurnameIsNull() 24 | { 25 | var model = new Model("+15854380259", null, null, false, null); 26 | var context = new ValidationContext(model); 27 | var results = new List(); 28 | 29 | var isValid = Validator.TryValidateObject(model, context, results, true); 30 | 31 | isValid.Should().BeFalse(); 32 | } 33 | 34 | [Fact(DisplayName = "Should be valid if firstname is not null")] 35 | public void ShouldBeValidIfFirstnameIsNotNull() 36 | { 37 | var model = new Model("+15854380259", "John", null, false, null); 38 | var context = new ValidationContext(model); 39 | var results = new List(); 40 | 41 | var isValid = Validator.TryValidateObject(model, context, results, true); 42 | 43 | isValid.Should().BeTrue(); 44 | } 45 | 46 | [Fact(DisplayName = "Should be valid if surname is not null")] 47 | public void ShouldBeValidIfSurnameIsNotNull() 48 | { 49 | var model = new Model("+15854380259", null, "Wick", false, null); 50 | var context = new ValidationContext(model); 51 | var results = new List(); 52 | 53 | var isValid = Validator.TryValidateObject(model, context, results, true); 54 | 55 | isValid.Should().BeTrue(); 56 | } 57 | 58 | [Fact(DisplayName = "Should not be valid if notice by email is null")] 59 | public void ShouldNotBeValidIfNoticeByEmailIsNull() 60 | { 61 | var model = new Model("+15854380259", "John", "Wick", null, null); 62 | var context = new ValidationContext(model); 63 | var results = new List(); 64 | 65 | var isValid = Validator.TryValidateObject(model, context, results, true); 66 | 67 | isValid.Should().BeFalse(); 68 | } 69 | 70 | [Fact(DisplayName = "Should not be valid if notice by email is activated and email address is null")] 71 | public void ShouldNotBeValidIfNoticeByEmailIsActivatedAndEmailAddressIsNull() 72 | { 73 | var model = new Model("+15854380259", "John", "Wick", true, null); 74 | var context = new ValidationContext(model); 75 | var results = new List(); 76 | 77 | var isValid = Validator.TryValidateObject(model, context, results, true); 78 | 79 | isValid.Should().BeFalse(); 80 | } 81 | 82 | public class Model 83 | { 84 | public Model(string? mobileNumber, string? firstname, string? surname, bool? noticeByEmail, string? emailAddress) 85 | { 86 | MobileNumber = mobileNumber; 87 | Firstname = firstname; 88 | FirstnameStaticPublic = firstname; 89 | FirstnameInstancePublic = firstname; 90 | FirstnameInstanceNonePublic = firstname; 91 | Surname = surname; 92 | NoticeByEmail = noticeByEmail; 93 | EmailAddress = emailAddress; 94 | } 95 | 96 | [Required] 97 | public string? MobileNumber { get; set; } 98 | 99 | [RequiredDynamic(nameof(ValidateRequiredFullname), "Fullname can't be empty (Static, NonePublic)")] 100 | public string? Firstname { get; set; } 101 | 102 | [RequiredDynamic(nameof(ValidateRequiredFullnameStaticPublic), "Fullname can't be empty (Static, Public)")] 103 | public string? FirstnameStaticPublic { get; set; } 104 | 105 | [RequiredDynamic(nameof(ValidateRequiredFullnameInstancePublic), "Fullname can't be empty (Instance, Public)")] 106 | public string? FirstnameInstancePublic { get; set; } 107 | 108 | [RequiredDynamic(nameof(ValidateRequiredFullnameInstanceNonePublic), "Fullname can't be empty (Instance, NonePublic)")] 109 | public string? FirstnameInstanceNonePublic { get; set; } 110 | 111 | [RequiredDynamic(nameof(ValidateRequiredFullname), "Fullname can't be empty")] 112 | public string? Surname { get; set; } 113 | 114 | [Required] 115 | public bool? NoticeByEmail { get; set; } 116 | 117 | [RequiredDynamic(nameof(ValidateRequiredNoticeByEmail), "Notice by email is activated")] 118 | public string? EmailAddress { get; set; } 119 | 120 | public static bool ValidateRequiredFullname(Model value) 121 | { 122 | if (string.IsNullOrWhiteSpace(value.Firstname) && string.IsNullOrWhiteSpace(value.Surname)) 123 | { 124 | return true; 125 | } 126 | else 127 | { 128 | return false; 129 | } 130 | } 131 | 132 | public bool ValidateRequiredFullnameInstanceNonePublic(Model value) => ValidateRequiredFullname(value); 133 | 134 | private static bool ValidateRequiredNoticeByEmail(Model value) 135 | { 136 | if (!value.NoticeByEmail.HasValue) 137 | { 138 | return false; 139 | } 140 | 141 | if (!value.NoticeByEmail.Value) 142 | { 143 | return false; 144 | } 145 | 146 | if (string.IsNullOrWhiteSpace(value.EmailAddress)) 147 | { 148 | return true; 149 | } 150 | else 151 | { 152 | return false; 153 | } 154 | } 155 | 156 | private static bool ValidateRequiredFullnameStaticPublic(Model value) => ValidateRequiredFullname(value); 157 | 158 | private bool ValidateRequiredFullnameInstancePublic(Model value) => ValidateRequiredFullname(value); 159 | } 160 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/MaxLengthIfInverseTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests; 8 | 9 | public class MaxLengthIfNotTests 10 | { 11 | [Fact] 12 | public void ShouldNotCheckForMaxIfNotRequired() 13 | { 14 | var model = new Model("123", false); 15 | var context = new ValidationContext(model); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(model, context, results, true); 19 | 20 | isValid.Should().BeTrue(); 21 | } 22 | 23 | [Fact] 24 | public void ShouldCheckWhenConditionIsNotMet() 25 | { 26 | var model = new Model("123", true); 27 | var context = new ValidationContext(model); 28 | var results = new List(); 29 | 30 | var isValid = Validator.TryValidateObject(model, context, results, true); 31 | 32 | isValid.Should().BeFalse(); 33 | results.Should().HaveCount(1); 34 | results.Single().ErrorMessage.Should().Be("The field 'Name' must be a string or array type with a maximum length of '1' when 'IsRequired' is not 'False'."); 35 | } 36 | 37 | public class Model 38 | { 39 | public Model(string name, bool isRequired) 40 | { 41 | Name = name; 42 | IsRequired = isRequired; 43 | } 44 | 45 | [MaxLengthIf(nameof(IsRequired), false, 1, true)] 46 | public string Name { get; set; } 47 | 48 | public bool IsRequired { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/MaxLengthIfTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests; 8 | 9 | public class MaxLengthIfTests 10 | { 11 | [Fact] 12 | public void ShouldNotCheckForMaxIfNotRequired() 13 | { 14 | var model = new Model("123", false); 15 | var context = new ValidationContext(model); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(model, context, results, true); 19 | 20 | isValid.Should().BeTrue(); 21 | } 22 | 23 | [Fact] 24 | public void ShouldCheckWhenConditionIsNotMet() 25 | { 26 | var model = new Model("123", true); 27 | var context = new ValidationContext(model); 28 | var results = new List(); 29 | 30 | var isValid = Validator.TryValidateObject(model, context, results, true); 31 | 32 | isValid.Should().BeFalse(); 33 | results.Should().HaveCount(1); 34 | results.Single().ErrorMessage.Should().Be("The field 'Name' must be a string or array type with a maximum length of '1' when 'IsRequired' is 'True'."); 35 | } 36 | 37 | public class Model 38 | { 39 | public Model(string name, bool isRequired) 40 | { 41 | Name = name; 42 | IsRequired = isRequired; 43 | } 44 | 45 | [MaxLengthIf(nameof(IsRequired), true, 1)] 46 | public string Name { get; set; } 47 | 48 | public bool IsRequired { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/MinLengthIfInverseTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests; 8 | 9 | public class MinLengthIfInverseTests 10 | { 11 | [Fact] 12 | public void ShouldCheckForMinIfNotRequired() 13 | { 14 | var model = new Model("1", false); 15 | var context = new ValidationContext(model); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(model, context, results, true); 19 | 20 | isValid.Should().BeTrue(); 21 | } 22 | 23 | [Fact] 24 | public void ShouldCheckWhenConditionIsNotMet() 25 | { 26 | var model = new Model("1", true); 27 | var context = new ValidationContext(model); 28 | var results = new List(); 29 | 30 | var isValid = Validator.TryValidateObject(model, context, results, true); 31 | 32 | isValid.Should().BeFalse(); 33 | results.Should().HaveCount(1); 34 | results.Single().ErrorMessage.Should().Be("The field 'Name' must be a string or array type with a minimum length of '10' when 'IsRequired' is not 'False'."); 35 | } 36 | 37 | public class Model 38 | { 39 | public Model(string name, bool isRequired) 40 | { 41 | Name = name; 42 | IsRequired = isRequired; 43 | } 44 | 45 | [MinLengthIf(nameof(IsRequired), false, 10, true)] 46 | public string Name { get; set; } 47 | 48 | public bool IsRequired { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/MinLengthIfTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests; 8 | 9 | public class MinLengthIfTests 10 | { 11 | [Fact] 12 | public void ShouldNotCheckForMinIfNotRequired() 13 | { 14 | var model = new Model("1", false); 15 | var context = new ValidationContext(model); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(model, context, results, true); 19 | 20 | isValid.Should().BeTrue(); 21 | } 22 | 23 | [Fact] 24 | public void ShouldCheckWhenConditionIsNotMet() 25 | { 26 | var model = new Model("1", true); 27 | var context = new ValidationContext(model); 28 | var results = new List(); 29 | 30 | var isValid = Validator.TryValidateObject(model, context, results, true); 31 | 32 | isValid.Should().BeFalse(); 33 | results.Should().HaveCount(1); 34 | results.Single().ErrorMessage.Should().Be("The field 'Name' must be a string or array type with a minimum length of '10' when 'IsRequired' is 'True'."); 35 | } 36 | 37 | public class Model 38 | { 39 | public Model(string name, bool isRequired) 40 | { 41 | Name = name; 42 | IsRequired = isRequired; 43 | } 44 | 45 | [MinLengthIf(nameof(IsRequired), true, 10)] 46 | public string Name { get; set; } 47 | 48 | public bool IsRequired { get; set; } 49 | } 50 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/RequiredIfNotTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace LinkDotNet.ValidationExtensions.Tests; 9 | 10 | public class RequiredIfNotTests 11 | { 12 | [Fact] 13 | public void ShouldBeValidIfIsDependentIsNotSameValue() 14 | { 15 | var model = new Model(null, false, "Test"); 16 | var context = new ValidationContext(model); 17 | var results = new List(); 18 | 19 | var isValid = Validator.TryValidateObject(model, context, results, true); 20 | 21 | isValid.Should().BeTrue(); 22 | } 23 | 24 | [Fact] 25 | public void ShouldBeInvalidWhenRequiredNotConditionNotMet() 26 | { 27 | var model = new Model(null, true, "test"); 28 | var context = new ValidationContext(model); 29 | var results = new List(); 30 | 31 | var isValid = Validator.TryValidateObject(model, context, results, true); 32 | 33 | isValid.Should().BeFalse(); 34 | results.Should().HaveCount(1); 35 | results.Single().ErrorMessage.Should().Be("Property 'SomeProperty' is required when 'IsDependent' is not 'False'"); 36 | } 37 | 38 | [Fact] 39 | public void ShouldCheckForNullValues() 40 | { 41 | var model = new Model("Test", false, null); 42 | var context = new ValidationContext(model); 43 | var results = new List(); 44 | 45 | var isValid = Validator.TryValidateObject(model, context, results, true); 46 | 47 | isValid.Should().BeTrue(); 48 | } 49 | 50 | [Fact] 51 | public void ShouldCheckNullValues() 52 | { 53 | var model = new Model(null, false, null); 54 | var context = new ValidationContext(model); 55 | var results = new List(); 56 | 57 | var isValid = Validator.TryValidateObject(model, context, results, true); 58 | 59 | isValid.Should().BeTrue(); 60 | } 61 | 62 | [Fact] 63 | public void ShouldThrowExceptionWhenPropertyNotFound() 64 | { 65 | var model = new InvalidModel(); 66 | var context = new ValidationContext(model); 67 | var results = new List(); 68 | 69 | var act = () => Validator.TryValidateObject(model, context, results, true); 70 | 71 | act.Should().Throw(); 72 | } 73 | 74 | [Fact] 75 | public void ShouldBeValidWhenNullAndDependentIsNull() 76 | { 77 | var model = new OtherModel(null, null); 78 | var context = new ValidationContext(model); 79 | var results = new List(); 80 | 81 | var isValid = Validator.TryValidateObject(model, context, results, true); 82 | 83 | isValid.Should().BeTrue(); 84 | } 85 | 86 | [Fact] 87 | public void ShouldBeInvalidWhenNullAndDependentIsNotNull() 88 | { 89 | var model = new OtherModel(null, "Test"); 90 | var context = new ValidationContext(model); 91 | var results = new List(); 92 | 93 | var isValid = Validator.TryValidateObject(model, context, results, true); 94 | 95 | isValid.Should().BeFalse(); 96 | } 97 | 98 | [Fact] 99 | public void ShouldBeValidWhenNotNullAndDependentIsNotNull() 100 | { 101 | var model = new OtherModel("Test", "Test"); 102 | var context = new ValidationContext(model); 103 | var results = new List(); 104 | 105 | var isValid = Validator.TryValidateObject(model, context, results, true); 106 | 107 | isValid.Should().BeTrue(); 108 | } 109 | 110 | [Fact] 111 | public void ShouldBeValidWhenNotNullAndDependentIsNull() 112 | { 113 | var model = new OtherModel("Test", null); 114 | var context = new ValidationContext(model); 115 | var results = new List(); 116 | 117 | var isValid = Validator.TryValidateObject(model, context, results, true); 118 | 119 | isValid.Should().BeTrue(); 120 | } 121 | 122 | public class Model 123 | { 124 | public Model(string? someProperty, bool isDependent, string? otherPropertyDependingOnString) 125 | { 126 | SomeProperty = someProperty; 127 | IsDependent = isDependent; 128 | OtherPropertyDependingOnString = otherPropertyDependingOnString; 129 | } 130 | 131 | [RequiredIf(nameof(IsDependent), false, true)] 132 | public string? SomeProperty { get; set; } 133 | 134 | [Required] 135 | public bool IsDependent { get; set; } 136 | 137 | [RequiredIf(nameof(SomeProperty), "Test", true)] 138 | public string? OtherPropertyDependingOnString { get; set; } 139 | } 140 | 141 | public class OtherModel 142 | { 143 | public OtherModel(string? someOtherProperty, string? isDependentNullable) 144 | { 145 | SomeOtherProperty = someOtherProperty; 146 | IsDependentNullable = isDependentNullable; 147 | } 148 | 149 | public string? IsDependentNullable { get; set; } 150 | 151 | [RequiredIf(nameof(IsDependentNullable), null, true)] 152 | public string? SomeOtherProperty { get; set; } 153 | } 154 | 155 | private class InvalidModel 156 | { 157 | [RequiredIf("not-existing property", false)] 158 | public string? Id { get; set; } 159 | } 160 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/If/RequiredIfTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace LinkDotNet.ValidationExtensions.Tests; 9 | 10 | public class RequiredIfTests 11 | { 12 | [Fact] 13 | public void ShouldBeValidIfIsDependentIsSameValue() 14 | { 15 | var model = new Model(null, false, "test"); 16 | var context = new ValidationContext(model); 17 | var results = new List(); 18 | 19 | var isValid = Validator.TryValidateObject(model, context, results, true); 20 | 21 | isValid.Should().BeTrue(); 22 | } 23 | 24 | [Fact] 25 | public void ShouldNotBeValidWhenIsSameValueIsDifferent() 26 | { 27 | var model = new Model(null, true, "test"); 28 | var context = new ValidationContext(model); 29 | var results = new List(); 30 | 31 | var isValid = Validator.TryValidateObject(model, context, results, true); 32 | 33 | isValid.Should().BeFalse(); 34 | results.Should().HaveCount(1); 35 | results.Single().ErrorMessage.Should().Be("Property 'SomeProperty' is required when 'IsDependent' is 'True'"); 36 | results.Single().MemberNames.Should().HaveCount(1).And.Contain("SomeProperty"); 37 | } 38 | 39 | [Fact] 40 | public void ShouldCheckForNullValues() 41 | { 42 | var model = new Model(null, false, null); 43 | var context = new ValidationContext(model); 44 | var results = new List(); 45 | 46 | var isValid = Validator.TryValidateObject(model, context, results, true); 47 | 48 | isValid.Should().BeTrue(); 49 | } 50 | 51 | [Fact] 52 | public void ShouldThrowExceptionWhenPropertyNotFound() 53 | { 54 | var model = new InvalidModel(); 55 | var context = new ValidationContext(model); 56 | var results = new List(); 57 | 58 | var act = () => Validator.TryValidateObject(model, context, results, true); 59 | 60 | act.Should().Throw(); 61 | } 62 | 63 | [Fact] 64 | public void ShouldBeValidWhenNullAndDependentIsNotNull() 65 | { 66 | var model = new OtherModel(null, "Test"); 67 | var context = new ValidationContext(model); 68 | var results = new List(); 69 | 70 | var isValid = Validator.TryValidateObject(model, context, results, true); 71 | 72 | isValid.Should().BeTrue(); 73 | } 74 | 75 | [Fact] 76 | public void ShouldBeInvalidWhenNullAndDependentIsNull() 77 | { 78 | var model = new OtherModel(null, null); 79 | var context = new ValidationContext(model); 80 | var results = new List(); 81 | 82 | var isValid = Validator.TryValidateObject(model, context, results, true); 83 | 84 | isValid.Should().BeFalse(); 85 | } 86 | 87 | [Fact] 88 | public void ShouldBeValidWhenNotNullAndDependentIsNotNull() 89 | { 90 | var model = new OtherModel("Test", "Test"); 91 | var context = new ValidationContext(model); 92 | var results = new List(); 93 | 94 | var isValid = Validator.TryValidateObject(model, context, results, true); 95 | 96 | isValid.Should().BeTrue(); 97 | } 98 | 99 | [Fact] 100 | public void ShouldBeValidWhenNotNullAndDependentIsNull() 101 | { 102 | var model = new OtherModel("Test", null); 103 | var context = new ValidationContext(model); 104 | var results = new List(); 105 | 106 | var isValid = Validator.TryValidateObject(model, context, results, true); 107 | 108 | isValid.Should().BeTrue(); 109 | } 110 | 111 | public class Model 112 | { 113 | public Model(string? someProperty, bool isDependent, string? otherPropertyDependingOnString) 114 | { 115 | SomeProperty = someProperty; 116 | IsDependent = isDependent; 117 | OtherPropertyDependingOnString = otherPropertyDependingOnString; 118 | } 119 | 120 | [RequiredIf(nameof(IsDependent), true)] 121 | public string? SomeProperty { get; set; } 122 | 123 | [Required] 124 | public bool IsDependent { get; set; } 125 | 126 | [RequiredIf(nameof(SomeProperty), "Test")] 127 | public string? OtherPropertyDependingOnString { get; set; } 128 | } 129 | 130 | public class OtherModel 131 | { 132 | public OtherModel(string? someOtherProperty, string? isDependentNullable) 133 | { 134 | SomeOtherProperty = someOtherProperty; 135 | IsDependentNullable = isDependentNullable; 136 | } 137 | 138 | public string? IsDependentNullable { get; set; } 139 | 140 | [RequiredIf(nameof(IsDependentNullable), null)] 141 | public string? SomeOtherProperty { get; set; } 142 | } 143 | 144 | private class InvalidModel 145 | { 146 | [RequiredIf("not-existing property", false)] 147 | public string? Id { get; set; } 148 | } 149 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/LinkDotNet.ValidationExtensions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0;net6.0;net7.0;net8.0 5 | latest 6 | false 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | $(SolutionDir)\stylecop.analyzers.ruleset 41 | 42 | 43 | -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/RangeAttributes/DynamicRangeBuiltInTypesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests.Generic; 8 | 9 | public class DynamicRangeBuiltInTypesTests 10 | { 11 | [Theory] 12 | [MemberData(nameof(TestData))] 13 | public static void TestAllBuiltInTypes(T minimumRange, T minimum, T maximum, T maximumRange, bool minimumIsValid, bool maximumIsValid) 14 | where T : IComparable 15 | { 16 | var data = new Model(minimum, maximum); 17 | var context = new ValidationContext(data); 18 | var minimumAttribute = new DynamicRangeAttribute(minimumRange, "Maximum"); 19 | var maximumAttribute = new DynamicRangeAttribute("Minimum", maximumRange); 20 | 21 | var minimumValidation = minimumAttribute.GetValidationResult(minimum, context); 22 | var maximumValidation = maximumAttribute.GetValidationResult(maximum, context); 23 | 24 | if (minimumIsValid) 25 | { 26 | minimumValidation.Should().Be(ValidationResult.Success); 27 | } 28 | else 29 | { 30 | minimumValidation.Should().NotBe(ValidationResult.Success); 31 | } 32 | 33 | if (maximumIsValid) 34 | { 35 | maximumValidation.Should().Be(ValidationResult.Success); 36 | } 37 | else 38 | { 39 | maximumValidation.Should().NotBe(ValidationResult.Success); 40 | } 41 | } 42 | 43 | // https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/built-in-types 44 | public static IEnumerable TestData() 45 | { 46 | // bool - it's weird but 'bool' type is IComparable 47 | // minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 48 | yield return new object[] { true, false, true, false, false, false }; 49 | yield return new object[] { false, false, true, true, true, true }; 50 | 51 | // sbyte - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 52 | yield return new object[] { (sbyte)3, (sbyte)2, (sbyte)5, (sbyte)4, false, false }; 53 | yield return new object[] { (sbyte)1, (sbyte)2, (sbyte)5, (sbyte)6, true, true }; 54 | yield return new object[] { (sbyte)2, (sbyte)2, (sbyte)5, (sbyte)5, true, true }; 55 | yield return new object[] { (sbyte)2, (sbyte)2, (sbyte)2, (sbyte)2, true, true }; 56 | 57 | // byte - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 58 | yield return new object[] { (byte)3, (byte)2, (byte)5, (byte)4, false, false }; 59 | yield return new object[] { (byte)1, (byte)2, (byte)5, (byte)6, true, true }; 60 | 61 | // short - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 62 | yield return new object[] { (short)3, (short)2, (short)5, (short)4, false, false }; 63 | yield return new object[] { (short)1, (short)2, (short)5, (short)6, true, true }; 64 | 65 | // ushort - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 66 | yield return new object[] { (ushort)3, (ushort)2, (ushort)5, (ushort)4, false, false }; 67 | yield return new object[] { (ushort)1, (ushort)2, (ushort)5, (ushort)6, true, true }; 68 | 69 | // int - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 70 | yield return new object[] { 3, 2, 5, 4, false, false }; 71 | yield return new object[] { 1, 2, 5, 6, true, true }; 72 | 73 | // uint - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 74 | yield return new object[] { 3u, 2u, 5u, 4u, false, false }; 75 | yield return new object[] { 1u, 2u, 5u, 6u, true, true }; 76 | 77 | // long - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 78 | yield return new object[] { 3L, 2L, 5L, 4L, false, false }; 79 | yield return new object[] { 1L, 2L, 5L, 6L, true, true }; 80 | 81 | // ulong - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 82 | yield return new object[] { 3ul, 2ul, 5ul, 4ul, false, false }; 83 | yield return new object[] { 1ul, 2ul, 5ul, 6ul, true, true }; 84 | 85 | // char - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 86 | yield return new object[] { 'c', 'b', 'f', 'e', false, false }; 87 | yield return new object[] { 'a', 'b', 'f', 'g', true, true }; 88 | 89 | // float - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 90 | yield return new object[] { 3.2f, 2.2f, 5.2f, 4.2f, false, false }; 91 | yield return new object[] { 1.2f, 2.2f, 5.2f, 6.2f, true, true }; 92 | 93 | // double - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 94 | yield return new object[] { 3.2, 2.2, 5.2, 4.2, false, false }; 95 | yield return new object[] { 1.2, 2.2, 5.2, 6.2, true, true }; 96 | 97 | // decimal - minimumRange <= minimum <= maximum <= maximumRange, minimumIsValid, maximumIsValid 98 | yield return new object[] { 3.2m, 2.2m, 5.2m, 4.2m, false, false }; 99 | yield return new object[] { 1.2m, 2.2m, 5.2m, 6.2m, true, true }; 100 | } 101 | 102 | public class Model 103 | where T : IComparable 104 | { 105 | public Model(T? minimum, T? maximum) 106 | { 107 | Minimum = minimum; 108 | Maximum = maximum; 109 | } 110 | 111 | public T? Minimum { get; set; } 112 | 113 | public T? Maximum { get; set; } 114 | } 115 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/RangeAttributes/DynamicRangeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests; 8 | 9 | public class DynamicRangeTests 10 | { 11 | [Fact(DisplayName = "Should not be valid if 'MinimumWeight' is greater than 'MaximumWeight'")] 12 | public static void ShouldNotBeValidIfMinimumIsGreaterThanMaximum() 13 | { 14 | var data = new Model(2, 1); 15 | var context = new ValidationContext(data); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(data, context, results, true); 19 | 20 | isValid.Should().BeFalse(); 21 | results.Should().HaveCount(2); 22 | results[0].ErrorMessage.Should().Be("The field MinimumWeight must be between 0.1 and 1."); 23 | results[1].ErrorMessage.Should().Be("The field MaximumWeight must be between 2 and 5.2."); 24 | } 25 | 26 | [Fact(DisplayName = "Should be valid if 'MaximumWeight' is greater than 'MinimumWeight'")] 27 | public static void ShouldBeValidIfMaximumIsGreaterThanMinimum() 28 | { 29 | var data = new Model(1, 2); 30 | var context = new ValidationContext(data); 31 | var results = new List(); 32 | 33 | var isValid = Validator.TryValidateObject(data, context, results, true); 34 | 35 | isValid.Should().BeTrue(); 36 | } 37 | 38 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum Property Value' is null")] 39 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyValueIsNull() 40 | { 41 | var data = new Model(null, 2); 42 | var context = new ValidationContext(data); 43 | var attribute = new DynamicRangeAttribute(typeof(decimal), "MinimumWeight", 5.2); 44 | 45 | var action = () => attribute.Validate("Any", context); 46 | 47 | action.Should() 48 | .Throw() 49 | .WithMessage("The value of 'MinimumWeight' property cannot be null (introduced for 'Minimum' in range)."); 50 | } 51 | 52 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum Property Value' is null")] 53 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyValueIsNull() 54 | { 55 | var data = new Model(2, null); 56 | var context = new ValidationContext(data); 57 | var attribute = new DynamicRangeAttribute(typeof(decimal), 0.1, "MaximumWeight"); 58 | 59 | var action = () => attribute.Validate("Any", context); 60 | 61 | action.Should() 62 | .Throw() 63 | .WithMessage("The value of 'MaximumWeight' property cannot be null (introduced for 'Maximum' in range)."); 64 | } 65 | 66 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum PropertyName' is null or empty")] 67 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyNameIsNullOrEmpty() 68 | { 69 | var attribute = new DynamicRangeAttribute(typeof(decimal), string.Empty, 5.2); 70 | 71 | var action = () => attribute.Validate("Any", new ValidationContext(new object())); 72 | 73 | action.Should() 74 | .Throw() 75 | .WithMessage("The 'Minimum PropertyName' cannot be null or empty."); 76 | } 77 | 78 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum PropertyName' is null or empty")] 79 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyNameIsNullOrEmpty() 80 | { 81 | var attribute = new DynamicRangeAttribute(typeof(decimal), 0.1, string.Empty); 82 | 83 | var action = () => attribute.Validate("Any", new ValidationContext(new object())); 84 | 85 | action.Should() 86 | .Throw() 87 | .WithMessage("The 'Maximum PropertyName' cannot be null or empty."); 88 | } 89 | 90 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum' PropertyName is wrong")] 91 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyNameIsWrong() 92 | { 93 | var data = new Model(1, 2); 94 | var context = new ValidationContext(data); 95 | var attribute = new DynamicRangeAttribute(typeof(decimal), "WRONG", 5.2); 96 | 97 | var action = () => attribute.Validate("Any", context); 98 | 99 | action.Should() 100 | .Throw() 101 | .WithMessage("The 'WRONG' property not found (introduced for 'Minimum' in range)."); 102 | } 103 | 104 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum' PropertyName is wrong")] 105 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyNameIsWrong() 106 | { 107 | var data = new Model(1, 2); 108 | var context = new ValidationContext(data); 109 | var attribute = new DynamicRangeAttribute(typeof(decimal), 0.1, "WRONG"); 110 | 111 | var action = () => attribute.Validate("Any", context); 112 | 113 | action.Should() 114 | .Throw() 115 | .WithMessage("The 'WRONG' property not found (introduced for 'Maximum' in range)."); 116 | } 117 | 118 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum' PropertyType and OperandType not the same")] 119 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyTypeAndOperandTypeNotTheSame() 120 | { 121 | var data = new ModelWrongMinimumPropertyType(1, 5); 122 | var context = new ValidationContext(data); 123 | 124 | var action = () => Validator.ValidateObject(data, context, true); 125 | 126 | action.Should() 127 | .Throw() 128 | .WithMessage("The 'MinimumWeight' type must be the same as the OperandType (introduced for 'Minimum' in range)."); 129 | } 130 | 131 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum' PropertyType and OperandType not the same")] 132 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyTypeAndOperandTypeNotTheSame() 133 | { 134 | var data = new ModelWrongMaximumPropertyType(1, 5); 135 | var context = new ValidationContext(data); 136 | 137 | var action = () => Validator.ValidateObject(data, context, true); 138 | 139 | action.Should() 140 | .Throw() 141 | .WithMessage("The 'MaximumWeight' type must be the same as the OperandType (introduced for 'Maximum' in range)."); 142 | } 143 | 144 | public class Model 145 | { 146 | public Model(decimal? minimumWeight, decimal? maximumWeight) 147 | { 148 | MinimumWeight = minimumWeight; 149 | MaximumWeight = maximumWeight; 150 | } 151 | 152 | [DynamicRange(type: typeof(decimal), minimum: 0.1, maximumPropertyName: nameof(MaximumWeight))] 153 | public decimal? MinimumWeight { get; set; } 154 | 155 | [DynamicRange(type: typeof(decimal), minimumPropertyName: nameof(MinimumWeight), maximum: 5.2)] 156 | public decimal? MaximumWeight { get; set; } 157 | } 158 | 159 | public class ModelWrongMaximumPropertyType 160 | { 161 | public ModelWrongMaximumPropertyType(decimal? minimumWeight, double? maximumWeight) 162 | { 163 | MinimumWeight = minimumWeight; 164 | MaximumWeight = maximumWeight; 165 | } 166 | 167 | [DynamicRange(type: typeof(decimal), minimum: 0.1, maximumPropertyName: nameof(MaximumWeight))] 168 | public decimal? MinimumWeight { get; set; } 169 | 170 | public double? MaximumWeight { get; set; } 171 | } 172 | 173 | public class ModelWrongMinimumPropertyType 174 | { 175 | public ModelWrongMinimumPropertyType(double? minimumWeight, decimal? maximumWeight) 176 | { 177 | MinimumWeight = minimumWeight; 178 | MaximumWeight = maximumWeight; 179 | } 180 | 181 | public double? MinimumWeight { get; set; } 182 | 183 | [DynamicRange(type: typeof(decimal), minimumPropertyName: nameof(MinimumWeight), maximum: 5.2)] 184 | public decimal? MaximumWeight { get; set; } 185 | } 186 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.Tests/RangeAttributes/GenericDynamicRangeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace LinkDotNet.ValidationExtensions.Tests.Generic; 8 | 9 | public class GenericDynamicRangeTests 10 | { 11 | [Fact(DisplayName = "Should not be valid if 'MinimumWeight' is greater than 'MaximumWeight'")] 12 | public static void ShouldNotBeValidIfMinimumIsGreaterThanMaximum() 13 | { 14 | var data = new Model(2, 1); 15 | var context = new ValidationContext(data); 16 | var results = new List(); 17 | 18 | var isValid = Validator.TryValidateObject(data, context, results, true); 19 | 20 | isValid.Should().BeFalse(); 21 | results.Should().HaveCount(2); 22 | results[0].ErrorMessage.Should().Be("The field MinimumWeight must be between 0.1 and 1."); 23 | results[1].ErrorMessage.Should().Be("The field MaximumWeight must be between 2 and 5.2."); 24 | } 25 | 26 | [Fact(DisplayName = "Should be valid if 'MaximumWeight' is greater than 'MinimumWeight'")] 27 | public static void ShouldBeValidIfMaximumIsGreaterThanMinimum() 28 | { 29 | var data = new Model(1, 2); 30 | var context = new ValidationContext(data); 31 | var results = new List(); 32 | 33 | var isValid = Validator.TryValidateObject(data, context, results, true); 34 | 35 | isValid.Should().BeTrue(); 36 | } 37 | 38 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum Property Value' is null")] 39 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyValueIsNull() 40 | { 41 | var data = new Model(null, 2); 42 | var context = new ValidationContext(data); 43 | var attribute = new DynamicRangeAttribute("MinimumWeight", 5.2); 44 | 45 | var action = () => attribute.Validate("Any", context); 46 | 47 | action.Should() 48 | .Throw() 49 | .WithMessage("The value of 'MinimumWeight' property cannot be null (introduced for 'Minimum' in range)."); 50 | } 51 | 52 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum Property Value' is null")] 53 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyValueIsNull() 54 | { 55 | var data = new Model(2, null); 56 | var context = new ValidationContext(data); 57 | var attribute = new DynamicRangeAttribute(0.1, "MaximumWeight"); 58 | 59 | var action = () => attribute.Validate("Any", context); 60 | 61 | action.Should() 62 | .Throw() 63 | .WithMessage("The value of 'MaximumWeight' property cannot be null (introduced for 'Maximum' in range)."); 64 | } 65 | 66 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum PropertyName' is null or empty")] 67 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyNameIsNullOrEmpty() 68 | { 69 | var attribute = new DynamicRangeAttribute(string.Empty, 5.2); 70 | 71 | var action = () => attribute.Validate("Any", new ValidationContext(new object())); 72 | 73 | action.Should() 74 | .Throw() 75 | .WithMessage("The 'Minimum PropertyName' cannot be null or empty."); 76 | } 77 | 78 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum PropertyName' is null or empty")] 79 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyNameIsNullOrEmpty() 80 | { 81 | var attribute = new DynamicRangeAttribute(0.1, string.Empty); 82 | 83 | var action = () => attribute.Validate("Any", new ValidationContext(new object())); 84 | 85 | action.Should() 86 | .Throw() 87 | .WithMessage("The 'Maximum PropertyName' cannot be null or empty."); 88 | } 89 | 90 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum' PropertyName is wrong")] 91 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyNameIsWrong() 92 | { 93 | var data = new Model(1, 2); 94 | var context = new ValidationContext(data); 95 | var attribute = new DynamicRangeAttribute("WRONG", 5.2); 96 | 97 | var action = () => attribute.Validate("Any", context); 98 | 99 | action.Should() 100 | .Throw() 101 | .WithMessage("The 'WRONG' property not found (introduced for 'Minimum' in range)."); 102 | } 103 | 104 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum' PropertyName is wrong")] 105 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyNameIsWrong() 106 | { 107 | var data = new Model(1, 2); 108 | var context = new ValidationContext(data); 109 | var attribute = new DynamicRangeAttribute(0.1, "WRONG"); 110 | 111 | var action = () => attribute.Validate("Any", context); 112 | 113 | action.Should() 114 | .Throw() 115 | .WithMessage("The 'WRONG' property not found (introduced for 'Maximum' in range)."); 116 | } 117 | 118 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Minimum' PropertyType and OperandType not the same")] 119 | public static void MustThrowsInvalidOperationExceptionWhenMinimumPropertyTypeAndOperandTypeNotTheSame() 120 | { 121 | var data = new ModelWrongMinimumPropertyType(1, 5); 122 | var context = new ValidationContext(data); 123 | 124 | var action = () => Validator.ValidateObject(data, context, true); 125 | 126 | action.Should() 127 | .Throw() 128 | .WithMessage("The 'MinimumWeight' type must be the same as the OperandType (introduced for 'Minimum' in range)."); 129 | } 130 | 131 | [Fact(DisplayName = "Must throws 'InvalidOperationException' when 'Maximum' PropertyType and OperandType not the same")] 132 | public static void MustThrowsInvalidOperationExceptionWhenMaximumPropertyTypeAndOperandTypeNotTheSame() 133 | { 134 | var data = new ModelWrongMaximumPropertyType(1, 5); 135 | var context = new ValidationContext(data); 136 | 137 | var action = () => Validator.ValidateObject(data, context, true); 138 | 139 | action.Should() 140 | .Throw() 141 | .WithMessage("The 'MaximumWeight' type must be the same as the OperandType (introduced for 'Maximum' in range)."); 142 | } 143 | 144 | public class Model 145 | { 146 | public Model(double? minimumWeight, double? maximumWeight) 147 | { 148 | MinimumWeight = minimumWeight; 149 | MaximumWeight = maximumWeight; 150 | } 151 | 152 | [DynamicRange(minimum: 0.1, maximumPropertyName: nameof(MaximumWeight))] 153 | public double? MinimumWeight { get; set; } 154 | 155 | [DynamicRange(minimumPropertyName: nameof(MinimumWeight), maximum: 5.2)] 156 | public double? MaximumWeight { get; set; } 157 | } 158 | 159 | public class ModelWrongMaximumPropertyType 160 | { 161 | public ModelWrongMaximumPropertyType(double? minimumWeight, float? maximumWeight) 162 | { 163 | MinimumWeight = minimumWeight; 164 | MaximumWeight = maximumWeight; 165 | } 166 | 167 | [DynamicRange(minimum: 0.1, maximumPropertyName: nameof(MaximumWeight))] 168 | public double? MinimumWeight { get; set; } 169 | 170 | public float? MaximumWeight { get; set; } 171 | } 172 | 173 | public class ModelWrongMinimumPropertyType 174 | { 175 | public ModelWrongMinimumPropertyType(float? minimumWeight, double? maximumWeight) 176 | { 177 | MinimumWeight = minimumWeight; 178 | MaximumWeight = maximumWeight; 179 | } 180 | 181 | public float? MinimumWeight { get; set; } 182 | 183 | [DynamicRange(minimumPropertyName: nameof(MinimumWeight), maximum: 5.2)] 184 | public double? MaximumWeight { get; set; } 185 | } 186 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.ValidationExtensions", "LinkDotNet.ValidationExtensions\LinkDotNet.ValidationExtensions.csproj", "{CC7ABFDD-9FD3-487B-A489-1238A2F7AE5C}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.ValidationExtensions.Tests", "LinkDotNet.ValidationExtensions.Tests\LinkDotNet.ValidationExtensions.Tests.csproj", "{7C762C04-2533-4E2D-A49B-199EABE002C7}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".text", ".text", "{B912949A-A456-4678-9040-1C04D83B3A25}" 8 | ProjectSection(SolutionItems) = preProject 9 | CHANGELOG.md = CHANGELOG.md 10 | Readme.md = Readme.md 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "Sample\Sample.csproj", "{11104103-D978-4D5C-BA4F-BE3447D8E390}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {CC7ABFDD-9FD3-487B-A489-1238A2F7AE5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {CC7ABFDD-9FD3-487B-A489-1238A2F7AE5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {CC7ABFDD-9FD3-487B-A489-1238A2F7AE5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {CC7ABFDD-9FD3-487B-A489-1238A2F7AE5C}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {7C762C04-2533-4E2D-A49B-199EABE002C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {7C762C04-2533-4E2D-A49B-199EABE002C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {7C762C04-2533-4E2D-A49B-199EABE002C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {7C762C04-2533-4E2D-A49B-199EABE002C7}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {11104103-D978-4D5C-BA4F-BE3447D8E390}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {11104103-D978-4D5C-BA4F-BE3447D8E390}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {11104103-D978-4D5C-BA4F-BE3447D8E390}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {11104103-D978-4D5C-BA4F-BE3447D8E390}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/DateAttributes/FutureDateValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | public sealed class FutureDateValidationAttribute : ValidationAttribute 6 | { 7 | public bool UseUtc { get; set; } = true; 8 | 9 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 10 | { 11 | if (value is null) 12 | { 13 | return ValidationResult.Success; 14 | } 15 | 16 | var now = UseUtc ? DateTime.UtcNow : DateTime.Now; 17 | 18 | return (DateTime)value <= now 19 | ? new ValidationResult(ErrorMessage ?? "The selected date must be in the future.") 20 | : ValidationResult.Success; 21 | } 22 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/DateAttributes/PastDateValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | public sealed class PastDateValidationAttribute : ValidationAttribute 6 | { 7 | public bool UseUtc { get; set; } = true; 8 | 9 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 10 | { 11 | if (value is null) 12 | { 13 | return ValidationResult.Success; 14 | } 15 | 16 | var now = UseUtc ? DateTime.UtcNow : DateTime.Now; 17 | 18 | return (DateTime)value >= now 19 | ? new ValidationResult(ErrorMessage ?? "The scheduled publish date must be in the future.") 20 | : ValidationResult.Success; 21 | } 22 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/Dynamic/RequiredDynamicAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Reflection; 3 | 4 | namespace LinkDotNet.ValidationExtensions; 5 | 6 | /// 7 | /// Validation attribute that can accept method to validate complicated requirement(s) and more readable code. 8 | /// 9 | public sealed class RequiredDynamicAttribute : ValidationAttribute 10 | { 11 | private static readonly List BindingFlagsForSearch = new() 12 | { 13 | BindingFlags.Static | BindingFlags.Public, 14 | BindingFlags.Static | BindingFlags.NonPublic, 15 | BindingFlags.Instance | BindingFlags.Public, 16 | BindingFlags.Instance | BindingFlags.NonPublic, 17 | }; 18 | 19 | private readonly string methodName; 20 | 21 | /// 22 | /// Initializes a new instance of the class. 23 | /// 24 | /// Name of method that check requirement(s). 25 | /// The method must have one parameter of owning type and return Boolean value. 26 | /// If requirement(s) not met return true; else return false. 27 | /// The error message to associate with a validation control. 28 | /// Final error message is: 'Parameter name' is required because 'errorMessage'. 29 | /// methodName can not be null. 30 | /// errorMessage can not be null. 31 | public RequiredDynamicAttribute(string methodName, string errorMessage) 32 | : base(errorMessage) 33 | { 34 | if (string.IsNullOrWhiteSpace(errorMessage)) 35 | { 36 | throw new ArgumentNullException(nameof(errorMessage)); 37 | } 38 | 39 | this.methodName = methodName ?? throw new ArgumentNullException(nameof(methodName)); 40 | } 41 | 42 | /// 43 | public override string FormatErrorMessage(string name) 44 | { 45 | return $"{name} is required because {ErrorMessageString}"; 46 | } 47 | 48 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 49 | { 50 | return IsRequired(validationContext, methodName) 51 | ? new ValidationResult(FormatErrorMessage(validationContext.DisplayName)) 52 | : ValidationResult.Success; 53 | } 54 | 55 | private static bool IsRequired(ValidationContext validationContext, string methodName) 56 | { 57 | ArgumentNullExceptionHelper.ThrowIfNull(validationContext, nameof(validationContext)); 58 | ArgumentNullExceptionHelper.ThrowIfNullOrEmpty(methodName, nameof(methodName)); 59 | 60 | var owningType = validationContext.ObjectType; 61 | var methodInfo = GetSuitableMethod(owningType, methodName); 62 | if (methodInfo == null) 63 | { 64 | throw new NotSupportedException($"Can't find {methodName} suitable method on searched type: {owningType.Name}"); 65 | } 66 | 67 | return Convert.ToBoolean(methodInfo.Invoke(validationContext.ObjectInstance, new[] { validationContext.ObjectInstance })); 68 | } 69 | 70 | private static MethodInfo? GetSuitableMethod(Type owningType, string methodName) 71 | { 72 | ArgumentNullExceptionHelper.ThrowIfNull(owningType, nameof(owningType)); 73 | ArgumentNullExceptionHelper.ThrowIfNullOrEmpty(methodName, nameof(methodName)); 74 | 75 | return BindingFlagsForSearch 76 | .Select(bindingFlag => owningType.GetMethod(methodName, bindingFlag)) 77 | .FirstOrDefault(methodInfo => IsValidMethod(methodInfo, owningType)); 78 | } 79 | 80 | private static bool IsValidMethod(MethodInfo? methodInfo, Type owningType) 81 | { 82 | if (methodInfo == null) 83 | { 84 | return false; 85 | } 86 | 87 | var parameters = methodInfo.GetParameters(); 88 | if (parameters.Length != 1 || parameters[0].ParameterType != owningType) 89 | { 90 | return false; 91 | } 92 | 93 | return methodInfo.ReturnType == typeof(bool); 94 | } 95 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/Helpers/ArgumentNullExceptionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #if NET6_0_OR_GREATER 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Runtime.CompilerServices; 5 | #endif 6 | 7 | namespace LinkDotNet.ValidationExtensions; 8 | 9 | internal static class ArgumentNullExceptionHelper 10 | { 11 | #if !NET7_0_OR_GREATER 12 | private static readonly string ArgumentEmptyString = "The value cannot be an empty string."; 13 | #endif 14 | 15 | #if NET7_0_OR_GREATER 16 | internal static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null) 17 | => ArgumentNullException.ThrowIfNull(argument, paramName); 18 | #elif NET6_0 19 | internal static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null) 20 | => ArgumentNullException.ThrowIfNull(argument, paramName); 21 | #else 22 | internal static void ThrowIfNull(object? argument, string paramName) 23 | { 24 | if (argument is null) 25 | { 26 | throw new ArgumentNullException(paramName); 27 | } 28 | } 29 | #endif 30 | 31 | #if NET7_0_OR_GREATER 32 | internal static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression("argument")] string? paramName = null) 33 | => ArgumentException.ThrowIfNullOrEmpty(argument, paramName); 34 | #elif NET6_0 35 | internal static void ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression("argument")] string? paramName = null) 36 | { 37 | if (string.IsNullOrEmpty(argument)) 38 | { 39 | ThrowNullOrEmptyException(argument, paramName); 40 | } 41 | } 42 | 43 | [DoesNotReturn] 44 | private static void ThrowNullOrEmptyException(string? argument, string? paramName) 45 | { 46 | ArgumentNullException.ThrowIfNull(argument, paramName); 47 | throw new ArgumentException(ArgumentEmptyString, paramName); 48 | } 49 | #else 50 | internal static void ThrowIfNullOrEmpty(string? argument, string paramName) 51 | { 52 | if (string.IsNullOrEmpty(argument)) 53 | { 54 | ThrowNullOrEmptyException(argument, paramName); 55 | } 56 | } 57 | 58 | private static void ThrowNullOrEmptyException(string? argument, string paramName) 59 | { 60 | ThrowIfNull(argument, paramName); 61 | throw new ArgumentException(ArgumentEmptyString, paramName); 62 | } 63 | #endif 64 | 65 | } 66 | -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/IfHelper.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | internal static class IfHelper 6 | { 7 | public static bool IsRequired( 8 | this ValidationContext? validationContext, 9 | string propertyName, 10 | object? requiredValue, 11 | bool invert) 12 | { 13 | ArgumentNullExceptionHelper.ThrowIfNull(validationContext, nameof(validationContext)); 14 | ArgumentNullExceptionHelper.ThrowIfNullOrEmpty(propertyName, nameof(propertyName)); 15 | 16 | var owningType = validationContext!.ObjectType; 17 | var property = owningType.GetProperty(propertyName); 18 | 19 | if (property == null) 20 | { 21 | throw new NotSupportedException($"Can't find {propertyName} on searched type: {owningType.Name}"); 22 | } 23 | 24 | var requiredIfTypeActualValue = property.GetValue(validationContext.ObjectInstance); 25 | 26 | if (requiredIfTypeActualValue == null && requiredValue != null) 27 | { 28 | return false; 29 | } 30 | 31 | if (!invert) 32 | { 33 | return requiredIfTypeActualValue == null || requiredIfTypeActualValue.Equals(requiredValue); 34 | } 35 | 36 | return requiredIfTypeActualValue != null && !requiredIfTypeActualValue.Equals(requiredValue); 37 | } 38 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/MaxIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a minimum value when the condition is (not) met. 7 | /// 8 | public sealed class MaxIfAttribute : MaxAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// The inclusive maximum value. 20 | /// If set to true, the value is not required when is 21 | /// not . 22 | public MaxIfAttribute(string propertyName, object isValue, int maximum, bool inverse = false) 23 | : base(maximum) 24 | { 25 | this.isValue = isValue; 26 | this.inverse = inverse; 27 | this.propertyName = propertyName; 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// Name of the depending property. 34 | /// Required value. If is then the property is not required. 35 | /// The inclusive maximum value. 36 | /// If set to true, the value is not required when is 37 | /// not . 38 | public MaxIfAttribute(string propertyName, object isValue, double maximum, bool inverse = false) 39 | : base(maximum) 40 | { 41 | this.isValue = isValue; 42 | this.inverse = inverse; 43 | this.propertyName = propertyName; 44 | } 45 | 46 | /// 47 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 48 | { 49 | var shouldCheck = validationContext.IsRequired(propertyName, isValue, inverse); 50 | 51 | return !shouldCheck ? ValidationResult.Success : base.IsValid(value, validationContext); 52 | } 53 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/MaxLengthIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Specifies the maximum length of collection/string data allowed in a property when the condition is (not) met. 7 | /// 8 | public sealed class MaxLengthIfAttribute : MaxLengthAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// The maximum allowable length of collection/string data. Value must be greater than or equal to zero. 20 | /// If set to true, the value is not required when is 21 | /// not . 22 | public MaxLengthIfAttribute(string propertyName, object? isValue, int length, bool inverse = false) 23 | : base(length) 24 | { 25 | this.isValue = isValue; 26 | this.inverse = inverse; 27 | this.propertyName = propertyName; 28 | } 29 | 30 | /// 31 | public override string FormatErrorMessage(string name) 32 | { 33 | var inverseString = !inverse ? string.Empty : "not "; 34 | return $"The field '{name}' must be a string or array type with a maximum length of '{Length}' when '{propertyName}' is {inverseString}'{isValue}'."; 35 | } 36 | 37 | /// 38 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 39 | { 40 | var shouldCheck = validationContext.IsRequired(propertyName, isValue, inverse); 41 | 42 | return !shouldCheck ? ValidationResult.Success : base.IsValid(value, validationContext); 43 | } 44 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/MinIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a minimum value when the condition is (not) met. 7 | /// 8 | public sealed class MinIfAttribute : MinAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// The inclusive minimum value. 20 | /// If set to true, the value is not required when is 21 | /// not . 22 | public MinIfAttribute(string propertyName, object isValue, int minimum, bool inverse = false) 23 | : base(minimum) 24 | { 25 | this.isValue = isValue; 26 | this.inverse = inverse; 27 | this.propertyName = propertyName; 28 | } 29 | 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | /// Name of the depending property. 34 | /// Required value. If is then the property is not required. 35 | /// The inclusive minimum value. 36 | /// If set to true, the value is not required when is 37 | /// not . 38 | public MinIfAttribute(string propertyName, object isValue, double minimum, bool inverse = false) 39 | : base(minimum) 40 | { 41 | this.isValue = isValue; 42 | this.inverse = inverse; 43 | this.propertyName = propertyName; 44 | } 45 | 46 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 47 | { 48 | var shouldCheck = validationContext.IsRequired(propertyName, isValue, inverse); 49 | 50 | return !shouldCheck ? ValidationResult.Success : base.IsValid(value, validationContext); 51 | } 52 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/MinLengthIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Specifies the minimum length of collection/string data allowed in a property when the condition is (not) met. 7 | /// 8 | public sealed class MinLengthIfAttribute : MinLengthAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// The minimum allowable length of collection/string data. Value must be greater than or equal to zero. 20 | /// If set to true, the value is not required when is 21 | /// not . 22 | public MinLengthIfAttribute(string propertyName, object? isValue, int length, bool inverse = false) 23 | : base(length) 24 | { 25 | this.isValue = isValue; 26 | this.inverse = inverse; 27 | this.propertyName = propertyName; 28 | } 29 | 30 | /// 31 | public override string FormatErrorMessage(string name) 32 | { 33 | var inverseString = !inverse ? string.Empty : "not "; 34 | return $"The field '{name}' must be a string or array type with a minimum length of '{Length}' when '{propertyName}' is {inverseString}'{isValue}'."; 35 | } 36 | 37 | /// 38 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 39 | { 40 | var shouldCheck = validationContext.IsRequired(propertyName, isValue, inverse); 41 | 42 | return !shouldCheck ? ValidationResult.Success : base.IsValid(value, validationContext); 43 | } 44 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/RangeIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a range constraint when the condition is (not) met. 7 | /// 8 | public sealed class RangeIfAttribute : RangeAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// The minimum value, inclusive. 20 | /// The maximum value, inclusive. 21 | /// If set to true, the value is not required when is 22 | /// not . 23 | public RangeIfAttribute(string propertyName, object? isValue, double minimum, double maximum, bool inverse = false) 24 | : base(minimum, maximum) 25 | { 26 | this.inverse = inverse; 27 | this.propertyName = propertyName; 28 | this.isValue = isValue; 29 | } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// Name of the depending property. 35 | /// Required value. If is then the property is not required. 36 | /// The minimum value, inclusive. 37 | /// The maximum value, inclusive. 38 | /// If set to true, the value is not required when is 39 | /// not . 40 | public RangeIfAttribute(string propertyName, object? isValue, int minimum, int maximum, bool inverse = false) 41 | : base(minimum, maximum) 42 | { 43 | this.inverse = inverse; 44 | this.propertyName = propertyName; 45 | this.isValue = isValue; 46 | } 47 | 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// Name of the depending property. 52 | /// Required value. If is then the property is not required. 53 | /// The type of the range parameters. Must implement IComparable. 54 | /// The minimum value, inclusive. 55 | /// The maximum value, inclusive. 56 | /// If set to true, the value is not required when is 57 | /// not . 58 | public RangeIfAttribute(string propertyName, object? isValue, Type type, string minimum, string maximum, bool inverse = false) 59 | : base(type, minimum, maximum) 60 | { 61 | this.inverse = inverse; 62 | this.propertyName = propertyName; 63 | this.isValue = isValue; 64 | } 65 | 66 | /// 67 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 68 | { 69 | var shouldCheck = validationContext.IsRequired(propertyName, isValue, inverse); 70 | 71 | return !shouldCheck ? ValidationResult.Success : base.IsValid(value, validationContext); 72 | } 73 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/If/RequiredIfAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Validation attribute to indicate that a property field or parameter is required when the condition is (not) met. 7 | /// 8 | public sealed class RequiredIfAttribute : ValidationAttribute 9 | { 10 | private readonly string propertyName; 11 | private readonly object? isValue; 12 | private readonly bool inverse; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// Name of the depending property. 18 | /// Required value. If is then the property is not required. 19 | /// If set to true, the value is not required when is 20 | /// not . 21 | public RequiredIfAttribute(string propertyName, object? isValue, bool inverse = false) 22 | { 23 | this.propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); 24 | this.isValue = isValue; 25 | this.inverse = inverse; 26 | } 27 | 28 | /// 29 | public override string FormatErrorMessage(string name) 30 | { 31 | var inverseString = !inverse ? string.Empty : "not "; 32 | var errorMessage = $"Property '{name}' is required when '{propertyName}' is {inverseString}'{isValue}'"; 33 | return ErrorMessage ?? errorMessage; 34 | } 35 | 36 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 37 | { 38 | var validationResult = ValidationResult.Success; 39 | var isRequired = validationContext.IsRequired(propertyName, isValue, inverse); 40 | 41 | if (!isRequired) 42 | { 43 | return ValidationResult.Success; 44 | } 45 | 46 | if (value == null) 47 | { 48 | var memberNames = validationContext.MemberName != null 49 | ? new[] { validationContext.MemberName } 50 | : null; 51 | 52 | validationResult = new ValidationResult(FormatErrorMessage(validationContext.DisplayName), memberNames); 53 | } 54 | 55 | return validationResult; 56 | } 57 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/LinkDotNet.ValidationExtensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net6.0;net7.0;net8.0;net9.0 5 | 13.0 6 | enable 7 | enable 8 | ValidationExtensions 9 | Steven Giesel (linkdotnet) 10 | Offers new attributes for Blazor validation 11 | Steven Giesel (linkdotnet) 12 | https://github.com/linkdotnet/ValidationExtensions 13 | https://github.com/linkdotnet/ValidationExtensions/LICENSE 14 | https://github.com/linkdotnet/ValidationExtensions 15 | Blazor,Validation,Attribute 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | all 29 | runtime; build; native; contentfiles; analyzers; buildtransitive 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | True 38 | \ 39 | 40 | 41 | 42 | 43 | $(SolutionDir)\stylecop.analyzers.ruleset 44 | Readme.md 45 | 46 | 47 | -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/RangeAttributes/DynamicRangeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a range that accepts another property-name for 'Minimum' or 'Maximum'. 7 | /// 8 | public sealed class DynamicRangeAttribute : ValidationAttribute 9 | { 10 | private readonly Func getMinimum; 11 | private readonly Func getMaximum; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 16 | /// will be converted to the target type. 17 | /// 18 | /// The type of the range parameters. Must implement IComparable. 19 | /// The property-name of minimum. 20 | /// The property-name of maximum. 21 | public DynamicRangeAttribute(Type type, string minimumPropertyName, string maximumPropertyName) 22 | : base() 23 | { 24 | OperandType = type; 25 | getMinimum = validationContext => GetActualValue(validationContext, OperandType, "Minimum", minimumPropertyName); 26 | getMaximum = validationContext => GetActualValue(validationContext, OperandType, "Maximum", maximumPropertyName); 27 | } 28 | 29 | /// 30 | /// Initializes a new instance of the class. 31 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 32 | /// will be converted to the target type. 33 | /// 34 | /// The type of the range parameters. Must implement IComparable. 35 | /// The property-name of minimum. 36 | /// The maximum allowable value. 37 | public DynamicRangeAttribute(Type type, string minimumPropertyName, object maximum) 38 | : base() 39 | { 40 | OperandType = type; 41 | getMinimum = validationContext => GetActualValue(validationContext, OperandType, "Minimum", minimumPropertyName); 42 | getMaximum = _ => maximum; 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 48 | /// will be converted to the target type. 49 | /// 50 | /// The type of the range parameters. Must implement IComparable. 51 | /// The minimum allowable value. 52 | /// The property-name of maximum. 53 | public DynamicRangeAttribute(Type type, object minimum, string maximumPropertyName) 54 | : base() 55 | { 56 | OperandType = type; 57 | getMinimum = _ => minimum; 58 | getMaximum = validationContext => GetActualValue(validationContext, OperandType, "Maximum", maximumPropertyName); 59 | } 60 | 61 | /// 62 | /// Gets the type of the and values 63 | /// (e.g. Int32, Double, or some custom type). 64 | /// 65 | /// The type of the and values. 66 | public Type OperandType { get; } 67 | 68 | /// 69 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 70 | { 71 | var minimumAsText = getMinimum.Invoke(validationContext).ToString()!; 72 | var maximumAsText = getMaximum.Invoke(validationContext).ToString()!; 73 | 74 | // Create RangeAttribute instance for validate value in actual range 75 | var rangeAttribute = new RangeAttribute(OperandType, minimumAsText, maximumAsText); 76 | 77 | if (ErrorMessage is not null) 78 | { 79 | rangeAttribute.ErrorMessage = ErrorMessage; 80 | } 81 | 82 | if (ErrorMessageResourceName is not null) 83 | { 84 | rangeAttribute.ErrorMessageResourceName = ErrorMessageResourceName; 85 | } 86 | 87 | if (ErrorMessageResourceType is not null) 88 | { 89 | rangeAttribute.ErrorMessageResourceType = ErrorMessageResourceType; 90 | } 91 | 92 | return rangeAttribute.GetValidationResult(value, validationContext); 93 | } 94 | 95 | /// 96 | /// Get actual value of subject; subject is 'Minimum' or 'Maximum'. 97 | /// 98 | /// 99 | /// A instance that provides 100 | /// context about the validation operation, such as the object and member being validated. 101 | /// 102 | /// The type of subject. 103 | /// The name of subject. 104 | /// The propertyName or value of subject. 105 | /// The actual value of subject. 106 | /// is thrown when propertyNameOrValue is null or nothing. 107 | /// is thrown when PropertyType of propertyName has not been the same as OperandType. 108 | private static object GetActualValue(ValidationContext validationContext, Type subjectType, string subjectName, string propertyName) 109 | { 110 | if (string.IsNullOrWhiteSpace(propertyName)) 111 | { 112 | throw new InvalidOperationException($"The '{subjectName} PropertyName' cannot be null or empty."); 113 | } 114 | 115 | var propertyInfo = validationContext.ObjectType.GetProperty(propertyName); 116 | 117 | if (propertyInfo is null) 118 | { 119 | throw new InvalidOperationException($"The '{propertyName}' property not found (introduced for '{subjectName}' in range)."); 120 | } 121 | 122 | var propertyType = propertyInfo.PropertyType; 123 | if (propertyType != subjectType) 124 | { 125 | var isNullableType = propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>); 126 | 127 | if (isNullableType) 128 | { 129 | propertyType = propertyType.GetGenericArguments()[0]; 130 | } 131 | 132 | if (propertyType != subjectType) 133 | { 134 | throw new InvalidOperationException($"The '{propertyName}' type must be the same as the OperandType (introduced for '{subjectName}' in range)."); 135 | } 136 | } 137 | 138 | var value = propertyInfo.GetValue(validationContext.ObjectInstance); 139 | 140 | if (value is null) 141 | { 142 | throw new InvalidOperationException($"The value of '{propertyName}' property cannot be null (introduced for '{subjectName}' in range)."); 143 | } 144 | 145 | return value; 146 | } 147 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/RangeAttributes/GenericDynamicRangeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a range that accepts another property-name for 'Minimum' or 'Maximum'. 7 | /// 8 | /// 9 | /// The operand type of range. 10 | /// 11 | public sealed class DynamicRangeAttribute : ValidationAttribute 12 | where T : IComparable 13 | { 14 | private readonly Func getMinimum; 15 | private readonly Func getMaximum; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 20 | /// will be converted to the target type. 21 | /// 22 | /// The type of the range parameters. Must implement IComparable. 23 | /// The property-name of minimum. 24 | /// The property-name of maximum. 25 | public DynamicRangeAttribute(string minimumPropertyName, string maximumPropertyName) 26 | : base() 27 | { 28 | getMinimum = validationContext => GetActualValue(validationContext, "Minimum", minimumPropertyName); 29 | getMaximum = validationContext => GetActualValue(validationContext, "Maximum", maximumPropertyName); 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 35 | /// will be converted to the target type. 36 | /// 37 | /// The type of the range parameters. Must implement IComparable. 38 | /// The property-name of minimum. 39 | /// The maximum allowable value. 40 | public DynamicRangeAttribute(string minimumPropertyName, T maximum) 41 | : base() 42 | { 43 | getMinimum = validationContext => GetActualValue(validationContext, "Minimum", minimumPropertyName); 44 | getMaximum = _ => maximum; 45 | } 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 50 | /// will be converted to the target type. 51 | /// 52 | /// The type of the range parameters. Must implement IComparable. 53 | /// The minimum allowable value. 54 | /// The property-name of maximum. 55 | public DynamicRangeAttribute(T minimum, string maximumPropertyName) 56 | { 57 | getMinimum = _ => minimum; 58 | getMaximum = validationContext => GetActualValue(validationContext, "Maximum", maximumPropertyName); 59 | } 60 | 61 | /// 62 | /// Initializes a new instance of the class. 63 | /// Allows for specifying range for arbitrary types. The minimum and maximum strings 64 | /// will be converted to the target type. 65 | /// 66 | /// The type of the range parameters. Must implement IComparable. 67 | /// The minimum allowable value. 68 | /// The maximum allowable value. 69 | public DynamicRangeAttribute(T minimum, T maximum) 70 | : base() 71 | { 72 | getMinimum = _ => minimum; 73 | getMaximum = _ => maximum; 74 | } 75 | 76 | /// 77 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) 78 | { 79 | var operandType = typeof(T); 80 | var minimumAsText = getMinimum.Invoke(validationContext).ToString()!; 81 | var maximumAsText = getMaximum.Invoke(validationContext).ToString()!; 82 | 83 | // Create RangeAttribute instance for validate value in actual range 84 | var rangeAttribute = new RangeAttribute(operandType, minimumAsText, maximumAsText); 85 | 86 | if (ErrorMessage is not null) 87 | { 88 | rangeAttribute.ErrorMessage = ErrorMessage; 89 | } 90 | 91 | if (ErrorMessageResourceName is not null) 92 | { 93 | rangeAttribute.ErrorMessageResourceName = ErrorMessageResourceName; 94 | } 95 | 96 | if (ErrorMessageResourceType is not null) 97 | { 98 | rangeAttribute.ErrorMessageResourceType = ErrorMessageResourceType; 99 | } 100 | 101 | return rangeAttribute.GetValidationResult(value, validationContext); 102 | } 103 | 104 | /// 105 | /// Get actual value of subject; subject is 'Minimum' or 'Maximum'. 106 | /// 107 | /// 108 | /// A instance that provides 109 | /// context about the validation operation, such as the object and member being validated. 110 | /// 111 | /// The name of subject. 112 | /// The propertyName or value of subject. 113 | /// The actual value of subject. 114 | /// is thrown when propertyNameOrValue is null or nothing. 115 | /// is thrown when PropertyType of propertyName has not been the same as OperandType. 116 | private static T GetActualValue(ValidationContext validationContext, string subjectName, string propertyName) 117 | { 118 | if (string.IsNullOrWhiteSpace(propertyName)) 119 | { 120 | throw new InvalidOperationException($"The '{subjectName} PropertyName' cannot be null or empty."); 121 | } 122 | 123 | var propertyInfo = validationContext.ObjectType.GetProperty(propertyName); 124 | 125 | if (propertyInfo is null) 126 | { 127 | throw new InvalidOperationException($"The '{propertyName}' property not found (introduced for '{subjectName}' in range)."); 128 | } 129 | 130 | var subjectType = typeof(T); 131 | var propertyType = propertyInfo.PropertyType; 132 | if (propertyType != subjectType) 133 | { 134 | var isNullableType = propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>); 135 | 136 | if (isNullableType) 137 | { 138 | propertyType = propertyType.GetGenericArguments()[0]; 139 | } 140 | 141 | if (propertyType != subjectType) 142 | { 143 | throw new InvalidOperationException($"The '{propertyName}' type must be the same as the OperandType (introduced for '{subjectName}' in range)."); 144 | } 145 | } 146 | 147 | var value = propertyInfo.GetValue(validationContext.ObjectInstance); 148 | 149 | if (value is null) 150 | { 151 | throw new InvalidOperationException($"The value of '{propertyName}' property cannot be null (introduced for '{subjectName}' in range)."); 152 | } 153 | 154 | return (T)value; 155 | } 156 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/RangeAttributes/MaxAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a minimum value. 7 | /// 8 | public class MaxAttribute : RangeAttribute 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The inclusive maximum value. 14 | public MaxAttribute(int maximum) 15 | : base(int.MinValue, maximum) 16 | { 17 | } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The inclusive maximum value. 23 | public MaxAttribute(double maximum) 24 | : base(double.MinValue, maximum) 25 | { 26 | } 27 | } -------------------------------------------------------------------------------- /LinkDotNet.ValidationExtensions/RangeAttributes/MinAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace LinkDotNet.ValidationExtensions; 4 | 5 | /// 6 | /// Used for specifying a minimum value. 7 | /// 8 | public class MinAttribute : RangeAttribute 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// The inclusive minimum value. 14 | public MinAttribute(int minimum) 15 | : base(minimum, int.MaxValue) 16 | { 17 | } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The inclusive minimum value. 23 | public MinAttribute(double minimum) 24 | : base(minimum, double.MaxValue) 25 | { 26 | } 27 | 28 | /// 29 | public override string FormatErrorMessage(string name) 30 | { 31 | return $"The field '{name}' must be at least {Minimum}."; 32 | } 33 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ValidationExtensions 2 | 3 | [![.NET](https://github.com/linkdotnet/ValidationExtensions/actions/workflows/dotnet.yml/badge.svg)](https://github.com/linkdotnet/ValidationExtensions/actions/workflows/dotnet.yml) 4 | [![nuget](https://img.shields.io/nuget/v/LinkDotNet.ValidationExtensions)](https://www.nuget.org/packages/LinkDotNet.ValidationExtensions) 5 | 6 | The motivation behind this small project is simple. Just imagine you have the following model in Blazor: 7 | ```csharp 8 | public class MyModel 9 | { 10 | [Required] 11 | public string Title { get; set; } 12 | 13 | [Required] 14 | public string Content { get; set; } 15 | 16 | [Required] 17 | public bool IsPublished { get; set; } 18 | } 19 | ``` 20 | 21 | Now as a consumer you have to provide all of those 3 values. That is all good and nice, but what if we want to say: 22 | "Okay as long as it doesn't get published, we don't have to provide the content?". Well that does not work with the default implementation. 23 | 24 | Here is where this small library comes into play: 25 | ```csharp 26 | public class MyModel 27 | { 28 | [Required] 29 | public string Title { get; set; } 30 | 31 | [RequiredIf(nameof(IsPublished), true)] 32 | public string Content { get; set; } 33 | 34 | [Required] 35 | public bool IsPublished { get; set; } 36 | } 37 | ``` 38 | 39 | Now `Title` will always be required. But as long as `IsPublished` is false `Content` can be null or empty. 40 | 41 | ## Get Started 42 | To install either go the [nuget](https://www.nuget.org/packages/LinkDotNet.ValidationExtensions) or execute the following command: 43 | ``` 44 | dotnet add LinkDotNet.ValidationExtensions 45 | ``` 46 | 47 | ## Example 48 | ```csharp 49 | using LinkDotNet.ValidationExtensions; 50 | 51 | public class BlogArticle 52 | { 53 | [Required] 54 | public string Title { get; set; } 55 | 56 | [RequiredIf(nameof(IsPublished), true)] 57 | public string ArticleContent { get; set; } 58 | 59 | [RequiredIfNot(nameof(ArticleContent), null)] 60 | public string ReplacementContent { get; set; } 61 | 62 | [Required] 63 | public bool? NoticeByEmail { get; set; } 64 | 65 | [RequiredDynamic(nameof(ValidateRequired_NoticeByEmail), "Notice by email is activated")] 66 | public string? EmailAddress { get; set; } 67 | 68 | [DynamicRange(typeof(decimal), minimum: 9.99, maximumPropertyName: nameof(MaximumPrice))] 69 | public decimal? MinimumPrice { get; set; } 70 | 71 | [DynamicRange(typeof(decimal), minimumPropertyName: nameof(MinimumPrice), maximum: 199.99)] 72 | public decimal? MaximumPrice { get; set; } 73 | 74 | [DynamicRange(minimum: 0.1d, maximumPropertyName: nameof(MaximumWeight))] 75 | public double? MinimumWeight { get; set; } 76 | 77 | [DynamicRange(minimumPropertyName: nameof(MinimumWeight), maximum: 500d)] 78 | public double? MaximumWeight { get; set; } 79 | 80 | private static bool ValidateRequired_NoticeByEmail(BlogArticle value) 81 | { 82 | if (!value.NoticeByEmail.HasValue) 83 | { 84 | return false; 85 | } 86 | 87 | if (!value.NoticeByEmail.Value) 88 | { 89 | return false; 90 | } 91 | 92 | if (string.IsNullOrWhiteSpace(value.EmailAddress)) 93 | { 94 | return true; 95 | } 96 | else 97 | { 98 | return false; 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | ## Currently implemented additional attributes: 105 | * `RequiredIf` 106 | * `MinLengthIf` / `MaxLengthIf` 107 | * `RangeIf` 108 | * `MinIf` / `MaxIf` 109 | * `Min` / `Max` 110 | * `Dynamic` 111 | * `DynamicRange` 112 | * `DynamicRange` [C# 11 Generic Attributes](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-11.0/generic-attributes) 113 | * `FutureDateValidation` / `PastDateValidation` -------------------------------------------------------------------------------- /Sample/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Sample/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
-------------------------------------------------------------------------------- /Sample/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | color-scheme: light only; 81 | background: lightyellow; 82 | bottom: 0; 83 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 84 | box-sizing: border-box; 85 | display: none; 86 | left: 0; 87 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 88 | position: fixed; 89 | width: 100%; 90 | z-index: 1000; 91 | } 92 | 93 | #blazor-error-ui .dismiss { 94 | cursor: pointer; 95 | position: absolute; 96 | right: 0.75rem; 97 | top: 0.5rem; 98 | } 99 | -------------------------------------------------------------------------------- /Sample/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sample/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | min-height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sample/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | 8 | 9 |
10 | 12 | 13 | 14 |
15 |
16 | 18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 | 26 | @code { 27 | 28 | private readonly Model model = new() 29 | { 30 | FirstName = "Steven" 31 | }; 32 | 33 | private void Callback() 34 | { 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /Sample/Components/Pages/Model.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using LinkDotNet.ValidationExtensions; 3 | 4 | namespace Sample.Components.Pages; 5 | 6 | public class Model 7 | { 8 | [Required] 9 | public required string FirstName { get; set; } 10 | 11 | [RequiredIf(nameof(FirstName), "Steven", ErrorMessage = "Custom message goes here.")] 12 | public string? LastName { get; set; } 13 | } -------------------------------------------------------------------------------- /Sample/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Sample/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using Sample 10 | @using Sample.Components -------------------------------------------------------------------------------- /Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using Sample.Components; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | builder.Services.AddRazorComponents() 7 | .AddInteractiveServerComponents(); 8 | 9 | var app = builder.Build(); 10 | 11 | // Configure the HTTP request pipeline. 12 | if (!app.Environment.IsDevelopment()) 13 | { 14 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 15 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 16 | app.UseHsts(); 17 | } 18 | 19 | app.UseHttpsRedirection(); 20 | 21 | 22 | app.UseAntiforgery(); 23 | 24 | app.MapStaticAssets(); 25 | app.MapRazorComponents() 26 | .AddInteractiveServerRenderMode(); 27 | 28 | app.Run(); -------------------------------------------------------------------------------- /Sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "http://localhost:5176", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://localhost:7192;http://localhost:5176", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sample/Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Sample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /Sample/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | 53 | .form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder { 54 | color: var(--bs-secondary-color); 55 | text-align: end; 56 | } 57 | 58 | .form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder { 59 | text-align: start; 60 | } -------------------------------------------------------------------------------- /Sample/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkdotnet/ValidationExtensions/7d50fb9b5ac49d63fe38b7d1e3b09a39f039115a/Sample/wwwroot/favicon.png -------------------------------------------------------------------------------- /Sample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | :root, 7 | [data-bs-theme=light] { 8 | --bs-blue: #0d6efd; 9 | --bs-indigo: #6610f2; 10 | --bs-purple: #6f42c1; 11 | --bs-pink: #d63384; 12 | --bs-red: #dc3545; 13 | --bs-orange: #fd7e14; 14 | --bs-yellow: #ffc107; 15 | --bs-green: #198754; 16 | --bs-teal: #20c997; 17 | --bs-cyan: #0dcaf0; 18 | --bs-black: #000; 19 | --bs-white: #fff; 20 | --bs-gray: #6c757d; 21 | --bs-gray-dark: #343a40; 22 | --bs-gray-100: #f8f9fa; 23 | --bs-gray-200: #e9ecef; 24 | --bs-gray-300: #dee2e6; 25 | --bs-gray-400: #ced4da; 26 | --bs-gray-500: #adb5bd; 27 | --bs-gray-600: #6c757d; 28 | --bs-gray-700: #495057; 29 | --bs-gray-800: #343a40; 30 | --bs-gray-900: #212529; 31 | --bs-primary: #0d6efd; 32 | --bs-secondary: #6c757d; 33 | --bs-success: #198754; 34 | --bs-info: #0dcaf0; 35 | --bs-warning: #ffc107; 36 | --bs-danger: #dc3545; 37 | --bs-light: #f8f9fa; 38 | --bs-dark: #212529; 39 | --bs-primary-rgb: 13, 110, 253; 40 | --bs-secondary-rgb: 108, 117, 125; 41 | --bs-success-rgb: 25, 135, 84; 42 | --bs-info-rgb: 13, 202, 240; 43 | --bs-warning-rgb: 255, 193, 7; 44 | --bs-danger-rgb: 220, 53, 69; 45 | --bs-light-rgb: 248, 249, 250; 46 | --bs-dark-rgb: 33, 37, 41; 47 | --bs-primary-text-emphasis: #052c65; 48 | --bs-secondary-text-emphasis: #2b2f32; 49 | --bs-success-text-emphasis: #0a3622; 50 | --bs-info-text-emphasis: #055160; 51 | --bs-warning-text-emphasis: #664d03; 52 | --bs-danger-text-emphasis: #58151c; 53 | --bs-light-text-emphasis: #495057; 54 | --bs-dark-text-emphasis: #495057; 55 | --bs-primary-bg-subtle: #cfe2ff; 56 | --bs-secondary-bg-subtle: #e2e3e5; 57 | --bs-success-bg-subtle: #d1e7dd; 58 | --bs-info-bg-subtle: #cff4fc; 59 | --bs-warning-bg-subtle: #fff3cd; 60 | --bs-danger-bg-subtle: #f8d7da; 61 | --bs-light-bg-subtle: #fcfcfd; 62 | --bs-dark-bg-subtle: #ced4da; 63 | --bs-primary-border-subtle: #9ec5fe; 64 | --bs-secondary-border-subtle: #c4c8cb; 65 | --bs-success-border-subtle: #a3cfbb; 66 | --bs-info-border-subtle: #9eeaf9; 67 | --bs-warning-border-subtle: #ffe69c; 68 | --bs-danger-border-subtle: #f1aeb5; 69 | --bs-light-border-subtle: #e9ecef; 70 | --bs-dark-border-subtle: #adb5bd; 71 | --bs-white-rgb: 255, 255, 255; 72 | --bs-black-rgb: 0, 0, 0; 73 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 74 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 75 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); 76 | --bs-body-font-family: var(--bs-font-sans-serif); 77 | --bs-body-font-size: 1rem; 78 | --bs-body-font-weight: 400; 79 | --bs-body-line-height: 1.5; 80 | --bs-body-color: #212529; 81 | --bs-body-color-rgb: 33, 37, 41; 82 | --bs-body-bg: #fff; 83 | --bs-body-bg-rgb: 255, 255, 255; 84 | --bs-emphasis-color: #000; 85 | --bs-emphasis-color-rgb: 0, 0, 0; 86 | --bs-secondary-color: rgba(33, 37, 41, 0.75); 87 | --bs-secondary-color-rgb: 33, 37, 41; 88 | --bs-secondary-bg: #e9ecef; 89 | --bs-secondary-bg-rgb: 233, 236, 239; 90 | --bs-tertiary-color: rgba(33, 37, 41, 0.5); 91 | --bs-tertiary-color-rgb: 33, 37, 41; 92 | --bs-tertiary-bg: #f8f9fa; 93 | --bs-tertiary-bg-rgb: 248, 249, 250; 94 | --bs-heading-color: inherit; 95 | --bs-link-color: #0d6efd; 96 | --bs-link-color-rgb: 13, 110, 253; 97 | --bs-link-decoration: underline; 98 | --bs-link-hover-color: #0a58ca; 99 | --bs-link-hover-color-rgb: 10, 88, 202; 100 | --bs-code-color: #d63384; 101 | --bs-highlight-color: #212529; 102 | --bs-highlight-bg: #fff3cd; 103 | --bs-border-width: 1px; 104 | --bs-border-style: solid; 105 | --bs-border-color: #dee2e6; 106 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175); 107 | --bs-border-radius: 0.375rem; 108 | --bs-border-radius-sm: 0.25rem; 109 | --bs-border-radius-lg: 0.5rem; 110 | --bs-border-radius-xl: 1rem; 111 | --bs-border-radius-xxl: 2rem; 112 | --bs-border-radius-2xl: var(--bs-border-radius-xxl); 113 | --bs-border-radius-pill: 50rem; 114 | --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 115 | --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); 116 | --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); 117 | --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); 118 | --bs-focus-ring-width: 0.25rem; 119 | --bs-focus-ring-opacity: 0.25; 120 | --bs-focus-ring-color: rgba(13, 110, 253, 0.25); 121 | --bs-form-valid-color: #198754; 122 | --bs-form-valid-border-color: #198754; 123 | --bs-form-invalid-color: #dc3545; 124 | --bs-form-invalid-border-color: #dc3545; 125 | } 126 | 127 | [data-bs-theme=dark] { 128 | color-scheme: dark; 129 | --bs-body-color: #dee2e6; 130 | --bs-body-color-rgb: 222, 226, 230; 131 | --bs-body-bg: #212529; 132 | --bs-body-bg-rgb: 33, 37, 41; 133 | --bs-emphasis-color: #fff; 134 | --bs-emphasis-color-rgb: 255, 255, 255; 135 | --bs-secondary-color: rgba(222, 226, 230, 0.75); 136 | --bs-secondary-color-rgb: 222, 226, 230; 137 | --bs-secondary-bg: #343a40; 138 | --bs-secondary-bg-rgb: 52, 58, 64; 139 | --bs-tertiary-color: rgba(222, 226, 230, 0.5); 140 | --bs-tertiary-color-rgb: 222, 226, 230; 141 | --bs-tertiary-bg: #2b3035; 142 | --bs-tertiary-bg-rgb: 43, 48, 53; 143 | --bs-primary-text-emphasis: #6ea8fe; 144 | --bs-secondary-text-emphasis: #a7acb1; 145 | --bs-success-text-emphasis: #75b798; 146 | --bs-info-text-emphasis: #6edff6; 147 | --bs-warning-text-emphasis: #ffda6a; 148 | --bs-danger-text-emphasis: #ea868f; 149 | --bs-light-text-emphasis: #f8f9fa; 150 | --bs-dark-text-emphasis: #dee2e6; 151 | --bs-primary-bg-subtle: #031633; 152 | --bs-secondary-bg-subtle: #161719; 153 | --bs-success-bg-subtle: #051b11; 154 | --bs-info-bg-subtle: #032830; 155 | --bs-warning-bg-subtle: #332701; 156 | --bs-danger-bg-subtle: #2c0b0e; 157 | --bs-light-bg-subtle: #343a40; 158 | --bs-dark-bg-subtle: #1a1d20; 159 | --bs-primary-border-subtle: #084298; 160 | --bs-secondary-border-subtle: #41464b; 161 | --bs-success-border-subtle: #0f5132; 162 | --bs-info-border-subtle: #087990; 163 | --bs-warning-border-subtle: #997404; 164 | --bs-danger-border-subtle: #842029; 165 | --bs-light-border-subtle: #495057; 166 | --bs-dark-border-subtle: #343a40; 167 | --bs-heading-color: inherit; 168 | --bs-link-color: #6ea8fe; 169 | --bs-link-hover-color: #8bb9fe; 170 | --bs-link-color-rgb: 110, 168, 254; 171 | --bs-link-hover-color-rgb: 139, 185, 254; 172 | --bs-code-color: #e685b5; 173 | --bs-highlight-color: #dee2e6; 174 | --bs-highlight-bg: #664d03; 175 | --bs-border-color: #495057; 176 | --bs-border-color-translucent: rgba(255, 255, 255, 0.15); 177 | --bs-form-valid-color: #75b798; 178 | --bs-form-valid-border-color: #75b798; 179 | --bs-form-invalid-color: #ea868f; 180 | --bs-form-invalid-border-color: #ea868f; 181 | } 182 | 183 | *, 184 | *::before, 185 | *::after { 186 | box-sizing: border-box; 187 | } 188 | 189 | @media (prefers-reduced-motion: no-preference) { 190 | :root { 191 | scroll-behavior: smooth; 192 | } 193 | } 194 | 195 | body { 196 | margin: 0; 197 | font-family: var(--bs-body-font-family); 198 | font-size: var(--bs-body-font-size); 199 | font-weight: var(--bs-body-font-weight); 200 | line-height: var(--bs-body-line-height); 201 | color: var(--bs-body-color); 202 | text-align: var(--bs-body-text-align); 203 | background-color: var(--bs-body-bg); 204 | -webkit-text-size-adjust: 100%; 205 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 206 | } 207 | 208 | hr { 209 | margin: 1rem 0; 210 | color: inherit; 211 | border: 0; 212 | border-top: var(--bs-border-width) solid; 213 | opacity: 0.25; 214 | } 215 | 216 | h6, h5, h4, h3, h2, h1 { 217 | margin-top: 0; 218 | margin-bottom: 0.5rem; 219 | font-weight: 500; 220 | line-height: 1.2; 221 | color: var(--bs-heading-color); 222 | } 223 | 224 | h1 { 225 | font-size: calc(1.375rem + 1.5vw); 226 | } 227 | @media (min-width: 1200px) { 228 | h1 { 229 | font-size: 2.5rem; 230 | } 231 | } 232 | 233 | h2 { 234 | font-size: calc(1.325rem + 0.9vw); 235 | } 236 | @media (min-width: 1200px) { 237 | h2 { 238 | font-size: 2rem; 239 | } 240 | } 241 | 242 | h3 { 243 | font-size: calc(1.3rem + 0.6vw); 244 | } 245 | @media (min-width: 1200px) { 246 | h3 { 247 | font-size: 1.75rem; 248 | } 249 | } 250 | 251 | h4 { 252 | font-size: calc(1.275rem + 0.3vw); 253 | } 254 | @media (min-width: 1200px) { 255 | h4 { 256 | font-size: 1.5rem; 257 | } 258 | } 259 | 260 | h5 { 261 | font-size: 1.25rem; 262 | } 263 | 264 | h6 { 265 | font-size: 1rem; 266 | } 267 | 268 | p { 269 | margin-top: 0; 270 | margin-bottom: 1rem; 271 | } 272 | 273 | abbr[title] { 274 | -webkit-text-decoration: underline dotted; 275 | text-decoration: underline dotted; 276 | cursor: help; 277 | -webkit-text-decoration-skip-ink: none; 278 | text-decoration-skip-ink: none; 279 | } 280 | 281 | address { 282 | margin-bottom: 1rem; 283 | font-style: normal; 284 | line-height: inherit; 285 | } 286 | 287 | ol, 288 | ul { 289 | padding-left: 2rem; 290 | } 291 | 292 | ol, 293 | ul, 294 | dl { 295 | margin-top: 0; 296 | margin-bottom: 1rem; 297 | } 298 | 299 | ol ol, 300 | ul ul, 301 | ol ul, 302 | ul ol { 303 | margin-bottom: 0; 304 | } 305 | 306 | dt { 307 | font-weight: 700; 308 | } 309 | 310 | dd { 311 | margin-bottom: 0.5rem; 312 | margin-left: 0; 313 | } 314 | 315 | blockquote { 316 | margin: 0 0 1rem; 317 | } 318 | 319 | b, 320 | strong { 321 | font-weight: bolder; 322 | } 323 | 324 | small { 325 | font-size: 0.875em; 326 | } 327 | 328 | mark { 329 | padding: 0.1875em; 330 | color: var(--bs-highlight-color); 331 | background-color: var(--bs-highlight-bg); 332 | } 333 | 334 | sub, 335 | sup { 336 | position: relative; 337 | font-size: 0.75em; 338 | line-height: 0; 339 | vertical-align: baseline; 340 | } 341 | 342 | sub { 343 | bottom: -0.25em; 344 | } 345 | 346 | sup { 347 | top: -0.5em; 348 | } 349 | 350 | a { 351 | color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); 352 | text-decoration: underline; 353 | } 354 | a:hover { 355 | --bs-link-color-rgb: var(--bs-link-hover-color-rgb); 356 | } 357 | 358 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 359 | color: inherit; 360 | text-decoration: none; 361 | } 362 | 363 | pre, 364 | code, 365 | kbd, 366 | samp { 367 | font-family: var(--bs-font-monospace); 368 | font-size: 1em; 369 | } 370 | 371 | pre { 372 | display: block; 373 | margin-top: 0; 374 | margin-bottom: 1rem; 375 | overflow: auto; 376 | font-size: 0.875em; 377 | } 378 | pre code { 379 | font-size: inherit; 380 | color: inherit; 381 | word-break: normal; 382 | } 383 | 384 | code { 385 | font-size: 0.875em; 386 | color: var(--bs-code-color); 387 | word-wrap: break-word; 388 | } 389 | a > code { 390 | color: inherit; 391 | } 392 | 393 | kbd { 394 | padding: 0.1875rem 0.375rem; 395 | font-size: 0.875em; 396 | color: var(--bs-body-bg); 397 | background-color: var(--bs-body-color); 398 | border-radius: 0.25rem; 399 | } 400 | kbd kbd { 401 | padding: 0; 402 | font-size: 1em; 403 | } 404 | 405 | figure { 406 | margin: 0 0 1rem; 407 | } 408 | 409 | img, 410 | svg { 411 | vertical-align: middle; 412 | } 413 | 414 | table { 415 | caption-side: bottom; 416 | border-collapse: collapse; 417 | } 418 | 419 | caption { 420 | padding-top: 0.5rem; 421 | padding-bottom: 0.5rem; 422 | color: var(--bs-secondary-color); 423 | text-align: left; 424 | } 425 | 426 | th { 427 | text-align: inherit; 428 | text-align: -webkit-match-parent; 429 | } 430 | 431 | thead, 432 | tbody, 433 | tfoot, 434 | tr, 435 | td, 436 | th { 437 | border-color: inherit; 438 | border-style: solid; 439 | border-width: 0; 440 | } 441 | 442 | label { 443 | display: inline-block; 444 | } 445 | 446 | button { 447 | border-radius: 0; 448 | } 449 | 450 | button:focus:not(:focus-visible) { 451 | outline: 0; 452 | } 453 | 454 | input, 455 | button, 456 | select, 457 | optgroup, 458 | textarea { 459 | margin: 0; 460 | font-family: inherit; 461 | font-size: inherit; 462 | line-height: inherit; 463 | } 464 | 465 | button, 466 | select { 467 | text-transform: none; 468 | } 469 | 470 | [role=button] { 471 | cursor: pointer; 472 | } 473 | 474 | select { 475 | word-wrap: normal; 476 | } 477 | select:disabled { 478 | opacity: 1; 479 | } 480 | 481 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { 482 | display: none !important; 483 | } 484 | 485 | button, 486 | [type=button], 487 | [type=reset], 488 | [type=submit] { 489 | -webkit-appearance: button; 490 | } 491 | button:not(:disabled), 492 | [type=button]:not(:disabled), 493 | [type=reset]:not(:disabled), 494 | [type=submit]:not(:disabled) { 495 | cursor: pointer; 496 | } 497 | 498 | ::-moz-focus-inner { 499 | padding: 0; 500 | border-style: none; 501 | } 502 | 503 | textarea { 504 | resize: vertical; 505 | } 506 | 507 | fieldset { 508 | min-width: 0; 509 | padding: 0; 510 | margin: 0; 511 | border: 0; 512 | } 513 | 514 | legend { 515 | float: left; 516 | width: 100%; 517 | padding: 0; 518 | margin-bottom: 0.5rem; 519 | font-size: calc(1.275rem + 0.3vw); 520 | line-height: inherit; 521 | } 522 | @media (min-width: 1200px) { 523 | legend { 524 | font-size: 1.5rem; 525 | } 526 | } 527 | legend + * { 528 | clear: left; 529 | } 530 | 531 | ::-webkit-datetime-edit-fields-wrapper, 532 | ::-webkit-datetime-edit-text, 533 | ::-webkit-datetime-edit-minute, 534 | ::-webkit-datetime-edit-hour-field, 535 | ::-webkit-datetime-edit-day-field, 536 | ::-webkit-datetime-edit-month-field, 537 | ::-webkit-datetime-edit-year-field { 538 | padding: 0; 539 | } 540 | 541 | ::-webkit-inner-spin-button { 542 | height: auto; 543 | } 544 | 545 | [type=search] { 546 | -webkit-appearance: textfield; 547 | outline-offset: -2px; 548 | } 549 | 550 | /* rtl:raw: 551 | [type="tel"], 552 | [type="url"], 553 | [type="email"], 554 | [type="number"] { 555 | direction: ltr; 556 | } 557 | */ 558 | ::-webkit-search-decoration { 559 | -webkit-appearance: none; 560 | } 561 | 562 | ::-webkit-color-swatch-wrapper { 563 | padding: 0; 564 | } 565 | 566 | ::-webkit-file-upload-button { 567 | font: inherit; 568 | -webkit-appearance: button; 569 | } 570 | 571 | ::file-selector-button { 572 | font: inherit; 573 | -webkit-appearance: button; 574 | } 575 | 576 | output { 577 | display: inline-block; 578 | } 579 | 580 | iframe { 581 | border: 0; 582 | } 583 | 584 | summary { 585 | display: list-item; 586 | cursor: pointer; 587 | } 588 | 589 | progress { 590 | vertical-align: baseline; 591 | } 592 | 593 | [hidden] { 594 | display: none !important; 595 | } 596 | 597 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /Sample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 6 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /Sample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | :root, 7 | [data-bs-theme=light] { 8 | --bs-blue: #0d6efd; 9 | --bs-indigo: #6610f2; 10 | --bs-purple: #6f42c1; 11 | --bs-pink: #d63384; 12 | --bs-red: #dc3545; 13 | --bs-orange: #fd7e14; 14 | --bs-yellow: #ffc107; 15 | --bs-green: #198754; 16 | --bs-teal: #20c997; 17 | --bs-cyan: #0dcaf0; 18 | --bs-black: #000; 19 | --bs-white: #fff; 20 | --bs-gray: #6c757d; 21 | --bs-gray-dark: #343a40; 22 | --bs-gray-100: #f8f9fa; 23 | --bs-gray-200: #e9ecef; 24 | --bs-gray-300: #dee2e6; 25 | --bs-gray-400: #ced4da; 26 | --bs-gray-500: #adb5bd; 27 | --bs-gray-600: #6c757d; 28 | --bs-gray-700: #495057; 29 | --bs-gray-800: #343a40; 30 | --bs-gray-900: #212529; 31 | --bs-primary: #0d6efd; 32 | --bs-secondary: #6c757d; 33 | --bs-success: #198754; 34 | --bs-info: #0dcaf0; 35 | --bs-warning: #ffc107; 36 | --bs-danger: #dc3545; 37 | --bs-light: #f8f9fa; 38 | --bs-dark: #212529; 39 | --bs-primary-rgb: 13, 110, 253; 40 | --bs-secondary-rgb: 108, 117, 125; 41 | --bs-success-rgb: 25, 135, 84; 42 | --bs-info-rgb: 13, 202, 240; 43 | --bs-warning-rgb: 255, 193, 7; 44 | --bs-danger-rgb: 220, 53, 69; 45 | --bs-light-rgb: 248, 249, 250; 46 | --bs-dark-rgb: 33, 37, 41; 47 | --bs-primary-text-emphasis: #052c65; 48 | --bs-secondary-text-emphasis: #2b2f32; 49 | --bs-success-text-emphasis: #0a3622; 50 | --bs-info-text-emphasis: #055160; 51 | --bs-warning-text-emphasis: #664d03; 52 | --bs-danger-text-emphasis: #58151c; 53 | --bs-light-text-emphasis: #495057; 54 | --bs-dark-text-emphasis: #495057; 55 | --bs-primary-bg-subtle: #cfe2ff; 56 | --bs-secondary-bg-subtle: #e2e3e5; 57 | --bs-success-bg-subtle: #d1e7dd; 58 | --bs-info-bg-subtle: #cff4fc; 59 | --bs-warning-bg-subtle: #fff3cd; 60 | --bs-danger-bg-subtle: #f8d7da; 61 | --bs-light-bg-subtle: #fcfcfd; 62 | --bs-dark-bg-subtle: #ced4da; 63 | --bs-primary-border-subtle: #9ec5fe; 64 | --bs-secondary-border-subtle: #c4c8cb; 65 | --bs-success-border-subtle: #a3cfbb; 66 | --bs-info-border-subtle: #9eeaf9; 67 | --bs-warning-border-subtle: #ffe69c; 68 | --bs-danger-border-subtle: #f1aeb5; 69 | --bs-light-border-subtle: #e9ecef; 70 | --bs-dark-border-subtle: #adb5bd; 71 | --bs-white-rgb: 255, 255, 255; 72 | --bs-black-rgb: 0, 0, 0; 73 | --bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 74 | --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 75 | --bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0)); 76 | --bs-body-font-family: var(--bs-font-sans-serif); 77 | --bs-body-font-size: 1rem; 78 | --bs-body-font-weight: 400; 79 | --bs-body-line-height: 1.5; 80 | --bs-body-color: #212529; 81 | --bs-body-color-rgb: 33, 37, 41; 82 | --bs-body-bg: #fff; 83 | --bs-body-bg-rgb: 255, 255, 255; 84 | --bs-emphasis-color: #000; 85 | --bs-emphasis-color-rgb: 0, 0, 0; 86 | --bs-secondary-color: rgba(33, 37, 41, 0.75); 87 | --bs-secondary-color-rgb: 33, 37, 41; 88 | --bs-secondary-bg: #e9ecef; 89 | --bs-secondary-bg-rgb: 233, 236, 239; 90 | --bs-tertiary-color: rgba(33, 37, 41, 0.5); 91 | --bs-tertiary-color-rgb: 33, 37, 41; 92 | --bs-tertiary-bg: #f8f9fa; 93 | --bs-tertiary-bg-rgb: 248, 249, 250; 94 | --bs-heading-color: inherit; 95 | --bs-link-color: #0d6efd; 96 | --bs-link-color-rgb: 13, 110, 253; 97 | --bs-link-decoration: underline; 98 | --bs-link-hover-color: #0a58ca; 99 | --bs-link-hover-color-rgb: 10, 88, 202; 100 | --bs-code-color: #d63384; 101 | --bs-highlight-color: #212529; 102 | --bs-highlight-bg: #fff3cd; 103 | --bs-border-width: 1px; 104 | --bs-border-style: solid; 105 | --bs-border-color: #dee2e6; 106 | --bs-border-color-translucent: rgba(0, 0, 0, 0.175); 107 | --bs-border-radius: 0.375rem; 108 | --bs-border-radius-sm: 0.25rem; 109 | --bs-border-radius-lg: 0.5rem; 110 | --bs-border-radius-xl: 1rem; 111 | --bs-border-radius-xxl: 2rem; 112 | --bs-border-radius-2xl: var(--bs-border-radius-xxl); 113 | --bs-border-radius-pill: 50rem; 114 | --bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); 115 | --bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); 116 | --bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175); 117 | --bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075); 118 | --bs-focus-ring-width: 0.25rem; 119 | --bs-focus-ring-opacity: 0.25; 120 | --bs-focus-ring-color: rgba(13, 110, 253, 0.25); 121 | --bs-form-valid-color: #198754; 122 | --bs-form-valid-border-color: #198754; 123 | --bs-form-invalid-color: #dc3545; 124 | --bs-form-invalid-border-color: #dc3545; 125 | } 126 | 127 | [data-bs-theme=dark] { 128 | color-scheme: dark; 129 | --bs-body-color: #dee2e6; 130 | --bs-body-color-rgb: 222, 226, 230; 131 | --bs-body-bg: #212529; 132 | --bs-body-bg-rgb: 33, 37, 41; 133 | --bs-emphasis-color: #fff; 134 | --bs-emphasis-color-rgb: 255, 255, 255; 135 | --bs-secondary-color: rgba(222, 226, 230, 0.75); 136 | --bs-secondary-color-rgb: 222, 226, 230; 137 | --bs-secondary-bg: #343a40; 138 | --bs-secondary-bg-rgb: 52, 58, 64; 139 | --bs-tertiary-color: rgba(222, 226, 230, 0.5); 140 | --bs-tertiary-color-rgb: 222, 226, 230; 141 | --bs-tertiary-bg: #2b3035; 142 | --bs-tertiary-bg-rgb: 43, 48, 53; 143 | --bs-primary-text-emphasis: #6ea8fe; 144 | --bs-secondary-text-emphasis: #a7acb1; 145 | --bs-success-text-emphasis: #75b798; 146 | --bs-info-text-emphasis: #6edff6; 147 | --bs-warning-text-emphasis: #ffda6a; 148 | --bs-danger-text-emphasis: #ea868f; 149 | --bs-light-text-emphasis: #f8f9fa; 150 | --bs-dark-text-emphasis: #dee2e6; 151 | --bs-primary-bg-subtle: #031633; 152 | --bs-secondary-bg-subtle: #161719; 153 | --bs-success-bg-subtle: #051b11; 154 | --bs-info-bg-subtle: #032830; 155 | --bs-warning-bg-subtle: #332701; 156 | --bs-danger-bg-subtle: #2c0b0e; 157 | --bs-light-bg-subtle: #343a40; 158 | --bs-dark-bg-subtle: #1a1d20; 159 | --bs-primary-border-subtle: #084298; 160 | --bs-secondary-border-subtle: #41464b; 161 | --bs-success-border-subtle: #0f5132; 162 | --bs-info-border-subtle: #087990; 163 | --bs-warning-border-subtle: #997404; 164 | --bs-danger-border-subtle: #842029; 165 | --bs-light-border-subtle: #495057; 166 | --bs-dark-border-subtle: #343a40; 167 | --bs-heading-color: inherit; 168 | --bs-link-color: #6ea8fe; 169 | --bs-link-hover-color: #8bb9fe; 170 | --bs-link-color-rgb: 110, 168, 254; 171 | --bs-link-hover-color-rgb: 139, 185, 254; 172 | --bs-code-color: #e685b5; 173 | --bs-highlight-color: #dee2e6; 174 | --bs-highlight-bg: #664d03; 175 | --bs-border-color: #495057; 176 | --bs-border-color-translucent: rgba(255, 255, 255, 0.15); 177 | --bs-form-valid-color: #75b798; 178 | --bs-form-valid-border-color: #75b798; 179 | --bs-form-invalid-color: #ea868f; 180 | --bs-form-invalid-border-color: #ea868f; 181 | } 182 | 183 | *, 184 | *::before, 185 | *::after { 186 | box-sizing: border-box; 187 | } 188 | 189 | @media (prefers-reduced-motion: no-preference) { 190 | :root { 191 | scroll-behavior: smooth; 192 | } 193 | } 194 | 195 | body { 196 | margin: 0; 197 | font-family: var(--bs-body-font-family); 198 | font-size: var(--bs-body-font-size); 199 | font-weight: var(--bs-body-font-weight); 200 | line-height: var(--bs-body-line-height); 201 | color: var(--bs-body-color); 202 | text-align: var(--bs-body-text-align); 203 | background-color: var(--bs-body-bg); 204 | -webkit-text-size-adjust: 100%; 205 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 206 | } 207 | 208 | hr { 209 | margin: 1rem 0; 210 | color: inherit; 211 | border: 0; 212 | border-top: var(--bs-border-width) solid; 213 | opacity: 0.25; 214 | } 215 | 216 | h6, h5, h4, h3, h2, h1 { 217 | margin-top: 0; 218 | margin-bottom: 0.5rem; 219 | font-weight: 500; 220 | line-height: 1.2; 221 | color: var(--bs-heading-color); 222 | } 223 | 224 | h1 { 225 | font-size: calc(1.375rem + 1.5vw); 226 | } 227 | @media (min-width: 1200px) { 228 | h1 { 229 | font-size: 2.5rem; 230 | } 231 | } 232 | 233 | h2 { 234 | font-size: calc(1.325rem + 0.9vw); 235 | } 236 | @media (min-width: 1200px) { 237 | h2 { 238 | font-size: 2rem; 239 | } 240 | } 241 | 242 | h3 { 243 | font-size: calc(1.3rem + 0.6vw); 244 | } 245 | @media (min-width: 1200px) { 246 | h3 { 247 | font-size: 1.75rem; 248 | } 249 | } 250 | 251 | h4 { 252 | font-size: calc(1.275rem + 0.3vw); 253 | } 254 | @media (min-width: 1200px) { 255 | h4 { 256 | font-size: 1.5rem; 257 | } 258 | } 259 | 260 | h5 { 261 | font-size: 1.25rem; 262 | } 263 | 264 | h6 { 265 | font-size: 1rem; 266 | } 267 | 268 | p { 269 | margin-top: 0; 270 | margin-bottom: 1rem; 271 | } 272 | 273 | abbr[title] { 274 | -webkit-text-decoration: underline dotted; 275 | text-decoration: underline dotted; 276 | cursor: help; 277 | -webkit-text-decoration-skip-ink: none; 278 | text-decoration-skip-ink: none; 279 | } 280 | 281 | address { 282 | margin-bottom: 1rem; 283 | font-style: normal; 284 | line-height: inherit; 285 | } 286 | 287 | ol, 288 | ul { 289 | padding-right: 2rem; 290 | } 291 | 292 | ol, 293 | ul, 294 | dl { 295 | margin-top: 0; 296 | margin-bottom: 1rem; 297 | } 298 | 299 | ol ol, 300 | ul ul, 301 | ol ul, 302 | ul ol { 303 | margin-bottom: 0; 304 | } 305 | 306 | dt { 307 | font-weight: 700; 308 | } 309 | 310 | dd { 311 | margin-bottom: 0.5rem; 312 | margin-right: 0; 313 | } 314 | 315 | blockquote { 316 | margin: 0 0 1rem; 317 | } 318 | 319 | b, 320 | strong { 321 | font-weight: bolder; 322 | } 323 | 324 | small { 325 | font-size: 0.875em; 326 | } 327 | 328 | mark { 329 | padding: 0.1875em; 330 | color: var(--bs-highlight-color); 331 | background-color: var(--bs-highlight-bg); 332 | } 333 | 334 | sub, 335 | sup { 336 | position: relative; 337 | font-size: 0.75em; 338 | line-height: 0; 339 | vertical-align: baseline; 340 | } 341 | 342 | sub { 343 | bottom: -0.25em; 344 | } 345 | 346 | sup { 347 | top: -0.5em; 348 | } 349 | 350 | a { 351 | color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1)); 352 | text-decoration: underline; 353 | } 354 | a:hover { 355 | --bs-link-color-rgb: var(--bs-link-hover-color-rgb); 356 | } 357 | 358 | a:not([href]):not([class]), a:not([href]):not([class]):hover { 359 | color: inherit; 360 | text-decoration: none; 361 | } 362 | 363 | pre, 364 | code, 365 | kbd, 366 | samp { 367 | font-family: var(--bs-font-monospace); 368 | font-size: 1em; 369 | } 370 | 371 | pre { 372 | display: block; 373 | margin-top: 0; 374 | margin-bottom: 1rem; 375 | overflow: auto; 376 | font-size: 0.875em; 377 | } 378 | pre code { 379 | font-size: inherit; 380 | color: inherit; 381 | word-break: normal; 382 | } 383 | 384 | code { 385 | font-size: 0.875em; 386 | color: var(--bs-code-color); 387 | word-wrap: break-word; 388 | } 389 | a > code { 390 | color: inherit; 391 | } 392 | 393 | kbd { 394 | padding: 0.1875rem 0.375rem; 395 | font-size: 0.875em; 396 | color: var(--bs-body-bg); 397 | background-color: var(--bs-body-color); 398 | border-radius: 0.25rem; 399 | } 400 | kbd kbd { 401 | padding: 0; 402 | font-size: 1em; 403 | } 404 | 405 | figure { 406 | margin: 0 0 1rem; 407 | } 408 | 409 | img, 410 | svg { 411 | vertical-align: middle; 412 | } 413 | 414 | table { 415 | caption-side: bottom; 416 | border-collapse: collapse; 417 | } 418 | 419 | caption { 420 | padding-top: 0.5rem; 421 | padding-bottom: 0.5rem; 422 | color: var(--bs-secondary-color); 423 | text-align: right; 424 | } 425 | 426 | th { 427 | text-align: inherit; 428 | text-align: -webkit-match-parent; 429 | } 430 | 431 | thead, 432 | tbody, 433 | tfoot, 434 | tr, 435 | td, 436 | th { 437 | border-color: inherit; 438 | border-style: solid; 439 | border-width: 0; 440 | } 441 | 442 | label { 443 | display: inline-block; 444 | } 445 | 446 | button { 447 | border-radius: 0; 448 | } 449 | 450 | button:focus:not(:focus-visible) { 451 | outline: 0; 452 | } 453 | 454 | input, 455 | button, 456 | select, 457 | optgroup, 458 | textarea { 459 | margin: 0; 460 | font-family: inherit; 461 | font-size: inherit; 462 | line-height: inherit; 463 | } 464 | 465 | button, 466 | select { 467 | text-transform: none; 468 | } 469 | 470 | [role=button] { 471 | cursor: pointer; 472 | } 473 | 474 | select { 475 | word-wrap: normal; 476 | } 477 | select:disabled { 478 | opacity: 1; 479 | } 480 | 481 | [list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator { 482 | display: none !important; 483 | } 484 | 485 | button, 486 | [type=button], 487 | [type=reset], 488 | [type=submit] { 489 | -webkit-appearance: button; 490 | } 491 | button:not(:disabled), 492 | [type=button]:not(:disabled), 493 | [type=reset]:not(:disabled), 494 | [type=submit]:not(:disabled) { 495 | cursor: pointer; 496 | } 497 | 498 | ::-moz-focus-inner { 499 | padding: 0; 500 | border-style: none; 501 | } 502 | 503 | textarea { 504 | resize: vertical; 505 | } 506 | 507 | fieldset { 508 | min-width: 0; 509 | padding: 0; 510 | margin: 0; 511 | border: 0; 512 | } 513 | 514 | legend { 515 | float: right; 516 | width: 100%; 517 | padding: 0; 518 | margin-bottom: 0.5rem; 519 | font-size: calc(1.275rem + 0.3vw); 520 | line-height: inherit; 521 | } 522 | @media (min-width: 1200px) { 523 | legend { 524 | font-size: 1.5rem; 525 | } 526 | } 527 | legend + * { 528 | clear: right; 529 | } 530 | 531 | ::-webkit-datetime-edit-fields-wrapper, 532 | ::-webkit-datetime-edit-text, 533 | ::-webkit-datetime-edit-minute, 534 | ::-webkit-datetime-edit-hour-field, 535 | ::-webkit-datetime-edit-day-field, 536 | ::-webkit-datetime-edit-month-field, 537 | ::-webkit-datetime-edit-year-field { 538 | padding: 0; 539 | } 540 | 541 | ::-webkit-inner-spin-button { 542 | height: auto; 543 | } 544 | 545 | [type=search] { 546 | -webkit-appearance: textfield; 547 | outline-offset: -2px; 548 | } 549 | 550 | [type="tel"], 551 | [type="url"], 552 | [type="email"], 553 | [type="number"] { 554 | direction: ltr; 555 | } 556 | ::-webkit-search-decoration { 557 | -webkit-appearance: none; 558 | } 559 | 560 | ::-webkit-color-swatch-wrapper { 561 | padding: 0; 562 | } 563 | 564 | ::-webkit-file-upload-button { 565 | font: inherit; 566 | -webkit-appearance: button; 567 | } 568 | 569 | ::file-selector-button { 570 | font: inherit; 571 | -webkit-appearance: button; 572 | } 573 | 574 | output { 575 | display: inline-block; 576 | } 577 | 578 | iframe { 579 | border: 0; 580 | } 581 | 582 | summary { 583 | display: list-item; 584 | cursor: pointer; 585 | } 586 | 587 | progress { 588 | vertical-align: baseline; 589 | } 590 | 591 | [hidden] { 592 | display: none !important; 593 | } 594 | /*# sourceMappingURL=bootstrap-reboot.rtl.css.map */ -------------------------------------------------------------------------------- /Sample/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.3.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2024 The Bootstrap Authors 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} 6 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */ -------------------------------------------------------------------------------- /stylecop.analyzers.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Tunnel Vision Laboratories, LLC", 6 | "copyrightText": "Copyright (c) {companyName}. All Rights Reserved.\r\nLicensed under the MIT License. See LICENSE in the project root for license information.", 7 | "xmlHeader": false, 8 | "fileNamingConvention": "metadata" 9 | }, 10 | "namingRules": { 11 | "tupleElementNameCasing": "camelCase" 12 | }, 13 | "orderingRules": { 14 | "usingDirectivesPlacement": "outsideNamespace" 15 | }, 16 | "indentation": { 17 | "indentationSize": 4, 18 | "tabSize": 4, 19 | "useTabs": false 20 | } 21 | } 22 | } --------------------------------------------------------------------------------