From dfbc792bf9d3941f67c174d1c6008d7e38a7c1ca Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 12:35:32 +0100 Subject: [PATCH 01/12] chore: update project SDKs and target frameworks; remove obsolete Windows-specific converters - Changed global.json to use new SDK versioning format. - Updated Directory.Build.props to enable nullable reference types and continuous integration build settings. - Refactored MADE.Collections project to target net8.0 and net10.0, switching to Microsoft.NET.Sdk. - Deleted obsolete Windows-specific BooleanToStringValueConverter and DateTimeToStringValueConverter files. - Updated MADE.Data.Converters to target net8.0 and net10.0, refactoring converters to remove partial class definitions. - Updated MADE.Data.EFCore project to target net8.0 and net10.0, updating package references for EF Core. - Updated all MADE.* projects to use Microsoft.NET.Sdk and target net8.0 and net10.0. - Updated test projects to target net8.0 and net10.0, updating package references to latest versions. --- .github/dependabot.yml | 46 +++++++- .github/workflows/ci.yml | 106 ++++++++++++++---- .github/workflows/docs.yml | 34 ++---- global.json | 8 +- src/Directory.Build.props | 5 +- src/MADE.Collections/MADE.Collections.csproj | 4 +- .../BooleanToStringValueConverter.Windows.cs | 92 --------------- .../BooleanToStringValueConverter.cs | 4 +- .../DateTimeToStringValueConverter.Windows.cs | 58 ---------- .../DateTimeToStringValueConverter.cs | 2 +- .../MADE.Data.Converters.csproj | 4 +- src/MADE.Data.EFCore/MADE.Data.EFCore.csproj | 21 ++-- .../MADE.Data.Serialization.csproj | 4 +- ...DE.Data.Validation.FluentValidation.csproj | 4 +- .../MADE.Data.Validation.csproj | 4 +- src/MADE.Diagnostics/AppDiagnostics.cs | 20 ---- src/MADE.Diagnostics/MADE.Diagnostics.csproj | 8 +- src/MADE.Foundation/MADE.Foundation.csproj | 4 +- src/MADE.Networking/MADE.Networking.csproj | 4 +- src/MADE.Runtime/MADE.Runtime.csproj | 4 +- src/MADE.Testing/MADE.Testing.csproj | 4 +- src/MADE.Threading/MADE.Threading.csproj | 4 +- src/MADE.Web.Mvc/MADE.Web.Mvc.csproj | 4 +- src/MADE.Web/MADE.Web.csproj | 21 ++-- tests/Directory.Build.props | 14 ++- .../MADE.Collections.Tests.csproj | 15 +-- .../MADE.Data.Converters.Tests.csproj | 15 +-- .../MADE.Data.EFCore.Tests.csproj | 28 ++--- .../MADE.Data.Serialization.Tests.csproj | 15 +-- ...a.Validation.FluentValidation.Tests.csproj | 15 +-- .../MADE.Data.Validation.Tests.csproj | 15 +-- .../MADE.Diagnostics.Tests.csproj | 15 +-- .../MADE.Networking.Tests.csproj | 15 +-- tests/MADE.Web.Tests/MADE.Web.Tests.csproj | 15 +-- 34 files changed, 230 insertions(+), 401 deletions(-) delete mode 100644 src/MADE.Data.Converters/BooleanToStringValueConverter.Windows.cs delete mode 100644 src/MADE.Data.Converters/DateTimeToStringValueConverter.Windows.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index aec207e4..a00f7030 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,43 @@ version: 2 updates: -- package-ecosystem: nuget - directory: "/" - schedule: - interval: daily - open-pull-requests-limit: 10 + - package-ecosystem: nuget + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 10 + commit-message: + prefix: "chore" + include: "scope" + groups: + testing: + patterns: + - "coverlet*" + - "Microsoft.NET.Test*" + - "NUnit*" + - "Shouldly" + - "Bogus" + - "Moq" + entity-framework: + patterns: + - "Microsoft.EntityFrameworkCore*" + - "Z.EntityFramework*" + aspnet: + patterns: + - "Asp.Versioning*" + analyzers: + patterns: + - "Microsoft.SourceLink*" + validation: + patterns: + - "FluentValidation*" + + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16df71cb..cd28448e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,34 +8,39 @@ on: - main paths: - src/** - - samples/** - tests/** - build/** - .github/workflows/ci.yml + - Directory.Build.props - global.json pull_request: branches: - main paths: - src/** - - samples/** - tests/** - build/** - .github/workflows/ci.yml + - Directory.Build.props - global.json workflow_dispatch: +permissions: + contents: read + pull-requests: write + jobs: - build: + build-test: + runs-on: ubuntu-latest env: BUILD_CONFIG: 'Release' SOLUTION: 'MADE.NET.sln' - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Get Build Version run: | @@ -45,33 +50,88 @@ jobs: echo "BUILD_VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append shell: pwsh - - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 + - name: Setup .NET 8.0 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Setup .NET 10.0 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 6.0.x + dotnet-version: 10.0.3xx + + - name: Restore dependencies + run: dotnet restore $SOLUTION - - name: Setup .NET 7.0 - uses: actions/setup-dotnet@v1 + - name: Build + run: dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore -p:Version=$BUILD_VERSION + + - name: Run tests + run: > + dotnet test + --configuration $BUILD_CONFIG + --no-restore + --no-build + --collect:"XPlat Code Coverage" + + - name: Generate coverage report + uses: danielpalme/ReportGenerator-GitHub-Action@5 with: - dotnet-version: 7.0.x + reports: '**/coverage.cobertura.xml' + targetdir: 'coverage-report' + reporttypes: 'MarkdownSummaryGithub;Cobertura' + + - name: Publish coverage to workflow summary + run: cat coverage-report/SummaryGithub.md >> $GITHUB_STEP_SUMMARY + + - name: Post coverage to PR + if: github.event_name == 'pull_request' + run: gh pr comment $PR_NUMBER --edit-last --create-if-none --body-file coverage-report/SummaryGithub.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + + publish: + needs: build-test + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + + env: + BUILD_CONFIG: 'Release' + SOLUTION: 'MADE.NET.sln' + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Get Build Version + run: | + Import-Module .\build\GetBuildVersion.psm1 + Write-Host $Env:GITHUB_REF + $version = GetBuildVersion -VersionString $Env:GITHUB_REF + echo "BUILD_VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append + shell: pwsh - name: Setup .NET 8.0 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.x + - name: Setup .NET 10.0 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.3xx + - name: Restore dependencies run: dotnet restore $SOLUTION - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.0.2 - - name: Build - run: msbuild $env:SOLUTION /p:Configuration=$env:BUILD_CONFIG /p:Platform="Any CPU" -p:Version=$env:BUILD_VERSION + run: dotnet build $SOLUTION --configuration $BUILD_CONFIG --no-restore -p:Version=$BUILD_VERSION - - name: Run tests - run: dotnet test /p:Configuration=$env:BUILD_CONFIG /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura --no-restore --no-build --verbosity normal - - - name: Publish - if: startsWith(github.ref, 'refs/tags/v') - run: nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} + - name: Publish to NuGet.org + run: | + Get-ChildItem . -Recurse -Filter *.nupkg | ForEach-Object { + dotnet nuget push $_.FullName --source 'https://api.nuget.org/v3/index.json' --api-key '${{ secrets.NUGET_API_KEY }}' --skip-duplicate + } + shell: pwsh diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index df19f0c9..41bde7a1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,47 +19,37 @@ on: jobs: generate-docs: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Setup .NET 6.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 6.0.x - - - name: Setup .NET 7.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 7.0.x + - uses: actions/checkout@v6 - name: Setup .NET 8.0 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: 8.0.x - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1.0.2 - - - name: Setup DocFX - uses: crazy-max/ghaction-chocolatey@v1 + - name: Setup .NET 10.0 + uses: actions/setup-dotnet@v5 with: - args: install docfx + dotnet-version: 10.0.3xx + + - name: Install DocFX + run: dotnet tool install -g docfx - name: Generate DocFX metadata working-directory: docs - run: docfx metadata .\docfx.json + run: docfx metadata docfx.json continue-on-error: false - name: Build DocFX site working-directory: docs - run: docfx .\docfx.json + run: docfx docfx.json continue-on-error: false - name: Publish DocFX site if: github.event_name == 'push' - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_site diff --git a/global.json b/global.json index 236e1830..3ca24a0f 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { - "msbuild-sdks": { - "MSBuild.Sdk.Extras": "3.0.44" - } + "sdk": { + "version": "10.0.200", + "rollForward": "latestFeature", + "allowPrerelease": false + } } \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 01a168f8..da279b35 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,10 +4,12 @@ true Library enable + enable true true true snupkg + true true 1.0.0.0 MADE Apps @@ -30,8 +32,7 @@ - - + \ No newline at end of file diff --git a/src/MADE.Collections/MADE.Collections.csproj b/src/MADE.Collections/MADE.Collections.csproj index d399800c..9f03942c 100644 --- a/src/MADE.Collections/MADE.Collections.csproj +++ b/src/MADE.Collections/MADE.Collections.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Collections MADE.NET Collections allows you to easily interact with collections, lists, arrays, and dictionaries. diff --git a/src/MADE.Data.Converters/BooleanToStringValueConverter.Windows.cs b/src/MADE.Data.Converters/BooleanToStringValueConverter.Windows.cs deleted file mode 100644 index 303783d4..00000000 --- a/src/MADE.Data.Converters/BooleanToStringValueConverter.Windows.cs +++ /dev/null @@ -1,92 +0,0 @@ -// MADE Apps licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if WINDOWS_UWP -namespace MADE.Data.Converters -{ - using System; - using MADE.Data.Converters.Strings; - using Windows.UI.Xaml; - using Windows.UI.Xaml.Data; - - /// - /// Defines a Windows components for a XAML value converter from to . - /// - [Obsolete("BooleanToStringValueConverter for Windows will be removed in a future major release. Use the replicated BooleanToStringValueConverter type from the MADE.UI.Data.Converters library instead.")] - public partial class BooleanToStringValueConverter : DependencyObject, IValueConverter, IValueConverter - { - /// - /// Defines the dependency property for . - /// - public static readonly DependencyProperty TrueValueProperty = - DependencyProperty.Register( - nameof(TrueValue), - typeof(string), - typeof(BooleanToStringValueConverter), - new PropertyMetadata(Resources.BooleanToStringValueConverter_TrueValue)); - - /// - /// Defines the dependency property for . - /// - public static readonly DependencyProperty FalseValueProperty = - DependencyProperty.Register( - nameof(FalseValue), - typeof(string), - typeof(BooleanToStringValueConverter), - new PropertyMetadata(Resources.BooleanToStringValueConverter_FalseValue)); - - /// - /// Gets or sets the positive/true value. - /// - public string TrueValue - { - get => (string)this.GetValue(TrueValueProperty); - set => this.SetValue(TrueValueProperty, value); - } - - /// - /// Gets or sets the negative/false value. - /// - public string FalseValue - { - get => (string)this.GetValue(FalseValueProperty); - set => this.SetValue(FalseValueProperty, value); - } - - /// - /// Converts the value to the type. - /// - /// The value to convert. - /// The target type (unused). - /// The optional parameter used to help with conversion (unused). - /// The display language for the conversion (unused). - /// The converted object. - public object Convert(object value, Type targetType, object parameter, string language) - { - return value switch - { - bool b => this.Convert(b, parameter), - _ => value - }; - } - - /// - /// Converts the value back to the type. - /// - /// The value to convert. - /// The target type (unused). - /// The optional parameter used to help with conversion (unused). - /// The display language for the conversion (unused). - /// The converted object. - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - if (value is not string b) - { - return value; - } - - return this.ConvertBack(b, parameter); - } - } -} -#endif \ No newline at end of file diff --git a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs index 0fe09944..21899b24 100644 --- a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs +++ b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs @@ -9,9 +9,8 @@ namespace MADE.Data.Converters /// /// Defines a value converter from to with a pre-determined and . /// - public partial class BooleanToStringValueConverter : IValueConverter + public class BooleanToStringValueConverter : IValueConverter { -#if !WINDOWS_UWP /// /// Gets or sets the positive/true value. /// @@ -21,7 +20,6 @@ public partial class BooleanToStringValueConverter : IValueConverter public string FalseValue { get; set; } -#endif /// /// Converts the value to the type. diff --git a/src/MADE.Data.Converters/DateTimeToStringValueConverter.Windows.cs b/src/MADE.Data.Converters/DateTimeToStringValueConverter.Windows.cs deleted file mode 100644 index 2f3ac5f4..00000000 --- a/src/MADE.Data.Converters/DateTimeToStringValueConverter.Windows.cs +++ /dev/null @@ -1,58 +0,0 @@ -// MADE Apps licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -#if WINDOWS_UWP -namespace MADE.Data.Converters -{ - using System; - using Windows.UI.Xaml.Data; - - /// - /// Defines a Windows components for a XAML value converter from to with an optional format string. - /// - [Obsolete("DateTimeToStringValueConverter for Windows will be removed in a future major release. Use the replicated DateTimeToStringValueConverter type from the MADE.UI.Data.Converters library instead.")] - public partial class DateTimeToStringValueConverter : IValueConverter - { - /// - /// Converts the value to the type. - /// - /// - /// The value to convert. - /// - /// The target type (unused). - /// - /// The optional string format parameter used to help with conversion. - /// - /// The display language for the conversion (unused). - /// - /// The converted object. - /// - public object Convert(object value, Type targetType, object parameter, string language) - { - switch (value) - { - case DateTime dateTime: - return this.Convert(dateTime, parameter?.ToString()); - default: - return value; - } - } - - /// - /// Converts the value back to the type. - /// - /// The value to convert. - /// The target type (unused). - /// The optional parameter used to help with conversion (unused). - /// The display language for the conversion (unused). - /// - /// The converted object. - /// - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - string dateTimeString = value?.ToString(); - return this.ConvertBack(dateTimeString); - } - } -} -#endif \ No newline at end of file diff --git a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs index b21c6227..41c65810 100644 --- a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs +++ b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs @@ -9,7 +9,7 @@ namespace MADE.Data.Converters /// /// Defines a value converter from to with an optional format string. /// - public partial class DateTimeToStringValueConverter : IValueConverter + public class DateTimeToStringValueConverter : IValueConverter { /// /// Converts the value to the type. diff --git a/src/MADE.Data.Converters/MADE.Data.Converters.csproj b/src/MADE.Data.Converters/MADE.Data.Converters.csproj index 6392f291..38e6333b 100644 --- a/src/MADE.Data.Converters/MADE.Data.Converters.csproj +++ b/src/MADE.Data.Converters/MADE.Data.Converters.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0;uap10.0.19041 + net8.0;net10.0 MADE.NET Data Converters MADE.NET Data Converters provide out-of-the-box value converters for taking values of one type and changing them to another. diff --git a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj index 9c6985ec..cfee4ba0 100644 --- a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj +++ b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj @@ -1,7 +1,7 @@ - + - net6.0;net7.0;net8.0 + net8.0;net10.0 MADE.NET for EF Core MADE.NET for EF Core builds on the base Entity Framework library to provide base classes and helpers for maintaining data in databases. @@ -9,19 +9,14 @@ MADE EFCore Entity Framework Extensions Queryable DbContext - - - - - - - - + + + - - - + + + diff --git a/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj b/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj index d35c9044..a8d13b5b 100644 --- a/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj +++ b/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Data Serialization MADE.NET Data Serialization provides out-of-the-box serialization helpers and extensions for ensuring data quality when serializing to and from different formats such as JSON and XML. diff --git a/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj b/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj index 0bc00a63..22f85f2c 100644 --- a/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj +++ b/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Data Validation for FluentValidation MADE.NET Data Validation for FluentValidation builds on the base FluentValidation library to provide an easy-to-use validator collection implementation that can chain together validators for a single value. diff --git a/src/MADE.Data.Validation/MADE.Data.Validation.csproj b/src/MADE.Data.Validation/MADE.Data.Validation.csproj index f9633ddd..53d87457 100644 --- a/src/MADE.Data.Validation/MADE.Data.Validation.csproj +++ b/src/MADE.Data.Validation/MADE.Data.Validation.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Data Validation MADE.NET Data Validation comes fully loaded with all the value validators you'd expect of any validation library. diff --git a/src/MADE.Diagnostics/AppDiagnostics.cs b/src/MADE.Diagnostics/AppDiagnostics.cs index 1681f876..d7e924c2 100644 --- a/src/MADE.Diagnostics/AppDiagnostics.cs +++ b/src/MADE.Diagnostics/AppDiagnostics.cs @@ -57,11 +57,7 @@ public async Task StartRecordingDiagnosticsAsync() this.EventLogger.WriteInfo("Application diagnostics initialized."); -#if WINDOWS_UWP - Windows.UI.Xaml.Application.Current.UnhandledException += this.OnAppUnhandledException; -#elif NETSTANDARD2_0 AppDomain.CurrentDomain.UnhandledException += this.OnAppUnhandledException; -#endif TaskScheduler.UnobservedTaskException += this.OnTaskUnobservedException; await Task.CompletedTask; @@ -77,11 +73,7 @@ public void StopRecordingDiagnostics() return; } -#if WINDOWS_UWP - Windows.UI.Xaml.Application.Current.UnhandledException -= this.OnAppUnhandledException; -#elif NETSTANDARD2_0 AppDomain.CurrentDomain.UnhandledException -= this.OnAppUnhandledException; -#endif TaskScheduler.UnobservedTaskException -= this.OnTaskUnobservedException; this.IsRecordingDiagnostics = false; @@ -101,17 +93,6 @@ private void OnTaskUnobservedException(object sender, UnobservedTaskExceptionEve this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception)); } -#if WINDOWS_UWP - private void OnAppUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs args) - { - args.Handled = true; - - this.EventLogger.WriteCritical( - args.Exception != null - ? $"An unhandled exception was thrown. Error: {args.Exception}" - : "An unhandled exception was thrown. Error: No exception information was available."); - } -#elif NETSTANDARD2_0 private void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs args) { if (args.IsTerminating) @@ -131,6 +112,5 @@ private void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, ex)); } -#endif } } \ No newline at end of file diff --git a/src/MADE.Diagnostics/MADE.Diagnostics.csproj b/src/MADE.Diagnostics/MADE.Diagnostics.csproj index 58b89c6b..345a6bf6 100644 --- a/src/MADE.Diagnostics/MADE.Diagnostics.csproj +++ b/src/MADE.Diagnostics/MADE.Diagnostics.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0;uap10.0.19041 + net8.0;net10.0 MADE.NET Diagnostics MADE.NET Diagnostics provides helpers to make debugging and logging results in your applications easier. @@ -9,8 +9,4 @@ MADE Diagnostics Extensions Logging EventLog Logger Exception Stopwatch - - - - diff --git a/src/MADE.Foundation/MADE.Foundation.csproj b/src/MADE.Foundation/MADE.Foundation.csproj index d6d248c4..161e3d2a 100644 --- a/src/MADE.Foundation/MADE.Foundation.csproj +++ b/src/MADE.Foundation/MADE.Foundation.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Foundation MADE.NET Foundation is a base library for MADE.NET that allows platform-specific logic to be defined with a helper for ensuring your code can continue to execute with API availability checks. diff --git a/src/MADE.Networking/MADE.Networking.csproj b/src/MADE.Networking/MADE.Networking.csproj index a0fe56ac..4b97f648 100644 --- a/src/MADE.Networking/MADE.Networking.csproj +++ b/src/MADE.Networking/MADE.Networking.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Networking MADE.NET Networking comes fully loaded with wrappers for easily executing network requests from applications, handling the responses, as well as providing extensions for common URI scenarios. diff --git a/src/MADE.Runtime/MADE.Runtime.csproj b/src/MADE.Runtime/MADE.Runtime.csproj index 449a700b..b83eed76 100644 --- a/src/MADE.Runtime/MADE.Runtime.csproj +++ b/src/MADE.Runtime/MADE.Runtime.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Runtime MADE.NET Runtime is a base library that builds on .NET to provide additional types and extensions that you might expect to find in the BCL, but might not exist, such as a WeakReferenceCallback. diff --git a/src/MADE.Testing/MADE.Testing.csproj b/src/MADE.Testing/MADE.Testing.csproj index 36cde721..9d2c4fe6 100644 --- a/src/MADE.Testing/MADE.Testing.csproj +++ b/src/MADE.Testing/MADE.Testing.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Testing MADE.NET Testing provides test framework agnostic helpers and extensions to perform assertions on your defined scenarios. diff --git a/src/MADE.Threading/MADE.Threading.csproj b/src/MADE.Threading/MADE.Threading.csproj index 4021374a..9574e52e 100644 --- a/src/MADE.Threading/MADE.Threading.csproj +++ b/src/MADE.Threading/MADE.Threading.csproj @@ -1,7 +1,7 @@ - + - netstandard2.0 + net8.0;net10.0 MADE.NET Threading MADE.NET Threading provides helpers and extensions that help with application scenarios that care about threads. diff --git a/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj b/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj index 513b9a29..357ad463 100644 --- a/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj +++ b/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj @@ -1,7 +1,7 @@ - + - net6.0;net7.0;net8.0 + net8.0;net10.0 MADE.NET for ASP.NET Core MVC MADE.NET for ASP.NET Core MVC builds on features of ASP.NET Core MVC to provide additional MVC-specific functionality to improve your web applications. diff --git a/src/MADE.Web/MADE.Web.csproj b/src/MADE.Web/MADE.Web.csproj index 6d35ce40..a4d139eb 100644 --- a/src/MADE.Web/MADE.Web.csproj +++ b/src/MADE.Web/MADE.Web.csproj @@ -1,7 +1,7 @@ - + - net6.0;net7.0;net8.0 + net8.0;net10.0 MADE.NET for ASP.NET Core MADE.NET for ASP.NET Core builds on features of ASP.NET Core to provide better support for your web API or MVC applications, including standardized pagination support, global authenticated user accessor, standardized exception handling with JSON responses, and API versioning. @@ -17,19 +17,14 @@ - - - - - - - - + + + - - - + + + \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index af336dfc..9a2d31e7 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,6 +1,11 @@ + enable + enable + false + true + false 1.0.0.0 MADE Apps MADE Apps @@ -10,7 +15,14 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + \ No newline at end of file diff --git a/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj b/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj index ce77d26f..aea6f21e 100644 --- a/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj +++ b/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj b/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj index 53a99c23..c81b576b 100644 --- a/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj +++ b/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj index 7e251db6..1596f45c 100644 --- a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj +++ b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj @@ -1,35 +1,21 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - + + - + + + + diff --git a/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj b/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj index 8fb4bc7b..50d5d721 100644 --- a/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj +++ b/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj b/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj index 348b0163..2f487473 100644 --- a/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj +++ b/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj b/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj index c1141dc5..d1d1734f 100644 --- a/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj +++ b/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj b/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj index 6a8ded1b..67cfb976 100644 --- a/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj +++ b/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj index 7086d164..b3d1933a 100644 --- a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj +++ b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + diff --git a/tests/MADE.Web.Tests/MADE.Web.Tests.csproj b/tests/MADE.Web.Tests/MADE.Web.Tests.csproj index 91e41ccd..f7b36a45 100644 --- a/tests/MADE.Web.Tests/MADE.Web.Tests.csproj +++ b/tests/MADE.Web.Tests/MADE.Web.Tests.csproj @@ -1,21 +1,12 @@ - net6.0;net7.0;net8.0 - false + net8.0;net10.0 - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + From 38c720efbd018120c046ad7b75945c37e02327c5 Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 14:30:11 +0100 Subject: [PATCH 02/12] feat: migrate to System.Text.Json and drop third-party dependencies Replace Newtonsoft.Json with System.Text.Json across Networking, Web, Web.Mvc, and Data.Serialization libraries. Replace JsonTypeMigrationSerializationBinder with JsonTypeMigrationConverter using System.Text.Json's JsonConverter API. Remove Z.EntityFramework.Plus.EFCore from Data.EFCore, reimplementing OrderBy using expression trees. Add QueryableExtensions tests for OrderBy and Page. Add CHANGELOG.md documenting all v3 breaking changes with migration guidance. --- .github/dependabot.yml | 1 - CHANGELOG.md | 97 ++++++++++ .../Extensions/QueryableExtensions.cs | 18 +- src/MADE.Data.EFCore/MADE.Data.EFCore.csproj | 2 - .../JsonTypeMigrationConverter.cs} | 91 ++++++--- .../Json/JsonTypeMigration.cs | 4 +- .../MADE.Data.Serialization.csproj | 4 - .../Requests/Json/JsonDeleteNetworkRequest.cs | 6 +- .../Requests/Json/JsonGetNetworkRequest.cs | 6 +- .../Requests/Json/JsonPatchNetworkRequest.cs | 6 +- .../Requests/Json/JsonPostNetworkRequest.cs | 6 +- .../Requests/Json/JsonPutNetworkRequest.cs | 6 +- .../Http/Responses/HttpResponseMessage{T}.cs | 6 +- src/MADE.Networking/MADE.Networking.csproj | 4 - .../Extensions/ControllerBaseExtensions.cs | 8 +- src/MADE.Web.Mvc/MADE.Web.Mvc.csproj | 4 - src/MADE.Web.Mvc/Responses/JsonResult.cs | 16 +- .../Extensions/HttpResponseExtensions.cs | 16 +- src/MADE.Web/MADE.Web.csproj | 4 - .../MADE.Data.EFCore.Tests.csproj | 2 + .../Tests/QueryableExtensionsTests.cs | 179 ++++++++++++++++++ ...onTypeMigrationSerializationBinderTests.cs | 53 ++++-- .../Tests/JsonDeleteNetworkRequestTests.cs | 6 +- .../Tests/JsonGetNetworkRequestTests.cs | 6 +- .../Tests/JsonPatchNetworkRequestTests.cs | 12 +- .../Tests/JsonPostNetworkRequestTests.cs | 12 +- .../Tests/JsonPutNetworkRequestTests.cs | 12 +- .../Tests/NetworkRequestManagerTests.cs | 6 +- 28 files changed, 463 insertions(+), 130 deletions(-) create mode 100644 CHANGELOG.md rename src/MADE.Data.Serialization/Json/{Binders/JsonTypeMigrationSerializationBinder.cs => Converters/JsonTypeMigrationConverter.cs} (50%) create mode 100644 tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a00f7030..7697f858 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,7 +21,6 @@ updates: entity-framework: patterns: - "Microsoft.EntityFrameworkCore*" - - "Z.EntityFramework*" aspnet: patterns: - "Asp.Versioning*" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..717f5231 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,97 @@ +# Changelog + +## v3.0.0 + +### Breaking Changes + +#### Target Framework Updates + +- All libraries now target `net8.0` and `net10.0`. Previous target frameworks have been removed. + +#### Newtonsoft.Json Replaced with System.Text.Json + +The following libraries have migrated from `Newtonsoft.Json` to `System.Text.Json`. All public APIs that previously accepted `Newtonsoft.Json.JsonSerializerSettings` now accept `System.Text.Json.JsonSerializerOptions`. + +**MADE.Web.Mvc** + +- `JsonResult` constructor parameter type changed from `JsonSerializerSettings` to `JsonSerializerOptions`. +- `JsonResult.SerializerOptions` property type changed from `JsonSerializerSettings` to `JsonSerializerOptions`. +- `ControllerBaseExtensions.Json()` parameter type changed from `JsonSerializerSettings` to `JsonSerializerOptions`. + +**MADE.Web** + +- All `HttpResponseExtensions.WriteJsonAsync()` overloads that accepted `JsonSerializerSettings` now accept `JsonSerializerOptions`. + +**MADE.Networking** + +- Internal serialization switched from `Newtonsoft.Json` to `System.Text.Json`. All deserialization uses `PropertyNameCaseInsensitive = true` to maintain behavioral compatibility. +- No public API signature changes. + +**MADE.Data.Serialization** + +- `JsonTypeMigrationSerializationBinder` has been removed. Use `JsonTypeMigrationConverter` instead (see migration guide below). + +### Removed Dependencies + +| Library | Removed Dependency | Replacement | +| --- | --- | --- | +| MADE.Networking | `Newtonsoft.Json` | `System.Text.Json` (built-in) | +| MADE.Web | `Newtonsoft.Json` | `System.Text.Json` (built-in) | +| MADE.Web.Mvc | `Newtonsoft.Json` | `System.Text.Json` (built-in) | +| MADE.Data.Serialization | `Newtonsoft.Json` | `System.Text.Json` (built-in) | +| MADE.Data.EFCore | `Z.EntityFramework.Plus.EFCore` | Custom implementation using Expression trees | + +### Migration Guide + +#### Newtonsoft.Json to System.Text.Json + +Replace `using Newtonsoft.Json` with `using System.Text.Json` and update any `JsonSerializerSettings` references to `JsonSerializerOptions`. + +```csharp +// Before (v2) +using Newtonsoft.Json; + +var result = controller.Json(value, HttpStatusCode.OK, new JsonSerializerSettings +{ + NullValueHandling = NullValueHandling.Ignore +}); + +// After (v3) +using System.Text.Json; + +var result = controller.Json(value, HttpStatusCode.OK, new JsonSerializerOptions +{ + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +}); +``` + +#### JsonTypeMigrationSerializationBinder to JsonTypeMigrationConverter + +The Newtonsoft.Json-based `JsonTypeMigrationSerializationBinder` in the `MADE.Data.Serialization.Json.Binders` namespace has been replaced with `JsonTypeMigrationConverter` in the `MADE.Data.Serialization.Json.Converters` namespace. + +```csharp +// Before (v2) +using MADE.Data.Serialization.Json.Binders; + +var binder = new JsonTypeMigrationSerializationBinder(); +binder.AddTypeMigration(new JsonTypeMigration("OldAssembly", "OldNamespace.OldType", typeof(NewType))); + +var settings = new JsonSerializerSettings +{ + TypeNameHandling = TypeNameHandling.All, + SerializationBinder = binder +}; +var result = JsonConvert.DeserializeObject(json, settings); + +// After (v3) +using MADE.Data.Serialization.Json.Converters; + +var converter = new JsonTypeMigrationConverter(); +await converter.AddTypeMigrationAsync(new JsonTypeMigration("OldAssembly", "OldNamespace.OldType", typeof(NewType))); + +var options = new JsonSerializerOptions(); +options.Converters.Add(converter); +var result = JsonSerializer.Deserialize(json, options); +``` + +Note: `AddTypeMigration` has been renamed to `AddTypeMigrationAsync` and is now asynchronous. diff --git a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs index 244bd0b2..2b9786f6 100644 --- a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs @@ -3,8 +3,9 @@ namespace MADE.Data.EFCore.Extensions { + using System; using System.Linq; - using Z.EntityFramework.Plus; + using System.Linq.Expressions; /// /// Defines a collection of extensions for Entity Framework queries. @@ -39,7 +40,20 @@ public static IQueryable OrderBy(this IQueryable query, string sortName return query; } - return !sortDesc ? query.AddOrAppendOrderBy(sortName) : query.AddOrAppendOrderByDescending(sortName); + ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); + MemberExpression property = Expression.Property(parameter, sortName); + LambdaExpression lambda = Expression.Lambda(property, parameter); + + string methodName = sortDesc ? "OrderByDescending" : "OrderBy"; + + MethodCallExpression call = Expression.Call( + typeof(Queryable), + methodName, + new[] { typeof(T), property.Type }, + query.Expression, + Expression.Quote(lambda)); + + return query.Provider.CreateQuery(call); } } } \ No newline at end of file diff --git a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj index cfee4ba0..fd57677a 100644 --- a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj +++ b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj @@ -11,12 +11,10 @@ - - diff --git a/src/MADE.Data.Serialization/Json/Binders/JsonTypeMigrationSerializationBinder.cs b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs similarity index 50% rename from src/MADE.Data.Serialization/Json/Binders/JsonTypeMigrationSerializationBinder.cs rename to src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs index 1d23bfee..4d22a372 100644 --- a/src/MADE.Data.Serialization/Json/Binders/JsonTypeMigrationSerializationBinder.cs +++ b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs @@ -1,45 +1,48 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Serialization.Json.Binders +namespace MADE.Data.Serialization.Json.Converters { using System; using System.Collections.Generic; using System.Linq; + using System.Text.Json; + using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; - using MADE.Data.Serialization.Json; using MADE.Data.Serialization.Json.Exceptions; - using Newtonsoft.Json.Serialization; /// - /// Defines a serialization binder for JSON.NET for migrating serialized declarations within a serialized JSON object. + /// Defines a JSON converter for migrating serialized declarations within a serialized JSON object. /// /// - /// This is for migrating serialized types where TypeNameHandling.All has been set in the JSON serializer settings. + /// This converter reads $type metadata from JSON objects and resolves the target type using registered type migrations. + /// It is designed to deserialize JSON that was previously serialized with type metadata (e.g., Newtonsoft.Json's TypeNameHandling.All). /// - public class JsonTypeMigrationSerializationBinder : DefaultSerializationBinder + public class JsonTypeMigrationConverter : JsonConverter { private readonly SemaphoreSlim migrationSemaphore; private readonly List migrations = new(); + private JsonSerializerOptions innerOptions; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// /// To add migrations, call the method. /// - public JsonTypeMigrationSerializationBinder() + public JsonTypeMigrationConverter() : this(null) { } /// - /// Initializes a new instance of the class with pre-configured type migrations. + /// Initializes a new instance of the class with pre-configured type migrations. /// /// The type migrations to initialize with. - public JsonTypeMigrationSerializationBinder(params JsonTypeMigration[] migrations) + public JsonTypeMigrationConverter(params JsonTypeMigration[] migrations) { this.migrationSemaphore = new SemaphoreSlim(1, 1); @@ -50,7 +53,7 @@ public JsonTypeMigrationSerializationBinder(params JsonTypeMigration[] migration } /// - /// Adds a JSON type migration to the binder. + /// Adds a JSON type migration to the converter. /// /// The type migration to add. /// An asynchronous operation. @@ -86,32 +89,68 @@ public async Task AddTypeMigrationAsync(JsonTypeMigration migration) } } - /// - /// When overridden in a derived class, controls the binding of a serialized object to a type. - /// - /// Specifies the name of the serialized object. - /// Specifies the name of the serialized object. - /// - /// The type of the object the formatter creates a new instance of. - /// - public override Type BindToType(string assemblyName, string typeName) + /// + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument doc = JsonDocument.ParseValue(ref reader); + JsonElement root = doc.RootElement; + + Type resolvedType = typeToConvert; + + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("$type", out JsonElement typeElement)) + { + string typeString = typeElement.GetString(); + resolvedType = this.ResolveType(typeString) ?? typeToConvert; + } + + return root.Deserialize(resolvedType, this.GetInnerOptions(options)); + } + + /// + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), this.GetInnerOptions(options)); + } + + private JsonSerializerOptions GetInnerOptions(JsonSerializerOptions options) + { + if (this.innerOptions == null) + { + var copy = new JsonSerializerOptions(options); + copy.Converters.Remove(this); + this.innerOptions = copy; + } + + return this.innerOptions; + } + + private Type ResolveType(string typeString) { - Task task = this.migrationSemaphore.WaitAsync(); - Task.WaitAll(task); + int commaIndex = typeString.IndexOf(','); + string typeName = commaIndex >= 0 ? typeString[..commaIndex].Trim() : typeString.Trim(); + string assemblyName = commaIndex >= 0 ? typeString[(commaIndex + 1)..].Trim() : null; + + this.migrationSemaphore.Wait(); - JsonTypeMigration migration = null; + JsonTypeMigration migration; try { migration = this.migrations.FirstOrDefault( m => - m.FromAssemblyName == assemblyName && m.FromTypeName == typeName); + m.FromTypeName == typeName && + (assemblyName == null || m.FromAssemblyName == assemblyName)); } finally { this.migrationSemaphore.Release(); } - return migration != null ? migration.ToType : base.BindToType(assemblyName, typeName); + if (migration != null) + { + return migration.ToType; + } + + return Type.GetType(typeString); } } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs index bd17a49d..5fd90c61 100644 --- a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs +++ b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs @@ -4,10 +4,10 @@ namespace MADE.Data.Serialization.Json { using System; - using MADE.Data.Serialization.Json.Binders; + using MADE.Data.Serialization.Json.Converters; /// - /// Defines the detail for migrating from one type to another using the . + /// Defines the detail for migrating from one type to another using the . /// public class JsonTypeMigration { diff --git a/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj b/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj index a8d13b5b..8a9dc3a5 100644 --- a/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj +++ b/src/MADE.Data.Serialization/MADE.Data.Serialization.csproj @@ -9,8 +9,4 @@ MADE Data Serialization Extensions JSON XML - - - - \ No newline at end of file diff --git a/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs index 799fdc13..c86acd92 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs @@ -9,7 +9,7 @@ namespace MADE.Networking.Http.Requests.Json using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a network request for a DELETE call with a JSON response. @@ -65,7 +65,7 @@ public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary ExecuteAsync(CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } /// @@ -85,7 +85,7 @@ public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json, expectedResponse); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) diff --git a/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs index e3375704..00dc0e41 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs @@ -9,7 +9,7 @@ namespace MADE.Networking.Http.Requests.Json using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a network request for a GET call with a JSON response. @@ -65,7 +65,7 @@ public JsonGetNetworkRequest(HttpClient client, string url, Dictionary ExecuteAsync(CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } /// @@ -85,7 +85,7 @@ public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json, expectedResponse); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs index d1294cd1..301e0afa 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs @@ -10,7 +10,7 @@ namespace MADE.Networking.Http.Requests.Json using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a network request for a PATCH call with a JSON response. @@ -96,7 +96,7 @@ public JsonPatchNetworkRequest( public override async Task ExecuteAsync(CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } /// @@ -116,7 +116,7 @@ public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json, expectedResponse); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs index 929bb065..653432a6 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs @@ -10,7 +10,7 @@ namespace MADE.Networking.Http.Requests.Json using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a network request for a POST call with a JSON response. @@ -96,7 +96,7 @@ public JsonPostNetworkRequest( public override async Task ExecuteAsync(CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } /// @@ -116,7 +116,7 @@ public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json, expectedResponse); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs index ab917279..0dd2e85e 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs @@ -10,7 +10,7 @@ namespace MADE.Networking.Http.Requests.Json using System.Threading; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a network request for a PUT call with a JSON response. @@ -92,7 +92,7 @@ public JsonPutNetworkRequest(HttpClient client, string url, string jsonData, Dic public override async Task ExecuteAsync(CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } /// @@ -112,7 +112,7 @@ public override async Task ExecuteAsync( CancellationToken cancellationToken = default) { string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonConvert.DeserializeObject(json, expectedResponse); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) diff --git a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs index 7fa104dc..6736157e 100644 --- a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs +++ b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs @@ -8,7 +8,7 @@ namespace MADE.Networking.Http.Responses using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a HTTP response message that includes a deserializing option for the response data. @@ -91,7 +91,9 @@ public static implicit operator HttpResponseMessage(HttpResponseMessage respo /// A representing the result of the asynchronous operation. public async Task DeserializeAsync() { - this.DeserializedContent = JsonConvert.DeserializeObject(await this.Content.ReadAsStringAsync()); + this.DeserializedContent = JsonSerializer.Deserialize( + await this.Content.ReadAsStringAsync(), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return this.DeserializedContent; } diff --git a/src/MADE.Networking/MADE.Networking.csproj b/src/MADE.Networking/MADE.Networking.csproj index 4b97f648..f1ee274d 100644 --- a/src/MADE.Networking/MADE.Networking.csproj +++ b/src/MADE.Networking/MADE.Networking.csproj @@ -10,10 +10,6 @@ MADE Networking Extensions Json Stream HttpClient - - - - diff --git a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs index d1ef5472..8a235f96 100644 --- a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs +++ b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs @@ -9,7 +9,7 @@ namespace MADE.Web.Mvc.Extensions using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; - using Newtonsoft.Json; + using System.Text.Json; using JsonResult = MADE.Web.Mvc.Responses.JsonResult; /// @@ -23,21 +23,21 @@ public static class ControllerBaseExtensions /// The controller that is performing the response. /// The value object to serialize. /// The expected result HTTP status code. - /// The Json.NET serializer settings for serializing the result. + /// The JSON serializer options for serializing the result. /// The created for the response. /// Thrown if the is . public static IActionResult Json( this ControllerBase controller, object value, HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerSettings serializerSettings = null) + JsonSerializerOptions serializerOptions = null) { if (controller == null) { throw new ArgumentNullException(nameof(controller)); } - return new JsonResult(value, statusCode, serializerSettings); + return new JsonResult(value, statusCode, serializerOptions); } /// diff --git a/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj b/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj index 357ad463..0bb988fc 100644 --- a/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj +++ b/src/MADE.Web.Mvc/MADE.Web.Mvc.csproj @@ -13,10 +13,6 @@ - - - - diff --git a/src/MADE.Web.Mvc/Responses/JsonResult.cs b/src/MADE.Web.Mvc/Responses/JsonResult.cs index c3508b3a..a82bc858 100644 --- a/src/MADE.Web.Mvc/Responses/JsonResult.cs +++ b/src/MADE.Web.Mvc/Responses/JsonResult.cs @@ -11,10 +11,10 @@ namespace MADE.Web.Mvc.Responses using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Infrastructure; - using Newtonsoft.Json; + using System.Text.Json; /// - /// Defines a model for a result of a request that is serialized as JSON using Json.NET. + /// Defines a model for a result of a request that is serialized as JSON. /// public class JsonResult : ActionResult, IStatusCodeActionResult { @@ -23,15 +23,15 @@ public class JsonResult : ActionResult, IStatusCodeActionResult /// /// The value object to serialize. /// The expected result HTTP status code. - /// The Json.Net serializer settings for serializing the result. + /// The JSON serializer options for serializing the result. public JsonResult( object value, HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerSettings serializerSettings = default) + JsonSerializerOptions serializerOptions = default) { this.Value = value; this.StatusCode = (int)statusCode; - this.SerializerSettings = serializerSettings; + this.SerializerOptions = serializerOptions; } /// @@ -45,9 +45,9 @@ public JsonResult( public int? StatusCode { get; } /// - /// Gets the Json.Net serializer settings for serializing the result. + /// Gets the JSON serializer options for serializing the result. /// - public JsonSerializerSettings SerializerSettings { get; } + public JsonSerializerOptions SerializerOptions { get; } /// /// Executes the result operation of the action method asynchronously writing the to the response. @@ -70,7 +70,7 @@ public override async Task ExecuteResultAsync(ActionContext context) await response.WriteJsonAsync( this.StatusCode.GetValueOrDefault((int)HttpStatusCode.OK), this.Value, - this.SerializerSettings); + this.SerializerOptions); } catch (Exception ex) { diff --git a/src/MADE.Web/Extensions/HttpResponseExtensions.cs b/src/MADE.Web/Extensions/HttpResponseExtensions.cs index d1194990..05e08b9d 100644 --- a/src/MADE.Web/Extensions/HttpResponseExtensions.cs +++ b/src/MADE.Web/Extensions/HttpResponseExtensions.cs @@ -8,7 +8,7 @@ namespace MADE.Web.Extensions using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; - using Newtonsoft.Json; + using System.Text.Json; /// /// Defines a collection of extensions for a object. @@ -51,15 +51,15 @@ public static async Task WriteJsonAsync( /// The HTTP response to write to. /// The status code of the response. /// The object to serialize as JSON. - /// The JSON serializer settings. + /// The JSON serializer options. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, HttpStatusCode statusCode, object value, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { - await WriteJsonAsync(response, (int)statusCode, value, serializerSettings); + await WriteJsonAsync(response, (int)statusCode, value, serializerOptions); } /// @@ -68,18 +68,20 @@ public static async Task WriteJsonAsync( /// The HTTP response to write to. /// The status code of the response. /// The object to serialize as JSON. - /// The JSON serializer settings. + /// The JSON serializer options. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, int statusCode, object value, - JsonSerializerSettings serializerSettings) + JsonSerializerOptions serializerOptions) { response.ContentType = new MediaTypeHeaderValue("application/json") { Encoding = Encoding.UTF8 }.ToString(); response.StatusCode = statusCode; - string json = JsonConvert.SerializeObject(value, Formatting.Indented, serializerSettings); + var options = serializerOptions ?? new JsonSerializerOptions { WriteIndented = true }; + + string json = JsonSerializer.Serialize(value, options); await response.WriteAsync(json, Encoding.UTF8); } diff --git a/src/MADE.Web/MADE.Web.csproj b/src/MADE.Web/MADE.Web.csproj index a4d139eb..b2e0753d 100644 --- a/src/MADE.Web/MADE.Web.csproj +++ b/src/MADE.Web/MADE.Web.csproj @@ -13,10 +13,6 @@ - - - - diff --git a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj index 1596f45c..5a11dcc2 100644 --- a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj +++ b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj @@ -11,10 +11,12 @@ + + diff --git a/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs new file mode 100644 index 00000000..3e846375 --- /dev/null +++ b/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs @@ -0,0 +1,179 @@ +namespace MADE.Data.EFCore.Tests.Tests +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Threading.Tasks; + using Data; + using Extensions; + using NUnit.Framework; + using Shouldly; + + [TestFixture] + [ExcludeFromCodeCoverage] + public class QueryableExtensionsTests + { + public class WhenOrderingByPropertyName + { + [Test] + public async Task ShouldOrderByNameAscending() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameAsc"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: false).ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Alice"); + result[1].Name.ShouldBe("Bob"); + result[2].Name.ShouldBe("Charlie"); + } + + [Test] + public async Task ShouldOrderByNameDescending() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameDesc"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: true).ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Charlie"); + result[1].Name.ShouldBe("Bob"); + result[2].Name.ShouldBe("Alice"); + } + + [Test] + public async Task ShouldReturnUnorderedQueryWhenSortNameIsEmpty() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByEmpty"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy("", sortDesc: false).ToList(); + + // Assert + result.Count.ShouldBe(2); + } + + [Test] + public async Task ShouldReturnUnorderedQueryWhenSortNameIsNull() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNull"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy(null, sortDesc: false).ToList(); + + // Assert + result.Count.ShouldBe(2); + } + } + + public class WhenPaging + { + [Test] + public async Task ShouldReturnFirstPage() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageFirst"); + + for (int i = 0; i < 10; i++) + { + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); + } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 1, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Entity00"); + result[1].Name.ShouldBe("Entity01"); + result[2].Name.ShouldBe("Entity02"); + } + + [Test] + public async Task ShouldReturnSecondPage() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageSecond"); + + for (int i = 0; i < 10; i++) + { + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); + } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 2, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Entity03"); + result[1].Name.ShouldBe("Entity04"); + result[2].Name.ShouldBe("Entity05"); + } + + [Test] + public async Task ShouldReturnPartialLastPage() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageLast"); + + for (int i = 0; i < 5; i++) + { + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); + } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 2, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(2); + result[0].Name.ShouldBe("Entity03"); + result[1].Name.ShouldBe("Entity04"); + } + } + } +} diff --git a/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs b/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs index 7a5ae889..51d9ecab 100644 --- a/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs +++ b/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs @@ -1,10 +1,10 @@ namespace MADE.Data.Serialization.Tests.Tests { using System.Diagnostics.CodeAnalysis; + using System.Text.Json; using System.Threading.Tasks; using MADE.Data.Serialization.Json; - using MADE.Data.Serialization.Json.Binders; - using Newtonsoft.Json; + using MADE.Data.Serialization.Json.Converters; using NUnit.Framework; using Shouldly; @@ -18,18 +18,27 @@ public class WhenMigratingFromOneTypeToAnother public async Task ShouldMigrateFromTypeToType() { // Arrange - var binder = new JsonTypeMigrationSerializationBinder(); - await binder.AddTypeMigrationAsync(new JsonTypeMigration(typeof(OldType), typeof(NewType))); + var converter = new JsonTypeMigrationConverter(); + await converter.AddTypeMigrationAsync(new JsonTypeMigration(typeof(OldType), typeof(NewType))); var oldType = new OldType(); - var serialized = JsonConvert.SerializeObject( - oldType, - new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}); + + // Simulate JSON with $type metadata (as previously serialized by Newtonsoft.Json with TypeNameHandling.All) + string serialized = JsonSerializer.Serialize(new + { + @__type = typeof(OldType).FullName + ", " + typeof(OldType).Assembly.GetName().Name, + oldType.Name, + oldType.Number + }); + + // Replace __type with $type since $ isn't valid in anonymous type member names + serialized = serialized.Replace("\"__type\"", "\"$type\""); + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); // Act - var deserialized = JsonConvert.DeserializeObject( - serialized, - new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All, SerializationBinder = binder}); + var deserialized = JsonSerializer.Deserialize(serialized, options); // Assert deserialized.ShouldBeOfType(typeof(NewType)); @@ -43,21 +52,29 @@ public async Task ShouldMigrateFromTypeToType() public async Task ShouldMigrateFromAssemblyAndTypeNameToType() { // Arrange - var binder = new JsonTypeMigrationSerializationBinder(); - await binder.AddTypeMigrationAsync(new JsonTypeMigration( + var converter = new JsonTypeMigrationConverter(); + await converter.AddTypeMigrationAsync(new JsonTypeMigration( "MADE.Data.Serialization.Tests", "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType", typeof(NewType))); var oldType = new OldType(); - var serialized = JsonConvert.SerializeObject( - oldType, - new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}); + + // Simulate JSON with $type metadata + string serialized = JsonSerializer.Serialize(new + { + @__type = "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType, MADE.Data.Serialization.Tests", + oldType.Name, + oldType.Number + }); + + serialized = serialized.Replace("\"__type\"", "\"$type\""); + + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); // Act - var deserialized = JsonConvert.DeserializeObject( - serialized, - new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All, SerializationBinder = binder}); + var deserialized = JsonSerializer.Deserialize(serialized, options); // Assert deserialized.ShouldBeOfType(typeof(NewType)); diff --git a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs index ab4e5597..7d844f15 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs @@ -5,7 +5,7 @@ namespace MADE.Networking.Tests.Tests using System.Net.Http; using System.Threading.Tasks; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -31,7 +31,7 @@ public async Task ShouldReturnSuccessFromDeleteEndpointWithResponse() // Assert response.ShouldNotBeNull(); response.Url.ShouldBe(requestUrl); - response.Args.Value(query).ShouldBe(queryValue); + bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); } [Test] @@ -54,7 +54,7 @@ public async Task ShouldReturnErrorFromGetEndpoint() public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Url { get; set; } } diff --git a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs index 18d18d41..7eb3460c 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs @@ -5,7 +5,7 @@ namespace MADE.Networking.Tests.Tests using System.Net.Http; using System.Threading.Tasks; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -31,7 +31,7 @@ public async Task ShouldReturnSuccessFromGetEndpointWithResponse() // Assert response.ShouldNotBeNull(); response.Url.ShouldBe(requestUrl); - response.Args.Value(query).ShouldBe(queryValue); + bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); } [Test] @@ -54,7 +54,7 @@ public async Task ShouldReturnErrorFromDeleteEndpoint() public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Url { get; set; } } diff --git a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs index a1179fab..87e02eec 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs @@ -5,8 +5,8 @@ namespace MADE.Networking.Tests.Tests using System.Net.Http; using System.Threading.Tasks; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -26,7 +26,7 @@ public async Task ShouldReturnSuccessFromPatchEndpointWithResponse() var request = new JsonPatchNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var response = await request.ExecuteAsync(); @@ -36,7 +36,7 @@ public async Task ShouldReturnSuccessFromPatchEndpointWithResponse() response.Url.ShouldBe(requestUrl); response.Data.ShouldNotBeNull(); - var responseData = JsonConvert.DeserializeObject(response.Data); + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); responseData.ShouldNotBeNull(); responseData.Key.ShouldBe(requestData.Key); responseData.Enabled.ShouldBe(requestData.Enabled); @@ -52,7 +52,7 @@ public async Task ShouldReturnErrorFromGetEndpoint() var request = new JsonPatchNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); @@ -71,7 +71,7 @@ public class RequestData public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Data { get; set; } diff --git a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs index e61572ca..6fb519a1 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs @@ -5,8 +5,8 @@ namespace MADE.Networking.Tests.Tests using System.Net.Http; using System.Threading.Tasks; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -26,7 +26,7 @@ public async Task ShouldReturnSuccessFromPostEndpointWithResponse() var request = new JsonPostNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var response = await request.ExecuteAsync(); @@ -36,7 +36,7 @@ public async Task ShouldReturnSuccessFromPostEndpointWithResponse() response.Url.ShouldBe(requestUrl); response.Data.ShouldNotBeNull(); - var responseData = JsonConvert.DeserializeObject(response.Data); + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); responseData.ShouldNotBeNull(); responseData.Key.ShouldBe(requestData.Key); responseData.Enabled.ShouldBe(requestData.Enabled); @@ -52,7 +52,7 @@ public async Task ShouldReturnErrorFromGetEndpoint() var request = new JsonPatchNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); @@ -71,7 +71,7 @@ public class RequestData public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Data { get; set; } diff --git a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs index d345da88..ee4b6af5 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs @@ -5,8 +5,8 @@ namespace MADE.Networking.Tests.Tests using System.Net.Http; using System.Threading.Tasks; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -26,7 +26,7 @@ public async Task ShouldReturnSuccessFromPutEndpointWithResponse() var request = new JsonPutNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var response = await request.ExecuteAsync(); @@ -36,7 +36,7 @@ public async Task ShouldReturnSuccessFromPutEndpointWithResponse() response.Url.ShouldBe(requestUrl); response.Data.ShouldNotBeNull(); - var responseData = JsonConvert.DeserializeObject(response.Data); + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); responseData.ShouldNotBeNull(); responseData.Key.ShouldBe(requestData.Key); responseData.Enabled.ShouldBe(requestData.Enabled); @@ -52,7 +52,7 @@ public async Task ShouldReturnErrorFromGetEndpoint() var request = new JsonPatchNetworkRequest( new HttpClient(), requestUrl, - JsonConvert.SerializeObject(requestData)); + JsonSerializer.Serialize(requestData)); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); @@ -71,7 +71,7 @@ public class RequestData public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Data { get; set; } diff --git a/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs b/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs index f77c7636..5c54bcb6 100644 --- a/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs +++ b/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs @@ -6,7 +6,7 @@ namespace MADE.Networking.Tests.Tests using System.Threading; using MADE.Networking.Http; using MADE.Networking.Http.Requests.Json; - using Newtonsoft.Json.Linq; + using System.Text.Json.Nodes; using NUnit.Framework; using Shouldly; @@ -146,7 +146,7 @@ public void ShouldProcessQueue() actualResponse.ShouldNotBeNull(); actualResponse.Url.ShouldBe(requestUrl); - actualResponse.Args.Value(query).ShouldBe(queryValue); + bool.Parse(actualResponse.Args[query].ToString()).ShouldBe(queryValue); } } @@ -192,7 +192,7 @@ public void ShouldStopProcessingQueue() public class RequestResponse { - public JObject Args { get; set; } + public JsonObject Args { get; set; } public string Url { get; set; } } From 8fa999cbae8be5f69a9b4ec59ce1ad2d5aa2a0c2 Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 19:00:46 +0100 Subject: [PATCH 03/12] refactor: modernize codebase with file-scoped namespaces, nullable annotations, and async improvements - Convert all source and test files to file-scoped namespace declarations - Add ConfigureAwait(false) to all 52 await expressions in library code - Replace manual null checks with ArgumentNullException.ThrowIfNull (22 locations) - Add nullable reference type annotations to public APIs (13 fixes across 11 files) - Change IEventLogger methods from void to Task for proper async support - Simplify JsonTypeMigrationConverter: rename AddTypeMigrationAsync to AddTypeMigration, replace SemaphoreSlim with lock - Fix flaky EntityBaseTests by capturing timestamps around save operations - Add comprehensive .editorconfig with modern .NET analysis rules - Update CHANGELOG.md with v3 breaking changes and QOL improvements --- .editorconfig | 237 +++--- CHANGELOG.md | 19 +- src/MADE.Collections/CollectionExtensions.cs | 732 +++++++++--------- .../Compare/GenericEqualityComparer{T}.cs | 111 ++- src/MADE.Collections/DictionaryExtensions.cs | 118 ++- ...eItemCollectionPropertyChangedEventArgs.cs | 79 +- ...emCollectionPropertyChangedEventHandler.cs | 33 +- .../ObservableItemCollection{T}.cs | 375 +++++---- src/MADE.Collections/QueryableExtensions.cs | 55 +- .../BooleanToStringValueConverter.cs | 103 ++- .../Constants/DateTimeConstants.cs | 29 +- .../DateTimeToStringValueConverter.cs | 89 ++- .../InvalidDataConversionException.cs | 91 ++- .../Extensions/BooleanExtensions.cs | 61 +- .../Extensions/CollectionExtensions.cs | 33 +- .../Extensions/DateTimeExtensions.cs | 585 +++++++------- .../Extensions/LengthExtensions.cs | 43 +- .../Extensions/MathExtensions.cs | 33 +- .../Extensions/StringExtensions.cs | 455 ++++++----- src/MADE.Data.Converters/IValueConverter.cs | 75 +- .../StringToBase64StringValueConverter.cs | 77 +- .../Strings/Resources.Designer.cs | 121 ++- .../Converters/UtcDateTimeConverter.cs | 111 ++- src/MADE.Data.EFCore/EntityBase.cs | 43 +- src/MADE.Data.EFCore/EntityBase{TKey}.cs | 45 +- .../Extensions/DbContextExtensions.cs | 243 +++--- .../Extensions/EntityBaseExtensions.cs | 95 ++- .../Extensions/QueryableExtensions.cs | 87 +-- src/MADE.Data.EFCore/IDatedEntity.cs | 31 +- src/MADE.Data.EFCore/IEntityBase.cs | 19 +- src/MADE.Data.EFCore/IEntityBase{TKey}.cs | 21 +- .../Converters/JsonTypeMigrationConverter.cs | 219 +++--- .../Exceptions/JsonTypeMigrationException.cs | 57 +- .../Json/JsonTypeMigration.cs | 85 +- .../FluentValidatorCollection{T}.cs | 139 ++-- .../Exceptions/InvalidRangeException.cs | 25 +- .../Extensions/ComparableExtensions.cs | 107 ++- .../Extensions/DateTimeExtensions.cs | 69 +- .../Extensions/MathExtensions.cs | 445 ++++++----- .../Extensions/StringExtensions.cs | 239 +++--- src/MADE.Data.Validation/IValidator.cs | 53 +- .../IValidatorCollection.cs | 51 +- .../InputValidatedEventArgs.cs | 77 +- .../InputValidatedEventHandler.cs | 25 +- .../Strings/Resources.Designer.cs | 353 +++++---- .../ValidatorCollection.cs | 127 ++- .../Validators/AlphaNumericValidator.cs | 45 +- .../Validators/AlphaValidator.cs | 47 +- .../Validators/Base64Validator.cs | 83 +- .../Validators/BetweenValidator.cs | 159 ++-- .../Validators/EmailValidator.cs | 45 +- .../Validators/GuidValidator.cs | 97 ++- .../Validators/IpAddressValidator.cs | 115 ++- .../Validators/LatitudeValidator.cs | 99 ++- .../Validators/LongitudeValidator.cs | 99 ++- .../Validators/MacAddressValidator.cs | 103 ++- .../Validators/MaxLengthValidator.cs | 125 ++- .../Validators/MaxValueValidator.cs | 117 ++- .../Validators/MinLengthValidator.cs | 125 ++- .../Validators/MinValueValidator.cs | 117 ++- .../Validators/PredicateValidator{T}.cs | 123 ++- .../Validators/RegexValidator.cs | 95 ++- .../Validators/RequiredValidator.cs | 101 ++- .../Validators/WellFormedUrlValidator.cs | 97 ++- src/MADE.Diagnostics/AppDiagnostics.cs | 168 ++-- .../Exceptions/ExceptionObservedEventArgs.cs | 57 +- .../ExceptionObservedEventHandler.cs | 25 +- src/MADE.Diagnostics/IAppDiagnostics.cs | 64 +- .../Logging/FileEventLogger.cs | 505 ++++++------ src/MADE.Diagnostics/Logging/IEventLogger.cs | 280 +++---- src/MADE.Diagnostics/StopwatchHelper.cs | 137 ++-- .../Platform/PlatformApiHelper.cs | 111 ++- .../Platform/PlatformNotSupportedAttribute.cs | 25 +- .../Platform/PlatformNotSupportedException.cs | 49 +- .../HttpResponseMessageExtensions.cs | 61 +- .../Extensions/UriExtensions.cs | 33 +- .../Http/INetworkRequestManager.cs | 182 +++-- .../Http/NetworkRequestManager.cs | 379 +++++---- .../Http/Requests/INetworkRequest.cs | 97 ++- .../Requests/Json/JsonDeleteNetworkRequest.cs | 198 +++-- .../Requests/Json/JsonGetNetworkRequest.cs | 198 +++-- .../Requests/Json/JsonPatchNetworkRequest.cs | 270 ++++--- .../Requests/Json/JsonPostNetworkRequest.cs | 266 ++++--- .../Requests/Json/JsonPutNetworkRequest.cs | 258 +++--- .../Http/Requests/NetworkRequest.cs | 145 ++-- .../Http/Requests/NetworkRequestCallback.cs | 126 ++- .../Streams/StreamGetNetworkRequest.cs | 193 +++-- .../Http/Responses/HttpResponseMessage{T}.cs | 243 +++--- src/MADE.Runtime/Actions/Chain.cs | 141 ++-- src/MADE.Runtime/Actions/IChain.cs | 69 +- .../Extensions/ReflectionExtensions.cs | 67 +- src/MADE.Runtime/WeakReferenceCallback.cs | 97 ++- ...tListener{TInstance,TSource,TEventArgs}.cs | 142 ++-- ...ferenceEventListener{TInstance,TSource}.cs | 130 ++-- .../CollectionAssertExtensions.cs | 353 +++++---- src/MADE.Threading/ITimer.cs | 109 ++- src/MADE.Threading/TaskExtensions.cs | 129 ++- src/MADE.Threading/Timer.cs | 243 +++--- .../Extensions/ControllerBaseExtensions.cs | 124 ++- .../InternalServerErrorObjectResult.cs | 62 +- src/MADE.Web.Mvc/Responses/JsonResult.cs | 136 ++-- .../Exceptions/DefaultExceptionHandler.cs | 41 +- .../ExceptionResponse{TException}.cs | 65 +- .../HttpContextExceptionHandlerExtensions.cs | 89 ++- .../HttpContextExceptionsMiddleware.cs | 243 +++--- ...HttpContextExceptionHandler{TException}.cs | 39 +- .../Extensions/ApiVersioningExtensions.cs | 99 ++- .../Extensions/HttpContextExtensions.cs | 27 +- .../Extensions/HttpResponseExtensions.cs | 149 ++-- src/MADE.Web/Extensions/IntExtensions.cs | 33 +- .../Extensions/QueryCollectionExtensions.cs | 109 ++- src/MADE.Web/Identity/AuthenticatedUser.cs | 109 ++- .../Identity/AuthenticatedUserAccessor.cs | 49 +- .../Identity/AuthenticatedUserExtensions.cs | 35 +- .../Identity/IAuthenticatedUserAccessor.cs | 31 +- src/MADE.Web/Requests/IPaginatedRequest{T}.cs | 45 +- src/MADE.Web/Requests/PaginatedRequest{T}.cs | 65 +- .../Responses/IPaginatedResponse{T}.cs | 59 +- .../Responses/PaginatedResponse{T}.cs | 95 ++- .../Fakes/TestEquatableObject.cs | 75 +- .../Fakes/TestObject.cs | 19 +- .../Fakes/TestObjectFaker.cs | 24 +- .../Fakes/TestObservableObject.cs | 69 +- .../Fakes/TestObservableObjectFaker.cs | 22 +- .../Tests/CollectionExtensionsTests.cs | 625 ++++++++------- .../Tests/DictionaryExtensionsTests.cs | 87 +-- .../Tests/GenericEqualityComparerTests.cs | 262 +++---- .../Tests/ObservableItemCollectionTests.cs | 394 +++++----- .../Tests/BooleanExtensionsTests.cs | 159 ++-- .../BooleanToStringValueConverterTests.cs | 134 ++-- .../Tests/CollectionExtensionsTests.cs | 61 +- .../Tests/DateTimeExtensionsTests.cs | 631 ++++++++------- .../DateTimeToStringValueConverterTests.cs | 183 +++-- .../Tests/MathExtensionsTests.cs | 49 +- .../Tests/StringExtensionsTests.cs | 313 ++++---- .../Data/TestDbContext.cs | 105 ++- .../Tests/EntityBaseTests.cs | 87 ++- .../Tests/QueryableExtensionsTests.cs | 305 ++++---- ...onTypeMigrationSerializationBinderTests.cs | 164 ++-- .../Tests/FluentValidatorCollectionTests.cs | 375 +++++---- .../Tests/AlphaNumericValidatorTests.cs | 101 ++- .../Tests/AlphaValidatorTests.cs | 103 ++- .../Tests/Base64ValidatorTests.cs | 113 ++- .../Tests/BetweenValidatorTests.cs | 233 +++--- .../Tests/DateTimeExtensionsTests.cs | 211 +++-- .../Tests/EmailValidatorTests.cs | 115 ++- .../Tests/GuidValidatorTests.cs | 111 ++- .../Tests/IpAddressValidatorTests.cs | 107 ++- .../Tests/LatitudeValidatorTests.cs | 87 +-- .../Tests/LongitudeValidatorTests.cs | 87 +-- .../Tests/MacAddressValidatorTests.cs | 111 ++- .../Tests/MathExtensionsTests.cs | 619 ++++++++------- .../Tests/MaxValueIndicatorTests.cs | 115 ++- .../Tests/MinValueValidatorTests.cs | 115 ++- .../Tests/PredicateValidatorTests.cs | 97 ++- .../Tests/RequiredValidatorTests.cs | 285 ++++--- .../Tests/StringExtensionsTests.cs | 289 ++++--- .../Tests/ValidatorCollectionTests.cs | 223 +++--- .../Tests/WellFormedUrlValidatorTests.cs | 121 ++- .../Tests/StopwatchHelperTests.cs | 177 +++-- .../Tests/JsonDeleteNetworkRequestTests.cs | 105 ++- .../Tests/JsonGetNetworkRequestTests.cs | 105 ++- .../Tests/JsonPatchNetworkRequestTests.cs | 125 ++- .../Tests/JsonPostNetworkRequestTests.cs | 125 ++- .../Tests/JsonPutNetworkRequestTests.cs | 125 ++- .../Tests/NetworkRequestManagerTests.cs | 301 ++++--- .../Tests/UriExtensionsTests.cs | 65 +- .../Tests/PaginatedResponseTests.cs | 61 +- 168 files changed, 11833 insertions(+), 12108 deletions(-) diff --git a/.editorconfig b/.editorconfig index d26a2941..0008338a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -59,38 +59,36 @@ end_of_line = lf ########################################## # .NET Language Conventions -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language-conventions +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules ########################################## # .NET Code Style Settings [*.cs] + # "this." and "Me." qualifiers -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#this_and_me dotnet_style_qualification_for_field = true:silent dotnet_style_qualification_for_property = true:silent dotnet_style_qualification_for_method = true:silent dotnet_style_qualification_for_event = true:silent # Language keywords instead of framework type names for type references -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language_keywords dotnet_style_predefined_type_for_locals_parameters_members = true:silent dotnet_style_predefined_type_for_member_access = true:silent # Modifier preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#normalize_modifiers -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async dotnet_style_readonly_field = true:warning +csharp_style_prefer_readonly_struct = true:warning +csharp_style_prefer_readonly_struct_member = true:warning # Parentheses preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#parentheses dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent # Expression-level preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_level dotnet_style_object_initializer = true:warning dotnet_style_collection_initializer = true:warning dotnet_style_explicit_tuple_names = true:warning @@ -100,20 +98,28 @@ dotnet_style_prefer_auto_properties = true:silent dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning dotnet_style_prefer_conditional_expression_over_assignment = true:silent dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_boolean_expressions = true:warning +dotnet_style_prefer_simplified_interpolation = true:warning +dotnet_style_namespace_match_folder = true:suggestion # Null-checking preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#null_checking dotnet_style_coalesce_expression = true:warning dotnet_style_null_propagation = true:warning +# Parameter preferences +dotnet_code_quality_unused_parameters = all:warning + +# Unnecessary code +dotnet_style_allow_multiple_blank_lines_experimental = false:warning +dotnet_style_allow_statement_immediately_after_block_experimental = false:warning + # Implicit and explicit types -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#implicit-and-explicit-types csharp_style_var_for_built_in_types = true:silent csharp_style_var_when_type_is_apparent = true:silent csharp_style_var_elsewhere = true:silent # Expression-bodied members -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_bodied_members csharp_style_expression_bodied_methods = false:warning csharp_style_expression_bodied_constructors = false:warning csharp_style_expression_bodied_operators = false:warning @@ -121,45 +127,65 @@ csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning csharp_style_expression_bodied_lambdas = true:warning +csharp_style_expression_bodied_local_functions = false:silent # Pattern matching -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#pattern_matching csharp_style_pattern_matching_over_is_with_cast_check = true:warning csharp_style_pattern_matching_over_as_with_null_check = true:warning +csharp_style_prefer_switch_expression = true:warning +csharp_style_prefer_pattern_matching = true:warning +csharp_style_prefer_not_pattern = true:warning +csharp_style_prefer_extended_property_pattern = true:warning # Inlined variable declarations -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#inlined_variable_declarations csharp_style_inlined_variable_declaration = true:suggestion # Expression-level preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_level_csharp csharp_prefer_simple_default_expression = true:warning csharp_style_deconstructed_variable_declaration = true:warning csharp_style_pattern_local_over_anonymous_function = true:warning +csharp_style_implicit_object_creation_when_type_is_apparent = true:warning +csharp_style_prefer_index_operator = true:warning +csharp_style_prefer_range_operator = true:warning +csharp_style_prefer_tuple_swap = true:warning +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_prefer_top_level_statements = true:silent -# "Null" checking preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#null_checking_csharp +# Null-checking preferences csharp_style_throw_expression = true:warning csharp_style_conditional_delegate_call = true:warning +csharp_style_prefer_null_check_over_type_check = true:warning # Code block preferences -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#code_block csharp_prefer_braces = true:warning +# Namespace declarations +csharp_style_namespace_declarations = file_scoped:warning + +# Using statement placement +csharp_using_directive_placement = outside_namespace:warning + +# Blank line preferences +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:warning +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:warning +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent + ########################################## # .NET Formatting Conventions -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/formatting-rules ########################################## # Organize usings -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#usings dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false -# Using statement placement (Undocumented) -csharp_using_directive_placement = inside_namespace:warning - -# C# formatting settings -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#c-formatting-settings +# Newline options csharp_new_line_before_open_brace = all csharp_new_line_before_else = true csharp_new_line_before_catch = true @@ -169,13 +195,14 @@ csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation options -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#indent csharp_indent_case_contents = true csharp_indent_switch_labels = true csharp_indent_labels = no_change +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents_when_block = false # Spacing options -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#spacing csharp_space_after_cast = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_between_method_declaration_parameter_list_parentheses = false @@ -187,18 +214,6 @@ csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_empty_parameter_list_parentheses = false - -# Wrapping options -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#wrapping -csharp_preserve_single_line_statements = true -csharp_preserve_single_line_blocks = true - -# More Indentation options (Undocumented) -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents_when_block = false - -# Spacing Options (Undocumented) csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_semicolon_in_for_statement = true @@ -211,9 +226,13 @@ csharp_space_between_empty_square_brackets = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_square_brackets = false +# Wrapping options +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + ########################################## # .NET Naming Conventions -# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/naming-rules ########################################## ########################################## @@ -230,12 +249,10 @@ dotnet_naming_style.prefix_type_parameters_with_t_style.required_prefix = T ########################################## # .NET Design Guideline Field Naming Rules -# Naming rules for fields follow the .NET Framework design guidelines -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/index +# https://learn.microsoft.com/dotnet/standard/design-guidelines/ ########################################## # All public/protected/protected_internal constant fields must be PascalCase -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/field dotnet_naming_symbols.public_protected_constant_fields_group.applicable_accessibilities = public, protected, protected_internal dotnet_naming_symbols.public_protected_constant_fields_group.required_modifiers = const dotnet_naming_symbols.public_protected_constant_fields_group.applicable_kinds = field @@ -244,7 +261,6 @@ dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.sty dotnet_naming_rule.public_protected_constant_fields_must_be_pascal_case_rule.severity = warning # All public/protected/protected_internal static readonly fields must be PascalCase -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/field dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_accessibilities = public, protected, protected_internal dotnet_naming_symbols.public_protected_static_readonly_fields_group.required_modifiers = static, readonly dotnet_naming_symbols.public_protected_static_readonly_fields_group.applicable_kinds = field @@ -253,7 +269,6 @@ dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_r dotnet_naming_rule.public_protected_static_readonly_fields_must_be_pascal_case_rule.severity = warning # No other public/protected/protected_internal fields are allowed -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/field dotnet_naming_symbols.other_public_protected_fields_group.applicable_accessibilities = public, protected, protected_internal dotnet_naming_symbols.other_public_protected_fields_group.applicable_kinds = field dotnet_naming_rule.other_public_protected_fields_disallowed_rule.symbols = other_public_protected_fields_group @@ -262,13 +277,10 @@ dotnet_naming_rule.other_public_protected_fields_disallowed_rule.severity ########################################## # StyleCop Field Naming Rules -# Naming rules for fields follow the StyleCop analyzers -# This does not override any rules using disallowed_style above # https://github.com/DotNetAnalyzers/StyleCopAnalyzers ########################################## -# All constant fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1303.md +# All constant fields must be PascalCase (SA1303) dotnet_naming_symbols.stylecop_constant_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private dotnet_naming_symbols.stylecop_constant_fields_group.required_modifiers = const dotnet_naming_symbols.stylecop_constant_fields_group.applicable_kinds = field @@ -276,8 +288,7 @@ dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.symbols dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.style = pascal_case_style dotnet_naming_rule.stylecop_constant_fields_must_be_pascal_case_rule.severity = warning -# All static readonly fields must be PascalCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1311.md +# All static readonly fields must be PascalCase (SA1311) dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected, private dotnet_naming_symbols.stylecop_static_readonly_fields_group.required_modifiers = static, readonly dotnet_naming_symbols.stylecop_static_readonly_fields_group.applicable_kinds = field @@ -285,33 +296,28 @@ dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.symb dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.style = pascal_case_style dotnet_naming_rule.stylecop_static_readonly_fields_must_be_pascal_case_rule.severity = warning -# No non-private instance fields are allowed -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1401.md +# No non-private instance fields are allowed (SA1401) dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_accessibilities = public, internal, protected_internal, protected, private_protected dotnet_naming_symbols.stylecop_fields_must_be_private_group.applicable_kinds = field dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.symbols = stylecop_fields_must_be_private_group dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.style = disallowed_style dotnet_naming_rule.stylecop_instance_fields_must_be_private_rule.severity = error -# Private fields must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1306.md +# Private fields must be camelCase (SA1306) dotnet_naming_symbols.stylecop_private_fields_group.applicable_accessibilities = private dotnet_naming_symbols.stylecop_private_fields_group.applicable_kinds = field dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.symbols = stylecop_private_fields_group dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.style = camel_case_style dotnet_naming_rule.stylecop_private_fields_must_be_camel_case_rule.severity = warning -# Local variables must be camelCase -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1312.md +# Local variables must be camelCase (SA1312) dotnet_naming_symbols.stylecop_local_fields_group.applicable_accessibilities = local dotnet_naming_symbols.stylecop_local_fields_group.applicable_kinds = local dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.symbols = stylecop_local_fields_group dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.style = camel_case_style dotnet_naming_rule.stylecop_local_fields_must_be_camel_case_rule.severity = silent -# This rule should never fire. However, it's included for at least two purposes: -# First, it helps to understand, reason about, and root-case certain types of issues, such as bugs in .editorconfig parsers. -# Second, it helps to raise immediate awareness if a new field type is added (as occurred recently in C#). +# Sanity check: catch-all for any uncovered field types dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_accessibilities = * dotnet_naming_symbols.sanity_check_uncovered_field_case_group.applicable_kinds = field dotnet_naming_rule.sanity_check_uncovered_field_case_rule.symbols = sanity_check_uncovered_field_case_group @@ -323,46 +329,31 @@ dotnet_naming_rule.sanity_check_uncovered_field_case_rule.severity = error ########################################## # All of the following must be PascalCase: -# - Namespaces -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-namespaces -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md -# - Classes and Enumerations -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces -# https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1300.md -# - Delegates -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces#names-of-common-types -# - Constructors, Properties, Events, Methods -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-type-members +# - Namespaces, Classes, Enumerations, Structs, Delegates, Events, Methods, Properties dotnet_naming_symbols.element_group.applicable_kinds = namespace, class, enum, struct, delegate, event, method, property dotnet_naming_rule.element_rule.symbols = element_group dotnet_naming_rule.element_rule.style = pascal_case_style dotnet_naming_rule.element_rule.severity = warning # Interfaces use PascalCase and are prefixed with uppercase 'I' -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces dotnet_naming_symbols.interface_group.applicable_kinds = interface dotnet_naming_rule.interface_rule.symbols = interface_group dotnet_naming_rule.interface_rule.style = prefix_interface_with_i_style dotnet_naming_rule.interface_rule.severity = warning -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i # Generics Type Parameters use PascalCase and are prefixed with uppercase 'T' -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-classes-structs-and-interfaces dotnet_naming_symbols.type_parameter_group.applicable_kinds = type_parameter dotnet_naming_rule.type_parameter_rule.symbols = type_parameter_group dotnet_naming_rule.type_parameter_rule.style = prefix_type_parameters_with_t_style dotnet_naming_rule.type_parameter_rule.severity = warning # Function parameters use camelCase -# https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-parameters dotnet_naming_symbols.parameters_group.applicable_kinds = parameter dotnet_naming_rule.parameters_rule.symbols = parameters_group dotnet_naming_rule.parameters_rule.style = camel_case_style dotnet_naming_rule.parameters_rule.severity = warning -# Async +# Async methods must end in "Async" dotnet_naming_rule.async_methods_end_in_async.severity = warning dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods dotnet_naming_rule.async_methods_end_in_async.style = end_in_async @@ -374,78 +365,79 @@ dotnet_naming_symbols.any_async_methods.required_modifiers = async dotnet_naming_style.end_in_async.required_suffix = Async dotnet_naming_style.end_in_async.capitalization = pascal_case +########################################## # .NET Code Analysis +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ +########################################## +# Design rules dotnet_diagnostic.CA1001.severity = warning -dotnet_diagnostic.CA1009.severity = warning dotnet_diagnostic.CA1016.severity = warning dotnet_diagnostic.CA1033.severity = warning -dotnet_diagnostic.CA1049.severity = warning dotnet_diagnostic.CA1060.severity = warning dotnet_diagnostic.CA1061.severity = warning dotnet_diagnostic.CA1063.severity = warning dotnet_diagnostic.CA1065.severity = warning -dotnet_diagnostic.CA1301.severity = warning -dotnet_diagnostic.CA1400.severity = warning + +# Globalization rules +dotnet_diagnostic.CA1304.severity = suggestion +dotnet_diagnostic.CA1305.severity = suggestion +dotnet_diagnostic.CA1307.severity = suggestion +dotnet_diagnostic.CA1310.severity = warning + +# Interop rules dotnet_diagnostic.CA1401.severity = warning -dotnet_diagnostic.CA1403.severity = warning -dotnet_diagnostic.CA1404.severity = warning -dotnet_diagnostic.CA1405.severity = warning -dotnet_diagnostic.CA1410.severity = warning -dotnet_diagnostic.CA1415.severity = warning + +# Performance rules dotnet_diagnostic.CA1821.severity = warning -dotnet_diagnostic.CA1900.severity = warning -dotnet_diagnostic.CA1901.severity = warning -dotnet_diagnostic.CA2002.severity = warning +dotnet_diagnostic.CA1822.severity = warning +dotnet_diagnostic.CA1825.severity = warning +dotnet_diagnostic.CA1826.severity = warning +dotnet_diagnostic.CA1827.severity = warning +dotnet_diagnostic.CA1829.severity = warning +dotnet_diagnostic.CA1830.severity = warning +dotnet_diagnostic.CA1841.severity = warning +dotnet_diagnostic.CA1845.severity = warning +dotnet_diagnostic.CA1847.severity = warning +dotnet_diagnostic.CA1849.severity = warning +dotnet_diagnostic.CA1851.severity = warning +dotnet_diagnostic.CA1852.severity = suggestion +dotnet_diagnostic.CA1854.severity = warning +dotnet_diagnostic.CA1858.severity = warning +dotnet_diagnostic.CA1859.severity = suggestion +dotnet_diagnostic.CA1860.severity = warning +dotnet_diagnostic.CA1861.severity = warning +dotnet_diagnostic.CA1862.severity = warning +dotnet_diagnostic.CA1864.severity = warning +dotnet_diagnostic.CA1869.severity = warning + +# Reliability rules +dotnet_diagnostic.CA2007.severity = warning +dotnet_diagnostic.CA2016.severity = warning + +# Security rules dotnet_diagnostic.CA2100.severity = warning -dotnet_diagnostic.CA2101.severity = warning -dotnet_diagnostic.CA2108.severity = warning -dotnet_diagnostic.CA2111.severity = warning -dotnet_diagnostic.CA2112.severity = warning -dotnet_diagnostic.CA2114.severity = warning -dotnet_diagnostic.CA2116.severity = warning -dotnet_diagnostic.CA2117.severity = warning -dotnet_diagnostic.CA2122.severity = warning -dotnet_diagnostic.CA2123.severity = warning -dotnet_diagnostic.CA2124.severity = warning -dotnet_diagnostic.CA2126.severity = warning -dotnet_diagnostic.CA2131.severity = warning -dotnet_diagnostic.CA2132.severity = warning -dotnet_diagnostic.CA2133.severity = warning -dotnet_diagnostic.CA2134.severity = warning -dotnet_diagnostic.CA2137.severity = warning -dotnet_diagnostic.CA2138.severity = warning -dotnet_diagnostic.CA2140.severity = warning -dotnet_diagnostic.CA2141.severity = warning -dotnet_diagnostic.CA2146.severity = warning -dotnet_diagnostic.CA2147.severity = warning -dotnet_diagnostic.CA2149.severity = warning + +# Usage rules dotnet_diagnostic.CA2200.severity = warning -dotnet_diagnostic.CA2202.severity = warning -dotnet_diagnostic.CA2207.severity = warning -dotnet_diagnostic.CA2212.severity = warning dotnet_diagnostic.CA2213.severity = warning dotnet_diagnostic.CA2214.severity = warning dotnet_diagnostic.CA2216.severity = warning -dotnet_diagnostic.CA2220.severity = warning dotnet_diagnostic.CA2229.severity = warning dotnet_diagnostic.CA2231.severity = warning -dotnet_diagnostic.CA2232.severity = warning -dotnet_diagnostic.CA2235.severity = warning -dotnet_diagnostic.CA2236.severity = warning -dotnet_diagnostic.CA2237.severity = warning -dotnet_diagnostic.CA2238.severity = warning -dotnet_diagnostic.CA2240.severity = warning dotnet_diagnostic.CA2241.severity = warning dotnet_diagnostic.CA2242.severity = warning +dotnet_diagnostic.CA2250.severity = warning +dotnet_diagnostic.CA2251.severity = warning +########################################## # StyleCop Code Analysis +########################################## -dotnet_diagnostic.SA1009.severity = none dotnet_diagnostic.SA1000.severity = none +dotnet_diagnostic.SA1009.severity = none dotnet_diagnostic.SA1011.severity = none dotnet_diagnostic.SA1101.severity = none -dotnet_diagnostic.SA1101.severity = none dotnet_diagnostic.SA1118.severity = none dotnet_diagnostic.SA1200.severity = none dotnet_diagnostic.SA1201.severity = none @@ -460,5 +452,4 @@ dotnet_diagnostic.SA1602.severity = none dotnet_diagnostic.SA1611.severity = none dotnet_diagnostic.SA1629.severity = none dotnet_diagnostic.SA1633.severity = none -dotnet_diagnostic.SA1634.severity = none -dotnet_diagnostic.SA1652.severity = none \ No newline at end of file +dotnet_diagnostic.SA1634.severity = none \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 717f5231..c511f3d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,11 +87,26 @@ var result = JsonConvert.DeserializeObject(json, settings); using MADE.Data.Serialization.Json.Converters; var converter = new JsonTypeMigrationConverter(); -await converter.AddTypeMigrationAsync(new JsonTypeMigration("OldAssembly", "OldNamespace.OldType", typeof(NewType))); +converter.AddTypeMigration(new JsonTypeMigration("OldAssembly", "OldNamespace.OldType", typeof(NewType))); var options = new JsonSerializerOptions(); options.Converters.Add(converter); var result = JsonSerializer.Deserialize(json, options); ``` -Note: `AddTypeMigration` has been renamed to `AddTypeMigrationAsync` and is now asynchronous. +#### IEventLogger Methods Changed from void to Task + +All 15 methods on `IEventLogger` (`WriteDebug`, `WriteInfo`, `WriteWarning`, `WriteError`, `WriteCritical` and their overloads) now return `Task` instead of `void`. Implementations must be updated accordingly. + +#### JsonTypeMigrationConverter Simplified + +- `AddTypeMigrationAsync` has been renamed to `AddTypeMigration` and is now synchronous (uses `lock` instead of `SemaphoreSlim`). + +### Code Quality Improvements + +- **File-scoped namespaces**: All source files converted to file-scoped namespace declarations. +- **ConfigureAwait(false)**: Added to all `await` expressions in library code (52 locations across 17 files) to prevent deadlocks in synchronization-context-bound environments. +- **ArgumentNullException.ThrowIfNull**: Replaced manual null-check-and-throw patterns with `ArgumentNullException.ThrowIfNull()` (22 locations across 7 files). +- **Nullable reference type annotations**: Added `?` annotations to parameters, return types, fields, and properties that accept or return `null` (17 fixes across 10 files). +- **Async correctness**: `FileEventLogger` and `AppDiagnostics` rewritten for proper async patterns, removing `async void` methods. +- **Comprehensive .editorconfig**: Added modern .NET analysis rules including CA2007, CA1822, CA1849, and async naming conventions. diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index 07b5f453..d212a0af 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -1,460 +1,418 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections -{ - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace MADE.Collections; +/// +/// Defines a collection of extensions for enumerables, lists, and collections. +/// +public static class CollectionExtensions +{ /// - /// Defines a collection of extensions for enumerables, lists, and collections. + /// Adds the specified item to the collection based on the specified condition being true. /// - public static class CollectionExtensions + /// The collection to add the item to. + /// The item to add. + /// The condition required to add the item. + /// The type of item within the collection. + /// Thrown if the or is . + /// Potentially thrown by the delegate callback. + public static void AddIf(this IList collection, T item, Func condition) { - /// - /// Adds the specified item to the collection based on the specified condition being true. - /// - /// The collection to add the item to. - /// The item to add. - /// The condition required to add the item. - /// The type of item within the collection. - /// Thrown if the or is . - /// Potentially thrown by the delegate callback. - public static void AddIf(this IList collection, T item, Func condition) - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } + ArgumentNullException.ThrowIfNull(collection); + ArgumentNullException.ThrowIfNull(condition); - if (condition == null) - { - throw new ArgumentNullException(nameof(condition)); - } - - if (condition()) - { - collection.Add(item); - } - } - - /// - /// Removes the specified item from the collection based on the specified condition being true. - /// - /// The collection to remove the item from. - /// The item to remove. - /// The condition required to remove the item. - /// The type of item within the collection. - /// Thrown if the or is . - /// Potentially thrown by the delegate callback. - public static void RemoveIf(this IList collection, T item, Func condition) + if (condition()) { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } - - if (condition == null) - { - throw new ArgumentNullException(nameof(condition)); - } - - if (condition()) - { - collection.Remove(item); - } + collection.Add(item); } + } - /// - /// Updates an item within the collection. - /// - /// - /// The type of item within the collection. - /// - /// - /// The collection to update an item in. - /// - /// - /// The item to update. - /// - /// - /// The function to find the item within the . - /// - /// - /// True if the item has been updated; otherwise, false. - /// - /// The or is . - /// The delegate callback throws an exception. - public static bool Update(this IList collection, T item, Func predicate) - { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } - - T existing = collection.FirstOrDefault(x => predicate.Invoke(x, item)); - if (existing == null) - { - return false; - } - - int idx = collection.IndexOf(existing); + /// + /// Removes the specified item from the collection based on the specified condition being true. + /// + /// The collection to remove the item from. + /// The item to remove. + /// The condition required to remove the item. + /// The type of item within the collection. + /// Thrown if the or is . + /// Potentially thrown by the delegate callback. + public static void RemoveIf(this IList collection, T item, Func condition) + { + ArgumentNullException.ThrowIfNull(collection); + ArgumentNullException.ThrowIfNull(condition); - collection.Remove(existing); - collection.Insert(idx, item); - return true; + if (condition()) + { + collection.Remove(item); } + } - /// - /// Makes the given destination collection items equal to the items in the given source collection by adding or removing items from the destination. - /// - /// - /// The destination collection to add or remove items to. - /// - /// - /// The source collection to provide the items. - /// - /// - /// The type of item within the collection. - /// - public static void MakeEqualTo(this ICollection destination, IEnumerable source) - { - var sourceList = source.ToList(); - foreach (T item in destination.Except(sourceList).ToList()) - { - destination.Remove(item); - } + /// + /// Updates an item within the collection. + /// + /// + /// The type of item within the collection. + /// + /// + /// The collection to update an item in. + /// + /// + /// The item to update. + /// + /// + /// The function to find the item within the . + /// + /// + /// True if the item has been updated; otherwise, false. + /// + /// The or is . + /// The delegate callback throws an exception. + public static bool Update(this IList collection, T item, Func predicate) + { + ArgumentNullException.ThrowIfNull(item); + ArgumentNullException.ThrowIfNull(collection); - foreach (T item in sourceList.Except(destination).ToList()) - { - destination.Add(item); - } + T existing = collection.FirstOrDefault(x => predicate.Invoke(x, item)); + if (existing == null) + { + return false; } - /// - /// Adds a collection of items to another. - /// - /// - /// The collection to add to. - /// - /// - /// The items to add. - /// - /// - /// The type of items in the collection. - /// - /// The or is . - public static void AddRange(this ICollection collection, IEnumerable itemsToAdd) - { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } + int idx = collection.IndexOf(existing); - if (itemsToAdd == null) - { - throw new ArgumentNullException(nameof(itemsToAdd)); - } + collection.Remove(existing); + collection.Insert(idx, item); + return true; + } - foreach (T item in itemsToAdd) - { - collection.Add(item); - } + /// + /// Makes the given destination collection items equal to the items in the given source collection by adding or removing items from the destination. + /// + /// + /// The destination collection to add or remove items to. + /// + /// + /// The source collection to provide the items. + /// + /// + /// The type of item within the collection. + /// + public static void MakeEqualTo(this ICollection destination, IEnumerable source) + { + var sourceList = source.ToList(); + foreach (T item in destination.Except(sourceList).ToList()) + { + destination.Remove(item); } - /// - /// Adds the specified collection of items to the collection based on the specified condition being true. - /// - /// The collection to add the items to. - /// The items to add. - /// The condition required to add the items. - /// The type of item within the collection. - /// Thrown if the , or is . - /// Potentially thrown by the delegate callback. - public static void AddRangeIf( - this ICollection collection, - IEnumerable itemsToAdd, - Func condition) + foreach (T item in sourceList.Except(destination).ToList()) { - if (condition == null) - { - throw new ArgumentNullException(nameof(condition)); - } - - if (condition()) - { - collection.AddRange(itemsToAdd); - } + destination.Add(item); } + } - /// - /// Removes a collection of items from another. - /// - /// - /// The collection to remove from. - /// - /// - /// The items to remove from the collection. - /// - /// - /// The type of items in the collection. - /// - /// The or is . - public static void RemoveRange(this ICollection collection, IEnumerable itemsToRemove) + /// + /// Adds a collection of items to another. + /// + /// + /// The collection to add to. + /// + /// + /// The items to add. + /// + /// + /// The type of items in the collection. + /// + /// The or is . + public static void AddRange(this ICollection collection, IEnumerable itemsToAdd) + { + ArgumentNullException.ThrowIfNull(collection); + ArgumentNullException.ThrowIfNull(itemsToAdd); + + foreach (T item in itemsToAdd) { - if (collection == null) - { - throw new ArgumentNullException(nameof(collection)); - } + collection.Add(item); + } + } - if (itemsToRemove == null) - { - throw new ArgumentNullException(nameof(itemsToRemove)); - } + /// + /// Adds the specified collection of items to the collection based on the specified condition being true. + /// + /// The collection to add the items to. + /// The items to add. + /// The condition required to add the items. + /// The type of item within the collection. + /// Thrown if the , or is . + /// Potentially thrown by the delegate callback. + public static void AddRangeIf( + this ICollection collection, + IEnumerable itemsToAdd, + Func condition) + { + ArgumentNullException.ThrowIfNull(condition); - foreach (T item in itemsToRemove) - { - if (collection.Contains(item)) - { - collection.Remove(item); - } - } + if (condition()) + { + collection.AddRange(itemsToAdd); } + } - /// - /// Removes the specified collection of items from the collection based on the specified condition being true. - /// - /// The collection to remove the items from. - /// The items to remove. - /// The condition required to remove the items. - /// The type of item within the collection. - /// Thrown if the , or is . - /// Potentially thrown by the delegate callback. - public static void RemoveRangeIf( - this ICollection collection, - IEnumerable itemsToRemove, - Func condition) - { - if (condition == null) - { - throw new ArgumentNullException(nameof(condition)); - } + /// + /// Removes a collection of items from another. + /// + /// + /// The collection to remove from. + /// + /// + /// The items to remove from the collection. + /// + /// + /// The type of items in the collection. + /// + /// The or is . + public static void RemoveRange(this ICollection collection, IEnumerable itemsToRemove) + { + ArgumentNullException.ThrowIfNull(collection); + ArgumentNullException.ThrowIfNull(itemsToRemove); - if (condition()) + foreach (T item in itemsToRemove) + { + if (collection.Contains(item)) { - collection.RemoveRange(itemsToRemove); + collection.Remove(item); } } + } + + /// + /// Removes the specified collection of items from the collection based on the specified condition being true. + /// + /// The collection to remove the items from. + /// The items to remove. + /// The condition required to remove the items. + /// The type of item within the collection. + /// Thrown if the , or is . + /// Potentially thrown by the delegate callback. + public static void RemoveRangeIf( + this ICollection collection, + IEnumerable itemsToRemove, + Func condition) + { + ArgumentNullException.ThrowIfNull(condition); - /// - /// Determines whether two collections are equivalent, containing all the same items with no regard to order. - /// - /// The type of item. - /// The expected collection. - /// The actual collection. - /// True if the collections are equivalent; otherwise, false. - public static bool AreEquivalent(this ICollection expected, ICollection actual) + if (condition()) { - return (expected == null && actual == null) - || (expected != null && actual != null - && expected.All(actual.Contains) - && expected.Count == actual.Count); + collection.RemoveRange(itemsToRemove); } + } - /// - /// Takes a number of elements from the specified collection from the specified starting index. - /// - /// - /// The to take items from. - /// - /// - /// The index to start at in the . - /// - /// - /// The number of items to take from the starting index of the . - /// - /// - /// The type of elements in the collection. - /// - /// - /// A collection of items. - /// - public static IEnumerable TakeFrom(this List list, int startingIndex, int takeCount) - { - var results = new List(); + /// + /// Determines whether two collections are equivalent, containing all the same items with no regard to order. + /// + /// The type of item. + /// The expected collection. + /// The actual collection. + /// True if the collections are equivalent; otherwise, false. + public static bool AreEquivalent(this ICollection? expected, ICollection? actual) + { + return (expected == null && actual == null) + || (expected != null && actual != null + && expected.All(actual.Contains) + && expected.Count == actual.Count); + } - int itemsToTake = takeCount; + /// + /// Takes a number of elements from the specified collection from the specified starting index. + /// + /// + /// The to take items from. + /// + /// + /// The index to start at in the . + /// + /// + /// The number of items to take from the starting index of the . + /// + /// + /// The type of elements in the collection. + /// + /// + /// A collection of items. + /// + public static IEnumerable TakeFrom(this List list, int startingIndex, int takeCount) + { + var results = new List(); - if (list.Count - 1 - startingIndex > itemsToTake) - { - List items = list.GetRange(startingIndex, itemsToTake); - results.AddRange(items); - } - else - { - itemsToTake = list.Count - startingIndex; - if (itemsToTake <= 0) - { - return results; - } - - List items = list.GetRange(startingIndex, itemsToTake); - results.AddRange(items); - } + int itemsToTake = takeCount; - return results; + if (list.Count - 1 - startingIndex > itemsToTake) + { + List items = list.GetRange(startingIndex, itemsToTake); + results.AddRange(items); } - - /// - /// Performs the specified action on each item in the collection. - /// - /// - /// The type of item in the collection. - /// - /// - /// The collection to action on. - /// - /// - /// The action to perform. - /// - /// Potentially thrown by the delegate callback. - public static void ForEach(this IEnumerable collection, Action action) + else { - foreach (T item in collection) + itemsToTake = list.Count - startingIndex; + if (itemsToTake <= 0) { - action?.Invoke(item); + return results; } - } - /// - /// Chunks a collection of items into a collection of collections grouped into the specified chunk size. - /// - /// The type of item. - /// The source collection to chunk. - /// The chunk size. - /// A collection of collections containing the chunked items. - public static IEnumerable> Chunk(this IEnumerable source, int chunkSize = 25) - { - return source - .Select((v, i) => new { Index = i, Value = v }) - .GroupBy(x => x.Index / chunkSize) - .Select(x => x.Select(v => v.Value)); + List items = list.GetRange(startingIndex, itemsToTake); + results.AddRange(items); } - /// Inserts an item to the specified at the potential index determined by the . - /// The source where the should be inserted. - /// The object to insert into the . - /// The action to run to determine the position of the item based on the provided and an item in the collection. - /// The type of items in the collection. - /// The inserted index of the item. - /// The is read-only. - public static int InsertAtPotentialIndex(this IList source, T value, Func predicate) + return results; + } + + /// + /// Performs the specified action on each item in the collection. + /// + /// + /// The type of item in the collection. + /// + /// + /// The collection to action on. + /// + /// + /// The action to perform. + /// + /// Potentially thrown by the delegate callback. + public static void ForEach(this IEnumerable collection, Action action) + { + foreach (T item in collection) { - var potentialIndex = source.PotentialIndexOf(value, predicate); - source.Insert(potentialIndex, value); - return potentialIndex; + action?.Invoke(item); } + } - /// Gets the potential index of an item that does not currently exist within a collection based on the specified criteria. - /// The collection to get the index from. - /// The object to determine an index for in the . - /// The action to run to determine the position of the item based on the provided and an item in the collection. - /// The type of items in the collection. - /// The potential index of the item. - /// Potentially thrown by the delegate callback. - public static int PotentialIndexOf(this IList source, T value, Func predicate) - { - var result = 0; + /// + /// Chunks a collection of items into a collection of collections grouped into the specified chunk size. + /// + /// The type of item. + /// The source collection to chunk. + /// The chunk size. + /// A collection of collections containing the chunked items. + public static IEnumerable> Chunk(this IEnumerable source, int chunkSize = 25) + { + return source + .Select((v, i) => new { Index = i, Value = v }) + .GroupBy(x => x.Index / chunkSize) + .Select(x => x.Select(v => v.Value)); + } - foreach (var item in source) - { - if (predicate(value, item)) - { - result = source.IndexOf(item) + 1; - continue; - } + /// Inserts an item to the specified at the potential index determined by the . + /// The source where the should be inserted. + /// The object to insert into the . + /// The action to run to determine the position of the item based on the provided and an item in the collection. + /// The type of items in the collection. + /// The inserted index of the item. + /// The is read-only. + public static int InsertAtPotentialIndex(this IList source, T value, Func predicate) + { + var potentialIndex = source.PotentialIndexOf(value, predicate); + source.Insert(potentialIndex, value); + return potentialIndex; + } - break; + /// Gets the potential index of an item that does not currently exist within a collection based on the specified criteria. + /// The collection to get the index from. + /// The object to determine an index for in the . + /// The action to run to determine the position of the item based on the provided and an item in the collection. + /// The type of items in the collection. + /// The potential index of the item. + /// Potentially thrown by the delegate callback. + public static int PotentialIndexOf(this IList source, T value, Func predicate) + { + var result = 0; + + foreach (var item in source) + { + if (predicate(value, item)) + { + result = source.IndexOf(item) + 1; + continue; } - return result; + break; } - /// - /// Shuffles the elements of a sequence randomly. - /// - /// The collection to shuffle. - /// The type of item in the collection. - /// The shuffled collection of items. - public static IEnumerable Shuffle(this IEnumerable source) + return result; + } + + /// + /// Shuffles the elements of a sequence randomly. + /// + /// The collection to shuffle. + /// The type of item in the collection. + /// The shuffled collection of items. + public static IEnumerable Shuffle(this IEnumerable source) + { + return source.OrderBy(x => Guid.NewGuid()); + } + + /// Sorts the elements in the entire using the specified comparer. + /// The source collection to sort. + /// The implementation to use when comparing elements. + /// The type of item in the collection. + /// The key value of the item to sort on. + public static void Sort(this ObservableCollection source, Func comparer) + { + if (source is not { Count: > 1 }) { - return source.OrderBy(x => Guid.NewGuid()); + return; } - /// Sorts the elements in the entire using the specified comparer. - /// The source collection to sort. - /// The implementation to use when comparing elements. - /// The type of item in the collection. - /// The key value of the item to sort on. - public static void Sort(this ObservableCollection source, Func comparer) + var idx = 0; + foreach (var originalIdx in source.OrderBy(comparer).Select(source.IndexOf)) { - if (source is not { Count: > 1 }) + if (originalIdx != idx) { - return; + source.Move(originalIdx, idx); } - var idx = 0; - foreach (var originalIdx in source.OrderBy(comparer).Select(source.IndexOf)) - { - if (originalIdx != idx) - { - source.Move(originalIdx, idx); - } + idx++; + } + } - idx++; - } + /// Sorts the elements in the entire using the specified comparer in descending order. + /// The source collection to sort. + /// The implementation to use when comparing elements. + /// The type of item in the collection. + /// The key value of the item to sort on. + public static void SortDescending(this ObservableCollection source, Func comparer) + { + if (source is not { Count: > 1 }) + { + return; } - /// Sorts the elements in the entire using the specified comparer in descending order. - /// The source collection to sort. - /// The implementation to use when comparing elements. - /// The type of item in the collection. - /// The key value of the item to sort on. - public static void SortDescending(this ObservableCollection source, Func comparer) + var idx = 0; + foreach (var originalIdx in source.OrderByDescending(comparer).Select(source.IndexOf)) { - if (source is not { Count: > 1 }) + if (originalIdx != idx) { - return; + source.Move(originalIdx, idx); } - var idx = 0; - foreach (var originalIdx in source.OrderByDescending(comparer).Select(source.IndexOf)) - { - if (originalIdx != idx) - { - source.Move(originalIdx, idx); - } - - idx++; - } + idx++; } + } - /// Indicates whether the specified collection is or empty (containing no items). - /// The collection to test. - /// The type of item in the collection. - /// - /// if the parameter is or empty (containing no items); otherwise, . - /// - public static bool IsNullOrEmpty(this IEnumerable source) - { - return source is null || !source.Any(); - } + /// Indicates whether the specified collection is or empty (containing no items). + /// The collection to test. + /// The type of item in the collection. + /// + /// if the parameter is or empty (containing no items); otherwise, . + /// + public static bool IsNullOrEmpty(this IEnumerable source) + { + return source is null || !source.Any(); } -} \ No newline at end of file +} diff --git a/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs b/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs index 5c0e9bb5..8d75de59 100644 --- a/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs +++ b/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs @@ -1,67 +1,66 @@ -// MADE Apps licenses this file to you under the MIT license. +// MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections.Compare -{ - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; + +namespace MADE.Collections.Compare; +/// +/// Defines an equality comparer for comparing two objects using a simple comparison function. +/// +/// +/// The type of object to comparison. +/// +public class GenericEqualityComparer : IEqualityComparer + where T : class +{ /// - /// Defines an equality comparer for comparing two objects using a simple comparison function. + /// Initializes a new instance of the class. /// - /// - /// The type of object to comparison. - /// - public class GenericEqualityComparer : IEqualityComparer - where T : class + /// + /// The comparison expression. + /// + public GenericEqualityComparer(Func comparison) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The comparison expression. - /// - public GenericEqualityComparer(Func comparison) - { - this.Comparison = comparison; - } + this.Comparison = comparison; + } - private Func Comparison { get; } + private Func Comparison { get; } - /// - /// Compares two objects of the same type for equality. - /// - /// - /// The first item. - /// - /// - /// The second item. - /// - /// - /// True if the two items are equal based on the comparison expression; otherwise, false. - /// - /// The callback throws an exception. - public bool Equals(T x, T y) - { - object first = this.Comparison.Invoke(x); - object second = this.Comparison.Invoke(y); + /// + /// Compares two objects of the same type for equality. + /// + /// + /// The first item. + /// + /// + /// The second item. + /// + /// + /// True if the two items are equal based on the comparison expression; otherwise, false. + /// + /// The callback throws an exception. + public bool Equals(T? x, T? y) + { + object first = this.Comparison.Invoke(x); + object second = this.Comparison.Invoke(y); - return first != null && first.Equals(second); - } + return first != null && first.Equals(second); + } - /// - /// Gets the hash code for the expected comparison object. - /// - /// - /// The object to get the comparison object hash code for. - /// - /// - /// A hash code for the comparison object. - /// - /// The callback throws an exception. - public int GetHashCode(T obj) - { - return this.Comparison.Invoke(obj).GetHashCode(); - } + /// + /// Gets the hash code for the expected comparison object. + /// + /// + /// The object to get the comparison object hash code for. + /// + /// + /// A hash code for the comparison object. + /// + /// The callback throws an exception. + public int GetHashCode(T obj) + { + return this.Comparison.Invoke(obj).GetHashCode(); } -} \ No newline at end of file +} diff --git a/src/MADE.Collections/DictionaryExtensions.cs b/src/MADE.Collections/DictionaryExtensions.cs index 3d4fab85..e803f055 100644 --- a/src/MADE.Collections/DictionaryExtensions.cs +++ b/src/MADE.Collections/DictionaryExtensions.cs @@ -1,77 +1,69 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections -{ - using System; - using System.Collections.Generic; +using System; +using System.Collections.Generic; + +namespace MADE.Collections; +/// +/// Defines a collection of extensions for dictionaries. +/// +public static class DictionaryExtensions +{ /// - /// Defines a collection of extensions for dictionaries. + /// Adds or updates a value within a dictionary. /// - public static class DictionaryExtensions + /// + /// The dictionary to update. + /// + /// + /// The key of the value to add or update. + /// + /// + /// The value to add or update. + /// + /// + /// The type of key item within the dictionary. + /// + /// + /// The type of value item within the dictionary. + /// + /// The or is . + public static void AddOrUpdate(this Dictionary dictionary, TKey key, TValue value) { - /// - /// Adds or updates a value within a dictionary. - /// - /// - /// The dictionary to update. - /// - /// - /// The key of the value to add or update. - /// - /// - /// The value to add or update. - /// - /// - /// The type of key item within the dictionary. - /// - /// - /// The type of value item within the dictionary. - /// - /// The or is . - public static void AddOrUpdate(this Dictionary dictionary, TKey key, TValue value) - { - if (dictionary == null) - { - throw new ArgumentNullException(nameof(dictionary)); - } + ArgumentNullException.ThrowIfNull(dictionary); + ArgumentNullException.ThrowIfNull(key); - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if (dictionary.ContainsKey(key)) - { - dictionary.Remove(key); - } - - dictionary.Add(key, value); + if (dictionary.ContainsKey(key)) + { + dictionary.Remove(key); } - /// - /// Gets a value from a dictionary by the specified key, or returns a default value. - /// - /// The type of key item within the dictionary. - /// The type of value item within the dictionary. - /// The dictionary to get a value from. - /// The key to get a value for. - /// The default value to return if not exists. Default, null. - /// The value if it exists for the key; otherwise, null. - public static TValue GetValueOrDefault( - this Dictionary dictionary, - TKey key, - TValue defaultValue = default) - { - var result = defaultValue; + dictionary.Add(key, value); + } - if (dictionary != null && dictionary.ContainsKey(key)) - { - result = dictionary[key]; - } + /// + /// Gets a value from a dictionary by the specified key, or returns a default value. + /// + /// The type of key item within the dictionary. + /// The type of value item within the dictionary. + /// The dictionary to get a value from. + /// The key to get a value for. + /// The default value to return if not exists. Default, null. + /// The value if it exists for the key; otherwise, null. + public static TValue GetValueOrDefault( + this Dictionary dictionary, + TKey key, + TValue defaultValue = default) + { + var result = defaultValue; - return result; + if (dictionary != null && dictionary.ContainsKey(key)) + { + result = dictionary[key]; } + + return result; } -} \ No newline at end of file +} diff --git a/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventArgs.cs b/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventArgs.cs index 0b806e40..7ee865a5 100644 --- a/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventArgs.cs +++ b/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventArgs.cs @@ -1,50 +1,49 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections.ObjectModel -{ - using System.ComponentModel; +using System.ComponentModel; + +namespace MADE.Collections.ObjectModel; +/// +/// Defines an event argument for when an object property has changed. +/// +public class ObservableItemCollectionPropertyChangedEventArgs +{ /// - /// Defines an event argument for when an object property has changed. + /// Initializes a new instance of the class. /// - public class ObservableItemCollectionPropertyChangedEventArgs + /// + /// The object sender. + /// + /// + /// The index of the within the . + /// + /// + /// The associated property changed event argument. + /// + public ObservableItemCollectionPropertyChangedEventArgs( + object sender, + int index, + PropertyChangedEventArgs eventArgs) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The object sender. - /// - /// - /// The index of the within the . - /// - /// - /// The associated property changed event argument. - /// - public ObservableItemCollectionPropertyChangedEventArgs( - object sender, - int index, - PropertyChangedEventArgs eventArgs) - { - this.Sender = sender; - this.Index = index; - this.EventArgs = eventArgs; - } + this.Sender = sender; + this.Index = index; + this.EventArgs = eventArgs; + } - /// - /// Gets the object sender. - /// - public object Sender { get; } + /// + /// Gets the object sender. + /// + public object Sender { get; } - /// - /// Gets index of the within the . - /// - public int Index { get; } + /// + /// Gets index of the within the . + /// + public int Index { get; } - /// - /// Gets associated property changed event argument. - /// - public PropertyChangedEventArgs EventArgs { get; } - } -} \ No newline at end of file + /// + /// Gets associated property changed event argument. + /// + public PropertyChangedEventArgs EventArgs { get; } +} diff --git a/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventHandler.cs b/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventHandler.cs index aef3147c..4e8ee08c 100644 --- a/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventHandler.cs +++ b/src/MADE.Collections/ObjectModel/ObservableItemCollectionPropertyChangedEventHandler.cs @@ -1,21 +1,20 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections.ObjectModel -{ - using System.ComponentModel; +using System.ComponentModel; - /// - /// Defines event handler for when an object has invoked the - /// event within a . - /// - /// - /// The sender. - /// - /// - /// The associated property changed event argument for the item. - /// - public delegate void ObservableItemCollectionPropertyChangedEventHandler( - object sender, - ObservableItemCollectionPropertyChangedEventArgs args); -} \ No newline at end of file +namespace MADE.Collections.ObjectModel; + +/// +/// Defines event handler for when an object has invoked the +/// event within a . +/// +/// +/// The sender. +/// +/// +/// The associated property changed event argument for the item. +/// +public delegate void ObservableItemCollectionPropertyChangedEventHandler( + object sender, + ObservableItemCollectionPropertyChangedEventArgs args); diff --git a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs index 4eb1e81d..92c99e9e 100644 --- a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs +++ b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs @@ -1,234 +1,233 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections.ObjectModel +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; + +namespace MADE.Collections.ObjectModel; + +/// +/// Defines an that manages the property changed events of the contained items. +/// +/// +/// The type of items. +/// +public class ObservableItemCollection : ObservableCollection, IDisposable + where T : INotifyPropertyChanged { - using System; - using System.Collections; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Collections.Specialized; - using System.ComponentModel; - using System.Linq; + private bool enableCollectionChanged = true; + + private bool disposed; /// - /// Defines an that manages the property changed events of the contained items. + /// Initializes a new instance of the class that is empty and has a default initial capacity. /// - /// - /// The type of items. - /// - public class ObservableItemCollection : ObservableCollection, IDisposable - where T : INotifyPropertyChanged + /// Potentially thrown by the callback. + public ObservableItemCollection() { - private bool enableCollectionChanged = true; - - private bool disposed; - - /// - /// Initializes a new instance of the class that is empty and has a default initial capacity. - /// - /// Potentially thrown by the callback. - public ObservableItemCollection() + base.CollectionChanged += (s, e) => { - base.CollectionChanged += (s, e) => + if (this.enableCollectionChanged) { - if (this.enableCollectionChanged) - { - this.CollectionChanged?.Invoke(this, e); - } - }; - } + this.CollectionChanged?.Invoke(this, e); + } + }; + } - /// - /// Initializes a new instance of the class that contains elements copied from the specified collection - /// and has sufficient capacity to accommodate the number of elements copied. - /// - /// - /// The collection whose elements are copied to the new list. - /// - /// The collection parameter cannot be null. - /// Potentially thrown by the callback. - public ObservableItemCollection(IEnumerable collection) - : base(collection) + /// + /// Initializes a new instance of the class that contains elements copied from the specified collection + /// and has sufficient capacity to accommodate the number of elements copied. + /// + /// + /// The collection whose elements are copied to the new list. + /// + /// The collection parameter cannot be null. + /// Potentially thrown by the callback. + public ObservableItemCollection(IEnumerable collection) + : base(collection) + { + base.CollectionChanged += (s, e) => { - base.CollectionChanged += (s, e) => + if (this.enableCollectionChanged) { - if (this.enableCollectionChanged) - { - this.CollectionChanged?.Invoke(this, e); - } - }; - } + this.CollectionChanged?.Invoke(this, e); + } + }; + } - /// - /// Initializes a new instance of the class that contains elements copied from the specified list. - /// - /// - /// The list whose elements are copied to the new list. - /// - /// The list parameter cannot be null. - /// Potentially thrown by the callback. - public ObservableItemCollection(List list) - : base(list) + /// + /// Initializes a new instance of the class that contains elements copied from the specified list. + /// + /// + /// The list whose elements are copied to the new list. + /// + /// The list parameter cannot be null. + /// Potentially thrown by the callback. + public ObservableItemCollection(List list) + : base(list) + { + base.CollectionChanged += (s, e) => { - base.CollectionChanged += (s, e) => + if (this.enableCollectionChanged) { - if (this.enableCollectionChanged) - { - this.CollectionChanged?.Invoke(this, e); - } - }; - } + this.CollectionChanged?.Invoke(this, e); + } + }; + } - /// - /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed. - /// - public override event NotifyCollectionChangedEventHandler CollectionChanged; - - /// - /// Occurs when an item's event is invoked. - /// - public event ObservableItemCollectionPropertyChangedEventHandler ItemPropertyChanged; - - /// - /// Adds a range of objects to the end of the collection. - /// - /// - /// The objects to add to the end of the collection. - /// - /// Potentially thrown by the callback. - public void AddRange(IEnumerable items) - { - this.CheckDisposed(); - this.enableCollectionChanged = false; + /// + /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed. + /// + public override event NotifyCollectionChangedEventHandler CollectionChanged; - var itemsToAdd = items.ToList(); + /// + /// Occurs when an item's event is invoked. + /// + public event ObservableItemCollectionPropertyChangedEventHandler ItemPropertyChanged; - foreach (T item in itemsToAdd) - { - this.Add(item); - } + /// + /// Adds a range of objects to the end of the collection. + /// + /// + /// The objects to add to the end of the collection. + /// + /// Potentially thrown by the callback. + public void AddRange(IEnumerable items) + { + this.CheckDisposed(); + this.enableCollectionChanged = false; - this.enableCollectionChanged = true; - this.CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, itemsToAdd)); - } + var itemsToAdd = items.ToList(); - /// - /// Removes a range of objects from the collection. - /// - /// - /// The objects to remove from the collection. - /// - /// Potentially thrown by the callback. - public void RemoveRange(IEnumerable items) + foreach (T item in itemsToAdd) { - this.CheckDisposed(); - this.enableCollectionChanged = false; + this.Add(item); + } - var itemsToRemove = items.ToList(); + this.enableCollectionChanged = true; + this.CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, itemsToAdd)); + } - foreach (T item in itemsToRemove) - { - this.Remove(item); - } + /// + /// Removes a range of objects from the collection. + /// + /// + /// The objects to remove from the collection. + /// + /// Potentially thrown by the callback. + public void RemoveRange(IEnumerable items) + { + this.CheckDisposed(); + this.enableCollectionChanged = false; - this.enableCollectionChanged = true; - this.CollectionChanged?.Invoke( - this, - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, itemsToRemove)); - } + var itemsToRemove = items.ToList(); - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - public void Dispose() + foreach (T item in itemsToRemove) { - if (this.disposed) - { - return; - } + this.Remove(item); + } - this.ClearItems(); - this.disposed = true; + this.enableCollectionChanged = true; + this.CollectionChanged?.Invoke( + this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, itemsToRemove)); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + if (this.disposed) + { + return; } - /// - /// Checks whether the collection is disposed. - /// - /// - /// Thrown if the object is disposed. - /// - public void CheckDisposed() + this.ClearItems(); + this.disposed = true; + } + + /// + /// Checks whether the collection is disposed. + /// + /// + /// Thrown if the object is disposed. + /// + public void CheckDisposed() + { + if (this.disposed) { - if (this.disposed) - { - throw new ObjectDisposedException(this.GetType().FullName); - } + throw new ObjectDisposedException(this.GetType().FullName); } + } - /// - /// Raises the event with the provided arguments. - /// - /// The arguments of the event being raised. - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + /// + /// Raises the event with the provided arguments. + /// + /// The arguments of the event being raised. + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + this.CheckDisposed(); + switch (e.Action) { - this.CheckDisposed(); - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: + case NotifyCollectionChangedAction.Add: + this.RegisterPropertyChangedEvents(e.NewItems); + break; + case NotifyCollectionChangedAction.Remove: + case NotifyCollectionChangedAction.Replace: + this.UnregisterPropertyChangedEvents(e.OldItems); + if (e.NewItems != null) + { this.RegisterPropertyChangedEvents(e.NewItems); - break; - case NotifyCollectionChangedAction.Remove: - case NotifyCollectionChangedAction.Replace: - this.UnregisterPropertyChangedEvents(e.OldItems); - if (e.NewItems != null) - { - this.RegisterPropertyChangedEvents(e.NewItems); - } - - break; - case NotifyCollectionChangedAction.Move: - case NotifyCollectionChangedAction.Reset: break; - } + } - base.OnCollectionChanged(e); + break; + case NotifyCollectionChangedAction.Move: + case NotifyCollectionChangedAction.Reset: break; } - /// - /// Removes all items from the collection. - /// - protected override void ClearItems() - { - this.UnregisterPropertyChangedEvents(this); - base.ClearItems(); - } + base.OnCollectionChanged(e); + } - private void RegisterPropertyChangedEvents(IEnumerable items) - { - this.CheckDisposed(); - foreach (INotifyPropertyChanged item in items.Cast().Where(item => item != null)) - { - item.PropertyChanged += this.OnItemPropertyChanged; - } - } + /// + /// Removes all items from the collection. + /// + protected override void ClearItems() + { + this.UnregisterPropertyChangedEvents(this); + base.ClearItems(); + } - private void UnregisterPropertyChangedEvents(IEnumerable items) + private void RegisterPropertyChangedEvents(IEnumerable items) + { + this.CheckDisposed(); + foreach (INotifyPropertyChanged item in items.Cast().Where(item => item != null)) { - this.CheckDisposed(); - foreach (INotifyPropertyChanged item in items.Cast().Where(item => item != null)) - { - item.PropertyChanged -= this.OnItemPropertyChanged; - } + item.PropertyChanged += this.OnItemPropertyChanged; } + } - private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) + private void UnregisterPropertyChangedEvents(IEnumerable items) + { + this.CheckDisposed(); + foreach (INotifyPropertyChanged item in items.Cast().Where(item => item != null)) { - this.CheckDisposed(); - this.ItemPropertyChanged?.Invoke( - this, - new ObservableItemCollectionPropertyChangedEventArgs(sender, this.IndexOf((T)sender), e)); + item.PropertyChanged -= this.OnItemPropertyChanged; } } -} \ No newline at end of file + + private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) + { + this.CheckDisposed(); + this.ItemPropertyChanged?.Invoke( + this, + new ObservableItemCollectionPropertyChangedEventArgs(sender, this.IndexOf((T)sender), e)); + } +} diff --git a/src/MADE.Collections/QueryableExtensions.cs b/src/MADE.Collections/QueryableExtensions.cs index e3752e71..8bafc3ff 100644 --- a/src/MADE.Collections/QueryableExtensions.cs +++ b/src/MADE.Collections/QueryableExtensions.cs @@ -1,40 +1,39 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Collections -{ - using System.Collections.Generic; - using System.Linq; +using System.Collections.Generic; +using System.Linq; + +namespace MADE.Collections; +/// +/// Defines a collection of extensions for queryable objects. +/// +public static class QueryableExtensions +{ /// - /// Defines a collection of extensions for queryable objects. + /// Chunks a query of items into the specified chunk size. /// - public static class QueryableExtensions + /// The type of item. + /// The source query to chunk. + /// The chunk size. + /// A collection of queries containing the chunked items. + public static IEnumerable> Chunk(this IQueryable source, int chunkSize = 25) { - /// - /// Chunks a query of items into the specified chunk size. - /// - /// The type of item. - /// The source query to chunk. - /// The chunk size. - /// A collection of queries containing the chunked items. - public static IEnumerable> Chunk(this IQueryable source, int chunkSize = 25) + int idx = 0; + while (true) { - int idx = 0; - while (true) + IQueryable q = idx == 0 + ? source + : source.Skip(idx * chunkSize); + IQueryable chunk = q.Take(chunkSize); + if (!chunk.Any()) { - IQueryable q = idx == 0 - ? source - : source.Skip(idx * chunkSize); - IQueryable chunk = q.Take(chunkSize); - if (!chunk.Any()) - { - yield break; - } - - yield return chunk.AsQueryable(); - idx++; + yield break; } + + yield return chunk.AsQueryable(); + idx++; } } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs index 21899b24..aac6c11b 100644 --- a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs +++ b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs @@ -1,68 +1,67 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters +using MADE.Data.Converters.Exceptions; +using MADE.Data.Converters.Extensions; + +namespace MADE.Data.Converters; + +/// +/// Defines a value converter from to with a pre-determined and . +/// +public class BooleanToStringValueConverter : IValueConverter { - using MADE.Data.Converters.Exceptions; - using MADE.Data.Converters.Extensions; + /// + /// Gets or sets the positive/true value. + /// + public string TrueValue { get; set; } /// - /// Defines a value converter from to with a pre-determined and . + /// Gets or sets the negative/false value. /// - public class BooleanToStringValueConverter : IValueConverter - { - /// - /// Gets or sets the positive/true value. - /// - public string TrueValue { get; set; } + public string FalseValue { get; set; } - /// - /// Gets or sets the negative/false value. - /// - public string FalseValue { get; set; } + /// + /// Converts the value to the type. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + public string Convert(bool value, object parameter = default) + { + return value.ToFormattedString(this.TrueValue, this.FalseValue); + } - /// - /// Converts the value to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - public string Convert(bool value, object parameter = default) + /// + /// Converts the value back to the type. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + public bool ConvertBack(string value, object parameter = default) + { + if (value == this.TrueValue) { - return value.ToFormattedString(this.TrueValue, this.FalseValue); + return true; } - /// - /// Converts the value back to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - public bool ConvertBack(string value, object parameter = default) + if (value == this.FalseValue) { - if (value == this.TrueValue) - { - return true; - } - - if (value == this.FalseValue) - { - return false; - } - - throw new InvalidDataConversionException(nameof(BooleanToStringValueConverter), value, $"The value to convert back is not of the expected {nameof(this.TrueValue)} or {nameof(this.FalseValue)}"); + return false; } + + throw new InvalidDataConversionException(nameof(BooleanToStringValueConverter), value, $"The value to convert back is not of the expected {nameof(this.TrueValue)} or {nameof(this.FalseValue)}"); } } diff --git a/src/MADE.Data.Converters/Constants/DateTimeConstants.cs b/src/MADE.Data.Converters/Constants/DateTimeConstants.cs index 2546799a..0aea9663 100644 --- a/src/MADE.Data.Converters/Constants/DateTimeConstants.cs +++ b/src/MADE.Data.Converters/Constants/DateTimeConstants.cs @@ -1,23 +1,22 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Constants -{ - using System; +using System; + +namespace MADE.Data.Converters.Constants; +/// +/// Defines a collection of constants for objects. +/// +public static class DateTimeConstants +{ /// - /// Defines a collection of constants for objects. + /// Defines the minimum value for a object determined by Unix. /// - public static class DateTimeConstants - { - /// - /// Defines the minimum value for a object determined by Unix. - /// - public static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0); + public static readonly DateTime UnixEpoch = new(1970, 1, 1, 0, 0, 0); - /// - /// Defines the time at the end of a day. - /// - public static readonly TimeSpan EndOfDayTime = new TimeSpan(1, 0, 0, 0).Subtract(TimeSpan.FromTicks(1)); - } + /// + /// Defines the time at the end of a day. + /// + public static readonly TimeSpan EndOfDayTime = new TimeSpan(1, 0, 0, 0).Subtract(TimeSpan.FromTicks(1)); } diff --git a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs index 41c65810..daacba5d 100644 --- a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs +++ b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs @@ -1,57 +1,56 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters +using System; +using System.Globalization; + +namespace MADE.Data.Converters; + +/// +/// Defines a value converter from to with an optional format string. +/// +public class DateTimeToStringValueConverter : IValueConverter { - using System; - using System.Globalization; + /// + /// Converts the value to the type. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + public string Convert(DateTime value, object parameter = default) + { + string format = parameter?.ToString(); + return !string.IsNullOrWhiteSpace(format) + ? value.ToString(format, CultureInfo.InvariantCulture) + : value.ToString(CultureInfo.InvariantCulture); + } /// - /// Defines a value converter from to with an optional format string. + /// Converts the value back to the type. /// - public class DateTimeToStringValueConverter : IValueConverter + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + public DateTime ConvertBack(string value, object parameter = default) { - /// - /// Converts the value to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - public string Convert(DateTime value, object parameter = default) + if (string.IsNullOrWhiteSpace(value)) { - string format = parameter?.ToString(); - return !string.IsNullOrWhiteSpace(format) - ? value.ToString(format, CultureInfo.InvariantCulture) - : value.ToString(CultureInfo.InvariantCulture); + return DateTime.MinValue; } - /// - /// Converts the value back to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - public DateTime ConvertBack(string value, object parameter = default) - { - if (string.IsNullOrWhiteSpace(value)) - { - return DateTime.MinValue; - } - - bool parsed = DateTime.TryParse(value, out DateTime dateTime); - return parsed ? dateTime : DateTime.MinValue; - } + bool parsed = DateTime.TryParse(value, out DateTime dateTime); + return parsed ? dateTime : DateTime.MinValue; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Exceptions/InvalidDataConversionException.cs b/src/MADE.Data.Converters/Exceptions/InvalidDataConversionException.cs index df33921e..86f116b5 100644 --- a/src/MADE.Data.Converters/Exceptions/InvalidDataConversionException.cs +++ b/src/MADE.Data.Converters/Exceptions/InvalidDataConversionException.cs @@ -1,55 +1,54 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Exceptions -{ - using System; +using System; - /// - /// Defines an exception thrown when a data conversion has failed. - /// - public class InvalidDataConversionException : Exception +namespace MADE.Data.Converters.Exceptions; + +/// +/// Defines an exception thrown when a data conversion has failed. +/// +public class InvalidDataConversionException : Exception +{ + /// Initializes a new instance of the class. + /// The name of the converter that failed to convert. + /// The value that failed to convert. + public InvalidDataConversionException(string converter, object value) { - /// Initializes a new instance of the class. - /// The name of the converter that failed to convert. - /// The value that failed to convert. - public InvalidDataConversionException(string converter, object value) - { - this.Converter = converter; - this.Value = value; - } + this.Converter = converter; + this.Value = value; + } - /// Initializes a new instance of the class with a specified error message. - /// The name of the converter that failed to convert. - /// The value that failed to convert. - /// The message that describes the error. - public InvalidDataConversionException(string converter, object value, string message) - : base(message) - { - this.Converter = converter; - this.Value = value; - } + /// Initializes a new instance of the class with a specified error message. + /// The name of the converter that failed to convert. + /// The value that failed to convert. + /// The message that describes the error. + public InvalidDataConversionException(string converter, object value, string message) + : base(message) + { + this.Converter = converter; + this.Value = value; + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The name of the converter that failed to convert. - /// The value that failed to convert. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - public InvalidDataConversionException(string converter, object value, string message, Exception innerException) - : base(message, innerException) - { - this.Converter = converter; - this.Value = value; - } + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The name of the converter that failed to convert. + /// The value that failed to convert. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public InvalidDataConversionException(string converter, object value, string message, Exception innerException) + : base(message, innerException) + { + this.Converter = converter; + this.Value = value; + } - /// - /// Gets the name of the converter that failed to convert. - /// - public string Converter { get; } + /// + /// Gets the name of the converter that failed to convert. + /// + public string Converter { get; } - /// - /// Gets the value that failed to convert. - /// - public object Value { get; } - } -} \ No newline at end of file + /// + /// Gets the value that failed to convert. + /// + public object Value { get; } +} diff --git a/src/MADE.Data.Converters/Extensions/BooleanExtensions.cs b/src/MADE.Data.Converters/Extensions/BooleanExtensions.cs index e9c04f89..73d7ca69 100644 --- a/src/MADE.Data.Converters/Extensions/BooleanExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/BooleanExtensions.cs @@ -1,40 +1,39 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions +namespace MADE.Data.Converters.Extensions; + +/// +/// Defines a collection of extensions for values. +/// +public static class BooleanExtensions { /// - /// Defines a collection of extensions for values. + /// Converts a value to a value with optional true/false values. /// - public static class BooleanExtensions + /// The value to format. + /// The format for when the is true. + /// The format for when the is false. + /// A formatted string + public static string ToFormattedString(this bool value, string trueValue = "True", string falseValue = "False") { - /// - /// Converts a value to a value with optional true/false values. - /// - /// The value to format. - /// The format for when the is true. - /// The format for when the is false. - /// A formatted string - public static string ToFormattedString(this bool value, string trueValue = "True", string falseValue = "False") - { - return value ? trueValue : falseValue; - } + return value ? trueValue : falseValue; + } - /// - /// Converts a nullable value to a value with optional true/false/null values. - /// - /// The value to format. - /// The format for when the is true. Default, True. - /// The format for when the is false. Default, False. - /// The format for when the is null. Default, Not set. - /// A formatted string - public static string ToFormattedString( - this bool? value, - string trueValue = "True", - string falseValue = "False", - string nullValue = "Not set") - { - return value.HasValue ? value.Value ? trueValue : falseValue : nullValue; - } + /// + /// Converts a nullable value to a value with optional true/false/null values. + /// + /// The value to format. + /// The format for when the is true. Default, True. + /// The format for when the is false. Default, False. + /// The format for when the is null. Default, Not set. + /// A formatted string + public static string ToFormattedString( + this bool? value, + string trueValue = "True", + string falseValue = "False", + string nullValue = "Not set") + { + return value.HasValue ? value.Value ? trueValue : falseValue : nullValue; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs b/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs index 25263859..306290c6 100644 --- a/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs @@ -1,25 +1,24 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions -{ - using System.Collections.Generic; +using System.Collections.Generic; + +namespace MADE.Data.Converters.Extensions; +/// +/// Defines a collection of extensions for collection objects. +/// +public static class CollectionExtensions +{ /// - /// Defines a collection of extensions for collection objects. + /// Converts a collection of items to a string separated by a delimiter. /// - public static class CollectionExtensions + /// The type of item within the collection. + /// The source collection to convert. + /// The delimiter to separate items by in the string. Default, comma. + /// A delimited string representing the collection. + public static string ToDelimitedString(this IEnumerable source, string delimiter = ",") { - /// - /// Converts a collection of items to a string separated by a delimiter. - /// - /// The type of item within the collection. - /// The source collection to convert. - /// The delimiter to separate items by in the string. Default, comma. - /// A delimited string representing the collection. - public static string ToDelimitedString(this IEnumerable source, string delimiter = ",") - { - return string.Join(delimiter, source); - } + return string.Join(delimiter, source); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Extensions/DateTimeExtensions.cs b/src/MADE.Data.Converters/Extensions/DateTimeExtensions.cs index 76631d94..d3b7a9af 100644 --- a/src/MADE.Data.Converters/Extensions/DateTimeExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/DateTimeExtensions.cs @@ -1,324 +1,323 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions -{ - using System; +using System; + +namespace MADE.Data.Converters.Extensions; +/// +/// Defines a collection of extensions for a date/time object. +/// +public static class DateTimeExtensions +{ /// - /// Defines a collection of extensions for a date/time object. + /// Gets the day suffix for the specified date, i.e. st, nd, rd, or th. /// - public static class DateTimeExtensions + /// The date to get a day suffix for. + /// The day suffix as a string. + public static string ToDaySuffix(this DateTime dateTime) { - /// - /// Gets the day suffix for the specified date, i.e. st, nd, rd, or th. - /// - /// The date to get a day suffix for. - /// The day suffix as a string. - public static string ToDaySuffix(this DateTime dateTime) + switch (dateTime.Day) { - switch (dateTime.Day) - { - case 1: - case 21: - case 31: - return "st"; - case 2: - case 22: - return "nd"; - case 3: - case 23: - return "rd"; - default: - return "th"; - } + case 1: + case 21: + case 31: + return "st"; + case 2: + case 22: + return "nd"; + case 3: + case 23: + return "rd"; + default: + return "th"; } + } - /// - /// Gets the current age in years based on the specified starting date and today's date. - /// - /// - /// The starting date. - /// - /// - /// An integer value representing the number of years. - /// - public static int ToCurrentAge(this DateTime startingDate) + /// + /// Gets the current age in years based on the specified starting date and today's date. + /// + /// + /// The starting date. + /// + /// + /// An integer value representing the number of years. + /// + public static int ToCurrentAge(this DateTime startingDate) + { + int yearDifference = DateTime.Now.Year - startingDate.Year; + if (DateTime.Now < startingDate.AddYears(yearDifference)) { - int yearDifference = DateTime.Now.Year - startingDate.Year; - if (DateTime.Now < startingDate.AddYears(yearDifference)) - { - yearDifference--; - } - - return yearDifference; + yearDifference--; } - /// - /// Rounds a value to its nearest hour. - /// - /// This is determined by the half hour of each hour, rounding up or down. - /// - /// - /// The to round. - /// The updated . - public static DateTime ToNearestHour(this DateTime dateTime) - { - int hour = dateTime.Minute < 30 - ? dateTime.Hour - : dateTime.Hour + 1; + return yearDifference; + } - return hour == 24 - ? new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0).AddDays(1) - : new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour, 0, 0); - } + /// + /// Rounds a value to its nearest hour. + /// + /// This is determined by the half hour of each hour, rounding up or down. + /// + /// + /// The to round. + /// The updated . + public static DateTime ToNearestHour(this DateTime dateTime) + { + int hour = dateTime.Minute < 30 + ? dateTime.Hour + : dateTime.Hour + 1; - /// - /// Gets the start of the day represented by the specified object. - /// - /// The . - /// A new object with the same date as this instance, and the time value set to midnight. - public static DateTime StartOfDay(this DateTime dateTime) - { - return dateTime.Date; - } + return hour == 24 + ? new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 0, 0, 0).AddDays(1) + : new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, hour, 0, 0); + } - /// - /// Gets the end of the day represented by the specified object. - /// - /// The . - /// A new object with the same date as this instance, and the time value set to just before midnight of the next day. - public static DateTime EndOfDay(this DateTime dateTime) - { - return dateTime.StartOfDay().AddDays(1).AddTicks(-1); - } + /// + /// Gets the start of the day represented by the specified object. + /// + /// The . + /// A new object with the same date as this instance, and the time value set to midnight. + public static DateTime StartOfDay(this DateTime dateTime) + { + return dateTime.Date; + } - /// - /// Gets the first day of the week represented by the specified object. - /// - /// The . - /// A new object with the first day of the week for this instance, and the time value set to midnight. - public static DateTime StartOfWeek(this DateTime dateTime) - { - return dateTime.AddDays(-(int)dateTime.DayOfWeek).StartOfDay(); - } + /// + /// Gets the end of the day represented by the specified object. + /// + /// The . + /// A new object with the same date as this instance, and the time value set to just before midnight of the next day. + public static DateTime EndOfDay(this DateTime dateTime) + { + return dateTime.StartOfDay().AddDays(1).AddTicks(-1); + } - /// - /// Gets the last day of the week represented by the specified object. - /// - /// The . - /// A new object with the last day of the week for this instance, and the time value set to just before midnight of the next day. - public static DateTime EndOfWeek(this DateTime dateTime) - { - return dateTime.StartOfWeek().AddDays(7).EndOfDay(); - } + /// + /// Gets the first day of the week represented by the specified object. + /// + /// The . + /// A new object with the first day of the week for this instance, and the time value set to midnight. + public static DateTime StartOfWeek(this DateTime dateTime) + { + return dateTime.AddDays(-(int)dateTime.DayOfWeek).StartOfDay(); + } - /// - /// Gets the first day of the month represented by the specified object. - /// - /// The . - /// A new object with the first day of the month for this instance, and the time value set to midnight. - public static DateTime StartOfMonth(this DateTime dateTime) - { - return new DateTime(dateTime.Year, dateTime.Month, 1); - } + /// + /// Gets the last day of the week represented by the specified object. + /// + /// The . + /// A new object with the last day of the week for this instance, and the time value set to just before midnight of the next day. + public static DateTime EndOfWeek(this DateTime dateTime) + { + return dateTime.StartOfWeek().AddDays(7).EndOfDay(); + } - /// - /// Gets the last day of the month represented by the specified object. - /// - /// The . - /// A new object with the last day of the month for this instance, and the time value set to just before midnight of the next day. - public static DateTime EndOfMonth(this DateTime dateTime) - { - return dateTime.StartOfMonth().AddMonths(1).AddDays(-1).EndOfDay(); - } + /// + /// Gets the first day of the month represented by the specified object. + /// + /// The . + /// A new object with the first day of the month for this instance, and the time value set to midnight. + public static DateTime StartOfMonth(this DateTime dateTime) + { + return new DateTime(dateTime.Year, dateTime.Month, 1); + } - /// - /// Gets the first day of the year represented by the specified object. - /// - /// The . - /// A new object with the first day of the year for this instance, and the time value set to midnight. - public static DateTime StartOfYear(this DateTime dateTime) - { - return new DateTime(dateTime.Year, 1, 1); - } + /// + /// Gets the last day of the month represented by the specified object. + /// + /// The . + /// A new object with the last day of the month for this instance, and the time value set to just before midnight of the next day. + public static DateTime EndOfMonth(this DateTime dateTime) + { + return dateTime.StartOfMonth().AddMonths(1).AddDays(-1).EndOfDay(); + } - /// - /// Gets the last day of the year represented by the specified object. - /// - /// The . - /// A new object with the last day of the year for this instance, and the time value set to just before midnight of the next day. - public static DateTime EndOfYear(this DateTime dateTime) - { - return dateTime.StartOfYear().AddYears(1).AddDays(-1).EndOfDay(); - } + /// + /// Gets the first day of the year represented by the specified object. + /// + /// The . + /// A new object with the first day of the year for this instance, and the time value set to midnight. + public static DateTime StartOfYear(this DateTime dateTime) + { + return new DateTime(dateTime.Year, 1, 1); + } - /// - /// Sets the time value of a nullable date/time value. - /// - /// The nullable date/time value to add a time to. - /// The time to set on the date/time value. - /// The updated date/time with the given time value. - public static DateTime? SetTime(this DateTime? dateTime, TimeSpan timeSpan) - { - return SetTime(dateTime, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds); - } + /// + /// Gets the last day of the year represented by the specified object. + /// + /// The . + /// A new object with the last day of the year for this instance, and the time value set to just before midnight of the next day. + public static DateTime EndOfYear(this DateTime dateTime) + { + return dateTime.StartOfYear().AddYears(1).AddDays(-1).EndOfDay(); + } - /// - /// Sets the time value of a nullable date/time value. - /// - /// - /// The nullable date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes) - { - return SetTime(dateTime, hours, minutes, 0, 0); - } + /// + /// Sets the time value of a nullable date/time value. + /// + /// The nullable date/time value to add a time to. + /// The time to set on the date/time value. + /// The updated date/time with the given time value. + public static DateTime? SetTime(this DateTime? dateTime, TimeSpan timeSpan) + { + return SetTime(dateTime, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds); + } - /// - /// Sets the time value of a nullable date/time value. - /// - /// - /// The nullable date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The seconds to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes, int seconds) - { - return SetTime(dateTime, hours, minutes, seconds, 0); - } + /// + /// Sets the time value of a nullable date/time value. + /// + /// + /// The nullable date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes) + { + return SetTime(dateTime, hours, minutes, 0, 0); + } - /// - /// Sets the time value of a nullable date/time value. - /// - /// - /// The nullable date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The seconds to set on the date/time value. - /// - /// - /// The milliseconds to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes, int seconds, int milliseconds) - { - return dateTime == null ? null : SetTime(dateTime.Value, hours, minutes, seconds, milliseconds); - } + /// + /// Sets the time value of a nullable date/time value. + /// + /// + /// The nullable date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The seconds to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes, int seconds) + { + return SetTime(dateTime, hours, minutes, seconds, 0); + } - /// - /// Sets the time value of a date/time value. - /// - /// The date/time value to add a time to. - /// The time to set on the date/time value. - /// The updated date/time with the given time value. - public static DateTime SetTime(this DateTime dateTime, TimeSpan timeSpan) - { - return SetTime(dateTime, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds); - } + /// + /// Sets the time value of a nullable date/time value. + /// + /// + /// The nullable date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The seconds to set on the date/time value. + /// + /// + /// The milliseconds to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime? SetTime(this DateTime? dateTime, int hours, int minutes, int seconds, int milliseconds) + { + return dateTime == null ? null : SetTime(dateTime.Value, hours, minutes, seconds, milliseconds); + } - /// - /// Sets the time value of a date/time value. - /// - /// - /// The date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime SetTime(this DateTime dateTime, int hours, int minutes) - { - return SetTime(dateTime, hours, minutes, 0, 0); - } + /// + /// Sets the time value of a date/time value. + /// + /// The date/time value to add a time to. + /// The time to set on the date/time value. + /// The updated date/time with the given time value. + public static DateTime SetTime(this DateTime dateTime, TimeSpan timeSpan) + { + return SetTime(dateTime, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds, timeSpan.Milliseconds); + } - /// - /// Sets the time value of a date/time value. - /// - /// - /// The date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The seconds to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime SetTime(this DateTime dateTime, int hours, int minutes, int seconds) - { - return SetTime(dateTime, hours, minutes, seconds, 0); - } + /// + /// Sets the time value of a date/time value. + /// + /// + /// The date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime SetTime(this DateTime dateTime, int hours, int minutes) + { + return SetTime(dateTime, hours, minutes, 0, 0); + } - /// - /// Sets the time value of a date/time value. - /// - /// - /// The date/time value to add a time to. - /// - /// - /// The hours to set on the date/time value. - /// - /// - /// The minutes to set on the date/time value. - /// - /// - /// The seconds to set on the date/time value. - /// - /// - /// The milliseconds to set on the date/time value. - /// - /// - /// The updated date/time with the given time value. - /// - public static DateTime SetTime(this DateTime dateTime, int hours, int minutes, int seconds, int milliseconds) - { - return new DateTime( - dateTime.Year, - dateTime.Month, - dateTime.Day, - hours, - minutes, - seconds, - milliseconds, - dateTime.Kind); - } + /// + /// Sets the time value of a date/time value. + /// + /// + /// The date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The seconds to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime SetTime(this DateTime dateTime, int hours, int minutes, int seconds) + { + return SetTime(dateTime, hours, minutes, seconds, 0); + } + + /// + /// Sets the time value of a date/time value. + /// + /// + /// The date/time value to add a time to. + /// + /// + /// The hours to set on the date/time value. + /// + /// + /// The minutes to set on the date/time value. + /// + /// + /// The seconds to set on the date/time value. + /// + /// + /// The milliseconds to set on the date/time value. + /// + /// + /// The updated date/time with the given time value. + /// + public static DateTime SetTime(this DateTime dateTime, int hours, int minutes, int seconds, int milliseconds) + { + return new DateTime( + dateTime.Year, + dateTime.Month, + dateTime.Day, + hours, + minutes, + seconds, + milliseconds, + dateTime.Kind); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Extensions/LengthExtensions.cs b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs index 40b3e6f3..82b23ac5 100644 --- a/src/MADE.Data.Converters/Extensions/LengthExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs @@ -1,31 +1,30 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions +namespace MADE.Data.Converters.Extensions; + +/// +/// Defines a collection of extensions for converting length measurements. +/// +public static class LengthExtensions { /// - /// Defines a collection of extensions for converting length measurements. + /// Converts a distance measured in miles to a distance measured in meters. /// - public static class LengthExtensions + /// The miles to convert to meters. + /// The meters that represent the miles. + public static double ToMeters(this double miles) { - /// - /// Converts a distance measured in miles to a distance measured in meters. - /// - /// The miles to convert to meters. - /// The meters that represent the miles. - public static double ToMeters(this double miles) - { - return miles * 1609.344; - } + return miles * 1609.344; + } - /// - /// Converts a distance measured in meters to a distance measured in miles. - /// - /// The meters to convert to miles. - /// The miles that represent the meters. - public static double ToMiles(this double meters) - { - return meters / 1609.344; - } + /// + /// Converts a distance measured in meters to a distance measured in miles. + /// + /// The meters to convert to miles. + /// The miles that represent the meters. + public static double ToMiles(this double meters) + { + return meters / 1609.344; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Extensions/MathExtensions.cs b/src/MADE.Data.Converters/Extensions/MathExtensions.cs index 5a8a1c8b..a7c72ce2 100644 --- a/src/MADE.Data.Converters/Extensions/MathExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/MathExtensions.cs @@ -1,25 +1,24 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions +namespace MADE.Data.Converters.Extensions; + +/// +/// Defines a collection of extensions for common mathematics expressions. +/// +public static class MathExtensions { /// - /// Defines a collection of extensions for common mathematics expressions. + /// Converts a degrees value to a radians value. /// - public static class MathExtensions + /// + /// The degrees value to convert. + /// + /// + /// The converted value as radians. + /// + public static double ToRadians(this double degrees) { - /// - /// Converts a degrees value to a radians value. - /// - /// - /// The degrees value to convert. - /// - /// - /// The converted value as radians. - /// - public static double ToRadians(this double degrees) - { - return degrees * (System.Math.PI / 180); - } + return degrees * (System.Math.PI / 180); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Extensions/StringExtensions.cs b/src/MADE.Data.Converters/Extensions/StringExtensions.cs index dc9aad44..d9dbabc2 100644 --- a/src/MADE.Data.Converters/Extensions/StringExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/StringExtensions.cs @@ -1,272 +1,271 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters.Extensions -{ - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace MADE.Data.Converters.Extensions; +/// +/// Defines a collection of extensions for string values. +/// +public static class StringExtensions +{ /// - /// Defines a collection of extensions for string values. + /// Converts a value to title case using the case rules of the invariant culture. /// - public static class StringExtensions + /// + /// The value to convert. + /// + /// + /// The converted title case string. + /// + /// + /// string converted = "HELLO, WORLD".ToTitleCase(); // converted = "Hello, World". + /// + public static string ToTitleCase(this string value) { - /// - /// Converts a value to title case using the case rules of the invariant culture. - /// - /// - /// The value to convert. - /// - /// - /// The converted title case string. - /// - /// - /// string converted = "HELLO, WORLD".ToTitleCase(); // converted = "Hello, World". - /// - public static string ToTitleCase(this string value) + if (string.IsNullOrEmpty(value)) { - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - var result = new StringBuilder(value); - result[0] = char.ToUpper(result[0]); - for (int i = 1; i < result.Length; ++i) - { - result[i] = char.IsWhiteSpace(result[i - 1]) ? char.ToUpper(result[i]) : char.ToLower(result[i]); - } - - return result.ToString(); + return string.Empty; } - /// - /// Truncates a string value to the specified length with an ellipsis. - /// - /// The value to truncate. - /// The maximum length of the value. - /// A truncated string with ellipsis if the value's length is greater than the . - public static string Truncate(this string value, int maxLength) + var result = new StringBuilder(value); + result[0] = char.ToUpper(result[0]); + for (int i = 1; i < result.Length; ++i) { - if (string.IsNullOrEmpty(value)) - { - return string.Empty; - } - - if (value.Length <= maxLength) - { - return value; - } - - const string suffix = "..."; - return value.Substring(0, maxLength - suffix.Length) + suffix; + result[i] = char.IsWhiteSpace(result[i - 1]) ? char.ToUpper(result[i]) : char.ToLower(result[i]); } - /// - /// Converts a value to default case using the case rules of the invariant culture. - /// - /// - /// The value to convert. - /// - /// - /// The converted default case string. - /// - /// - /// string converted = "HELLO, WORLD".ToDefaultCase(); // converted = "Hello, world". - /// - public static string ToDefaultCase(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return string.Empty; - } - - string result = value.Substring(0, 1).ToUpperInvariant() + value.Substring(1).ToLowerInvariant(); - return result; - } + return result.ToString(); + } - /// - /// Converts a value to a Base64 string using the specified encoding. - /// - /// Default encoding is UTF-8. - /// - /// - /// The string value to convert. - /// The encoding to get the value bytes while converting. - /// The Base64 string representing the value. - public static string ToBase64(this string value, Encoding encoding = default) + /// + /// Truncates a string value to the specified length with an ellipsis. + /// + /// The value to truncate. + /// The maximum length of the value. + /// A truncated string with ellipsis if the value's length is greater than the . + public static string Truncate(this string value, int maxLength) + { + if (string.IsNullOrEmpty(value)) { - encoding ??= Encoding.UTF8; - return Convert.ToBase64String(encoding.GetBytes(value)); + return string.Empty; } - /// - /// Converts a Base64 string to a value using the specified encoding. - /// - /// The Base64 value to convert. - /// The encoding to get the value string while converting. - /// The string value representing the Base64 string. - public static string FromBase64(this string base64Value, Encoding encoding = default) + if (value.Length <= maxLength) { - encoding ??= Encoding.UTF8; - return encoding.GetString(Convert.FromBase64String(base64Value)); + return value; } - /// - /// Converts a string value to a . - /// - /// The value to convert. - /// A representing the string value. - public static async Task ToMemoryStreamAsync(this string value) + const string suffix = "..."; + return value.Substring(0, maxLength - suffix.Length) + suffix; + } + + /// + /// Converts a value to default case using the case rules of the invariant culture. + /// + /// + /// The value to convert. + /// + /// + /// The converted default case string. + /// + /// + /// string converted = "HELLO, WORLD".ToDefaultCase(); // converted = "Hello, world". + /// + public static string ToDefaultCase(this string value) + { + if (string.IsNullOrWhiteSpace(value)) { - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - await writer.WriteAsync(value); - await writer.FlushAsync(); - stream.Position = 0; - return stream; + return string.Empty; } - /// - /// Converts a string value to an integer. - /// - /// - /// The value to convert. - /// - /// - /// The int representing the value. - /// - public static int ToInt(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return 0; - } + string result = value.Substring(0, 1).ToUpperInvariant() + value.Substring(1).ToLowerInvariant(); + return result; + } - bool parsed = int.TryParse(value, out int intValue); - return parsed ? intValue : 0; - } + /// + /// Converts a value to a Base64 string using the specified encoding. + /// + /// Default encoding is UTF-8. + /// + /// + /// The string value to convert. + /// The encoding to get the value bytes while converting. + /// The Base64 string representing the value. + public static string ToBase64(this string value, Encoding encoding = default) + { + encoding ??= Encoding.UTF8; + return Convert.ToBase64String(encoding.GetBytes(value)); + } - /// - /// Converts a string value to a nullable integer. - /// - /// - /// The value to convert. - /// - /// - /// The nullable integer representing the value. - /// - public static int? ToNullableInt(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } + /// + /// Converts a Base64 string to a value using the specified encoding. + /// + /// The Base64 value to convert. + /// The encoding to get the value string while converting. + /// The string value representing the Base64 string. + public static string FromBase64(this string base64Value, Encoding encoding = default) + { + encoding ??= Encoding.UTF8; + return encoding.GetString(Convert.FromBase64String(base64Value)); + } - bool parsed = int.TryParse(value, out int intValue); - return parsed ? intValue : null; - } + /// + /// Converts a string value to a . + /// + /// The value to convert. + /// A representing the string value. + public static async Task ToMemoryStreamAsync(this string value) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + await writer.WriteAsync(value).ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + stream.Position = 0; + return stream; + } - /// - /// Converts a string value to a boolean. - /// - /// - /// The value to convert. - /// - /// - /// The boolean representing the value. - /// - public static bool ToBoolean(this string value) + /// + /// Converts a string value to an integer. + /// + /// + /// The value to convert. + /// + /// + /// The int representing the value. + /// + public static int ToInt(this string value) + { + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - bool parsed = bool.TryParse(value, out bool booleanValue); - return parsed && booleanValue; + return 0; } - /// - /// Converts a string value to a float. - /// - /// - /// The value to convert. - /// - /// - /// The float representing the value. - /// - public static float ToFloat(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return 0; - } + bool parsed = int.TryParse(value, out int intValue); + return parsed ? intValue : 0; + } - bool parsed = float.TryParse(value, out float floatValue); - return parsed ? floatValue : 0; + /// + /// Converts a string value to a nullable integer. + /// + /// + /// The value to convert. + /// + /// + /// The nullable integer representing the value. + /// + public static int? ToNullableInt(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; } - /// - /// Converts a string value to a nullable float. - /// - /// - /// The value to convert. - /// - /// - /// The nullable float representing the value. - /// - public static float? ToNullableFloat(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } + bool parsed = int.TryParse(value, out int intValue); + return parsed ? intValue : null; + } - bool parsed = float.TryParse(value, out float floatValue); - return parsed ? floatValue : null; + /// + /// Converts a string value to a boolean. + /// + /// + /// The value to convert. + /// + /// + /// The boolean representing the value. + /// + public static bool ToBoolean(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; } - /// - /// Converts a string value to a double. - /// - /// - /// The value to convert. - /// - /// - /// The double representing the value. - /// - public static double ToDouble(this string value) + bool parsed = bool.TryParse(value, out bool booleanValue); + return parsed && booleanValue; + } + + /// + /// Converts a string value to a float. + /// + /// + /// The value to convert. + /// + /// + /// The float representing the value. + /// + public static float ToFloat(this string value) + { + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) - { - return 0; - } + return 0; + } - bool parsed = double.TryParse(value, out double doubleValue); - return parsed ? doubleValue : 0; + bool parsed = float.TryParse(value, out float floatValue); + return parsed ? floatValue : 0; + } + + /// + /// Converts a string value to a nullable float. + /// + /// + /// The value to convert. + /// + /// + /// The nullable float representing the value. + /// + public static float? ToNullableFloat(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; } - /// - /// Converts a string value to a nullable double. - /// - /// - /// The value to convert. - /// - /// - /// The nullable double representing the value. - /// - public static double? ToNullableDouble(this string value) + bool parsed = float.TryParse(value, out float floatValue); + return parsed ? floatValue : null; + } + + /// + /// Converts a string value to a double. + /// + /// + /// The value to convert. + /// + /// + /// The double representing the value. + /// + public static double ToDouble(this string value) + { + if (string.IsNullOrWhiteSpace(value)) { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } + return 0; + } - bool parsed = double.TryParse(value, out double doubleValue); - return parsed ? doubleValue : null; + bool parsed = double.TryParse(value, out double doubleValue); + return parsed ? doubleValue : 0; + } + + /// + /// Converts a string value to a nullable double. + /// + /// + /// The value to convert. + /// + /// + /// The nullable double representing the value. + /// + public static double? ToNullableDouble(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; } + + bool parsed = double.TryParse(value, out double doubleValue); + return parsed ? doubleValue : null; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/IValueConverter.cs b/src/MADE.Data.Converters/IValueConverter.cs index a8bf29af..ad5f41de 100644 --- a/src/MADE.Data.Converters/IValueConverter.cs +++ b/src/MADE.Data.Converters/IValueConverter.cs @@ -1,45 +1,44 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters +namespace MADE.Data.Converters; + +/// +/// Defines an interface for a value converter from to . +/// +/// +/// The type of object to convert from. +/// +/// +/// The type of object to convert to. +/// +public interface IValueConverter { /// - /// Defines an interface for a value converter from to . + /// Converts the value to the type. /// - /// - /// The type of object to convert from. - /// - /// - /// The type of object to convert to. - /// - public interface IValueConverter - { - /// - /// Converts the value to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - TTo Convert(TFrom value, object parameter = default); + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + TTo Convert(TFrom value, object parameter = default); - /// - /// Converts the value back to the type. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - TFrom ConvertBack(TTo value, object parameter = default); - } -} \ No newline at end of file + /// + /// Converts the value back to the type. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + TFrom ConvertBack(TTo value, object parameter = default); +} diff --git a/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs b/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs index 5f9173a1..98009097 100644 --- a/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs +++ b/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs @@ -1,48 +1,47 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Converters -{ - using System.Text; - using MADE.Data.Converters.Extensions; +using System.Text; +using MADE.Data.Converters.Extensions; + +namespace MADE.Data.Converters; +/// +/// Defines a value converter from to Base64 with an optional Encoding parameter. +/// +public partial class StringToBase64StringValueConverter : IValueConverter +{ /// - /// Defines a value converter from to Base64 with an optional Encoding parameter. + /// Converts the value to the Base64 . /// - public partial class StringToBase64StringValueConverter : IValueConverter + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted Base64 object. + /// + public string Convert(string value, object parameter = default) { - /// - /// Converts the value to the Base64 . - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted Base64 object. - /// - public string Convert(string value, object parameter = default) - { - return value.ToBase64(parameter as Encoding ?? Encoding.UTF8); - } + return value.ToBase64(parameter as Encoding ?? Encoding.UTF8); + } - /// - /// Converts the Base64 value back to the original value. - /// - /// - /// The value to convert. - /// - /// - /// The optional parameter used to help with conversion. - /// - /// - /// The converted object. - /// - public string ConvertBack(string value, object parameter = default) - { - return value.FromBase64(parameter as Encoding ?? Encoding.UTF8); - } + /// + /// Converts the Base64 value back to the original value. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted object. + /// + public string ConvertBack(string value, object parameter = default) + { + return value.FromBase64(parameter as Encoding ?? Encoding.UTF8); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Converters/Strings/Resources.Designer.cs b/src/MADE.Data.Converters/Strings/Resources.Designer.cs index ba745bb2..a52c7d21 100644 --- a/src/MADE.Data.Converters/Strings/Resources.Designer.cs +++ b/src/MADE.Data.Converters/Strings/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -8,74 +8,73 @@ // //------------------------------------------------------------------------------ -namespace MADE.Data.Converters.Strings { - using System; +using System; + +namespace MADE.Data.Converters.Strings; + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +public class Resources { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } /// - /// A strongly-typed resource class, for looking up localized strings, etc. + /// Returns the cached ResourceManager instance used by this class. /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MADE.Data.Converters.Strings.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MADE.Data.Converters.Strings.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// - /// Looks up a localized string similar to No. - /// - public static string BooleanToStringValueConverter_FalseValue { - get { - return ResourceManager.GetString("BooleanToStringValueConverter_FalseValue", resourceCulture); - } + set { + resourceCulture = value; } - - /// - /// Looks up a localized string similar to Yes. - /// - public static string BooleanToStringValueConverter_TrueValue { - get { - return ResourceManager.GetString("BooleanToStringValueConverter_TrueValue", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to No. + /// + public static string BooleanToStringValueConverter_FalseValue { + get { + return ResourceManager.GetString("BooleanToStringValueConverter_FalseValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + public static string BooleanToStringValueConverter_TrueValue { + get { + return ResourceManager.GetString("BooleanToStringValueConverter_TrueValue", resourceCulture); } } } diff --git a/src/MADE.Data.EFCore/Converters/UtcDateTimeConverter.cs b/src/MADE.Data.EFCore/Converters/UtcDateTimeConverter.cs index 0e460af2..2cbab189 100644 --- a/src/MADE.Data.EFCore/Converters/UtcDateTimeConverter.cs +++ b/src/MADE.Data.EFCore/Converters/UtcDateTimeConverter.cs @@ -1,74 +1,73 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore.Converters +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace MADE.Data.EFCore.Converters; + +/// +/// Defines a converter to help with the storing of dates in a UTC format. +/// +public static class UtcDateTimeConverter { - using System; - using Microsoft.EntityFrameworkCore; - using Microsoft.EntityFrameworkCore.Metadata; - using Microsoft.EntityFrameworkCore.Metadata.Builders; - using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + internal static readonly ValueConverter UtcConverter = + new( + value => value, + value => DateTime.SpecifyKind(value, DateTimeKind.Utc)); + + private const string IsUtcAnnotation = "IsUtc"; /// - /// Defines a converter to help with the storing of dates in a UTC format. + /// Defines an annotation on a property that it should be in a UTC format. + /// + /// The intended use for this is on properties which are a or DateTime?. + /// /// - public static class UtcDateTimeConverter + /// The property builder. + /// A value indicating whether the property value should be in UTC format. + /// The type of property. + /// The configured property builder. + public static PropertyBuilder IsUtc( + this PropertyBuilder builder, + bool isUtc = true) { - internal static readonly ValueConverter UtcConverter = - new( - value => value, - value => DateTime.SpecifyKind(value, DateTimeKind.Utc)); - - private const string IsUtcAnnotation = "IsUtc"; - - /// - /// Defines an annotation on a property that it should be in a UTC format. - /// - /// The intended use for this is on properties which are a or DateTime?. - /// - /// - /// The property builder. - /// A value indicating whether the property value should be in UTC format. - /// The type of property. - /// The configured property builder. - public static PropertyBuilder IsUtc( - this PropertyBuilder builder, - bool isUtc = true) - { - return builder.HasAnnotation(IsUtcAnnotation, isUtc); - } + return builder.HasAnnotation(IsUtcAnnotation, isUtc); + } - /// - /// Determines whether the has the IsUtc annotation. - /// - /// The property to check. - /// A value indicating whether the property has the IsUtc annotation. - public static bool IsUtc(this IMutableProperty property) - { - return (bool?)property.FindAnnotation(IsUtcAnnotation)?.Value ?? false; - } + /// + /// Determines whether the has the IsUtc annotation. + /// + /// The property to check. + /// A value indicating whether the property has the IsUtc annotation. + public static bool IsUtc(this IMutableProperty property) + { + return (bool?)property.FindAnnotation(IsUtcAnnotation)?.Value ?? false; + } - /// - /// Applies a UTC converter to the . - /// - /// The model builder to apply the converter to. - public static void ApplyUtcDateTimeConverter(this ModelBuilder builder) + /// + /// Applies a UTC converter to the . + /// + /// The model builder to apply the converter to. + public static void ApplyUtcDateTimeConverter(this ModelBuilder builder) + { + foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes()) { - foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes()) + foreach (IMutableProperty property in entityType.GetProperties()) { - foreach (IMutableProperty property in entityType.GetProperties()) + if (!property.IsUtc()) { - if (!property.IsUtc()) - { - continue; - } + continue; + } - if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) - { - property.SetValueConverter(UtcConverter); - } + if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?)) + { + property.SetValueConverter(UtcConverter); } } } } -} \ No newline at end of file +} diff --git a/src/MADE.Data.EFCore/EntityBase.cs b/src/MADE.Data.EFCore/EntityBase.cs index 513ef344..6a8eab23 100644 --- a/src/MADE.Data.EFCore/EntityBase.cs +++ b/src/MADE.Data.EFCore/EntityBase.cs @@ -1,30 +1,29 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore -{ - using System; - using System.ComponentModel.DataAnnotations.Schema; +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MADE.Data.EFCore; +/// +/// Defines a base definition for an entity. +/// +public abstract class EntityBase : IEntityBase +{ /// - /// Defines a base definition for an entity. + /// Gets or sets the identifier of the entity. /// - public abstract class EntityBase : IEntityBase - { - /// - /// Gets or sets the identifier of the entity. - /// - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } - /// - /// Gets or sets the date of the entity's creation. - /// - public virtual DateTime CreatedDate { get; set; } = DateTime.UtcNow; + /// + /// Gets or sets the date of the entity's creation. + /// + public virtual DateTime CreatedDate { get; set; } = DateTime.UtcNow; - /// - /// Gets or sets the date of the entity's last update. - /// - public virtual DateTime? UpdatedDate { get; set; } = DateTime.UtcNow; - } -} \ No newline at end of file + /// + /// Gets or sets the date of the entity's last update. + /// + public virtual DateTime? UpdatedDate { get; set; } = DateTime.UtcNow; +} diff --git a/src/MADE.Data.EFCore/EntityBase{TKey}.cs b/src/MADE.Data.EFCore/EntityBase{TKey}.cs index 1cb1f4dd..39ecf7f8 100644 --- a/src/MADE.Data.EFCore/EntityBase{TKey}.cs +++ b/src/MADE.Data.EFCore/EntityBase{TKey}.cs @@ -1,31 +1,30 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore -{ - using System; - using System.ComponentModel.DataAnnotations.Schema; +using System; +using System.ComponentModel.DataAnnotations.Schema; + +namespace MADE.Data.EFCore; +/// +/// Defines a base definition for an entity. +/// +/// The type of unique identifier for the entity. +public abstract class EntityBase : IEntityBase +{ /// - /// Defines a base definition for an entity. + /// Gets or sets the identifier of the entity. /// - /// The type of unique identifier for the entity. - public abstract class EntityBase : IEntityBase - { - /// - /// Gets or sets the identifier of the entity. - /// - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public TKey Id { get; set; } + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public TKey Id { get; set; } - /// - /// Gets or sets the date of the entity's creation. - /// - public DateTime CreatedDate { get; set; } + /// + /// Gets or sets the date of the entity's creation. + /// + public DateTime CreatedDate { get; set; } - /// - /// Gets or sets the date of the entity's last update. - /// - public DateTime? UpdatedDate { get; set; } - } -} \ No newline at end of file + /// + /// Gets or sets the date of the entity's last update. + /// + public DateTime? UpdatedDate { get; set; } +} diff --git a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs index 31a38706..2dac37e8 100644 --- a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs @@ -1,146 +1,145 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore.Extensions +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; + +namespace MADE.Data.EFCore.Extensions; + +/// +/// Defines a collection of extensions for types. +/// +public static class DbContextExtensions { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.EntityFrameworkCore; - using Microsoft.EntityFrameworkCore.ChangeTracking; + /// + /// Updates an entity within the context and saves the changes. + /// + /// The . + /// The entity to update. + /// The cancellation token. + /// The type of entity to update. + /// An asynchronous operation. + /// An error is encountered while saving to the database. + /// A concurrency violation is encountered while saving to the database. + /// A concurrency violation occurs when an unexpected number of rows are affected during save. + /// This is usually because the data in the database has been modified since it was loaded into memory. + /// If the is canceled. + public static async Task UpdateAsync( + this DbContext context, + T entity, + CancellationToken cancellationToken = default) + { + context.Update(entity); + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } /// - /// Defines a collection of extensions for types. + /// Removes entities from a using the specified predicate. /// - public static class DbContextExtensions + /// The data set to remove entities from. + /// The function for determining the items to remove. + /// The type of entity to remove. + public static void RemoveWhere(this DbSet set, Expression> predicate) + where T : class { - /// - /// Updates an entity within the context and saves the changes. - /// - /// The . - /// The entity to update. - /// The cancellation token. - /// The type of entity to update. - /// An asynchronous operation. - /// An error is encountered while saving to the database. - /// A concurrency violation is encountered while saving to the database. - /// A concurrency violation occurs when an unexpected number of rows are affected during save. - /// This is usually because the data in the database has been modified since it was loaded into memory. - /// If the is canceled. - public static async Task UpdateAsync( - this DbContext context, - T entity, - CancellationToken cancellationToken = default) - { - context.Update(entity); - await context.SaveChangesAsync(cancellationToken); - } + IQueryable toRemove = set.Where(predicate); + set.RemoveRange(toRemove); + } - /// - /// Removes entities from a using the specified predicate. - /// - /// The data set to remove entities from. - /// The function for determining the items to remove. - /// The type of entity to remove. - public static void RemoveWhere(this DbSet set, Expression> predicate) - where T : class - { - IQueryable toRemove = set.Where(predicate); - set.RemoveRange(toRemove); - } + /// + /// Sets the dates of entities being tracked in an added or modified state. + /// + /// It is best to call this method in an override of the DbContext.SaveChangesAsync method in your data context. + /// + /// + /// The to update entity dates for. + public static void SetEntityDates(this DbContext context) + { + IEnumerable entries = context.ChangeTracker + .Entries() + .Where( + entry => entry.Entity is IDatedEntity && + entry.State is EntityState.Added or EntityState.Modified); - /// - /// Sets the dates of entities being tracked in an added or modified state. - /// - /// It is best to call this method in an override of the DbContext.SaveChangesAsync method in your data context. - /// - /// - /// The to update entity dates for. - public static void SetEntityDates(this DbContext context) - { - IEnumerable entries = context.ChangeTracker - .Entries() - .Where( - entry => entry.Entity is IDatedEntity && - entry.State is EntityState.Added or EntityState.Modified); + DateTime now = DateTime.UtcNow; - DateTime now = DateTime.UtcNow; + foreach (EntityEntry entry in entries) + { + var entity = (IDatedEntity)entry.Entity; + entity.UpdatedDate = now; - foreach (EntityEntry entry in entries) + if (entry.State == EntityState.Added && entity.CreatedDate == DateTime.MinValue) { - var entity = (IDatedEntity)entry.Entity; - entity.UpdatedDate = now; - - if (entry.State == EntityState.Added && entity.CreatedDate == DateTime.MinValue) - { - entity.CreatedDate = now; - } + entity.CreatedDate = now; } } + } - /// - /// Attempts to save all changes made in this context to the database. - /// - /// The . - /// An exception for handling the exception thrown, for example, event logging. - /// A to observe while waiting for the task to complete. - /// - /// True if the changes saved successfully; otherwise, false. - /// - /// If the is canceled. - /// Potentially thrown by the delegate callback. - public static async Task TrySaveChangesAsync( - this DbContext context, - Action onError = null, - CancellationToken cancellationToken = default) + /// + /// Attempts to save all changes made in this context to the database. + /// + /// The . + /// An exception for handling the exception thrown, for example, event logging. + /// A to observe while waiting for the task to complete. + /// + /// True if the changes saved successfully; otherwise, false. + /// + /// If the is canceled. + /// Potentially thrown by the delegate callback. + public static async Task TrySaveChangesAsync( + this DbContext context, + Action? onError = null, + CancellationToken cancellationToken = default) + { + try { - try - { - await context.SaveChangesAsync(cancellationToken); - return true; - } - catch (DbUpdateException ex) - { - onError?.Invoke(ex); - } - - return false; + await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + return true; } - - /// - /// Attempts to perform an action on the data context. - /// - /// The . - /// The action to run. - /// An exception for handling the exception thrown, for example, event logging. - /// The type of data context. - /// True if the action ran successfully; otherwise, false. - /// Potentially thrown by the delegate callback. - public static async Task TryAsync( - this TContext context, - Func action, - Action onError = null) - where TContext : DbContext + catch (DbUpdateException ex) { - if (action == null) - { - return false; - } + onError?.Invoke(ex); + } - try - { - await action.Invoke(context); - return true; - } - catch (Exception ex) - { - onError?.Invoke(ex); - } + return false; + } + /// + /// Attempts to perform an action on the data context. + /// + /// The . + /// The action to run. + /// An exception for handling the exception thrown, for example, event logging. + /// The type of data context. + /// True if the action ran successfully; otherwise, false. + /// Potentially thrown by the delegate callback. + public static async Task TryAsync( + this TContext context, + Func? action, + Action? onError = null) + where TContext : DbContext + { + if (action == null) + { return false; } + + try + { + await action.Invoke(context).ConfigureAwait(false); + return true; + } + catch (Exception ex) + { + onError?.Invoke(ex); + } + + return false; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs b/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs index 8ae0f13d..a3a9b525 100644 --- a/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/EntityBaseExtensions.cs @@ -1,58 +1,57 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore.Extensions -{ - using System; - using MADE.Data.EFCore.Converters; - using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using MADE.Data.EFCore.Converters; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MADE.Data.EFCore.Extensions; +/// +/// Defines a collection of extensions for the type. +/// +public static class EntityBaseExtensions +{ /// - /// Defines a collection of extensions for the type. + /// Configures the default properties of an entity. /// - public static class EntityBaseExtensions + /// The type of entity to configure. + /// The entity type builder associated with the entity. + /// The entity type builder. + public static EntityTypeBuilder Configure(this EntityTypeBuilder builder) + where TEntity : class, IEntityBase { - /// - /// Configures the default properties of an entity. - /// - /// The type of entity to configure. - /// The entity type builder associated with the entity. - /// The entity type builder. - public static EntityTypeBuilder Configure(this EntityTypeBuilder builder) - where TEntity : class, IEntityBase - { - builder.ConfigureWithKey(); - return builder; - } + builder.ConfigureWithKey(); + return builder; + } - /// - /// Configures the default properties of an entity. - /// - /// The type of entity to configure. - /// The type of unique identifier for the entity. - /// The entity type builder associated with the entity. - /// The entity type builder. - public static EntityTypeBuilder ConfigureWithKey(this EntityTypeBuilder builder) - where TEntity : class, IEntityBase - { - builder.HasKey(e => e.Id); - builder.ConfigureDateProperties(); - return builder; - } + /// + /// Configures the default properties of an entity. + /// + /// The type of entity to configure. + /// The type of unique identifier for the entity. + /// The entity type builder associated with the entity. + /// The entity type builder. + public static EntityTypeBuilder ConfigureWithKey(this EntityTypeBuilder builder) + where TEntity : class, IEntityBase + { + builder.HasKey(e => e.Id); + builder.ConfigureDateProperties(); + return builder; + } - /// - /// Configures the created and updated date properties of an entity as UTC. - /// - /// The type of entity to configure. - /// The entity type builder associated with the entity. - /// The entity type builder. - public static EntityTypeBuilder ConfigureDateProperties( - this EntityTypeBuilder builder) - where TEntity : class, IDatedEntity - { - builder.Property(x => x.CreatedDate).IsUtc(); - builder.Property(x => x.UpdatedDate).IsUtc(); - return builder; - } + /// + /// Configures the created and updated date properties of an entity as UTC. + /// + /// The type of entity to configure. + /// The entity type builder associated with the entity. + /// The entity type builder. + public static EntityTypeBuilder ConfigureDateProperties( + this EntityTypeBuilder builder) + where TEntity : class, IDatedEntity + { + builder.Property(x => x.CreatedDate).IsUtc(); + builder.Property(x => x.UpdatedDate).IsUtc(); + return builder; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs index 2b9786f6..2ef562a0 100644 --- a/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/QueryableExtensions.cs @@ -1,59 +1,58 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore.Extensions +using System; +using System.Linq; +using System.Linq.Expressions; + +namespace MADE.Data.EFCore.Extensions; + +/// +/// Defines a collection of extensions for Entity Framework queries. +/// +public static class QueryableExtensions { - using System; - using System.Linq; - using System.Linq.Expressions; + /// + /// Skips and takes a subset of a data query based on the specified current page and page size requested. + /// + /// The type of entity being queried. + /// The current query. + /// The current page being requested. + /// The size of the page being requested. + /// The paginated query. + public static IQueryable Page(this IQueryable query, int page, int pageSize) + { + return query.Skip((page - 1) * pageSize).Take(pageSize); + } /// - /// Defines a collection of extensions for Entity Framework queries. + /// Orders the query results by the specified property name from the entity with the option for order by ascending or descending. /// - public static class QueryableExtensions + /// The type of entity being ordered. + /// The query to order. + /// The property/column name to sort on for the entity. + /// A value indicating whether to sort descending. + /// The ordered query. + public static IQueryable OrderBy(this IQueryable query, string sortName, bool sortDesc) { - /// - /// Skips and takes a subset of a data query based on the specified current page and page size requested. - /// - /// The type of entity being queried. - /// The current query. - /// The current page being requested. - /// The size of the page being requested. - /// The paginated query. - public static IQueryable Page(this IQueryable query, int page, int pageSize) + if (string.IsNullOrWhiteSpace(sortName)) { - return query.Skip((page - 1) * pageSize).Take(pageSize); + return query; } - /// - /// Orders the query results by the specified property name from the entity with the option for order by ascending or descending. - /// - /// The type of entity being ordered. - /// The query to order. - /// The property/column name to sort on for the entity. - /// A value indicating whether to sort descending. - /// The ordered query. - public static IQueryable OrderBy(this IQueryable query, string sortName, bool sortDesc) - { - if (string.IsNullOrWhiteSpace(sortName)) - { - return query; - } - - ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); - MemberExpression property = Expression.Property(parameter, sortName); - LambdaExpression lambda = Expression.Lambda(property, parameter); + ParameterExpression parameter = Expression.Parameter(typeof(T), "x"); + MemberExpression property = Expression.Property(parameter, sortName); + LambdaExpression lambda = Expression.Lambda(property, parameter); - string methodName = sortDesc ? "OrderByDescending" : "OrderBy"; + string methodName = sortDesc ? "OrderByDescending" : "OrderBy"; - MethodCallExpression call = Expression.Call( - typeof(Queryable), - methodName, - new[] { typeof(T), property.Type }, - query.Expression, - Expression.Quote(lambda)); + MethodCallExpression call = Expression.Call( + typeof(Queryable), + methodName, + new[] { typeof(T), property.Type }, + query.Expression, + Expression.Quote(lambda)); - return query.Provider.CreateQuery(call); - } + return query.Provider.CreateQuery(call); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.EFCore/IDatedEntity.cs b/src/MADE.Data.EFCore/IDatedEntity.cs index 684b927a..db5b381b 100644 --- a/src/MADE.Data.EFCore/IDatedEntity.cs +++ b/src/MADE.Data.EFCore/IDatedEntity.cs @@ -1,23 +1,22 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore -{ - using System; +using System; + +namespace MADE.Data.EFCore; +/// +/// Defines a base definition for an entity with defined created and updated date. +/// +public interface IDatedEntity +{ /// - /// Defines a base definition for an entity with defined created and updated date. + /// Gets or sets the date of the entity's creation. /// - public interface IDatedEntity - { - /// - /// Gets or sets the date of the entity's creation. - /// - DateTime CreatedDate { get; set; } + DateTime CreatedDate { get; set; } - /// - /// Gets or sets the date of the entity's last update. - /// - DateTime? UpdatedDate { get; set; } - } -} \ No newline at end of file + /// + /// Gets or sets the date of the entity's last update. + /// + DateTime? UpdatedDate { get; set; } +} diff --git a/src/MADE.Data.EFCore/IEntityBase.cs b/src/MADE.Data.EFCore/IEntityBase.cs index 2f526e6a..f7c159cb 100644 --- a/src/MADE.Data.EFCore/IEntityBase.cs +++ b/src/MADE.Data.EFCore/IEntityBase.cs @@ -1,14 +1,13 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore -{ - using System; +using System; + +namespace MADE.Data.EFCore; - /// - /// Defines a base definition for an entity. - /// - public interface IEntityBase : IEntityBase - { - } -} \ No newline at end of file +/// +/// Defines a base definition for an entity. +/// +public interface IEntityBase : IEntityBase +{ +} diff --git a/src/MADE.Data.EFCore/IEntityBase{TKey}.cs b/src/MADE.Data.EFCore/IEntityBase{TKey}.cs index c3e30083..cd35ae7b 100644 --- a/src/MADE.Data.EFCore/IEntityBase{TKey}.cs +++ b/src/MADE.Data.EFCore/IEntityBase{TKey}.cs @@ -1,17 +1,16 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.EFCore +namespace MADE.Data.EFCore; + +/// +/// Defines a base definition for an entity with a defined primary key type. +/// +/// The type of unique identifier for the entity. +public interface IEntityBase : IDatedEntity { /// - /// Defines a base definition for an entity with a defined primary key type. + /// Gets or sets the identifier of the entity. /// - /// The type of unique identifier for the entity. - public interface IEntityBase : IDatedEntity - { - /// - /// Gets or sets the identifier of the entity. - /// - TKey Id { get; set; } - } -} \ No newline at end of file + TKey Id { get; set; } +} diff --git a/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs index 4d22a372..f981d7da 100644 --- a/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs +++ b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs @@ -1,156 +1,135 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Serialization.Json.Converters +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using MADE.Data.Serialization.Json.Exceptions; + +namespace MADE.Data.Serialization.Json.Converters; + +/// +/// Defines a JSON converter for migrating serialized declarations within a serialized JSON object. +/// +/// +/// This converter reads $type metadata from JSON objects and resolves the target type using registered type migrations. +/// It is designed to deserialize JSON that was previously serialized with type metadata (e.g., Newtonsoft.Json's TypeNameHandling.All). +/// +public class JsonTypeMigrationConverter : JsonConverter { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.Json; - using System.Text.Json.Serialization; - using System.Threading; - using System.Threading.Tasks; - using MADE.Data.Serialization.Json.Exceptions; + private readonly object migrationLock = new(); + + private readonly List migrations = new(); + + private JsonSerializerOptions? innerOptions; /// - /// Defines a JSON converter for migrating serialized declarations within a serialized JSON object. + /// Initializes a new instance of the class. /// /// - /// This converter reads $type metadata from JSON objects and resolves the target type using registered type migrations. - /// It is designed to deserialize JSON that was previously serialized with type metadata (e.g., Newtonsoft.Json's TypeNameHandling.All). + /// To add migrations, call the method. /// - public class JsonTypeMigrationConverter : JsonConverter + public JsonTypeMigrationConverter() + : this(null) { - private readonly SemaphoreSlim migrationSemaphore; - - private readonly List migrations = new(); - - private JsonSerializerOptions innerOptions; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// To add migrations, call the method. - /// - public JsonTypeMigrationConverter() - : this(null) + /// + /// Initializes a new instance of the class with pre-configured type migrations. + /// + /// The type migrations to initialize with. + public JsonTypeMigrationConverter(params JsonTypeMigration[] migrations) + { + if (migrations != null && migrations.Any()) { + this.migrations.AddRange(migrations); } + } - /// - /// Initializes a new instance of the class with pre-configured type migrations. - /// - /// The type migrations to initialize with. - public JsonTypeMigrationConverter(params JsonTypeMigration[] migrations) - { - this.migrationSemaphore = new SemaphoreSlim(1, 1); - - if (migrations != null && migrations.Any()) - { - this.migrations.AddRange(migrations); - } - } + /// + /// Adds a JSON type migration to the converter. + /// + /// The type migration to add. + /// Thrown if the is null. + /// Thrown if a already exists for the from type. + public void AddTypeMigration(JsonTypeMigration migration) + { + ArgumentNullException.ThrowIfNull(migration); - /// - /// Adds a JSON type migration to the converter. - /// - /// The type migration to add. - /// An asynchronous operation. - /// Thrown if the is null. - /// Thrown if a already exists for the from type. - public async Task AddTypeMigrationAsync(JsonTypeMigration migration) + lock (this.migrationLock) { - if (migration == null) - { - throw new ArgumentNullException(nameof(migration)); - } - - await this.migrationSemaphore.WaitAsync(); + JsonTypeMigration existingMigration = this.migrations.FirstOrDefault( + m => + m.FromAssemblyName == migration.FromAssemblyName && + m.FromTypeName == migration.FromTypeName); - try + if (existingMigration != null) { - JsonTypeMigration existingMigration = this.migrations.FirstOrDefault( - m => - m.FromAssemblyName == migration.FromAssemblyName && - m.FromTypeName == migration.FromTypeName); - - if (existingMigration != null) - { - throw new JsonTypeMigrationException( - $"A type migration is already registered for type {existingMigration.FromTypeName} in assembly {existingMigration.FromAssemblyName} to {existingMigration.ToType.FullName}"); - } - - this.migrations.Add(migration); + throw new JsonTypeMigrationException( + $"A type migration is already registered for type {existingMigration.FromTypeName} in assembly {existingMigration.FromAssemblyName} to {existingMigration.ToType.FullName}"); } - finally - { - this.migrationSemaphore.Release(); - } - } - /// - public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - using JsonDocument doc = JsonDocument.ParseValue(ref reader); - JsonElement root = doc.RootElement; - - Type resolvedType = typeToConvert; + this.migrations.Add(migration); + } + } - if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("$type", out JsonElement typeElement)) - { - string typeString = typeElement.GetString(); - resolvedType = this.ResolveType(typeString) ?? typeToConvert; - } + /// + public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using JsonDocument doc = JsonDocument.ParseValue(ref reader); + JsonElement root = doc.RootElement; - return root.Deserialize(resolvedType, this.GetInnerOptions(options)); - } + Type resolvedType = typeToConvert; - /// - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("$type", out JsonElement typeElement)) { - JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), this.GetInnerOptions(options)); + string typeString = typeElement.GetString(); + resolvedType = this.ResolveType(typeString) ?? typeToConvert; } - private JsonSerializerOptions GetInnerOptions(JsonSerializerOptions options) - { - if (this.innerOptions == null) - { - var copy = new JsonSerializerOptions(options); - copy.Converters.Remove(this); - this.innerOptions = copy; - } + return root.Deserialize(resolvedType, this.GetInnerOptions(options)); + } - return this.innerOptions; - } + /// + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(object), this.GetInnerOptions(options)); + } - private Type ResolveType(string typeString) + private JsonSerializerOptions GetInnerOptions(JsonSerializerOptions options) + { + if (this.innerOptions == null) { - int commaIndex = typeString.IndexOf(','); - string typeName = commaIndex >= 0 ? typeString[..commaIndex].Trim() : typeString.Trim(); - string assemblyName = commaIndex >= 0 ? typeString[(commaIndex + 1)..].Trim() : null; + var copy = new JsonSerializerOptions(options); + copy.Converters.Remove(this); + this.innerOptions = copy; + } - this.migrationSemaphore.Wait(); + return this.innerOptions; + } - JsonTypeMigration migration; - try - { - migration = this.migrations.FirstOrDefault( - m => - m.FromTypeName == typeName && - (assemblyName == null || m.FromAssemblyName == assemblyName)); - } - finally - { - this.migrationSemaphore.Release(); - } + private Type ResolveType(string typeString) + { + int commaIndex = typeString.IndexOf(','); + string typeName = commaIndex >= 0 ? typeString[..commaIndex].Trim() : typeString.Trim(); + string assemblyName = commaIndex >= 0 ? typeString[(commaIndex + 1)..].Trim() : null; - if (migration != null) - { - return migration.ToType; - } + JsonTypeMigration migration; + lock (this.migrationLock) + { + migration = this.migrations.FirstOrDefault( + m => + m.FromTypeName == typeName && + (assemblyName == null || m.FromAssemblyName == assemblyName)); + } - return Type.GetType(typeString); + if (migration != null) + { + return migration.ToType; } + + return Type.GetType(typeString); } } diff --git a/src/MADE.Data.Serialization/Json/Exceptions/JsonTypeMigrationException.cs b/src/MADE.Data.Serialization/Json/Exceptions/JsonTypeMigrationException.cs index 2c22aec2..73038c3f 100644 --- a/src/MADE.Data.Serialization/Json/Exceptions/JsonTypeMigrationException.cs +++ b/src/MADE.Data.Serialization/Json/Exceptions/JsonTypeMigrationException.cs @@ -1,39 +1,38 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Serialization.Json.Exceptions -{ - using System; +using System; + +namespace MADE.Data.Serialization.Json.Exceptions; +/// +/// Defines an exception for errors occurred when interacting with JSON type migrations. +/// +public class JsonTypeMigrationException : Exception +{ /// - /// Defines an exception for errors occurred when interacting with JSON type migrations. + /// Initializes a new instance of the class. /// - public class JsonTypeMigrationException : Exception + public JsonTypeMigrationException() { - /// - /// Initializes a new instance of the class. - /// - public JsonTypeMigrationException() - { - } + } - /// - /// Initializes a new instance of the class with a message that describes the error. - /// - /// The message that describes the error. - public JsonTypeMigrationException(string message) - : base(message) - { - } + /// + /// Initializes a new instance of the class with a message that describes the error. + /// + /// The message that describes the error. + public JsonTypeMigrationException(string message) + : base(message) + { + } - /// - /// Initializes a new instance of the class with a message that describes the error and inner exception. - /// - /// The message that describes the error. - /// The exception that caused this exception to be thrown. - public JsonTypeMigrationException(string message, Exception innerException) - : base(message, innerException) - { - } + /// + /// Initializes a new instance of the class with a message that describes the error and inner exception. + /// + /// The message that describes the error. + /// The exception that caused this exception to be thrown. + public JsonTypeMigrationException(string message, Exception innerException) + : base(message, innerException) + { } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs index 5fd90c61..543a69bc 100644 --- a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs +++ b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs @@ -1,54 +1,53 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Serialization.Json -{ - using System; - using MADE.Data.Serialization.Json.Converters; +using System; +using MADE.Data.Serialization.Json.Converters; + +namespace MADE.Data.Serialization.Json; +/// +/// Defines the detail for migrating from one type to another using the . +/// +public class JsonTypeMigration +{ /// - /// Defines the detail for migrating from one type to another using the . + /// Initializes a new instance of the class with the expected from and to migration types. /// - public class JsonTypeMigration + /// The type being migrated from. + /// The type being migrated to. + public JsonTypeMigration(Type fromType, Type toType) { - /// - /// Initializes a new instance of the class with the expected from and to migration types. - /// - /// The type being migrated from. - /// The type being migrated to. - public JsonTypeMigration(Type fromType, Type toType) - { - this.FromAssemblyName = fromType.Assembly.GetName().Name; - this.FromTypeName = fromType.FullName; - this.ToType = toType; - } + this.FromAssemblyName = fromType.Assembly.GetName().Name; + this.FromTypeName = fromType.FullName; + this.ToType = toType; + } - /// - /// Initializes a new instance of the class with the expected from and to migration types. - /// - /// The name of the assembly being migrated from. - /// The name of the type being migrated from. - /// The type being migrated to. - public JsonTypeMigration(string fromAssemblyName, string fromTypeName, Type toType) - { - this.FromAssemblyName = fromAssemblyName; - this.FromTypeName = fromTypeName; - this.ToType = toType; - } + /// + /// Initializes a new instance of the class with the expected from and to migration types. + /// + /// The name of the assembly being migrated from. + /// The name of the type being migrated from. + /// The type being migrated to. + public JsonTypeMigration(string fromAssemblyName, string fromTypeName, Type toType) + { + this.FromAssemblyName = fromAssemblyName; + this.FromTypeName = fromTypeName; + this.ToType = toType; + } - /// - /// Gets the name of the assembly being migrated from. - /// - public string FromAssemblyName { get; } + /// + /// Gets the name of the assembly being migrated from. + /// + public string FromAssemblyName { get; } - /// - /// Gets the name of the type being migrated from. - /// - public string FromTypeName { get; } + /// + /// Gets the name of the type being migrated from. + /// + public string FromTypeName { get; } - /// - /// Gets the type being migrated to. - /// - public Type ToType { get; } - } -} \ No newline at end of file + /// + /// Gets the type being migrated to. + /// + public Type ToType { get; } +} diff --git a/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs b/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs index 2b7485cb..14226b07 100644 --- a/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs +++ b/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs @@ -1,91 +1,90 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation +using System; +using System.Collections.Generic; +using FluentValidation; + +namespace MADE.Data.Validation; + +/// +/// Defines a list of objects that can be accessed by index. +/// +/// The type of item being validated. +public class FluentValidatorCollection : List>, IValidatorCollection { - using System; - using System.Collections.Generic; - using FluentValidation; + private readonly List feedbackMessages = new(); - /// - /// Defines a list of objects that can be accessed by index. - /// - /// The type of item being validated. - public class FluentValidatorCollection : List>, IValidatorCollection + /// Initializes a new instance of the class that is empty and has the default initial capacity. + public FluentValidatorCollection() { - private readonly List feedbackMessages = new(); - - /// Initializes a new instance of the class that is empty and has the default initial capacity. - public FluentValidatorCollection() - { - } + } - /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. - /// The collection whose elements are copied to the new list. - /// collection is null. - public FluentValidatorCollection(IEnumerable> collection) - : base(collection) - { - } + /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. + /// The collection whose elements are copied to the new list. + /// collection is null. + public FluentValidatorCollection(IEnumerable> collection) + : base(collection) + { + } - /// - /// Initializes a new instance of the class that is empty and has the specified initial capacity. - /// The number of elements that the new list can initially store. - /// capacity is less than 0. - public FluentValidatorCollection(int capacity) - : base(capacity) - { - } + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// The number of elements that the new list can initially store. + /// capacity is less than 0. + public FluentValidatorCollection(int capacity) + : base(capacity) + { + } - /// - /// Occurs when the input value is validated against the collection of validators. - /// - public event InputValidatedEventHandler Validated; + /// + /// Occurs when the input value is validated against the collection of validators. + /// + public event InputValidatedEventHandler Validated; - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets the validator feedback messages for ones which are invalid. - /// - public IEnumerable FeedbackMessages => this.feedbackMessages; + /// + /// Gets the validator feedback messages for ones which are invalid. + /// + public IEnumerable FeedbackMessages => this.feedbackMessages; - /// - /// Executes data validation on the provided against the validators provided. - /// - /// The value to be validated. - /// Potentially thrown by the delegate callback. - public void Validate(object value) - { - this.feedbackMessages.Clear(); + /// + /// Executes data validation on the provided against the validators provided. + /// + /// The value to be validated. + /// Potentially thrown by the delegate callback. + public void Validate(object value) + { + this.feedbackMessages.Clear(); - this.IsDirty = true; + this.IsDirty = true; - this.ForEach(validator => + this.ForEach(validator => + { + var result = validator.Validate((T)value); + if (!result.IsValid) { - var result = validator.Validate((T)value); - if (!result.IsValid) - { - IsInvalid = true; - } + IsInvalid = true; + } - if (result.Errors != null) + if (result.Errors != null) + { + foreach (var message in result.Errors) { - foreach (var message in result.Errors) - { - this.feedbackMessages.Add(message.ErrorMessage); - } + this.feedbackMessages.Add(message.ErrorMessage); } - }); + } + }); - this.Validated?.Invoke(this, new InputValidatedEventArgs(this.IsInvalid, this.IsDirty)); - } + this.Validated?.Invoke(this, new InputValidatedEventArgs(this.IsInvalid, this.IsDirty)); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Exceptions/InvalidRangeException.cs b/src/MADE.Data.Validation/Exceptions/InvalidRangeException.cs index a5e32f47..06b876d3 100644 --- a/src/MADE.Data.Validation/Exceptions/InvalidRangeException.cs +++ b/src/MADE.Data.Validation/Exceptions/InvalidRangeException.cs @@ -1,22 +1,21 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Exceptions -{ - using System; +using System; + +namespace MADE.Data.Validation.Exceptions; +/// +/// Defines an exception for an invalid range. +/// +public class InvalidRangeException : Exception +{ /// - /// Defines an exception for an invalid range. + /// Initializes a new instance of the class. /// - public class InvalidRangeException : Exception + /// The exception message. + public InvalidRangeException(string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public InvalidRangeException(string message) - : base(message) - { - } } } diff --git a/src/MADE.Data.Validation/Extensions/ComparableExtensions.cs b/src/MADE.Data.Validation/Extensions/ComparableExtensions.cs index 43c8672b..dad3533d 100644 --- a/src/MADE.Data.Validation/Extensions/ComparableExtensions.cs +++ b/src/MADE.Data.Validation/Extensions/ComparableExtensions.cs @@ -1,65 +1,64 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Extensions -{ - using System; +using System; + +namespace MADE.Data.Validation.Extensions; +/// +/// Defines a collection of extensions for objects. +/// +public static class ComparableExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Determines whether the value is greater than the value. /// - public static class ComparableExtensions + /// The type. + /// The value to compare. + /// The value to compare against. + /// True if the is greater than the value. + public static bool IsGreaterThan(this T value, T other) + where T : IComparable { - /// - /// Determines whether the value is greater than the value. - /// - /// The type. - /// The value to compare. - /// The value to compare against. - /// True if the is greater than the value. - public static bool IsGreaterThan(this T value, T other) - where T : IComparable - { - return value.CompareTo(other) > 0; - } + return value.CompareTo(other) > 0; + } - /// - /// Determines whether the value is greater than or equal to the value. - /// - /// The type. - /// The value to compare. - /// The value to compare against. - /// True if the is greater than or equal to the value. - public static bool IsGreaterThanOrEqualTo(this T value, T other) - where T : IComparable - { - return value.CompareTo(other) >= 0; - } + /// + /// Determines whether the value is greater than or equal to the value. + /// + /// The type. + /// The value to compare. + /// The value to compare against. + /// True if the is greater than or equal to the value. + public static bool IsGreaterThanOrEqualTo(this T value, T other) + where T : IComparable + { + return value.CompareTo(other) >= 0; + } - /// - /// Determines whether the value is less than the value. - /// - /// The type. - /// The value to compare. - /// The value to compare against. - /// True if the is less than the value. - public static bool IsLessThan(this T value, T other) - where T : IComparable - { - return value.CompareTo(other) < 0; - } + /// + /// Determines whether the value is less than the value. + /// + /// The type. + /// The value to compare. + /// The value to compare against. + /// True if the is less than the value. + public static bool IsLessThan(this T value, T other) + where T : IComparable + { + return value.CompareTo(other) < 0; + } - /// - /// Determines whether the value is less than or equal to the value. - /// - /// The type. - /// The value to compare. - /// The value to compare against. - /// True if the is less than or equal to the value. - public static bool IsLessThanOrEqualTo(this T value, T other) - where T : IComparable - { - return value.CompareTo(other) <= 0; - } + /// + /// Determines whether the value is less than or equal to the value. + /// + /// The type. + /// The value to compare. + /// The value to compare against. + /// True if the is less than or equal to the value. + public static bool IsLessThanOrEqualTo(this T value, T other) + where T : IComparable + { + return value.CompareTo(other) <= 0; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Extensions/DateTimeExtensions.cs b/src/MADE.Data.Validation/Extensions/DateTimeExtensions.cs index 2a97892d..bc4a3488 100644 --- a/src/MADE.Data.Validation/Extensions/DateTimeExtensions.cs +++ b/src/MADE.Data.Validation/Extensions/DateTimeExtensions.cs @@ -1,45 +1,44 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Extensions -{ - using System; +using System; + +namespace MADE.Data.Validation.Extensions; +/// +/// Defines a collection of data validation extensions for objects. +/// +public static class DateTimeExtensions +{ /// - /// Defines a collection of data validation extensions for objects. + /// Determines whether a is within a valid range. /// - public static class DateTimeExtensions + /// The to check. + /// The lower bound of the range. + /// The upper bound of the range. + /// True if the date is within the valid range. + public static bool IsInRange(this DateTime date, DateTime from, DateTime to) { - /// - /// Determines whether a is within a valid range. - /// - /// The to check. - /// The lower bound of the range. - /// The upper bound of the range. - /// True if the date is within the valid range. - public static bool IsInRange(this DateTime date, DateTime from, DateTime to) - { - return date >= from && date <= to; - } + return date >= from && date <= to; + } - /// - /// Determines whether a is a day of the week other than Sunday or Saturday. - /// - /// The to check. - /// True if the day of week is between Monday and Friday; otherwise, false. - public static bool IsWeekday(this DateTime date) - { - return date.DayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday; - } + /// + /// Determines whether a is a day of the week other than Sunday or Saturday. + /// + /// The to check. + /// True if the day of week is between Monday and Friday; otherwise, false. + public static bool IsWeekday(this DateTime date) + { + return date.DayOfWeek is >= DayOfWeek.Monday and <= DayOfWeek.Friday; + } - /// - /// Determines whether a is a day of the week other than Monday through Friday. - /// - /// The to check. - /// True if the day of week is Saturday or Sunday; otherwise, false. - public static bool IsWeekend(this DateTime date) - { - return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; - } + /// + /// Determines whether a is a day of the week other than Monday through Friday. + /// + /// The to check. + /// True if the day of week is Saturday or Sunday; otherwise, false. + public static bool IsWeekend(this DateTime date) + { + return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Extensions/MathExtensions.cs b/src/MADE.Data.Validation/Extensions/MathExtensions.cs index 2be9b89f..aa4c4161 100644 --- a/src/MADE.Data.Validation/Extensions/MathExtensions.cs +++ b/src/MADE.Data.Validation/Extensions/MathExtensions.cs @@ -1,266 +1,265 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Extensions +using System; +using MADE.Data.Validation.Exceptions; + +namespace MADE.Data.Validation.Extensions; + +/// +/// Defines a collection of extensions for common mathematics expressions. +/// +public static class MathExtensions { - using System; - using MADE.Data.Validation.Exceptions; + /// + /// Gets a value for Epsilon. + /// + public static readonly double Epsilon = 2.2204460492503131E-16; /// - /// Defines a collection of extensions for common mathematics expressions. + /// Checks whether a double value is zero. /// - public static class MathExtensions + /// + /// The value to check. + /// + /// + /// True if zero; otherwise, false. + /// + public static bool IsZero(this double value) { - /// - /// Gets a value for Epsilon. - /// - public static readonly double Epsilon = 2.2204460492503131E-16; + return Math.Abs(value) < Epsilon; + } - /// - /// Checks whether a double value is zero. - /// - /// - /// The value to check. - /// - /// - /// True if zero; otherwise, false. - /// - public static bool IsZero(this double value) + /// + /// Checks whether a float value is zero. + /// + /// + /// The value to check. + /// + /// + /// True if zero; otherwise, false. + /// + public static bool IsZero(this float value) + { + return Math.Abs(value) < Epsilon; + } + + /// + /// Checks whether two values are close in value. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the values are close; otherwise, false. + /// + /// Thrown if the value equals . + public static bool IsCloseTo(this int value, int compare) + { + if (Math.Abs(value - compare) < 1) { - return Math.Abs(value) < Epsilon; + return true; } - /// - /// Checks whether a float value is zero. - /// - /// - /// The value to check. - /// - /// - /// True if zero; otherwise, false. - /// - public static bool IsZero(this float value) + double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; + int b = value - compare; + return -a < b && a > b; + } + + /// + /// Checks whether two values are close in value which have a precision point. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the values are close; otherwise, false. + /// + public static bool IsCloseTo(this double value, double compare) + { + if (Math.Abs(value - compare) < Epsilon) { - return Math.Abs(value) < Epsilon; + return true; } - /// - /// Checks whether two values are close in value. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the values are close; otherwise, false. - /// - /// Thrown if the value equals . - public static bool IsCloseTo(this int value, int compare) - { - if (Math.Abs(value - compare) < 1) - { - return true; - } + double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; + double b = value - compare; + return -a < b && a > b; + } - double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; - int b = value - compare; - return -a < b && a > b; + /// + /// Checks whether two values are close in value which have a precision point. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the values are close; otherwise, false. + /// + public static bool IsCloseTo(this double? value, double? compare) + { + if (!value.HasValue || !compare.HasValue) + { + return false; } - /// - /// Checks whether two values are close in value which have a precision point. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the values are close; otherwise, false. - /// - public static bool IsCloseTo(this double value, double compare) + if (Math.Abs(value.Value - compare.Value) < Epsilon) { - if (Math.Abs(value - compare) < Epsilon) - { - return true; - } - - double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; - double b = value - compare; - return -a < b && a > b; + return true; } - /// - /// Checks whether two values are close in value which have a precision point. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the values are close; otherwise, false. - /// - public static bool IsCloseTo(this double? value, double? compare) + double a = (Math.Abs(value.Value) + Math.Abs(compare.Value) + 10.0) * Epsilon; + double? b = value - compare; + return -a < b && a > b; + } + + /// + /// Checks whether two values are close in value which have a precision point. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the values are close; otherwise, false. + /// + public static bool IsCloseTo(this float value, float compare) + { + if (Math.Abs(value - compare) < Epsilon) { - if (!value.HasValue || !compare.HasValue) - { - return false; - } + return true; + } - if (Math.Abs(value.Value - compare.Value) < Epsilon) - { - return true; - } + double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; + float b = value - compare; + return -a < b && a > b; + } - double a = (Math.Abs(value.Value) + Math.Abs(compare.Value) + 10.0) * Epsilon; - double? b = value - compare; - return -a < b && a > b; + /// + /// Checks whether two values are close in value which have a precision point. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the values are close; otherwise, false. + /// + public static bool IsCloseTo(this float? value, float? compare) + { + if (!value.HasValue || !compare.HasValue) + { + return false; } - /// - /// Checks whether two values are close in value which have a precision point. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the values are close; otherwise, false. - /// - public static bool IsCloseTo(this float value, float compare) + if (Math.Abs(value.Value - compare.Value) < Epsilon) { - if (Math.Abs(value - compare) < Epsilon) - { - return true; - } - - double a = (Math.Abs(value) + Math.Abs(compare) + 10.0) * Epsilon; - float b = value - compare; - return -a < b && a > b; + return true; } - /// - /// Checks whether two values are close in value which have a precision point. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the values are close; otherwise, false. - /// - public static bool IsCloseTo(this float? value, float? compare) - { - if (!value.HasValue || !compare.HasValue) - { - return false; - } + double a = (Math.Abs(value.Value) + Math.Abs(compare.Value) + 10.0) * Epsilon; + float? b = value - compare; + return -a < b && a > b; + } - if (Math.Abs(value.Value - compare.Value) < Epsilon) - { - return true; - } + /// + /// Checks whether a value is significantly greater than another. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the first value is greater than the second; otherwise, false. + /// + public static bool IsGreaterThan(this double value, double compare) + { + return value > compare && !value.IsCloseTo(compare); + } - double a = (Math.Abs(value.Value) + Math.Abs(compare.Value) + 10.0) * Epsilon; - float? b = value - compare; - return -a < b && a > b; - } + /// + /// Checks whether a value is significantly less than another. + /// + /// + /// The first value. + /// + /// + /// The second value. + /// + /// + /// True if the first value is less than the second; otherwise, false. + /// + public static bool IsLessThan(this double value, double compare) + { + return value < compare && !value.IsCloseTo(compare); + } - /// - /// Checks whether a value is significantly greater than another. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the first value is greater than the second; otherwise, false. - /// - public static bool IsGreaterThan(this double value, double compare) + /// + /// Checks whether a value is within a range. + /// + /// The value to check is in range. + /// The lower range band. + /// The upper range band. + /// True if the value is in the range; otherwise, false. + /// Thrown if the range is invalid. + public static bool IsInRange(this int value, int lower, int upper) + { + if (lower > upper) { - return value > compare && !value.IsCloseTo(compare); + throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); } - /// - /// Checks whether a value is significantly less than another. - /// - /// - /// The first value. - /// - /// - /// The second value. - /// - /// - /// True if the first value is less than the second; otherwise, false. - /// - public static bool IsLessThan(this double value, double compare) - { - return value < compare && !value.IsCloseTo(compare); - } + return value >= lower && value <= upper; + } - /// - /// Checks whether a value is within a range. - /// - /// The value to check is in range. - /// The lower range band. - /// The upper range band. - /// True if the value is in the range; otherwise, false. - /// Thrown if the range is invalid. - public static bool IsInRange(this int value, int lower, int upper) + /// + /// Checks whether a value is within a range. + /// + /// The value to check is in range. + /// The lower range band. + /// The upper range band. + /// True if the value is in the range; otherwise, false. + /// Thrown if the range is invalid. + public static bool IsInRange(this float value, float lower, float upper) + { + if (lower > upper) { - if (lower > upper) - { - throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); - } - - return value >= lower && value <= upper; + throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); } - /// - /// Checks whether a value is within a range. - /// - /// The value to check is in range. - /// The lower range band. - /// The upper range band. - /// True if the value is in the range; otherwise, false. - /// Thrown if the range is invalid. - public static bool IsInRange(this float value, float lower, float upper) - { - if (lower > upper) - { - throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); - } - - return value >= lower && value <= upper; - } + return value >= lower && value <= upper; + } - /// - /// Checks whether a value is within a range. - /// - /// The value to check is in range. - /// The lower range band. - /// The upper range band. - /// True if the value is in the range; otherwise, false. - /// Thrown if the range is invalid. - public static bool IsInRange(this double value, double lower, double upper) + /// + /// Checks whether a value is within a range. + /// + /// The value to check is in range. + /// The lower range band. + /// The upper range band. + /// True if the value is in the range; otherwise, false. + /// Thrown if the range is invalid. + public static bool IsInRange(this double value, double lower, double upper) + { + if (lower > upper) { - if (lower > upper) - { - throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); - } - - return value >= lower && value <= upper; + throw new InvalidRangeException($"Lower value, {lower}, must be less than upper value, {upper}"); } + + return value >= lower && value <= upper; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Extensions/StringExtensions.cs b/src/MADE.Data.Validation/Extensions/StringExtensions.cs index 7815c96f..3aaddf35 100644 --- a/src/MADE.Data.Validation/Extensions/StringExtensions.cs +++ b/src/MADE.Data.Validation/Extensions/StringExtensions.cs @@ -1,139 +1,138 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Extensions +using System.Globalization; +using System.Text.RegularExpressions; + +namespace MADE.Data.Validation.Extensions; + +/// +/// Defines a collection of extensions for string values. +/// +public static class StringExtensions { - using System.Globalization; - using System.Text.RegularExpressions; + /// + /// Indicates whether a specified string is null, empty, or consists only of white-space characters. + /// + /// The string to test. + /// + /// True if the parameter is null or empty, or if consists exclusively of white-space characters. + /// + public static bool IsNullOrWhiteSpace(this string value) + { + return string.IsNullOrWhiteSpace(value); + } /// - /// Defines a collection of extensions for string values. + /// Checks whether a phrase contains a specified value using a comparison option. /// - public static class StringExtensions + /// + /// The phrase to check. + /// + /// + /// The value to find. + /// + /// + /// The compare option. + /// + /// + /// True if the phrase contains the value; otherwise, false. + /// + public static bool Contains(this string phrase, string value, CompareOptions compareOption) { - /// - /// Indicates whether a specified string is null, empty, or consists only of white-space characters. - /// - /// The string to test. - /// - /// True if the parameter is null or empty, or if consists exclusively of white-space characters. - /// - public static bool IsNullOrWhiteSpace(this string value) - { - return string.IsNullOrWhiteSpace(value); - } + return CultureInfo.CurrentCulture.CompareInfo.IndexOf(phrase, value, compareOption) >= 0; + } - /// - /// Checks whether a phrase contains a specified value using a comparison option. - /// - /// - /// The phrase to check. - /// - /// - /// The value to find. - /// - /// - /// The compare option. - /// - /// - /// True if the phrase contains the value; otherwise, false. - /// - public static bool Contains(this string phrase, string value, CompareOptions compareOption) + /// + /// Compares a string value against a wildcard pattern, similar to the Visual Basic like operator. + /// + /// + /// An example of this in use comparing strings with * wildcard pattern. + /// + /// // result is true + /// bool result = "MyValue".IsLike("My*"); + /// // result is false + /// result = "MyValue".IsLike("Hello"); + /// + /// + /// The value to compare is like. + /// The wildcard like pattern to match on. + /// True if the value is like the pattern; otherwise, false. + /// Throw if a Regex time-out occurred. + public static bool IsLike(this string value, string likePattern) + { + if (value.IsNullOrWhiteSpace() || likePattern.IsNullOrWhiteSpace()) { - return CultureInfo.CurrentCulture.CompareInfo.IndexOf(phrase, value, compareOption) >= 0; + return false; } - /// - /// Compares a string value against a wildcard pattern, similar to the Visual Basic like operator. - /// - /// - /// An example of this in use comparing strings with * wildcard pattern. - /// - /// // result is true - /// bool result = "MyValue".IsLike("My*"); - /// // result is false - /// result = "MyValue".IsLike("Hello"); - /// - /// - /// The value to compare is like. - /// The wildcard like pattern to match on. - /// True if the value is like the pattern; otherwise, false. - /// Throw if a Regex time-out occurred. - public static bool IsLike(this string value, string likePattern) - { - if (value.IsNullOrWhiteSpace() || likePattern.IsNullOrWhiteSpace()) - { - return false; - } + // Escape any special characters in pattern + var regex = "^" + Regex.Escape(likePattern) + "$"; - // Escape any special characters in pattern - var regex = "^" + Regex.Escape(likePattern) + "$"; + // Replace wildcard characters with regular expression equivalents + regex = regex.Replace(@"\[!", "[^") + .Replace(@"\[", "[") + .Replace(@"\]", "]") + .Replace(@"\?", ".") + .Replace(@"\*", ".*") + .Replace(@"\#", @"\d"); - // Replace wildcard characters with regular expression equivalents - regex = regex.Replace(@"\[!", "[^") - .Replace(@"\[", "[") - .Replace(@"\]", "]") - .Replace(@"\?", ".") - .Replace(@"\*", ".*") - .Replace(@"\#", @"\d"); - - return Regex.IsMatch(value, regex); - } + return Regex.IsMatch(value, regex); + } - /// - /// Checks whether a string value is an integer. - /// - /// - /// The value to check. - /// - /// - /// True if safely parses to an integer; otherwise, false. - /// - public static bool IsInt(this string value) - { - return int.TryParse(value, out int _); - } + /// + /// Checks whether a string value is an integer. + /// + /// + /// The value to check. + /// + /// + /// True if safely parses to an integer; otherwise, false. + /// + public static bool IsInt(this string value) + { + return int.TryParse(value, out int _); + } - /// - /// Checks whether a string value is a double. - /// - /// - /// The value to check. - /// - /// - /// True if safely parses to a double; otherwise, false. - /// - public static bool IsDouble(this string value) - { - return double.TryParse(value, out double _); - } + /// + /// Checks whether a string value is a double. + /// + /// + /// The value to check. + /// + /// + /// True if safely parses to a double; otherwise, false. + /// + public static bool IsDouble(this string value) + { + return double.TryParse(value, out double _); + } - /// - /// Checks whether a string value is a boolean. - /// - /// - /// The value to check. - /// - /// - /// True if safely parses to a boolean; otherwise, false. - /// - public static bool IsBoolean(this string value) - { - return bool.TryParse(value, out bool _); - } + /// + /// Checks whether a string value is a boolean. + /// + /// + /// The value to check. + /// + /// + /// True if safely parses to a boolean; otherwise, false. + /// + public static bool IsBoolean(this string value) + { + return bool.TryParse(value, out bool _); + } - /// - /// Checks whether a string value is a float. - /// - /// - /// The value to check. - /// - /// - /// True if safely parses to a float; otherwise, false. - /// - public static bool IsFloat(this string value) - { - return float.TryParse(value, out float _); - } + /// + /// Checks whether a string value is a float. + /// + /// + /// The value to check. + /// + /// + /// True if safely parses to a float; otherwise, false. + /// + public static bool IsFloat(this string value) + { + return float.TryParse(value, out float _); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/IValidator.cs b/src/MADE.Data.Validation/IValidator.cs index 2c799b37..05e62803 100644 --- a/src/MADE.Data.Validation/IValidator.cs +++ b/src/MADE.Data.Validation/IValidator.cs @@ -1,37 +1,36 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation +namespace MADE.Data.Validation; + +/// +/// Defines an interface for a data validator. +/// +public interface IValidator { /// - /// Defines an interface for a data validator. + /// Gets or sets the key associated with the validator. /// - public interface IValidator - { - /// - /// Gets or sets the key associated with the validator. - /// - string Key { get; set; } + string Key { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - string FeedbackMessage { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + string FeedbackMessage { get; set; } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - void Validate(object value); - } -} \ No newline at end of file + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + void Validate(object value); +} diff --git a/src/MADE.Data.Validation/IValidatorCollection.cs b/src/MADE.Data.Validation/IValidatorCollection.cs index 77ce3c75..ee08fd61 100644 --- a/src/MADE.Data.Validation/IValidatorCollection.cs +++ b/src/MADE.Data.Validation/IValidatorCollection.cs @@ -1,35 +1,34 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation -{ - using System.Collections; - using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; + +namespace MADE.Data.Validation; +/// +/// Defines an interface for a collection of validators. +/// +public interface IValidatorCollection : IList +{ /// - /// Defines an interface for a collection of validators. + /// Gets or sets a value indicating whether the data provided is in an invalid state. /// - public interface IValidatorCollection : IList - { - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - bool IsInvalid { get; set; } + bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + bool IsDirty { get; set; } - /// - /// Gets the validator feedback messages for ones which are invalid. - /// - IEnumerable FeedbackMessages { get; } + /// + /// Gets the validator feedback messages for ones which are invalid. + /// + IEnumerable FeedbackMessages { get; } - /// - /// Executes data validation on the provided against the validators provided. - /// - /// The value to be validated. - void Validate(object value); - } -} \ No newline at end of file + /// + /// Executes data validation on the provided against the validators provided. + /// + /// The value to be validated. + void Validate(object value); +} diff --git a/src/MADE.Data.Validation/InputValidatedEventArgs.cs b/src/MADE.Data.Validation/InputValidatedEventArgs.cs index b708737b..091630f5 100644 --- a/src/MADE.Data.Validation/InputValidatedEventArgs.cs +++ b/src/MADE.Data.Validation/InputValidatedEventArgs.cs @@ -1,49 +1,48 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation -{ - using System; +using System; + +namespace MADE.Data.Validation; +/// +/// Defines an event argument for an input validated request. +/// +public class InputValidatedEventArgs : EventArgs +{ /// - /// Defines an event argument for an input validated request. + /// Initializes a new instance of the class. /// - public class InputValidatedEventArgs : EventArgs + /// + /// A value indicating whether the input is invalid. + /// + public InputValidatedEventArgs(bool isInvalid) + : this(isInvalid, true) { - /// - /// Initializes a new instance of the class. - /// - /// - /// A value indicating whether the input is invalid. - /// - public InputValidatedEventArgs(bool isInvalid) - : this(isInvalid, true) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// A value indicating whether the input is invalid. - /// - /// - /// A value indicating whether the input is dirty. - /// - public InputValidatedEventArgs(bool isInvalid, bool isDirty) - { - this.IsInvalid = isInvalid; - this.IsDirty = isDirty; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// A value indicating whether the input is invalid. + /// + /// + /// A value indicating whether the input is dirty. + /// + public InputValidatedEventArgs(bool isInvalid, bool isDirty) + { + this.IsInvalid = isInvalid; + this.IsDirty = isDirty; + } - /// - /// Gets a value indicating whether the input is dirty. - /// - public bool IsDirty { get; } + /// + /// Gets a value indicating whether the input is dirty. + /// + public bool IsDirty { get; } - /// - /// Gets a value indicating whether the input is invalid. - /// - public bool IsInvalid { get; } - } -} \ No newline at end of file + /// + /// Gets a value indicating whether the input is invalid. + /// + public bool IsInvalid { get; } +} diff --git a/src/MADE.Data.Validation/InputValidatedEventHandler.cs b/src/MADE.Data.Validation/InputValidatedEventHandler.cs index 8a205dc7..5e0fba7f 100644 --- a/src/MADE.Data.Validation/InputValidatedEventHandler.cs +++ b/src/MADE.Data.Validation/InputValidatedEventHandler.cs @@ -1,16 +1,15 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation -{ - /// - /// Defines a delegate for an event handler for observing when an input is validated. - /// - /// - /// The sender. - /// - /// - /// The input validated event argument. - /// - public delegate void InputValidatedEventHandler(object sender, InputValidatedEventArgs args); -} \ No newline at end of file +namespace MADE.Data.Validation; + +/// +/// Defines a delegate for an event handler for observing when an input is validated. +/// +/// +/// The sender. +/// +/// +/// The input validated event argument. +/// +public delegate void InputValidatedEventHandler(object sender, InputValidatedEventArgs args); diff --git a/src/MADE.Data.Validation/Strings/Resources.Designer.cs b/src/MADE.Data.Validation/Strings/Resources.Designer.cs index 24ea2ea2..4ed0c5d4 100644 --- a/src/MADE.Data.Validation/Strings/Resources.Designer.cs +++ b/src/MADE.Data.Validation/Strings/Resources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -8,200 +8,199 @@ // //------------------------------------------------------------------------------ -namespace MADE.Data.Validation.Strings { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - public class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MADE.Data.Validation.Strings.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; +using System; + +namespace MADE.Data.Validation.Strings; + +/// +/// A strongly-typed resource class, for looking up localized strings, etc. +/// +// This class was auto-generated by the StronglyTypedResourceBuilder +// class via a tool like ResGen or Visual Studio. +// To add or remove a member, edit your .ResX file then rerun ResGen +// with the /str option, or rebuild your VS project. +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] +public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MADE.Data.Validation.Strings.Resources", typeof(Resources).Assembly); + resourceMan = temp; } + return resourceMan; } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - public static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; } - - /// - /// Looks up a localized string similar to The value must only contain letters or numbers.. - /// - public static string AlphaNumericValidator_FeedbackMessage { - get { - return ResourceManager.GetString("AlphaNumericValidator_FeedbackMessage", resourceCulture); - } + set { + resourceCulture = value; } - - /// - /// Looks up a localized string similar to The value must only contain letters.. - /// - public static string AlphaValidator_FeedbackMessage { - get { - return ResourceManager.GetString("AlphaValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must only contain letters or numbers.. + /// + public static string AlphaNumericValidator_FeedbackMessage { + get { + return ResourceManager.GetString("AlphaNumericValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid base64 string.. - /// - public static string Base64Validator_FeedbackMessage { - get { - return ResourceManager.GetString("Base64Validator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must only contain letters.. + /// + public static string AlphaValidator_FeedbackMessage { + get { + return ResourceManager.GetString("AlphaValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be between {0} and {1}.. - /// - public static string BetweenValidator_FeedbackMessage { - get { - return ResourceManager.GetString("BetweenValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be a valid base64 string.. + /// + public static string Base64Validator_FeedbackMessage { + get { + return ResourceManager.GetString("Base64Validator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid email address, e.g. test@example.com.. - /// - public static string EmailValidator_FeedbackMessage { - get { - return ResourceManager.GetString("EmailValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be between {0} and {1}.. + /// + public static string BetweenValidator_FeedbackMessage { + get { + return ResourceManager.GetString("BetweenValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid GUID.. - /// - public static string GuidValidator_FeedbackMessage { - get { - return ResourceManager.GetString("GuidValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be a valid email address, e.g. test@example.com.. + /// + public static string EmailValidator_FeedbackMessage { + get { + return ResourceManager.GetString("EmailValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid IP address, e.g. 192.168.0.1.. - /// - public static string IpAddressValidator_FeedbackMessage { - get { - return ResourceManager.GetString("IpAddressValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be a valid GUID.. + /// + public static string GuidValidator_FeedbackMessage { + get { + return ResourceManager.GetString("GuidValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid MAC address, e.g. 00:11:22:33:44:55.. - /// - public static string MacAddressValidator_FeedbackMessage { - get { - return ResourceManager.GetString("MacAddressValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be a valid IP address, e.g. 192.168.0.1.. + /// + public static string IpAddressValidator_FeedbackMessage { + get { + return ResourceManager.GetString("IpAddressValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The length must be less than {0}.. - /// - public static string MaxLengthValidator_FeedbackMessage { - get { - return ResourceManager.GetString("MaxLengthValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be a valid MAC address, e.g. 00:11:22:33:44:55.. + /// + public static string MacAddressValidator_FeedbackMessage { + get { + return ResourceManager.GetString("MacAddressValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be less than {0}.. - /// - public static string MaxValueValidator_FeedbackMessage { - get { - return ResourceManager.GetString("MaxValueValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The length must be less than {0}.. + /// + public static string MaxLengthValidator_FeedbackMessage { + get { + return ResourceManager.GetString("MaxLengthValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The length must be greater than {0}.. - /// - public static string MinLengthValidator_FeedbackMessage { - get { - return ResourceManager.GetString("MinLengthValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be less than {0}.. + /// + public static string MaxValueValidator_FeedbackMessage { + get { + return ResourceManager.GetString("MaxValueValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be greater than {0}.. - /// - public static string MinValueValidator_FeedbackMessage { - get { - return ResourceManager.GetString("MinValueValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The length must be greater than {0}.. + /// + public static string MinLengthValidator_FeedbackMessage { + get { + return ResourceManager.GetString("MinLengthValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value is not valid.. - /// - public static string PredicateValidator_FeedbackMessage { - get { - return ResourceManager.GetString("PredicateValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value must be greater than {0}.. + /// + public static string MinValueValidator_FeedbackMessage { + get { + return ResourceManager.GetString("MinValueValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value does not match the valid mask.. - /// - public static string RegexValidator_FeedbackMessage { - get { - return ResourceManager.GetString("RegexValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value is not valid.. + /// + public static string PredicateValidator_FeedbackMessage { + get { + return ResourceManager.GetString("PredicateValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to A value is required.. - /// - public static string RequiredValidator_FeedbackMessage { - get { - return ResourceManager.GetString("RequiredValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to The value does not match the valid mask.. + /// + public static string RegexValidator_FeedbackMessage { + get { + return ResourceManager.GetString("RegexValidator_FeedbackMessage", resourceCulture); } - - /// - /// Looks up a localized string similar to The value must be a valid URL, e.g. https://example.com.. - /// - public static string UrlValidator_FeedbackMessage { - get { - return ResourceManager.GetString("UrlValidator_FeedbackMessage", resourceCulture); - } + } + + /// + /// Looks up a localized string similar to A value is required.. + /// + public static string RequiredValidator_FeedbackMessage { + get { + return ResourceManager.GetString("RequiredValidator_FeedbackMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The value must be a valid URL, e.g. https://example.com.. + /// + public static string UrlValidator_FeedbackMessage { + get { + return ResourceManager.GetString("UrlValidator_FeedbackMessage", resourceCulture); } } } diff --git a/src/MADE.Data.Validation/ValidatorCollection.cs b/src/MADE.Data.Validation/ValidatorCollection.cs index bbe57e67..48a68655 100644 --- a/src/MADE.Data.Validation/ValidatorCollection.cs +++ b/src/MADE.Data.Validation/ValidatorCollection.cs @@ -1,77 +1,76 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation -{ - using System; - using System.Collections.Generic; - using System.Linq; - using MADE.Data.Validation.Extensions; +using System; +using System.Collections.Generic; +using System.Linq; +using MADE.Data.Validation.Extensions; - /// - /// Defines a list of objects that can be accessed by index. - /// - public class ValidatorCollection : List, IValidatorCollection +namespace MADE.Data.Validation; + +/// +/// Defines a list of objects that can be accessed by index. +/// +public class ValidatorCollection : List, IValidatorCollection +{ + /// Initializes a new instance of the class that is empty and has the default initial capacity. + public ValidatorCollection() { - /// Initializes a new instance of the class that is empty and has the default initial capacity. - public ValidatorCollection() - { - } + } - /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. - /// The collection whose elements are copied to the new list. - /// collection is null. - public ValidatorCollection(IEnumerable collection) - : base(collection) - { - } + /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. + /// The collection whose elements are copied to the new list. + /// collection is null. + public ValidatorCollection(IEnumerable collection) + : base(collection) + { + } - /// - /// Initializes a new instance of the class that is empty and has the specified initial capacity. - /// The number of elements that the new list can initially store. - /// capacity is less than 0. - public ValidatorCollection(int capacity) - : base(capacity) - { - } + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// The number of elements that the new list can initially store. + /// capacity is less than 0. + public ValidatorCollection(int capacity) + : base(capacity) + { + } - /// - /// Occurs when the input value is validated against the collection of validators. - /// - public event InputValidatedEventHandler Validated; + /// + /// Occurs when the input value is validated against the collection of validators. + /// + public event InputValidatedEventHandler Validated; - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid - { - get => this.Any(validator => validator.IsInvalid); - set => this.ForEach(validator => validator.IsInvalid = value); - } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid + { + get => this.Any(validator => validator.IsInvalid); + set => this.ForEach(validator => validator.IsInvalid = value); + } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty - { - get => this.Any(validator => validator.IsDirty); - set => this.ForEach(validator => validator.IsDirty = value); - } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty + { + get => this.Any(validator => validator.IsDirty); + set => this.ForEach(validator => validator.IsDirty = value); + } - /// - /// Gets the validator feedback messages for ones which are invalid. - /// - public IEnumerable FeedbackMessages => this.Where(x => x.IsInvalid).Select(x => x.FeedbackMessage).Where(x => !x.IsNullOrWhiteSpace()); + /// + /// Gets the validator feedback messages for ones which are invalid. + /// + public IEnumerable FeedbackMessages => this.Where(x => x.IsInvalid).Select(x => x.FeedbackMessage).Where(x => !x.IsNullOrWhiteSpace()); - /// - /// Executes data validation on the provided against the validators provided. - /// - /// The value to be validated. - /// Potentially thrown by the delegate callback. - public void Validate(object value) - { - this.ForEach(validator => validator.Validate(value)); - this.Validated?.Invoke(this, new InputValidatedEventArgs(this.IsInvalid, this.IsDirty)); - } + /// + /// Executes data validation on the provided against the validators provided. + /// + /// The value to be validated. + /// Potentially thrown by the delegate callback. + public void Validate(object value) + { + this.ForEach(validator => validator.Validate(value)); + this.Validated?.Invoke(this, new InputValidatedEventArgs(this.IsInvalid, this.IsDirty)); } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs b/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs index 74462c10..68b29366 100644 --- a/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs +++ b/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs @@ -1,34 +1,33 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value contains alphanumeric characters. +/// +public class AlphaNumericValidator : RegexValidator { - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value contains alphanumeric characters. + /// Initializes a new instance of the class with the expected RegEx pattern. /// - public class AlphaNumericValidator : RegexValidator + public AlphaNumericValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class with the expected RegEx pattern. - /// - public AlphaNumericValidator() - { - this.Key = nameof(AlphaNumericValidator); - this.Pattern = "^[a-zA-Z0-9]*$"; - } + this.Key = nameof(AlphaNumericValidator); + this.Pattern = "^[a-zA-Z0-9]*$"; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public override string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.AlphaNumericValidator_FeedbackMessage : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public override string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.AlphaNumericValidator_FeedbackMessage : this.feedbackMessage; + set => this.feedbackMessage = value; } } diff --git a/src/MADE.Data.Validation/Validators/AlphaValidator.cs b/src/MADE.Data.Validation/Validators/AlphaValidator.cs index bc234efb..ba91a924 100644 --- a/src/MADE.Data.Validation/Validators/AlphaValidator.cs +++ b/src/MADE.Data.Validation/Validators/AlphaValidator.cs @@ -1,34 +1,33 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value contains alpha characters. +/// +public class AlphaValidator : RegexValidator { - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value contains alpha characters. + /// Initializes a new instance of the class with the expected RegEx pattern. /// - public class AlphaValidator : RegexValidator + public AlphaValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class with the expected RegEx pattern. - /// - public AlphaValidator() - { - this.Key = nameof(AlphaValidator); - this.Pattern = "^[a-zA-Z]*$"; - } + this.Key = nameof(AlphaValidator); + this.Pattern = "^[a-zA-Z]*$"; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public override string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.AlphaValidator_FeedbackMessage : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public override string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.AlphaValidator_FeedbackMessage : this.feedbackMessage; + set => this.feedbackMessage = value; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/Base64Validator.cs b/src/MADE.Data.Validation/Validators/Base64Validator.cs index 65c8a326..8b44744d 100644 --- a/src/MADE.Data.Validation/Validators/Base64Validator.cs +++ b/src/MADE.Data.Validation/Validators/Base64Validator.cs @@ -1,57 +1,56 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System.Text.RegularExpressions; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is a valid base64 value. +/// +public class Base64Validator : RegexValidator { - using System.Text.RegularExpressions; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is a valid base64 value. + /// Initializes a new instance of the class. /// - public class Base64Validator : RegexValidator + public Base64Validator() { - private string feedbackMessage; + this.Key = nameof(Base64Validator); + this.Pattern = @"^[a-zA-Z0-9\+/]*={0,3}$"; + } - /// - /// Initializes a new instance of the class. - /// - public Base64Validator() - { - this.Key = nameof(Base64Validator); - this.Pattern = @"^[a-zA-Z0-9\+/]*={0,3}$"; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public override string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.Base64Validator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public override string FeedbackMessage + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + /// Thrown if a Regex time-out occurred. + public override void Validate(object value) + { + var stringValue = value?.ToString() ?? string.Empty; + if (stringValue.Length % 4 != 0) { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.Base64Validator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; + this.IsInvalid = true; } - - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - /// Thrown if a Regex time-out occurred. - public override void Validate(object value) + else { - var stringValue = value?.ToString() ?? string.Empty; - if (stringValue.Length % 4 != 0) - { - this.IsInvalid = true; - } - else - { - base.Validate(value); - } - - this.IsDirty = true; + base.Validate(value); } + + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/BetweenValidator.cs b/src/MADE.Data.Validation/Validators/BetweenValidator.cs index 940503e2..ad658890 100644 --- a/src/MADE.Data.Validation/Validators/BetweenValidator.cs +++ b/src/MADE.Data.Validation/Validators/BetweenValidator.cs @@ -1,103 +1,102 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is within a minimum and maximum range. +/// +public class BetweenValidator : IValidator { - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is within a minimum and maximum range. + /// Initializes a new instance of the class. /// - public class BetweenValidator : IValidator + public BetweenValidator() { - private string feedbackMessage; + } - /// - /// Initializes a new instance of the class. - /// - public BetweenValidator() - { - } + /// + /// Initializes a new instance of the class with a minimum and maximum range. + /// + /// The minimum value within the range. + /// The maximum value within the range. + public BetweenValidator(IComparable min, IComparable max) + { + this.Min = min; + this.Max = max; + } - /// - /// Initializes a new instance of the class with a minimum and maximum range. - /// - /// The minimum value within the range. - /// The maximum value within the range. - public BetweenValidator(IComparable min, IComparable max) - { - this.Min = min; - this.Max = max; - } + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(BetweenValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(BetweenValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? string.Format(Resources.BetweenValidator_FeedbackMessage, this.Min, this.Max) + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? string.Format(Resources.BetweenValidator_FeedbackMessage, this.Min, this.Max) - : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the minimum value within the range. + /// + public IComparable Min { get; set; } - /// - /// Gets or sets the minimum value within the range. - /// - public IComparable Min { get; set; } + /// + /// Gets or sets the maximum value within the range. + /// + public IComparable Max { get; set; } - /// - /// Gets or sets the maximum value within the range. - /// - public IComparable Max { get; set; } + /// + /// Gets or sets a value indicating whether the range is inclusive. + /// + /// + /// By default, the value is true. + /// + public bool Inclusive { get; set; } = true; - /// - /// Gets or sets a value indicating whether the range is inclusive. - /// - /// - /// By default, the value is true. - /// - public bool Inclusive { get; set; } = true; + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid = true; - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + if (value is IComparable comparable) { - bool isInvalid = true; - - if (value is IComparable comparable) + if (this.Inclusive) { - if (this.Inclusive) - { - isInvalid = comparable.IsLessThan(this.Min) || comparable.IsGreaterThan(this.Max); - } - else - { - isInvalid = comparable.IsLessThanOrEqualTo(this.Min) || comparable.IsGreaterThanOrEqualTo(this.Max); - } + isInvalid = comparable.IsLessThan(this.Min) || comparable.IsGreaterThan(this.Max); + } + else + { + isInvalid = comparable.IsLessThanOrEqualTo(this.Min) || comparable.IsGreaterThanOrEqualTo(this.Max); } - - this.IsInvalid = isInvalid; - this.IsDirty = true; } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/EmailValidator.cs b/src/MADE.Data.Validation/Validators/EmailValidator.cs index ac570f4a..14246153 100644 --- a/src/MADE.Data.Validation/Validators/EmailValidator.cs +++ b/src/MADE.Data.Validation/Validators/EmailValidator.cs @@ -1,34 +1,33 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is an email address. +/// +public class EmailValidator : RegexValidator { - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is an email address. + /// Initializes a new instance of the class. /// - public class EmailValidator : RegexValidator + public EmailValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class. - /// - public EmailValidator() - { - this.Key = nameof(EmailValidator); - this.Pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|" + @"([-a-zA-Z0-9!#$%&'*+/=?^_`{|}~]|(? - /// Gets or sets the feedback message to display when is true. - /// - public override string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.EmailValidator_FeedbackMessage : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public override string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.EmailValidator_FeedbackMessage : this.feedbackMessage; + set => this.feedbackMessage = value; } } diff --git a/src/MADE.Data.Validation/Validators/GuidValidator.cs b/src/MADE.Data.Validation/Validators/GuidValidator.cs index 321a100f..1dcf727e 100644 --- a/src/MADE.Data.Validation/Validators/GuidValidator.cs +++ b/src/MADE.Data.Validation/Validators/GuidValidator.cs @@ -1,65 +1,64 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is a . +/// +public class GuidValidator : IValidator { - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is a . + /// Gets or sets the key associated with the validator. /// - public class GuidValidator : IValidator - { - private string feedbackMessage; + public string Key { get; set; } = nameof(GuidValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(GuidValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.GuidValidator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } + + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid; - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage + if (value is Guid) { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.GuidValidator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; + isInvalid = false; } - - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + else { - bool isInvalid; - - if (value is Guid) - { - isInvalid = false; - } - else - { - var stringValue = value?.ToString() ?? string.Empty; - isInvalid = !Guid.TryParse(stringValue, out _); - } - - this.IsInvalid = isInvalid; - this.IsDirty = true; + var stringValue = value?.ToString() ?? string.Empty; + isInvalid = !Guid.TryParse(stringValue, out _); } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/IpAddressValidator.cs b/src/MADE.Data.Validation/Validators/IpAddressValidator.cs index 3fee521e..c84dd01d 100644 --- a/src/MADE.Data.Validation/Validators/IpAddressValidator.cs +++ b/src/MADE.Data.Validation/Validators/IpAddressValidator.cs @@ -1,78 +1,77 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using System.Linq; +using System.Text.RegularExpressions; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is a valid IP address. +/// +public class IpAddressValidator : IValidator { - using System; - using System.Linq; - using System.Text.RegularExpressions; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is a valid IP address. + /// Gets or sets the key associated with the validator. /// - public class IpAddressValidator : IValidator - { - private string feedbackMessage; + public string Key { get; set; } = nameof(IpAddressValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(IpAddressValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.IpAddressValidator_FeedbackMessage : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + /// The array is multidimensional and contains more than elements. + public void Validate(object value) + { + string str = value?.ToString() ?? string.Empty; + + string[] nibbles = str.Split('.'); + this.IsInvalid = nibbles.Length != 4 || !nibbles.All(IsNibbleValid); + this.IsDirty = true; + } + + private static bool IsNibbleValid(string nibble) + { + if (nibble.Length is > 3 or 0) { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.IpAddressValidator_FeedbackMessage : this.feedbackMessage; - set => this.feedbackMessage = value; + return false; } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - /// The array is multidimensional and contains more than elements. - public void Validate(object value) + if (nibble[0] == '0' && nibble != "0") { - string str = value?.ToString() ?? string.Empty; - - string[] nibbles = str.Split('.'); - this.IsInvalid = nibbles.Length != 4 || !nibbles.All(IsNibbleValid); - this.IsDirty = true; + return false; } - private static bool IsNibbleValid(string nibble) + if (!Regex.IsMatch(nibble, @"^\d+$")) { - if (nibble.Length is > 3 or 0) - { - return false; - } - - if (nibble[0] == '0' && nibble != "0") - { - return false; - } - - if (!Regex.IsMatch(nibble, @"^\d+$")) - { - return false; - } - - int.TryParse(nibble, out int numeric); - return numeric is >= 0 and <= 255; + return false; } + + int.TryParse(nibble, out int numeric); + return numeric is >= 0 and <= 255; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/LatitudeValidator.cs b/src/MADE.Data.Validation/Validators/LatitudeValidator.cs index f2c4e0c6..6bbe3840 100644 --- a/src/MADE.Data.Validation/Validators/LatitudeValidator.cs +++ b/src/MADE.Data.Validation/Validators/LatitudeValidator.cs @@ -1,64 +1,63 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators -{ - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; +/// +/// Defines a data validator for ensuring a value is within the valid range for a latitude value. +/// +public class LatitudeValidator : IValidator +{ /// - /// Defines a data validator for ensuring a value is within the valid range for a latitude value. + /// The minimum valid latitude value. /// - public class LatitudeValidator : IValidator - { - /// - /// The minimum valid latitude value. - /// - public const double Min = -90; + public const double Min = -90; - /// - /// The maximum valid latitude value. - /// - public const double Max = 90; + /// + /// The maximum valid latitude value. + /// + public const double Max = 90; - private string feedbackMessage; + private string feedbackMessage; - /// - /// Gets or sets the key associated with the validator. - /// - public virtual string Key { get; set; } = nameof(LatitudeValidator); + /// + /// Gets or sets the key associated with the validator. + /// + public virtual string Key { get; set; } = nameof(LatitudeValidator); - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public virtual string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? string.Format(Resources.BetweenValidator_FeedbackMessage, Min, Max) - : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public virtual string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? string.Format(Resources.BetweenValidator_FeedbackMessage, Min, Max) + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) - { - bool parsed = double.TryParse(value?.ToString() ?? string.Empty, out double latitude); - this.IsInvalid = !parsed || latitude is < Min or > Max; - this.IsDirty = true; - } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool parsed = double.TryParse(value?.ToString() ?? string.Empty, out double latitude); + this.IsInvalid = !parsed || latitude is < Min or > Max; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/LongitudeValidator.cs b/src/MADE.Data.Validation/Validators/LongitudeValidator.cs index 321658cf..48000195 100644 --- a/src/MADE.Data.Validation/Validators/LongitudeValidator.cs +++ b/src/MADE.Data.Validation/Validators/LongitudeValidator.cs @@ -1,64 +1,63 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators -{ - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; +/// +/// Defines a data validator for ensuring a value is within the valid range for a longitude value. +/// +public class LongitudeValidator : IValidator +{ /// - /// Defines a data validator for ensuring a value is within the valid range for a longitude value. + /// The minimum valid longitude value. /// - public class LongitudeValidator : IValidator - { - /// - /// The minimum valid longitude value. - /// - public const double Min = -180; + public const double Min = -180; - /// - /// The maximum valid longitude value. - /// - public const double Max = 180; + /// + /// The maximum valid longitude value. + /// + public const double Max = 180; - private string feedbackMessage; + private string feedbackMessage; - /// - /// Gets or sets the key associated with the validator. - /// - public virtual string Key { get; set; } = nameof(LongitudeValidator); + /// + /// Gets or sets the key associated with the validator. + /// + public virtual string Key { get; set; } = nameof(LongitudeValidator); - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public virtual string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? string.Format(Resources.BetweenValidator_FeedbackMessage, Min, Max) - : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public virtual string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? string.Format(Resources.BetweenValidator_FeedbackMessage, Min, Max) + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) - { - bool parsed = double.TryParse(value?.ToString() ?? string.Empty, out double longitude); - this.IsInvalid = !parsed || longitude is < Min or > Max; - this.IsDirty = true; - } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool parsed = double.TryParse(value?.ToString() ?? string.Empty, out double longitude); + this.IsInvalid = !parsed || longitude is < Min or > Max; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/MacAddressValidator.cs b/src/MADE.Data.Validation/Validators/MacAddressValidator.cs index 12e99e10..1cc35c75 100644 --- a/src/MADE.Data.Validation/Validators/MacAddressValidator.cs +++ b/src/MADE.Data.Validation/Validators/MacAddressValidator.cs @@ -1,68 +1,67 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using System.Net.NetworkInformation; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is a valid MAC address. +/// +public class MacAddressValidator : IValidator { - using System; - using System.Net.NetworkInformation; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is a valid MAC address. + /// Gets or sets the key associated with the validator. /// - public class MacAddressValidator : IValidator - { - private string feedbackMessage; + public string Key { get; set; } = nameof(MacAddressValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(MacAddressValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.MacAddressValidator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } + + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + /// The array is multidimensional and contains more than elements. + public void Validate(object value) + { + bool isInvalid; + var stringValue = value?.ToString() ?? string.Empty; - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage + try { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.MacAddressValidator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; + PhysicalAddress newAddress = PhysicalAddress.Parse(stringValue); + isInvalid = PhysicalAddress.None.Equals(newAddress); } - - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - /// The array is multidimensional and contains more than elements. - public void Validate(object value) + catch (FormatException) { - bool isInvalid; - var stringValue = value?.ToString() ?? string.Empty; - - try - { - PhysicalAddress newAddress = PhysicalAddress.Parse(stringValue); - isInvalid = PhysicalAddress.None.Equals(newAddress); - } - catch (FormatException) - { - isInvalid = true; - } - - this.IsInvalid = isInvalid; - this.IsDirty = true; + isInvalid = true; } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs index ed689026..09b1357d 100644 --- a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs @@ -1,81 +1,80 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using System.Collections; +using MADE.Data.Validation; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is less than a maximum length. +/// +public class MaxLengthValidator : IValidator { - using System; - using System.Collections; - using MADE.Data.Validation; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is less than a maximum length. + /// Initializes a new instance of the class. /// - public class MaxLengthValidator : IValidator + public MaxLengthValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class. - /// - public MaxLengthValidator() - { - } + } - /// - /// Initializes a new instance of the class with a maximum value. - /// - /// The maximum value. - public MaxLengthValidator(IComparable max) - { - this.Max = max; - } + /// + /// Initializes a new instance of the class with a maximum value. + /// + /// The maximum value. + public MaxLengthValidator(IComparable max) + { + this.Max = max; + } - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(MaxLengthValidator); + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(MaxLengthValidator); - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MaxLengthValidator_FeedbackMessage, this.Max) : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MaxLengthValidator_FeedbackMessage, this.Max) : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the maximum value. - /// - public IComparable Max { get; set; } + /// + /// Gets or sets the maximum value. + /// + public IComparable Max { get; set; } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid = value switch { - bool isInvalid = value switch - { - string str => str.Length.IsGreaterThan(this.Max), - ICollection col => col.Count.IsGreaterThan(this.Max), - _ => true - }; + string str => str.Length.IsGreaterThan(this.Max), + ICollection col => col.Count.IsGreaterThan(this.Max), + _ => true + }; - this.IsInvalid = isInvalid; - this.IsDirty = true; - } + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs index c7f6aed0..94fb88f9 100644 --- a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs @@ -1,79 +1,78 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is less than a maximum value. +/// +public class MaxValueValidator : IValidator { - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is less than a maximum value. + /// Initializes a new instance of the class. /// - public class MaxValueValidator : IValidator + public MaxValueValidator() { - private string feedbackMessage; + } - /// - /// Initializes a new instance of the class. - /// - public MaxValueValidator() - { - } + /// + /// Initializes a new instance of the class with a maximum value. + /// + /// The maximum value. + public MaxValueValidator(IComparable max) + { + this.Max = max; + } - /// - /// Initializes a new instance of the class with a maximum value. - /// - /// The maximum value. - public MaxValueValidator(IComparable max) - { - this.Max = max; - } + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(MaxValueValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(MaxValueValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MaxValueValidator_FeedbackMessage, this.Max) : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MaxValueValidator_FeedbackMessage, this.Max) : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the minimum value. + /// + public IComparable Max { get; set; } - /// - /// Gets or sets the minimum value. - /// - public IComparable Max { get; set; } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid = true; - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + if (value is IComparable comparable) { - bool isInvalid = true; - - if (value is IComparable comparable) - { - isInvalid = comparable.IsGreaterThan(this.Max); - } - - this.IsInvalid = isInvalid; - this.IsDirty = true; + isInvalid = comparable.IsGreaterThan(this.Max); } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } } diff --git a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs index 6e3bc702..c473ce09 100644 --- a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs @@ -1,81 +1,80 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using System.Collections; +using MADE.Data.Validation; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is greater than a minimum length. +/// +public class MinLengthValidator : IValidator { - using System; - using System.Collections; - using MADE.Data.Validation; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is greater than a minimum length. + /// Initializes a new instance of the class. /// - public class MinLengthValidator : IValidator + public MinLengthValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class. - /// - public MinLengthValidator() - { - } + } - /// - /// Initializes a new instance of the class with a minimum value. - /// - /// The maximum value. - public MinLengthValidator(IComparable min) - { - this.Min = min; - } + /// + /// Initializes a new instance of the class with a minimum value. + /// + /// The maximum value. + public MinLengthValidator(IComparable min) + { + this.Min = min; + } - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(MinLengthValidator); + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(MinLengthValidator); - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MinLengthValidator_FeedbackMessage, this.Min) : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MinLengthValidator_FeedbackMessage, this.Min) : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the minimum value. - /// - public IComparable Min { get; set; } + /// + /// Gets or sets the minimum value. + /// + public IComparable Min { get; set; } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid = value switch { - bool isInvalid = value switch - { - string str => str.Length.IsLessThan(this.Min), - ICollection col => col.Count.IsLessThan(this.Min), - _ => true - }; + string str => str.Length.IsLessThan(this.Min), + ICollection col => col.Count.IsLessThan(this.Min), + _ => true + }; - this.IsInvalid = isInvalid; - this.IsDirty = true; - } + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/MinValueValidator.cs b/src/MADE.Data.Validation/Validators/MinValueValidator.cs index 386558f5..d127c715 100644 --- a/src/MADE.Data.Validation/Validators/MinValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinValueValidator.cs @@ -1,79 +1,78 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is greater than a minimum value. +/// +public class MinValueValidator : IValidator { - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is greater than a minimum value. + /// Initializes a new instance of the class. /// - public class MinValueValidator : IValidator + public MinValueValidator() { - private string feedbackMessage; + } - /// - /// Initializes a new instance of the class. - /// - public MinValueValidator() - { - } + /// + /// Initializes a new instance of the class with a minimum value. + /// + /// The minimum value. + public MinValueValidator(IComparable min) + { + this.Min = min; + } - /// - /// Initializes a new instance of the class with a minimum value. - /// - /// The minimum value. - public MinValueValidator(IComparable min) - { - this.Min = min; - } + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(MinValueValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(MinValueValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MinValueValidator_FeedbackMessage, this.Min) : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? string.Format(Resources.MinValueValidator_FeedbackMessage, this.Min) : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the minimum value. + /// + public IComparable Min { get; set; } - /// - /// Gets or sets the minimum value. - /// - public IComparable Min { get; set; } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid = true; - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + if (value is IComparable comparable) { - bool isInvalid = true; - - if (value is IComparable comparable) - { - isInvalid = comparable.IsLessThan(this.Min); - } - - this.IsInvalid = isInvalid; - this.IsDirty = true; + isInvalid = comparable.IsLessThan(this.Min); } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } } diff --git a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs index 57a57384..652ea4c7 100644 --- a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs +++ b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs @@ -1,78 +1,77 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using System.Text.RegularExpressions; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a generic data validator that performs custom validation logic based on the value. +/// +/// The type of value being validated. +public class PredicateValidator : IValidator { - using System; - using System.Text.RegularExpressions; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a generic data validator that performs custom validation logic based on the value. + /// Initializes a new instance of the class. /// - /// The type of value being validated. - public class PredicateValidator : IValidator + public PredicateValidator() { - private string feedbackMessage; - - /// - /// Initializes a new instance of the class. - /// - public PredicateValidator() - { - } + } - /// - /// Initializes a new instance of the class with the custom validation logic. - /// - /// The logic for performing validation on the value. - public PredicateValidator(Func predicate) - { - this.Predicate = predicate; - } + /// + /// Initializes a new instance of the class with the custom validation logic. + /// + /// The logic for performing validation on the value. + public PredicateValidator(Func predicate) + { + this.Predicate = predicate; + } - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(PredicateValidator); + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(PredicateValidator); - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public virtual string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.PredicateValidator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public virtual string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.PredicateValidator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Gets or sets the logic for performing validation on the value. - /// - public Func Predicate { get; set; } + /// + /// Gets or sets the logic for performing validation on the value. + /// + public Func Predicate { get; set; } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - /// Thrown if a Regex time-out occurred. - public void Validate(object value) - { - var typedValue = (T)value; - this.IsInvalid = !this.Predicate(typedValue); - this.IsDirty = true; - } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + /// Thrown if a Regex time-out occurred. + public void Validate(object value) + { + var typedValue = (T)value; + this.IsInvalid = !this.Predicate(typedValue); + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/RegexValidator.cs b/src/MADE.Data.Validation/Validators/RegexValidator.cs index 31164d8f..88921129 100644 --- a/src/MADE.Data.Validation/Validators/RegexValidator.cs +++ b/src/MADE.Data.Validation/Validators/RegexValidator.cs @@ -1,58 +1,57 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System.Text.RegularExpressions; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a generic regular expression data validator. +/// +public class RegexValidator : IValidator { - using System.Text.RegularExpressions; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; + + /// + /// Gets or sets the key associated with the validator. + /// + public string Key { get; set; } = nameof(RegexValidator); + + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } + + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } + + /// + /// Gets or sets the feedback message to display when is true. + /// + public virtual string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.RegexValidator_FeedbackMessage : this.feedbackMessage; + set => this.feedbackMessage = value; + } + + /// + /// Gets or sets the RegEx pattern to match on. + /// + public string Pattern { get; set; } /// - /// Defines a generic regular expression data validator. + /// Executes data validation on the provided . /// - public class RegexValidator : IValidator + /// The value to be validated. + /// Thrown if a Regex time-out occurred. + public virtual void Validate(object value) { - private string feedbackMessage; - - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(RegexValidator); - - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } - - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } - - /// - /// Gets or sets the feedback message to display when is true. - /// - public virtual string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() ? Resources.RegexValidator_FeedbackMessage : this.feedbackMessage; - set => this.feedbackMessage = value; - } - - /// - /// Gets or sets the RegEx pattern to match on. - /// - public string Pattern { get; set; } - - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - /// Thrown if a Regex time-out occurred. - public virtual void Validate(object value) - { - string str = value?.ToString() ?? string.Empty; - this.IsInvalid = !Regex.IsMatch(str, this.Pattern); - this.IsDirty = true; - } + string str = value?.ToString() ?? string.Empty; + this.IsInvalid = !Regex.IsMatch(str, this.Pattern); + this.IsDirty = true; } } diff --git a/src/MADE.Data.Validation/Validators/RequiredValidator.cs b/src/MADE.Data.Validation/Validators/RequiredValidator.cs index 3858b5a1..07d6e4e1 100644 --- a/src/MADE.Data.Validation/Validators/RequiredValidator.cs +++ b/src/MADE.Data.Validation/Validators/RequiredValidator.cs @@ -1,65 +1,64 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System.Collections; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is provided. +/// +public class RequiredValidator : IValidator { - using System.Collections; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage = Resources.ResourceManager.GetString("RequiredValidator_FeedbackMessage"); /// - /// Defines a data validator for ensuring a value is provided. + /// Gets or sets the key associated with the validator. /// - public class RequiredValidator : IValidator - { - private string feedbackMessage = Resources.ResourceManager.GetString("RequiredValidator_FeedbackMessage"); + public string Key { get; set; } = nameof(RequiredValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(RequiredValidator); - - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage - { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.RequiredValidator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; - } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.RequiredValidator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) - { - this.IsInvalid = DetermineIsInvalid(value); - this.IsDirty = true; - } + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + this.IsInvalid = DetermineIsInvalid(value); + this.IsDirty = true; + } - private static bool DetermineIsInvalid(object value) + private static bool DetermineIsInvalid(object value) + { + return value switch { - return value switch - { - null => true, - ICollection collection => collection.Count <= 0, - bool isTrue => !isTrue, - string str => str.IsNullOrWhiteSpace(), - _ => false - }; - } + null => true, + ICollection collection => collection.Count <= 0, + bool isTrue => !isTrue, + string str => str.IsNullOrWhiteSpace(), + _ => false + }; } -} \ No newline at end of file +} diff --git a/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs b/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs index da2463e5..e4c9f694 100644 --- a/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs +++ b/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs @@ -1,65 +1,64 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Data.Validation.Validators +using System; +using MADE.Data.Validation.Extensions; +using MADE.Data.Validation.Strings; + +namespace MADE.Data.Validation.Validators; + +/// +/// Defines a data validator for ensuring a value is a valid well formed URL, e.g. https://www.example.com. +/// +public class WellFormedUrlValidator : IValidator { - using System; - using MADE.Data.Validation.Extensions; - using MADE.Data.Validation.Strings; + private string feedbackMessage; /// - /// Defines a data validator for ensuring a value is a valid well formed URL, e.g. https://www.example.com. + /// Gets or sets the key associated with the validator. /// - public class WellFormedUrlValidator : IValidator - { - private string feedbackMessage; + public string Key { get; set; } = nameof(WellFormedUrlValidator); - /// - /// Gets or sets the key associated with the validator. - /// - public string Key { get; set; } = nameof(WellFormedUrlValidator); + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid { get; set; } - /// - /// Gets or sets a value indicating whether the data provided is in an invalid state. - /// - public bool IsInvalid { get; set; } + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty { get; set; } - /// - /// Gets or sets a value indicating whether the data is dirty. - /// - public bool IsDirty { get; set; } + /// + /// Gets or sets the feedback message to display when is true. + /// + public string FeedbackMessage + { + get => this.feedbackMessage.IsNullOrWhiteSpace() + ? Resources.UrlValidator_FeedbackMessage + : this.feedbackMessage; + set => this.feedbackMessage = value; + } + + /// + /// Executes data validation on the provided . + /// + /// The value to be validated. + public void Validate(object value) + { + bool isInvalid; - /// - /// Gets or sets the feedback message to display when is true. - /// - public string FeedbackMessage + if (value is Uri uri) { - get => this.feedbackMessage.IsNullOrWhiteSpace() - ? Resources.UrlValidator_FeedbackMessage - : this.feedbackMessage; - set => this.feedbackMessage = value; + isInvalid = uri.IsWellFormedOriginalString() == false; } - - /// - /// Executes data validation on the provided . - /// - /// The value to be validated. - public void Validate(object value) + else { - bool isInvalid; - - if (value is Uri uri) - { - isInvalid = uri.IsWellFormedOriginalString() == false; - } - else - { - var stringValue = value?.ToString() ?? string.Empty; - isInvalid = !Uri.IsWellFormedUriString(stringValue, UriKind.Absolute); - } - - this.IsInvalid = isInvalid; - this.IsDirty = true; + var stringValue = value?.ToString() ?? string.Empty; + isInvalid = !Uri.IsWellFormedUriString(stringValue, UriKind.Absolute); } + + this.IsInvalid = isInvalid; + this.IsDirty = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Diagnostics/AppDiagnostics.cs b/src/MADE.Diagnostics/AppDiagnostics.cs index d7e924c2..bc608197 100644 --- a/src/MADE.Diagnostics/AppDiagnostics.cs +++ b/src/MADE.Diagnostics/AppDiagnostics.cs @@ -1,116 +1,112 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics +using System; +using System.Threading.Tasks; +using MADE.Diagnostics.Exceptions; +using MADE.Diagnostics.Logging; + +namespace MADE.Diagnostics; + +/// +/// Defines a service for managing application wide event logging for exceptions. +/// +public class AppDiagnostics : IAppDiagnostics { - using System; - using System.Threading.Tasks; + /// + /// Initializes a new instance of the class. + /// + /// + /// The instance of the service for logging application event messages. + /// + public AppDiagnostics(IEventLogger eventLogger) + { + this.EventLogger = eventLogger; + } + + /// + /// Occurs when an exception is observed. + /// + public event ExceptionObservedEventHandler ExceptionObserved; + + /// + /// Gets the service for logging application event messages. + /// + public IEventLogger EventLogger { get; } - using MADE.Diagnostics.Exceptions; - using MADE.Diagnostics.Logging; + /// + /// Gets a value indicating whether application diagnostic messages are being recorded. + /// + public bool IsRecordingDiagnostics { get; private set; } /// - /// Defines a service for managing application wide event logging for exceptions. + /// Starts tracking and recording the application diagnostic messages. /// - public class AppDiagnostics : IAppDiagnostics + /// + /// An asynchronous operation. + /// + public async Task StartRecordingDiagnosticsAsync() { - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance of the service for logging application event messages. - /// - public AppDiagnostics(IEventLogger eventLogger) + if (this.IsRecordingDiagnostics) { - this.EventLogger = eventLogger; + return; } - /// - /// Occurs when an exception is observed. - /// - public event ExceptionObservedEventHandler ExceptionObserved; - - /// - /// Gets the service for logging application event messages. - /// - public IEventLogger EventLogger { get; } - - /// - /// Gets a value indicating whether application diagnostic messages are being recorded. - /// - public bool IsRecordingDiagnostics { get; private set; } - - /// - /// Starts tracking and recording the application diagnostic messages. - /// - /// - /// An asynchronous operation. - /// - public async Task StartRecordingDiagnosticsAsync() - { - if (this.IsRecordingDiagnostics) - { - await Task.CompletedTask; - } - - this.IsRecordingDiagnostics = true; + this.IsRecordingDiagnostics = true; - this.EventLogger.WriteInfo("Application diagnostics initialized."); + await this.EventLogger.WriteInfo("Application diagnostics initialized.").ConfigureAwait(false); - AppDomain.CurrentDomain.UnhandledException += this.OnAppUnhandledException; - TaskScheduler.UnobservedTaskException += this.OnTaskUnobservedException; + AppDomain.CurrentDomain.UnhandledException += this.OnAppUnhandledException; + TaskScheduler.UnobservedTaskException += this.OnTaskUnobservedException; + } - await Task.CompletedTask; + /// + /// Stops tracking and recording the application diagnostic messages. + /// + public void StopRecordingDiagnostics() + { + if (!this.IsRecordingDiagnostics) + { + return; } - /// - /// Stops tracking and recording the application diagnostic messages. - /// - public void StopRecordingDiagnostics() - { - if (!this.IsRecordingDiagnostics) - { - return; - } + AppDomain.CurrentDomain.UnhandledException -= this.OnAppUnhandledException; + TaskScheduler.UnobservedTaskException -= this.OnTaskUnobservedException; - AppDomain.CurrentDomain.UnhandledException -= this.OnAppUnhandledException; - TaskScheduler.UnobservedTaskException -= this.OnTaskUnobservedException; + this.IsRecordingDiagnostics = false; + } - this.IsRecordingDiagnostics = false; - } + private async void OnTaskUnobservedException(object sender, UnobservedTaskExceptionEventArgs args) + { + args.SetObserved(); - private void OnTaskUnobservedException(object sender, UnobservedTaskExceptionEventArgs args) - { - args.SetObserved(); + var correlationId = Guid.NewGuid(); - var correlationId = Guid.NewGuid(); + await this.EventLogger.WriteCritical( + args.Exception != null + ? $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: {args.Exception}." + : $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: No exception information was available.").ConfigureAwait(false); - this.EventLogger.WriteCritical( - args.Exception != null - ? $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: {args.Exception}." - : $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: No exception information was available."); + this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception)); + } - this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception)); + private async void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs args) + { + if (args.IsTerminating) + { + await this.EventLogger.WriteCritical( + "The application is terminating due to an unhandled exception being thrown.").ConfigureAwait(false); } - private void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs args) + if (args.ExceptionObject is not Exception ex) { - if (args.IsTerminating) - { - this.EventLogger.WriteCritical( - "The application is terminating due to an unhandled exception being thrown."); - } - - if (args.ExceptionObject is not Exception ex) - { - return; - } + return; + } - var correlationId = Guid.NewGuid(); + var correlationId = Guid.NewGuid(); - this.EventLogger.WriteCritical($"An unhandled exception was thrown. Correlation ID: {correlationId}. Error: {ex}"); + await this.EventLogger.WriteCritical($"An unhandled exception was thrown. Correlation ID: {correlationId}. Error: {ex}").ConfigureAwait(false); - this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, ex)); - } + this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, ex)); } -} \ No newline at end of file +} diff --git a/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventArgs.cs b/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventArgs.cs index dc0e8c9f..d9c5b3b2 100644 --- a/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventArgs.cs +++ b/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventArgs.cs @@ -1,38 +1,37 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics.Exceptions -{ - using System; +using System; + +namespace MADE.Diagnostics.Exceptions; +/// +/// Defines an event argument for an observed exception. +/// +public class ExceptionObservedEventArgs : EventArgs +{ /// - /// Defines an event argument for an observed exception. + /// Initializes a new instance of the class. /// - public class ExceptionObservedEventArgs : EventArgs + /// + /// The unique identifier for correlating the exception. + /// + /// + /// The exception that was observed. + /// + public ExceptionObservedEventArgs(Guid correlationId, Exception exception) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The unique identifier for correlating the exception. - /// - /// - /// The exception that was observed. - /// - public ExceptionObservedEventArgs(Guid correlationId, Exception exception) - { - this.CorrelationId = correlationId; - this.Exception = exception; - } + this.CorrelationId = correlationId; + this.Exception = exception; + } - /// - /// Gets the unique identifier for correlating the exception. - /// - public Guid CorrelationId { get; } + /// + /// Gets the unique identifier for correlating the exception. + /// + public Guid CorrelationId { get; } - /// - /// Gets the exception that was observed. - /// - public Exception Exception { get; } - } -} \ No newline at end of file + /// + /// Gets the exception that was observed. + /// + public Exception Exception { get; } +} diff --git a/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventHandler.cs b/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventHandler.cs index a22cc3e2..24ef93d4 100644 --- a/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventHandler.cs +++ b/src/MADE.Diagnostics/Exceptions/ExceptionObservedEventHandler.cs @@ -1,16 +1,15 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics.Exceptions -{ - /// - /// Defines a delegate for an event handler for observing exceptions that were thrown. - /// - /// - /// The sender. - /// - /// - /// The exception observed event argument. - /// - public delegate void ExceptionObservedEventHandler(object sender, ExceptionObservedEventArgs args); -} \ No newline at end of file +namespace MADE.Diagnostics.Exceptions; + +/// +/// Defines a delegate for an event handler for observing exceptions that were thrown. +/// +/// +/// The sender. +/// +/// +/// The exception observed event argument. +/// +public delegate void ExceptionObservedEventHandler(object sender, ExceptionObservedEventArgs args); diff --git a/src/MADE.Diagnostics/IAppDiagnostics.cs b/src/MADE.Diagnostics/IAppDiagnostics.cs index f2662c21..8b61f6af 100644 --- a/src/MADE.Diagnostics/IAppDiagnostics.cs +++ b/src/MADE.Diagnostics/IAppDiagnostics.cs @@ -1,44 +1,42 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics -{ - using System.Threading.Tasks; +using System.Threading.Tasks; +using MADE.Diagnostics.Exceptions; +using MADE.Diagnostics.Logging; - using MADE.Diagnostics.Exceptions; - using MADE.Diagnostics.Logging; +namespace MADE.Diagnostics; +/// +/// Defines an interface for handling application diagnostics. +/// +public interface IAppDiagnostics +{ /// - /// Defines an interface for handling application diagnostics. + /// Occurs when an exception is observed. /// - public interface IAppDiagnostics - { - /// - /// Occurs when an exception is observed. - /// - event ExceptionObservedEventHandler ExceptionObserved; + event ExceptionObservedEventHandler ExceptionObserved; - /// - /// Gets the service for logging application event messages. - /// - IEventLogger EventLogger { get; } + /// + /// Gets the service for logging application event messages. + /// + IEventLogger EventLogger { get; } - /// - /// Gets a value indicating whether application diagnostic messages are being recorded. - /// - bool IsRecordingDiagnostics { get; } + /// + /// Gets a value indicating whether application diagnostic messages are being recorded. + /// + bool IsRecordingDiagnostics { get; } - /// - /// Starts tracking and recording the application diagnostic messages. - /// - /// - /// An asynchronous operation. - /// - Task StartRecordingDiagnosticsAsync(); + /// + /// Starts tracking and recording the application diagnostic messages. + /// + /// + /// An asynchronous operation. + /// + Task StartRecordingDiagnosticsAsync(); - /// - /// Stops tracking and recording the application diagnostic messages. - /// - void StopRecordingDiagnostics(); - } -} \ No newline at end of file + /// + /// Stops tracking and recording the application diagnostic messages. + /// + void StopRecordingDiagnostics(); +} diff --git a/src/MADE.Diagnostics/Logging/FileEventLogger.cs b/src/MADE.Diagnostics/Logging/FileEventLogger.cs index 23b5b21a..c72b24ce 100644 --- a/src/MADE.Diagnostics/Logging/FileEventLogger.cs +++ b/src/MADE.Diagnostics/Logging/FileEventLogger.cs @@ -1,312 +1,309 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics.Logging +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MADE.Diagnostics.Logging; + +/// +/// Defines a service for logging events to a log file. +/// +public class FileEventLogger : IEventLogger { - using System; - using System.IO; - using System.Threading; - using System.Threading.Tasks; + private readonly SemaphoreSlim fileSemaphore = new(1, 1); /// - /// Defines a service for logging events to a log file. + /// Gets or sets the full file path to where the current log exists. /// - public class FileEventLogger : IEventLogger - { - private const string LogFormat = "{0:G}\tLevel: {1}\tId: {2}\tMessage: '{3}'"; - - private readonly SemaphoreSlim fileSemaphore = new(1, 1); - - /// - /// Gets or sets the full file path to where the current log exists. - /// - public string LogPath { get; set; } - - /// - /// Gets or sets the name of the folder where log files are stored. - /// - public string LogsFolderName { get; set; } = "Logs"; - - /// - /// Gets or sets the format for the name of the log file. - /// - public string LogFileNameFormat { get; set; } = "Log-{0:yyyyMMdd}.txt"; - - /// - /// Writes a debug information message to the event log when in DEBUG mode. - /// - /// - /// The message to write out. - /// - public async void WriteDebug(string message) - { - if (!System.Diagnostics.Debugger.IsAttached) - { - return; - } + public string LogPath { get; set; } - string log = string.Format(LogFormat, DateTime.Now, "Debug", Guid.NewGuid(), message); - await this.WriteToFileAsync(log); - } + /// + /// Gets or sets the name of the folder where log files are stored. + /// + public string LogsFolderName { get; set; } = "Logs"; - /// - /// Writes a generic information message to the event log. - /// - /// - /// The message to write out. - /// - public async void WriteInfo(string message) - { - string log = string.Format(LogFormat, DateTime.Now, "Info", Guid.NewGuid(), message); - await this.WriteToFileAsync(log); - } + /// + /// Gets or sets the format for the name of the log file. + /// + public string LogFileNameFormat { get; set; } = "Log-{0:yyyyMMdd}.txt"; - /// - /// Writes a warning message to the event log. - /// - /// - /// The message to write out. - /// - public async void WriteWarning(string message) + /// + /// Writes a debug information message to the event log when in DEBUG mode. + /// + /// + /// The message to write out. + /// + public Task WriteDebug(string message) + { + if (!System.Diagnostics.Debugger.IsAttached) { - string log = string.Format(LogFormat, DateTime.Now, "Warning", Guid.NewGuid(), message); - await this.WriteToFileAsync(log); + return Task.CompletedTask; } - /// - /// Writes an error message to the event log. - /// - /// - /// The message to write out. - /// - public async void WriteError(string message) - { - string log = string.Format(LogFormat, DateTime.Now, "Error", Guid.NewGuid(), message); - await this.WriteToFileAsync(log); - } + var log = $"{DateTime.Now:G}\tLevel: Debug\tId: {Guid.NewGuid()}\tMessage: '{message}'"; + return this.WriteToFileAsync(log); + } - /// - /// Writes a critical error message to the event log. - /// - /// - /// The message to write out. - /// - public async void WriteCritical(string message) - { - string log = string.Format(LogFormat, DateTime.Now, "Critical", Guid.NewGuid(), message); - await this.WriteToFileAsync(log); - } + /// + /// Writes a generic information message to the event log. + /// + /// + /// The message to write out. + /// + public Task WriteInfo(string message) + { + var log = $"{DateTime.Now:G}\tLevel: Info\tId: {Guid.NewGuid()}\tMessage: '{message}'"; + return this.WriteToFileAsync(log); + } - /// - /// Writes an exception to the event log as a debug message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - public void WriteDebug(string message, Exception ex) - { - this.WriteDebug($"{message} - Error: '{ex}'"); - } + /// + /// Writes a warning message to the event log. + /// + /// + /// The message to write out. + /// + public Task WriteWarning(string message) + { + var log = $"{DateTime.Now:G}\tLevel: Warning\tId: {Guid.NewGuid()}\tMessage: '{message}'"; + return this.WriteToFileAsync(log); + } - /// - /// Writes an exception to the event log as a debug message. - /// - /// - /// The exception to write out. - /// - public void WriteDebug(Exception ex) - { - this.WriteDebug($"Error: '{ex}'"); - } + /// + /// Writes an error message to the event log. + /// + /// + /// The message to write out. + /// + public Task WriteError(string message) + { + var log = $"{DateTime.Now:G}\tLevel: Error\tId: {Guid.NewGuid()}\tMessage: '{message}'"; + return this.WriteToFileAsync(log); + } - /// - /// Writes an exception to the event log as a generic information message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - public void WriteInfo(string message, Exception ex) - { - this.WriteInfo($"{message} - Error: '{ex}'"); - } + /// + /// Writes a critical error message to the event log. + /// + /// + /// The message to write out. + /// + public Task WriteCritical(string message) + { + var log = $"{DateTime.Now:G}\tLevel: Critical\tId: {Guid.NewGuid()}\tMessage: '{message}'"; + return this.WriteToFileAsync(log); + } - /// - /// Writes an exception to the event log as a generic information message. - /// - /// - /// The exception to write out. - /// - public void WriteInfo(Exception ex) - { - this.WriteInfo($"Error: '{ex}'"); - } + /// + /// Writes an exception to the event log as a debug message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + public Task WriteDebug(string message, Exception ex) + { + return this.WriteDebug($"{message} - Error: '{ex}'"); + } - /// - /// Writes an exception to the event log as a warning message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - public void WriteWarning(string message, Exception ex) - { - this.WriteWarning($"{message} - Error: '{ex}'"); - } + /// + /// Writes an exception to the event log as a debug message. + /// + /// + /// The exception to write out. + /// + public Task WriteDebug(Exception ex) + { + return this.WriteDebug($"Error: '{ex}'"); + } - /// - /// Writes an exception to the event log as a warning message. - /// - /// - /// The exception to write out. - /// - public void WriteWarning(Exception ex) - { - this.WriteWarning($"Error: '{ex}'"); - } + /// + /// Writes an exception to the event log as a generic information message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + public Task WriteInfo(string message, Exception ex) + { + return this.WriteInfo($"{message} - Error: '{ex}'"); + } - /// - /// Writes an exception to the event log as an error message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - public void WriteError(string message, Exception ex) - { - this.WriteError($"{message} - Error: '{ex}'"); - } + /// + /// Writes an exception to the event log as a generic information message. + /// + /// + /// The exception to write out. + /// + public Task WriteInfo(Exception ex) + { + return this.WriteInfo($"Error: '{ex}'"); + } - /// - /// Writes an exception to the event log as an error message. - /// - /// - /// The exception to write out. - /// - public void WriteError(Exception ex) - { - this.WriteError($"Error: '{ex}'"); - } + /// + /// Writes an exception to the event log as a warning message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + public Task WriteWarning(string message, Exception ex) + { + return this.WriteWarning($"{message} - Error: '{ex}'"); + } - /// - /// Writes an exception to the event log as a critical message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - public void WriteCritical(string message, Exception ex) + /// + /// Writes an exception to the event log as a warning message. + /// + /// + /// The exception to write out. + /// + public Task WriteWarning(Exception ex) + { + return this.WriteWarning($"Error: '{ex}'"); + } + + /// + /// Writes an exception to the event log as an error message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + public Task WriteError(string message, Exception ex) + { + return this.WriteError($"{message} - Error: '{ex}'"); + } + + /// + /// Writes an exception to the event log as an error message. + /// + /// + /// The exception to write out. + /// + public Task WriteError(Exception ex) + { + return this.WriteError($"Error: '{ex}'"); + } + + /// + /// Writes an exception to the event log as a critical message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + public Task WriteCritical(string message, Exception ex) + { + return this.WriteCritical($"{message} - Error: '{ex}'"); + } + + /// + /// Writes an exception to the event log as a critical message. + /// + /// + /// The exception to write out. + /// + public Task WriteCritical(Exception ex) + { + return this.WriteCritical($"Error: '{ex}'"); + } + + private async Task WriteToFileAsync(string line) + { + await this.fileSemaphore.WaitAsync().ConfigureAwait(false); + + if (System.Diagnostics.Debugger.IsAttached) { - this.WriteCritical($"{message} - Error: '{ex}'"); + System.Diagnostics.Debug.WriteLine(line); } - /// - /// Writes an exception to the event log as a critical message. - /// - /// - /// The exception to write out. - /// - public void WriteCritical(Exception ex) + if (string.IsNullOrWhiteSpace(this.LogPath)) { - this.WriteCritical($"Error: '{ex}'"); + await this.SetupLogFileAsync().ConfigureAwait(false); } - private async Task WriteToFileAsync(string line) + if (!string.IsNullOrWhiteSpace(this.LogPath)) { - await this.fileSemaphore.WaitAsync(); - - if (System.Diagnostics.Debugger.IsAttached) + try { - System.Diagnostics.Debug.WriteLine(line); + using StreamWriter sw = File.AppendText(this.LogPath); + await sw.WriteLineAsync(line).ConfigureAwait(false); } - - if (string.IsNullOrWhiteSpace(this.LogPath)) + catch (Exception ex) { - await this.SetupLogFileAsync(); - } - - if (!string.IsNullOrWhiteSpace(this.LogPath)) - { - try - { - using StreamWriter sw = File.AppendText(this.LogPath); - await sw.WriteLineAsync(line); - } - catch (Exception ex) + if (System.Diagnostics.Debugger.IsAttached) { - if (System.Diagnostics.Debugger.IsAttached) - { - System.Diagnostics.Debug.WriteLine( - $"An exception was thrown while writing to the log file. Error: {ex}"); - } - } - finally - { - this.fileSemaphore.Release(); + System.Diagnostics.Debug.WriteLine( + $"An exception was thrown while writing to the log file. Error: {ex}"); } } + finally + { + this.fileSemaphore.Release(); + } } + } - private async Task SetupLogFileAsync() + private async Task SetupLogFileAsync() + { + if (string.IsNullOrWhiteSpace(this.LogPath)) { - if (string.IsNullOrWhiteSpace(this.LogPath)) - { - string logFileName = string.Format(this.LogFileNameFormat, DateTime.Now); + string logFileName = string.Format(this.LogFileNameFormat, DateTime.Now); - string logFileFolderPath = string.Empty; + string logFileFolderPath = string.Empty; #if WINDOWS_UWP || __ANDROID__ || __IOS__ - XPlat.Storage.IStorageFolder logsFolder = - await XPlat.Storage.ApplicationData.Current.LocalFolder.CreateFolderAsync( - this.LogsFolderName, - XPlat.Storage.CreationCollisionOption.OpenIfExists); + XPlat.Storage.IStorageFolder logsFolder = + await XPlat.Storage.ApplicationData.Current.LocalFolder.CreateFolderAsync( + this.LogsFolderName, + XPlat.Storage.CreationCollisionOption.OpenIfExists).ConfigureAwait(false); - XPlat.Storage.IStorageFile logFile = await logsFolder.CreateFileAsync( - logFileName, - XPlat.Storage.CreationCollisionOption.OpenIfExists); + XPlat.Storage.IStorageFile logFile = await logsFolder.CreateFileAsync( + logFileName, + XPlat.Storage.CreationCollisionOption.OpenIfExists).ConfigureAwait(false); - logFileFolderPath = logFile.Path; + logFileFolderPath = logFile.Path; #elif NETSTANDARD2_0 - string appFolderPath = AppDomain.CurrentDomain.BaseDirectory; - string logsFolderPath = Path.Combine(appFolderPath, this.LogsFolderName); + string appFolderPath = AppDomain.CurrentDomain.BaseDirectory; + string logsFolderPath = Path.Combine(appFolderPath, this.LogsFolderName); #else - string appFolderPath = AppContext.BaseDirectory; - string logsFolderPath = Path.Combine(appFolderPath, this.LogsFolderName); + string appFolderPath = AppContext.BaseDirectory; + string logsFolderPath = Path.Combine(appFolderPath, this.LogsFolderName); #endif #if !(WINDOWS_UWP || __ANDROID__ || __IOS__) - if (!string.IsNullOrWhiteSpace(logsFolderPath)) + if (!string.IsNullOrWhiteSpace(logsFolderPath)) + { + if (!Directory.Exists(logsFolderPath)) { - if (!Directory.Exists(logsFolderPath)) - { - Directory.CreateDirectory(logsFolderPath); - } - - logFileFolderPath = Path.Combine(logsFolderPath, logFileName); - if (!File.Exists(logFileFolderPath)) - { - File.Create(logFileFolderPath); - } + Directory.CreateDirectory(logsFolderPath); } -#endif - this.LogPath = logFileFolderPath; + logFileFolderPath = Path.Combine(logsFolderPath, logFileName); + if (!File.Exists(logFileFolderPath)) + { + File.Create(logFileFolderPath); + } } +#endif + + this.LogPath = logFileFolderPath; + } #if !(WINDOWS_UWP || __ANDROID__ || __IOS__) - await Task.CompletedTask; + await Task.CompletedTask; #endif - } } } diff --git a/src/MADE.Diagnostics/Logging/IEventLogger.cs b/src/MADE.Diagnostics/Logging/IEventLogger.cs index 053c7ef9..0d9540eb 100644 --- a/src/MADE.Diagnostics/Logging/IEventLogger.cs +++ b/src/MADE.Diagnostics/Logging/IEventLogger.cs @@ -1,148 +1,148 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics.Logging +using System; +using System.Threading.Tasks; + +namespace MADE.Diagnostics.Logging; + +/// +/// Defines an interface for an event logging service. +/// +public interface IEventLogger { - using System; + /// + /// Writes a debug information message to the event log when in DEBUG mode. + /// + /// + /// The message to write out. + /// + Task WriteDebug(string message); + + /// + /// Writes an exception to the event log as a debug message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + Task WriteDebug(string message, Exception ex); + + /// + /// Writes an exception to the event log as a debug message. + /// + /// + /// The exception to write out. + /// + Task WriteDebug(Exception ex); + + /// + /// Writes a generic information message to the event log. + /// + /// + /// The message to write out. + /// + Task WriteInfo(string message); + + /// + /// Writes an exception to the event log as a generic information message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + Task WriteInfo(string message, Exception ex); + + /// + /// Writes an exception to the event log as a generic information message. + /// + /// + /// The exception to write out. + /// + Task WriteInfo(Exception ex); + + /// + /// Writes a warning message to the event log. + /// + /// + /// The message to write out. + /// + Task WriteWarning(string message); + + /// + /// Writes an exception to the event log as a warning message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + Task WriteWarning(string message, Exception ex); + + /// + /// Writes an exception to the event log as a warning message. + /// + /// + /// The exception to write out. + /// + Task WriteWarning(Exception ex); + + /// + /// Writes an error message to the event log. + /// + /// + /// The message to write out. + /// + Task WriteError(string message); + + /// + /// Writes an exception to the event log as an error message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + Task WriteError(string message, Exception ex); + + /// + /// Writes an exception to the event log as an error message. + /// + /// + /// The exception to write out. + /// + Task WriteError(Exception ex); + + /// + /// Writes a critical error message to the event log. + /// + /// + /// The message to write out. + /// + Task WriteCritical(string message); + + /// + /// Writes an exception to the event log as a critical message. + /// + /// + /// The message to write out. + /// + /// + /// The exception to write out. + /// + Task WriteCritical(string message, Exception ex); /// - /// Defines an interface for an event logging service. + /// Writes an exception to the event log as a critical message. /// - public interface IEventLogger - { - /// - /// Writes a debug information message to the event log when in DEBUG mode. - /// - /// - /// The message to write out. - /// - void WriteDebug(string message); - - /// - /// Writes an exception to the event log as a debug message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - void WriteDebug(string message, Exception ex); - - /// - /// Writes an exception to the event log as a debug message. - /// - /// - /// The exception to write out. - /// - void WriteDebug(Exception ex); - - /// - /// Writes a generic information message to the event log. - /// - /// - /// The message to write out. - /// - void WriteInfo(string message); - - /// - /// Writes an exception to the event log as a generic information message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - void WriteInfo(string message, Exception ex); - - /// - /// Writes an exception to the event log as a generic information message. - /// - /// - /// The exception to write out. - /// - void WriteInfo(Exception ex); - - /// - /// Writes a warning message to the event log. - /// - /// - /// The message to write out. - /// - void WriteWarning(string message); - - /// - /// Writes an exception to the event log as a warning message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - void WriteWarning(string message, Exception ex); - - /// - /// Writes an exception to the event log as a warning message. - /// - /// - /// The exception to write out. - /// - void WriteWarning(Exception ex); - - /// - /// Writes an error message to the event log. - /// - /// - /// The message to write out. - /// - void WriteError(string message); - - /// - /// Writes an exception to the event log as an error message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - void WriteError(string message, Exception ex); - - /// - /// Writes an exception to the event log as an error message. - /// - /// - /// The exception to write out. - /// - void WriteError(Exception ex); - - /// - /// Writes a critical error message to the event log. - /// - /// - /// The message to write out. - /// - void WriteCritical(string message); - - /// - /// Writes an exception to the event log as a critical message. - /// - /// - /// The message to write out. - /// - /// - /// The exception to write out. - /// - void WriteCritical(string message, Exception ex); - - /// - /// Writes an exception to the event log as a critical message. - /// - /// - /// The exception to write out. - /// - void WriteCritical(Exception ex); - } + /// + /// The exception to write out. + /// + Task WriteCritical(Exception ex); } diff --git a/src/MADE.Diagnostics/StopwatchHelper.cs b/src/MADE.Diagnostics/StopwatchHelper.cs index 4c60007f..12924a3b 100644 --- a/src/MADE.Diagnostics/StopwatchHelper.cs +++ b/src/MADE.Diagnostics/StopwatchHelper.cs @@ -1,91 +1,90 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Diagnostics +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace MADE.Diagnostics; + +/// +/// Defines a helper class for creating instances for method calls to track how long they take to execute. +/// +public static class StopwatchHelper { - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Runtime.CompilerServices; + private static readonly Dictionary Stopwatches = new(); /// - /// Defines a helper class for creating instances for method calls to track how long they take to execute. + /// Starts a with the specified and . /// - public static class StopwatchHelper + /// + /// The caller for the , assumed as the file path by the caller if not set. + /// + /// + /// The name of the unit of code under test, assumed as the member name of the caller if not set. + /// + /// + /// A display message for an output containing the key. + /// + public static string? Start([CallerFilePath] string? caller = null, [CallerMemberName] string? name = null) { - private static readonly Dictionary Stopwatches = new(); + string key = $"{caller}_{name}"; - /// - /// Starts a with the specified and . - /// - /// - /// The caller for the , assumed as the file path by the caller if not set. - /// - /// - /// The name of the unit of code under test, assumed as the member name of the caller if not set. - /// - /// - /// A display message for an output containing the key. - /// - public static string Start([CallerFilePath] string caller = null, [CallerMemberName] string name = null) + if (Stopwatches.ContainsKey(key)) { - string key = $"{caller}_{name}"; + return null; + } - if (Stopwatches.ContainsKey(key)) - { - return null; - } + KeyValuePair stopwatch = Stopwatches.FirstOrDefault( + kvp => kvp.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)); - KeyValuePair stopwatch = Stopwatches.FirstOrDefault( - kvp => kvp.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)); + if (stopwatch.Value != null) + { + return null; + } - if (stopwatch.Value != null) - { - return null; - } + var stopWatch = new Stopwatch(); + Stopwatches.Add(key, stopWatch); + stopWatch.Start(); - var stopWatch = new Stopwatch(); - Stopwatches.Add(key, stopWatch); - stopWatch.Start(); + return $"Stopwatch '{key}' started."; + } - return $"Stopwatch '{key}' started."; - } + /// + /// Stops a with the specified and . + /// + /// + /// The caller for the , assumed as the file path by the caller if not set. + /// + /// + /// The name of the unit of code under test, assumed as the member name of the caller if not set. + /// + /// + /// A display message for an output containing the details of the elapsed time, and the elapsed time value. + /// + public static (string?, TimeSpan) Stop([CallerFilePath] string? caller = null, [CallerMemberName] string? name = null) + { + string key = $"{caller}_{name}"; - /// - /// Stops a with the specified and . - /// - /// - /// The caller for the , assumed as the file path by the caller if not set. - /// - /// - /// The name of the unit of code under test, assumed as the member name of the caller if not set. - /// - /// - /// A display message for an output containing the details of the elapsed time, and the elapsed time value. - /// - public static (string, TimeSpan) Stop([CallerFilePath] string caller = null, [CallerMemberName] string name = null) + if (!Stopwatches.ContainsKey(key)) { - string key = $"{caller}_{name}"; - - if (!Stopwatches.ContainsKey(key)) - { - return (null, TimeSpan.Zero); - } + return (null, TimeSpan.Zero); + } - KeyValuePair stopwatch = Stopwatches.FirstOrDefault( - kvp => kvp.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)); + KeyValuePair stopwatch = Stopwatches.FirstOrDefault( + kvp => kvp.Key.Equals(key, StringComparison.CurrentCultureIgnoreCase)); - if (stopwatch.Value == null) - { - return (null, TimeSpan.Zero); - } + if (stopwatch.Value == null) + { + return (null, TimeSpan.Zero); + } - stopwatch.Value.Stop(); - Stopwatches.Remove(key); + stopwatch.Value.Stop(); + Stopwatches.Remove(key); - TimeSpan elapsed = stopwatch.Value.Elapsed; - return ($"Stopwatch '{key}' took {elapsed} to run.", elapsed); - } + TimeSpan elapsed = stopwatch.Value.Elapsed; + return ($"Stopwatch '{key}' took {elapsed} to run.", elapsed); } -} \ No newline at end of file +} diff --git a/src/MADE.Foundation/Platform/PlatformApiHelper.cs b/src/MADE.Foundation/Platform/PlatformApiHelper.cs index 8567d5b2..1255b251 100644 --- a/src/MADE.Foundation/Platform/PlatformApiHelper.cs +++ b/src/MADE.Foundation/Platform/PlatformApiHelper.cs @@ -1,78 +1,77 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Foundation.Platform +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MADE.Foundation.Platform; + +/// +/// Defines a helper for checking platform support for APIs. +/// +public static class PlatformApiHelper { - using System; - using System.Collections.Generic; - using System.Reflection; + private static readonly object Lock = new(); + + private static readonly Dictionary CheckedTypes = new(); /// - /// Defines a helper for checking platform support for APIs. + /// Indicates whether the specified is supported by the platform. /// - public static class PlatformApiHelper + /// The type to check. + /// True if supported; otherwise, false. + public static bool IsTypeSupported(Type type) { - private static readonly object Lock = new(); - - private static readonly Dictionary CheckedTypes = new(); - - /// - /// Indicates whether the specified is supported by the platform. - /// - /// The type to check. - /// True if supported; otherwise, false. - public static bool IsTypeSupported(Type type) - { - lock (Lock) - { - if (!CheckedTypes.TryGetValue(type, out var result)) - { - CheckedTypes[type] = result = IsSupported(type); - } - - return result; - } - } - - /// - /// Indicates whether the specified on is supported by the platform. - /// - /// The type where the method should be checked. - /// The name of the method to check. - /// True if supported; otherwise, false. - /// More than one method is found with the specified name. - public static bool IsMethodSupported(Type type, string methodName) + lock (Lock) { - var result = IsTypeSupported(type); - if (!result) + if (!CheckedTypes.TryGetValue(type, out var result)) { - result = IsSupported(type.GetMethod(methodName)); + CheckedTypes[type] = result = IsSupported(type); } return result; } + } - /// - /// Indicates whether the specified on is supported by the platform. - /// - /// The type where the property should be checked. - /// The name of the property to check. - /// True if supported; otherwise, false. - /// More than one property is found with the specified name. - public static bool IsPropertySupported(Type type, string propertyName) + /// + /// Indicates whether the specified on is supported by the platform. + /// + /// The type where the method should be checked. + /// The name of the method to check. + /// True if supported; otherwise, false. + /// More than one method is found with the specified name. + public static bool IsMethodSupported(Type type, string methodName) + { + var result = IsTypeSupported(type); + if (!result) { - var result = IsTypeSupported(type); - if (!result) - { - result = IsSupported(type.GetProperty(propertyName)); - } - - return result; + result = IsSupported(type.GetMethod(methodName)); } - private static bool IsSupported(ICustomAttributeProvider attributeProvider) + return result; + } + + /// + /// Indicates whether the specified on is supported by the platform. + /// + /// The type where the property should be checked. + /// The name of the property to check. + /// True if supported; otherwise, false. + /// More than one property is found with the specified name. + public static bool IsPropertySupported(Type type, string propertyName) + { + var result = IsTypeSupported(type); + if (!result) { - return (attributeProvider?.GetCustomAttributes(typeof(PlatformNotSupportedAttribute), false).Length ?? -1) == 0; + result = IsSupported(type.GetProperty(propertyName)); } + + return result; + } + + private static bool IsSupported(ICustomAttributeProvider attributeProvider) + { + return (attributeProvider?.GetCustomAttributes(typeof(PlatformNotSupportedAttribute), false).Length ?? -1) == 0; } } diff --git a/src/MADE.Foundation/Platform/PlatformNotSupportedAttribute.cs b/src/MADE.Foundation/Platform/PlatformNotSupportedAttribute.cs index 7245d06d..317d84e4 100644 --- a/src/MADE.Foundation/Platform/PlatformNotSupportedAttribute.cs +++ b/src/MADE.Foundation/Platform/PlatformNotSupportedAttribute.cs @@ -1,21 +1,20 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Foundation.Platform -{ - using System; +using System; + +namespace MADE.Foundation.Platform; +/// +/// Defines an attribute that marks a component as not supported by a specific platform. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +public sealed class PlatformNotSupportedAttribute : Attribute +{ /// - /// Defines an attribute that marks a component as not supported by a specific platform. + /// Initializes a new instance of the class. /// - [AttributeUsage(AttributeTargets.All, Inherited = false)] - public sealed class PlatformNotSupportedAttribute : Attribute + public PlatformNotSupportedAttribute() { - /// - /// Initializes a new instance of the class. - /// - public PlatformNotSupportedAttribute() - { - } } -} \ No newline at end of file +} diff --git a/src/MADE.Foundation/Platform/PlatformNotSupportedException.cs b/src/MADE.Foundation/Platform/PlatformNotSupportedException.cs index 0b3e637d..471c1aed 100644 --- a/src/MADE.Foundation/Platform/PlatformNotSupportedException.cs +++ b/src/MADE.Foundation/Platform/PlatformNotSupportedException.cs @@ -1,33 +1,32 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Foundation.Platform -{ - using System; +using System; + +namespace MADE.Foundation.Platform; - /// - /// Defines an exception thrown when code is called for a platform that is not supported. - /// - public class PlatformNotSupportedException : NotImplementedException +/// +/// Defines an exception thrown when code is called for a platform that is not supported. +/// +public class PlatformNotSupportedException : NotImplementedException +{ + /// Initializes a new instance of the class with default properties. + public PlatformNotSupportedException() { - /// Initializes a new instance of the class with default properties. - public PlatformNotSupportedException() - { - } + } - /// Initializes a new instance of the class with a specified error message. - /// The error message that explains the reason for the exception. - public PlatformNotSupportedException(string message) - : base(message) - { - } + /// Initializes a new instance of the class with a specified error message. + /// The error message that explains the reason for the exception. + public PlatformNotSupportedException(string message) + : base(message) + { + } - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. If the parameter is not , the current exception is raised in a block that handles the inner exception. - public PlatformNotSupportedException(string message, Exception inner) - : base(message, inner) - { - } + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. If the parameter is not , the current exception is raised in a block that handles the inner exception. + public PlatformNotSupportedException(string message, Exception inner) + : base(message, inner) + { } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs b/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs index bbfb177b..6bd4952c 100644 --- a/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs +++ b/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs @@ -1,40 +1,39 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Extensions -{ - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Responses; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Responses; + +namespace MADE.Networking.Extensions; +/// +/// Defines a collection of extensions for objects. +/// +public static class HttpResponseMessageExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Deserializes the content of the specified response task to a . /// - public static class HttpResponseMessageExtensions + /// The type of response expected. + /// The task associated with the . + /// A with deserialized content. + public static async Task> DeserializeAsync(this Task responseTask) { - /// - /// Deserializes the content of the specified response task to a . - /// - /// The type of response expected. - /// The task associated with the . - /// A with deserialized content. - public static async Task> DeserializeAsync(this Task responseTask) - { - HttpResponseMessage response = await responseTask; - return await DeserializeAsync(response); - } + HttpResponseMessage response = await responseTask.ConfigureAwait(false); + return await DeserializeAsync(response).ConfigureAwait(false); + } - /// - /// Deserializes the content of the specified response to a . - /// - /// The type of response expected. - /// The to deserialize. - /// A with deserialized content. - public static async Task> DeserializeAsync(this HttpResponseMessage response) - { - var deserializedResponse = new HttpResponseMessage(response); - await deserializedResponse.DeserializeAsync(); - return deserializedResponse; - } + /// + /// Deserializes the content of the specified response to a . + /// + /// The type of response expected. + /// The to deserialize. + /// A with deserialized content. + public static async Task> DeserializeAsync(this HttpResponseMessage response) + { + var deserializedResponse = new HttpResponseMessage(response); + await deserializedResponse.DeserializeAsync().ConfigureAwait(false); + return deserializedResponse; } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Extensions/UriExtensions.cs b/src/MADE.Networking/Extensions/UriExtensions.cs index d1071a1f..89be6fd7 100644 --- a/src/MADE.Networking/Extensions/UriExtensions.cs +++ b/src/MADE.Networking/Extensions/UriExtensions.cs @@ -1,26 +1,25 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Extensions -{ - using System; - using System.Collections.Specialized; +using System; +using System.Collections.Specialized; + +namespace MADE.Networking.Extensions; +/// +/// Defines a collection of extensions for objects. +/// +public static class UriExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Gets a value from a query in the specified with the specified query parameter key. /// - public static class UriExtensions + /// The to extract a query value from. + /// The key of the parameter in the query to extract the value for. + /// The value for the query parameter. + public static string GetQueryValue(this Uri uri, string queryParam) { - /// - /// Gets a value from a query in the specified with the specified query parameter key. - /// - /// The to extract a query value from. - /// The key of the parameter in the query to extract the value for. - /// The value for the query parameter. - public static string GetQueryValue(this Uri uri, string queryParam) - { - NameValueCollection queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query); - return queryDictionary.Get(queryParam); - } + NameValueCollection queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query); + return queryDictionary.Get(queryParam); } } diff --git a/src/MADE.Networking/Http/INetworkRequestManager.cs b/src/MADE.Networking/Http/INetworkRequestManager.cs index a54857e7..92a2133f 100644 --- a/src/MADE.Networking/Http/INetworkRequestManager.cs +++ b/src/MADE.Networking/Http/INetworkRequestManager.cs @@ -1,107 +1,105 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http -{ - using System; - using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; +using MADE.Networking.Http.Requests; - using MADE.Networking.Http.Requests; +namespace MADE.Networking.Http; +/// +/// Defines an interface for a network request manager. +/// +public interface INetworkRequestManager +{ /// - /// Defines an interface for a network request manager. + /// Gets the current queue of network requests. /// - public interface INetworkRequestManager - { - /// - /// Gets the current queue of network requests. - /// - ConcurrentDictionary CurrentQueue { get; } + ConcurrentDictionary CurrentQueue { get; } - /// - /// Starts the manager processing the queue of network requests at a default time period of 1 minute. - /// - void Start(); + /// + /// Starts the manager processing the queue of network requests at a default time period of 1 minute. + /// + void Start(); - /// - /// Starts the manager processing the queue of network requests. - /// - /// - /// The time period between each process of the queue. - /// - void Start(TimeSpan processPeriod); + /// + /// Starts the manager processing the queue of network requests. + /// + /// + /// The time period between each process of the queue. + /// + void Start(TimeSpan processPeriod); - /// - /// Stops the processing of the network manager queues. - /// - void Stop(); + /// + /// Stops the processing of the network manager queues. + /// + void Stop(); - /// - /// Adds or updates a network request in the queue. - /// - /// - /// The type of network request. - /// - /// - /// The expected response type. - /// - /// - /// The network request to execute. - /// - /// - /// The action to execute when receiving a successful response. - /// - void AddOrUpdate(TRequest request, Action successCallback) - where TRequest : NetworkRequest; + /// + /// Adds or updates a network request in the queue. + /// + /// + /// The type of network request. + /// + /// + /// The expected response type. + /// + /// + /// The network request to execute. + /// + /// + /// The action to execute when receiving a successful response. + /// + void AddOrUpdate(TRequest request, Action successCallback) + where TRequest : NetworkRequest; - /// - /// Adds or updates a network request in the queue. - /// - /// - /// The type of network request. - /// - /// - /// The expected response type. - /// - /// - /// The expected error response type. - /// - /// - /// The network request to execute. - /// - /// - /// The action to execute when receiving a successful response. - /// - /// - /// The action to execute when receiving an error response. - /// - void AddOrUpdate( - TRequest request, - Action successCallback, - Action errorCallback) - where TRequest : NetworkRequest; + /// + /// Adds or updates a network request in the queue. + /// + /// + /// The type of network request. + /// + /// + /// The expected response type. + /// + /// + /// The expected error response type. + /// + /// + /// The network request to execute. + /// + /// + /// The action to execute when receiving a successful response. + /// + /// + /// The action to execute when receiving an error response. + /// + void AddOrUpdate( + TRequest request, + Action successCallback, + Action errorCallback) + where TRequest : NetworkRequest; - /// - /// Removes a network request from the queue. - /// - /// If the request is no longer in the queue, this method does nothing. - /// - /// - /// The request to remove from the queue. - void Remove(INetworkRequest request); + /// + /// Removes a network request from the queue. + /// + /// If the request is no longer in the queue, this method does nothing. + /// + /// + /// The request to remove from the queue. + void Remove(INetworkRequest request); - /// - /// Removes a network request from the queue by the registered key identifier. - /// - /// If the request is no longer in the queue, this method does nothing. - /// - /// - /// The key corresponding to the network request to remove from the queue. - void RemoveByKey(string key); + /// + /// Removes a network request from the queue by the registered key identifier. + /// + /// If the request is no longer in the queue, this method does nothing. + /// + /// + /// The key corresponding to the network request to remove from the queue. + void RemoveByKey(string key); - /// - /// Processes the current queue of network requests. - /// - void ProcessCurrentQueue(); - } -} \ No newline at end of file + /// + /// Processes the current queue of network requests. + /// + void ProcessCurrentQueue(); +} diff --git a/src/MADE.Networking/Http/NetworkRequestManager.cs b/src/MADE.Networking/Http/NetworkRequestManager.cs index 31f7ff42..8e7f7523 100644 --- a/src/MADE.Networking/Http/NetworkRequestManager.cs +++ b/src/MADE.Networking/Http/NetworkRequestManager.cs @@ -1,231 +1,230 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests; +using MADE.Runtime; +using Timer = MADE.Threading.Timer; + +namespace MADE.Networking.Http; + +/// +/// Defines a manager for executing queued network requests. +/// +public sealed class NetworkRequestManager : INetworkRequestManager { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests; - using MADE.Runtime; - using Timer = MADE.Threading.Timer; + private readonly Timer processTimer; + + private bool isProcessingRequests; /// - /// Defines a manager for executing queued network requests. + /// Initializes a new instance of the class. /// - public sealed class NetworkRequestManager : INetworkRequestManager + public NetworkRequestManager() { - private readonly Timer processTimer; + this.CurrentQueue = new ConcurrentDictionary(); + this.processTimer = new Timer(); + this.processTimer.Tick += this.OnProcessTimerTick; + } - private bool isProcessingRequests; + /// + /// Gets the current queue of network requests. + /// + public ConcurrentDictionary CurrentQueue { get; } - /// - /// Initializes a new instance of the class. - /// - public NetworkRequestManager() - { - this.CurrentQueue = new ConcurrentDictionary(); - this.processTimer = new Timer(); - this.processTimer.Tick += this.OnProcessTimerTick; - } + /// + /// Starts the manager processing the queue of network requests at a default time period of 1 minute. + /// + public void Start() + { + this.Start(TimeSpan.FromMinutes(1)); + } - /// - /// Gets the current queue of network requests. - /// - public ConcurrentDictionary CurrentQueue { get; } + /// + /// Starts the manager processing the queue of network requests. + /// + /// + /// The time period between each process of the queue. + /// + public void Start(TimeSpan processPeriod) + { + this.processTimer.Interval = processPeriod; + this.processTimer.Start(); + } - /// - /// Starts the manager processing the queue of network requests at a default time period of 1 minute. - /// - public void Start() - { - this.Start(TimeSpan.FromMinutes(1)); - } + /// + /// Stops the processing of the network manager queues. + /// + public void Stop() + { + this.processTimer.Stop(); + } - /// - /// Starts the manager processing the queue of network requests. - /// - /// - /// The time period between each process of the queue. - /// - public void Start(TimeSpan processPeriod) + /// + /// Processes the current queue of network requests. + /// + public void ProcessCurrentQueue() + { + if (this.CurrentQueue.Count == 0 || this.isProcessingRequests) { - this.processTimer.Interval = processPeriod; - this.processTimer.Start(); + return; } - /// - /// Stops the processing of the network manager queues. - /// - public void Stop() - { - this.processTimer.Stop(); - } + this.isProcessingRequests = true; - /// - /// Processes the current queue of network requests. - /// - public void ProcessCurrentQueue() + try { - if (this.CurrentQueue.Count == 0 || this.isProcessingRequests) - { - return; - } - - this.isProcessingRequests = true; + var cts = new CancellationTokenSource(); + var requestTasks = new List(); + var requestCallbacks = new List(); - try + while (this.CurrentQueue.Count > 0) { - var cts = new CancellationTokenSource(); - var requestTasks = new List(); - var requestCallbacks = new List(); - - while (this.CurrentQueue.Count > 0) + if (this.CurrentQueue.TryRemove( + this.CurrentQueue.FirstOrDefault().Key, + out NetworkRequestCallback request)) { - if (this.CurrentQueue.TryRemove( - this.CurrentQueue.FirstOrDefault().Key, - out NetworkRequestCallback request)) - { - requestCallbacks.Add(request); - } - } - - foreach (NetworkRequestCallback container in requestCallbacks) - { - requestTasks.Add(ExecuteRequestsAsync(this.CurrentQueue, container, cts.Token)); + requestCallbacks.Add(request); } } - finally + + foreach (NetworkRequestCallback container in requestCallbacks) { - this.isProcessingRequests = false; + requestTasks.Add(ExecuteRequestsAsync(this.CurrentQueue, container, cts.Token)); } } - - /// - /// Adds or updates a network request in the queue. - /// - /// - /// The type of network request. - /// - /// - /// The expected response type. - /// - /// - /// The network request to execute. - /// - /// - /// The action to execute when receiving a successful response. - /// - /// The throws an exception acquiring method info. - /// The already contains the maximum number of elements (). - public void AddOrUpdate(TRequest request, Action successCallback) - where TRequest : NetworkRequest + finally { - this.AddOrUpdate(request, successCallback, null); + this.isProcessingRequests = false; } + } - /// - /// Adds or updates a network request in the queue. - /// - /// - /// The type of network request. - /// - /// - /// The expected response type. - /// - /// - /// The expected error response type. - /// - /// - /// The network request to execute. - /// - /// - /// The action to execute when receiving a successful response. - /// - /// - /// The action to execute when receiving an error response. - /// - /// The or throws an exception acquiring method info. - /// The already contains the maximum number of elements (). - public void AddOrUpdate( - TRequest request, - Action successCallback, - Action errorCallback) - where TRequest : NetworkRequest + /// + /// Adds or updates a network request in the queue. + /// + /// + /// The type of network request. + /// + /// + /// The expected response type. + /// + /// + /// The network request to execute. + /// + /// + /// The action to execute when receiving a successful response. + /// + /// The throws an exception acquiring method info. + /// The already contains the maximum number of elements (). + public void AddOrUpdate(TRequest request, Action successCallback) + where TRequest : NetworkRequest + { + this.AddOrUpdate(request, successCallback, null); + } + + /// + /// Adds or updates a network request in the queue. + /// + /// + /// The type of network request. + /// + /// + /// The expected response type. + /// + /// + /// The expected error response type. + /// + /// + /// The network request to execute. + /// + /// + /// The action to execute when receiving a successful response. + /// + /// + /// The action to execute when receiving an error response. + /// + /// The or throws an exception acquiring method info. + /// The already contains the maximum number of elements (). + public void AddOrUpdate( + TRequest request, + Action successCallback, + Action errorCallback) + where TRequest : NetworkRequest + { + var weakSuccessCallback = new WeakReferenceCallback(successCallback, typeof(TResponse)); + var weakErrorCallback = errorCallback == null + ? null + : new WeakReferenceCallback(errorCallback, typeof(TErrorResponse)); + var requestCallback = new NetworkRequestCallback(request, weakSuccessCallback, weakErrorCallback); + + this.CurrentQueue.AddOrUpdate( + request.Identifier.ToString(), + requestCallback, + (s, callback) => requestCallback); + } + + /// + /// Removes a network request from the queue. + /// + /// If the request is no longer in the queue, this method does nothing. + /// + /// + /// The request to remove from the queue. + public void Remove(INetworkRequest request) + { + RemoveByKey(request.Identifier.ToString()); + } + + /// + /// Removes a network request from the queue by the registered key identifier. + /// + /// If the request is no longer in the queue, this method does nothing. + /// + /// + /// The key corresponding to the network request to remove from the queue. + public void RemoveByKey(string key) + { + this.CurrentQueue.TryRemove(key, out NetworkRequestCallback _); + } + + private static async Task ExecuteRequestsAsync( + ConcurrentDictionary queue, + NetworkRequestCallback requestCallback, + CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) { - var weakSuccessCallback = new WeakReferenceCallback(successCallback, typeof(TResponse)); - var weakErrorCallback = errorCallback == null - ? null - : new WeakReferenceCallback(errorCallback, typeof(TErrorResponse)); - var requestCallback = new NetworkRequestCallback(request, weakSuccessCallback, weakErrorCallback); - - this.CurrentQueue.AddOrUpdate( - request.Identifier.ToString(), + queue.AddOrUpdate( + requestCallback.Request.Identifier.ToString(), requestCallback, (s, callback) => requestCallback); - } - /// - /// Removes a network request from the queue. - /// - /// If the request is no longer in the queue, this method does nothing. - /// - /// - /// The request to remove from the queue. - public void Remove(INetworkRequest request) - { - RemoveByKey(request.Identifier.ToString()); + return; } - /// - /// Removes a network request from the queue by the registered key identifier. - /// - /// If the request is no longer in the queue, this method does nothing. - /// - /// - /// The key corresponding to the network request to remove from the queue. - public void RemoveByKey(string key) - { - this.CurrentQueue.TryRemove(key, out NetworkRequestCallback _); - } + NetworkRequest request = requestCallback.Request; + WeakReferenceCallback successCallback = requestCallback.SuccessCallback; + WeakReferenceCallback errorCallback = requestCallback.ErrorCallback; - private static async Task ExecuteRequestsAsync( - ConcurrentDictionary queue, - NetworkRequestCallback requestCallback, - CancellationToken cancellationToken = default) + try { - if (cancellationToken.IsCancellationRequested) - { - queue.AddOrUpdate( - requestCallback.Request.Identifier.ToString(), - requestCallback, - (s, callback) => requestCallback); - - return; - } - - NetworkRequest request = requestCallback.Request; - WeakReferenceCallback successCallback = requestCallback.SuccessCallback; - WeakReferenceCallback errorCallback = requestCallback.ErrorCallback; - - try - { - object response = await request.ExecuteAsync(successCallback.Type, cancellationToken); - successCallback.Invoke(response); - } - catch (Exception ex) - { - successCallback.Invoke(Activator.CreateInstance(successCallback.Type)); - errorCallback?.Invoke(ex); - } + object response = await request.ExecuteAsync(successCallback.Type, cancellationToken).ConfigureAwait(false); + successCallback.Invoke(response); } - - private void OnProcessTimerTick(object sender, object e) + catch (Exception ex) { - this.ProcessCurrentQueue(); + successCallback.Invoke(Activator.CreateInstance(successCallback.Type)); + errorCallback?.Invoke(ex); } } -} \ No newline at end of file + + private void OnProcessTimerTick(object sender, object e) + { + this.ProcessCurrentQueue(); + } +} diff --git a/src/MADE.Networking/Http/Requests/INetworkRequest.cs b/src/MADE.Networking/Http/Requests/INetworkRequest.cs index e26b8eae..7c40e00f 100644 --- a/src/MADE.Networking/Http/Requests/INetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/INetworkRequest.cs @@ -1,59 +1,58 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MADE.Networking.Http.Requests; +/// +/// Defines an interface for a basic network request. +/// +public interface INetworkRequest +{ /// - /// Defines an interface for a basic network request. + /// Gets the identifier for the request. /// - public interface INetworkRequest - { - /// - /// Gets the identifier for the request. - /// - Guid Identifier { get; } + Guid Identifier { get; } - /// - /// Gets or sets the URL for the request. - /// - string Url { get; set; } + /// + /// Gets or sets the URL for the request. + /// + string Url { get; set; } - /// - /// Gets the headers for the request. - /// - Dictionary Headers { get; } + /// + /// Gets the headers for the request. + /// + Dictionary Headers { get; } - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - Task ExecuteAsync(CancellationToken cancellationToken = default); + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + Task ExecuteAsync(CancellationToken cancellationToken = default); - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - Task ExecuteAsync(Type expectedResponse, CancellationToken cancellationToken = default); - } -} \ No newline at end of file + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + Task ExecuteAsync(Type expectedResponse, CancellationToken cancellationToken = default); +} diff --git a/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs index c86acd92..e389d276 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs @@ -1,125 +1,123 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Json +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests.Json; + +/// +/// Defines a network request for a DELETE call with a JSON response. +/// +public sealed class JsonDeleteNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; - using System.Text.Json; + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public JsonDeleteNetworkRequest(HttpClient client, string url) + : this(client, url, null) + { + } /// - /// Defines a network request for a DELETE call with a JSON response. + /// Initializes a new instance of the class. /// - public sealed class JsonDeleteNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The additional headers. + /// + public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary headers) + : base(url, headers) { - private readonly HttpClient client; + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public JsonDeleteNetworkRequest(HttpClient client, string url) - : this(client, url, null) - { - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The additional headers. - /// - public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - } + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException("No URL has been specified for executing the network request."); } - private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) - { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } + var uri = new Uri(this.Url); + var request = new HttpRequestMessage(HttpMethod.Delete, uri); - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } - - var uri = new Uri(this.Url); - var request = new HttpRequestMessage(HttpMethod.Delete, uri); - - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs index 00dc0e41..8e7650d2 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs @@ -1,125 +1,123 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Json +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests.Json; + +/// +/// Defines a network request for a GET call with a JSON response. +/// +public sealed class JsonGetNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; - using System.Text.Json; + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public JsonGetNetworkRequest(HttpClient client, string url) + : this(client, url, null) + { + } /// - /// Defines a network request for a GET call with a JSON response. + /// Initializes a new instance of the class. /// - public sealed class JsonGetNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The additional headers. + /// + public JsonGetNetworkRequest(HttpClient client, string url, Dictionary headers) + : base(url, headers) { - private readonly HttpClient client; + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public JsonGetNetworkRequest(HttpClient client, string url) - : this(client, url, null) - { - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The additional headers. - /// - public JsonGetNetworkRequest(HttpClient client, string url, Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - } + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException("No URL has been specified for executing the network request."); } - private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) - { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } + var uri = new Uri(this.Url); + var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } - - var uri = new Uri(this.Url); - var request = new HttpRequestMessage(HttpMethod.Get, uri); - - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs index 301e0afa..f99ff5af 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPatchNetworkRequest.cs @@ -1,162 +1,160 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Json +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests.Json; + +/// +/// Defines a network request for a PATCH call with a JSON response. +/// +public sealed class JsonPatchNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; - using System.Text.Json; + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public JsonPatchNetworkRequest(HttpClient client, string url) + : this(client, url, null, null) + { + } /// - /// Defines a network request for a PATCH call with a JSON response. + /// Initializes a new instance of the class. /// - public sealed class JsonPatchNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to post. + /// + public JsonPatchNetworkRequest(HttpClient client, string url, string jsonData) + : this(client, url, jsonData, null) { - private readonly HttpClient client; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public JsonPatchNetworkRequest(HttpClient client, string url) - : this(client, url, null, null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to post. - /// - public JsonPatchNetworkRequest(HttpClient client, string url, string jsonData) - : this(client, url, jsonData, null) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to post. + /// + /// + /// The additional headers. + /// + public JsonPatchNetworkRequest( + HttpClient client, + string url, + string jsonData, + Dictionary headers) + : base(url, headers) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.Data = jsonData; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to post. - /// - /// - /// The additional headers. - /// - public JsonPatchNetworkRequest( - HttpClient client, - string url, - string jsonData, - Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - this.Data = jsonData; - } + /// + /// Gets or sets the data. + /// + public string Data { get; set; } - /// - /// Gets or sets the data. - /// - public string Data { get; set; } - - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } - - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } + throw new InvalidOperationException("No URL has been specified for executing the network request."); + } - var uri = new Uri(this.Url); + var uri = new Uri(this.Url); - var request = new HttpRequestMessage - { - Method = new HttpMethod("PATCH"), - RequestUri = uri, - Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), - }; + var request = new HttpRequestMessage + { + Method = new HttpMethod("PATCH"), + RequestUri = uri, + Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), + }; - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs index 653432a6..11589e52 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPostNetworkRequest.cs @@ -1,160 +1,158 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Json +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests.Json; + +/// +/// Defines a network request for a POST call with a JSON response. +/// +public sealed class JsonPostNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; - using System.Text.Json; + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public JsonPostNetworkRequest(HttpClient client, string url) + : this(client, url, null, null) + { + } /// - /// Defines a network request for a POST call with a JSON response. + /// Initializes a new instance of the class. /// - public sealed class JsonPostNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to post. + /// + public JsonPostNetworkRequest(HttpClient client, string url, string jsonData) + : this(client, url, jsonData, null) { - private readonly HttpClient client; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public JsonPostNetworkRequest(HttpClient client, string url) - : this(client, url, null, null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to post. - /// - public JsonPostNetworkRequest(HttpClient client, string url, string jsonData) - : this(client, url, jsonData, null) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to post. + /// + /// + /// The additional headers. + /// + public JsonPostNetworkRequest( + HttpClient client, + string url, + string jsonData, + Dictionary headers) + : base(url, headers) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.Data = jsonData; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to post. - /// - /// - /// The additional headers. - /// - public JsonPostNetworkRequest( - HttpClient client, - string url, - string jsonData, - Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - this.Data = jsonData; - } + /// + /// Gets or sets the data. + /// + public string Data { get; set; } - /// - /// Gets or sets the data. - /// - public string Data { get; set; } - - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } - - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } + throw new InvalidOperationException("No URL has been specified for executing the network request."); + } - var uri = new Uri(this.Url); + var uri = new Uri(this.Url); - var request = new HttpRequestMessage(HttpMethod.Post, uri) - { - Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), - }; + var request = new HttpRequestMessage(HttpMethod.Post, uri) + { + Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), + }; - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs index 0dd2e85e..93eda6c6 100644 --- a/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Json/JsonPutNetworkRequest.cs @@ -1,156 +1,154 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Json +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests.Json; + +/// +/// Defines a network request for a PUT call with a JSON response. +/// +public sealed class JsonPutNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Text; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; - using System.Text.Json; + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public JsonPutNetworkRequest(HttpClient client, string url) + : this(client, url, null, null) + { + } /// - /// Defines a network request for a PUT call with a JSON response. + /// Initializes a new instance of the class. /// - public sealed class JsonPutNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to put. + /// + public JsonPutNetworkRequest(HttpClient client, string url, string jsonData) + : this(client, url, jsonData, null) { - private readonly HttpClient client; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public JsonPutNetworkRequest(HttpClient client, string url) - : this(client, url, null, null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to put. - /// - public JsonPutNetworkRequest(HttpClient client, string url, string jsonData) - : this(client, url, jsonData, null) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The JSON data to put. + /// + /// + /// The additional headers. + /// + public JsonPutNetworkRequest(HttpClient client, string url, string jsonData, Dictionary headers) + : base(url, headers) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.Data = jsonData; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The JSON data to put. - /// - /// - /// The additional headers. - /// - public JsonPutNetworkRequest(HttpClient client, string url, string jsonData, Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - this.Data = jsonData; - } + /// + /// Gets or sets the data. + /// + public string Data { get; set; } - /// - /// Gets or sets the data. - /// - public string Data { get; set; } - - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - string json = await this.GetJsonResponseAsync(cancellationToken); - return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - private async Task GetJsonResponseAsync(CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } - - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } + throw new InvalidOperationException("No URL has been specified for executing the network request."); + } - var uri = new Uri(this.Url); + var uri = new Uri(this.Url); - var request = new HttpRequestMessage(HttpMethod.Put, uri) - { - Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), - }; + var request = new HttpRequestMessage(HttpMethod.Put, uri) + { + Content = new StringContent(this.Data, Encoding.UTF8, "application/json"), + }; - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStringAsync(); - } + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Requests/NetworkRequest.cs b/src/MADE.Networking/Http/Requests/NetworkRequest.cs index d85e4ede..7cd5def0 100644 --- a/src/MADE.Networking/Http/Requests/NetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/NetworkRequest.cs @@ -1,86 +1,85 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace MADE.Networking.Http.Requests; +/// +/// Defines the model for a network request. +/// +public abstract class NetworkRequest : INetworkRequest +{ /// - /// Defines the model for a network request. + /// Initializes a new instance of the class. /// - public abstract class NetworkRequest : INetworkRequest + /// + /// The URL for the request. + /// + protected NetworkRequest(string url) + : this(url, null) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The URL for the request. - /// - protected NetworkRequest(string url) - : this(url, null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The URL for the request. - /// - /// - /// Additional headers for the request. - /// - protected NetworkRequest(string url, Dictionary headers) - { - this.Identifier = Guid.NewGuid(); - this.Url = url; - this.Headers = headers ?? new Dictionary(); - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The URL for the request. + /// + /// + /// Additional headers for the request. + /// + protected NetworkRequest(string url, Dictionary headers) + { + this.Identifier = Guid.NewGuid(); + this.Url = url; + this.Headers = headers ?? new Dictionary(); + } - /// - /// Gets the identifier for the request. - /// - public Guid Identifier { get; } + /// + /// Gets the identifier for the request. + /// + public Guid Identifier { get; } - /// - /// Gets or sets the URL for the request. - /// - public string Url { get; set; } + /// + /// Gets or sets the URL for the request. + /// + public string Url { get; set; } - /// - /// Gets the headers for the request. - /// - public Dictionary Headers { get; } + /// + /// Gets the headers for the request. + /// + public Dictionary Headers { get; } - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public abstract Task ExecuteAsync(CancellationToken cancellationToken = default); + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public abstract Task ExecuteAsync(CancellationToken cancellationToken = default); - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public abstract Task ExecuteAsync(Type expectedResponse, CancellationToken cancellationToken = default); - } -} \ No newline at end of file + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public abstract Task ExecuteAsync(Type expectedResponse, CancellationToken cancellationToken = default); +} diff --git a/src/MADE.Networking/Http/Requests/NetworkRequestCallback.cs b/src/MADE.Networking/Http/Requests/NetworkRequestCallback.cs index 38f4528b..ac6c738e 100644 --- a/src/MADE.Networking/Http/Requests/NetworkRequestCallback.cs +++ b/src/MADE.Networking/Http/Requests/NetworkRequestCallback.cs @@ -1,77 +1,75 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests -{ - using System; +using System; +using MADE.Runtime; - using MADE.Runtime; +namespace MADE.Networking.Http.Requests; +/// +/// Defines a model for a network request callback. +/// +public sealed class NetworkRequestCallback +{ /// - /// Defines a model for a network request callback. + /// Initializes a new instance of the class. /// - public sealed class NetworkRequestCallback + /// + /// The network request. + /// + public NetworkRequestCallback(NetworkRequest request) + : this(request, null, null) { - /// - /// Initializes a new instance of the class. - /// - /// - /// The network request. - /// - public NetworkRequestCallback(NetworkRequest request) - : this(request, null, null) - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The network request. - /// - /// - /// The success callback. - /// - public NetworkRequestCallback(NetworkRequest request, WeakReferenceCallback successCallback) - : this(request, successCallback, null) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The network request. + /// + /// + /// The success callback. + /// + public NetworkRequestCallback(NetworkRequest request, WeakReferenceCallback successCallback) + : this(request, successCallback, null) + { + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The network request. - /// - /// - /// The success callback. - /// - /// - /// The error callback. - /// - public NetworkRequestCallback( - NetworkRequest request, - WeakReferenceCallback successCallback, - WeakReferenceCallback errorCallback) - { - this.Request = request ?? throw new ArgumentNullException(nameof(request)); - this.SuccessCallback = successCallback; - this.ErrorCallback = errorCallback; - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The network request. + /// + /// + /// The success callback. + /// + /// + /// The error callback. + /// + public NetworkRequestCallback( + NetworkRequest request, + WeakReferenceCallback successCallback, + WeakReferenceCallback errorCallback) + { + this.Request = request ?? throw new ArgumentNullException(nameof(request)); + this.SuccessCallback = successCallback; + this.ErrorCallback = errorCallback; + } - /// - /// Gets the network process. - /// - public NetworkRequest Request { get; } + /// + /// Gets the network process. + /// + public NetworkRequest Request { get; } - /// - /// Gets the success callback. - /// - public WeakReferenceCallback SuccessCallback { get; } + /// + /// Gets the success callback. + /// + public WeakReferenceCallback SuccessCallback { get; } - /// - /// Gets the error callback. - /// - public WeakReferenceCallback ErrorCallback { get; } - } -} \ No newline at end of file + /// + /// Gets the error callback. + /// + public WeakReferenceCallback ErrorCallback { get; } +} diff --git a/src/MADE.Networking/Http/Requests/Streams/StreamGetNetworkRequest.cs b/src/MADE.Networking/Http/Requests/Streams/StreamGetNetworkRequest.cs index 28c92d25..4a0a3590 100644 --- a/src/MADE.Networking/Http/Requests/Streams/StreamGetNetworkRequest.cs +++ b/src/MADE.Networking/Http/Requests/Streams/StreamGetNetworkRequest.cs @@ -1,121 +1,120 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Requests.Streams +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace MADE.Networking.Http.Requests.Streams; + +/// +/// Defines a network request for a GET call with a data stream response. +/// +public sealed class StreamGetNetworkRequest : NetworkRequest { - using System; - using System.Collections.Generic; - using System.Net.Http; - using System.Threading; - using System.Threading.Tasks; + private readonly HttpClient client; /// - /// Defines a network request for a GET call with a data stream response. + /// Initializes a new instance of the class. /// - public sealed class StreamGetNetworkRequest : NetworkRequest + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + public StreamGetNetworkRequest(HttpClient client, string url) + : this(client, url, null) { - private readonly HttpClient client; + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - public StreamGetNetworkRequest(HttpClient client, string url) - : this(client, url, null) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The for executing the request. + /// + /// + /// The URL for the request. + /// + /// + /// The additional headers. + /// + public StreamGetNetworkRequest(HttpClient client, string url, Dictionary headers) + : base(url, headers) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + } - /// - /// Initializes a new instance of the class. - /// - /// - /// The for executing the request. - /// - /// - /// The URL for the request. - /// - /// - /// The additional headers. - /// - public StreamGetNetworkRequest(HttpClient client, string url, Dictionary headers) - : base(url, headers) - { - this.client = client ?? throw new ArgumentNullException(nameof(client)); - } + /// + /// Executes the network request. + /// + /// + /// The type of object returned from the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as the specified type. + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + return (TResponse)await this.GetStreamResponseAsync(cancellationToken).ConfigureAwait(false); + } - /// - /// Executes the network request. - /// - /// - /// The type of object returned from the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as the specified type. - /// - public override async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - return (TResponse)await this.GetStreamResponseAsync(cancellationToken); - } + /// + /// Executes the network request. + /// + /// + /// The type expected by the response of the request. + /// + /// + /// The cancellation token. + /// + /// + /// Returns the response of the request as an object. + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + return await this.GetStreamResponseAsync(cancellationToken).ConfigureAwait(false); + } - /// - /// Executes the network request. - /// - /// - /// The type expected by the response of the request. - /// - /// - /// The cancellation token. - /// - /// - /// Returns the response of the request as an object. - /// - public override async Task ExecuteAsync( - Type expectedResponse, - CancellationToken cancellationToken = default) + private async Task GetStreamResponseAsync(CancellationToken cancellationToken = default) + { + if (this.client == null) { - return await this.GetStreamResponseAsync(cancellationToken); + throw new InvalidOperationException( + "No HttpClient has been specified for executing the network request."); } - private async Task GetStreamResponseAsync(CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(this.Url)) { - if (this.client == null) - { - throw new InvalidOperationException( - "No HttpClient has been specified for executing the network request."); - } - - if (string.IsNullOrWhiteSpace(this.Url)) - { - throw new InvalidOperationException("No URL has been specified for executing the network request."); - } + throw new InvalidOperationException("No URL has been specified for executing the network request."); + } - var uri = new Uri(this.Url); - var request = new HttpRequestMessage(HttpMethod.Get, uri); + var uri = new Uri(this.Url); + var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (this.Headers != null) + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) { - foreach (KeyValuePair header in this.Headers) - { - request.Headers.Add(header.Key, header.Value); - } + request.Headers.Add(header.Key, header.Value); } + } - HttpResponseMessage response = await this.client.SendAsync( - request, - HttpCompletionOption.ResponseHeadersRead, - cancellationToken); + HttpResponseMessage response = await this.client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStreamAsync(); - } + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs index 6736157e..03c03fd4 100644 --- a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs +++ b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs @@ -1,140 +1,139 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Networking.Http.Responses +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using System.Text.Json; + +namespace MADE.Networking.Http.Responses; + +/// +/// Defines a HTTP response message that includes a deserializing option for the response data. +/// +/// The type of response expected. +public class HttpResponseMessage : IDisposable { - using System; - using System.Net; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Threading.Tasks; - using System.Text.Json; + private HttpResponseMessage? response; + private bool disposed; /// - /// Defines a HTTP response message that includes a deserializing option for the response data. + /// Initializes a new instance of the class with the original . /// - /// The type of response expected. - public class HttpResponseMessage : IDisposable + /// The original . + public HttpResponseMessage(HttpResponseMessage response) { - private HttpResponseMessage response; - private bool disposed; - - /// - /// Initializes a new instance of the class with the original . - /// - /// The original . - public HttpResponseMessage(HttpResponseMessage response) - { - this.response = response; - } + this.response = response; + } - /// - /// Gets the content of the HTTP response message. - /// - public HttpContent Content => this.response.Content; - - /// - /// Gets the collection of HTTP response headers. - /// - public HttpResponseHeaders Headers => this.response.Headers; - - /// - /// Gets a value indicating whether the HTTP response was successful. - /// - public bool IsSuccessStatusCode => this.response.IsSuccessStatusCode; - - /// - /// Gets the reason phrase that typically is sent by servers together with the status code. - /// - public string ReasonPhrase => this.response.ReasonPhrase; - - /// - /// Gets the request message which led to this response message. - /// - public HttpRequestMessage RequestMessage => this.response.RequestMessage; - - /// - /// Gets the status code of the HTTP response. - /// - public HttpStatusCode StatusCode => this.response.StatusCode; - - /// - /// Gets the HTTP message version. - /// - public Version Version => this.response.Version; - - /// - /// Gets the deserialized content of the original as the specified type. - /// - /// Note, ensure that has been called first, otherwise this value will be default. - /// - /// - public T DeserializedContent { get; private set; } - - /// - /// Allows conversion of a to the without direct casting. - /// - /// - /// The . - /// - /// - /// The . - /// - public static implicit operator HttpResponseMessage(HttpResponseMessage response) - { - return new HttpResponseMessage(response); - } + /// + /// Gets the content of the HTTP response message. + /// + public HttpContent Content => this.response.Content; - /// - /// Deserializes the content of the into the value. - /// - /// A representing the result of the asynchronous operation. - public async Task DeserializeAsync() - { - this.DeserializedContent = JsonSerializer.Deserialize( - await this.Content.ReadAsStringAsync(), - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - return this.DeserializedContent; - } + /// + /// Gets the collection of HTTP response headers. + /// + public HttpResponseHeaders Headers => this.response.Headers; - /// - /// Throws an exception if the property for the HTTP response is false. - /// - /// The HTTP response message if the call is successful. - public HttpResponseMessage EnsureSuccessStatusCode() - { - this.response.EnsureSuccessStatusCode(); - return this; - } + /// + /// Gets a value indicating whether the HTTP response was successful. + /// + public bool IsSuccessStatusCode => this.response.IsSuccessStatusCode; + + /// + /// Gets the reason phrase that typically is sent by servers together with the status code. + /// + public string ReasonPhrase => this.response.ReasonPhrase; + + /// + /// Gets the request message which led to this response message. + /// + public HttpRequestMessage RequestMessage => this.response.RequestMessage; + + /// + /// Gets the status code of the HTTP response. + /// + public HttpStatusCode StatusCode => this.response.StatusCode; + + /// + /// Gets the HTTP message version. + /// + public Version Version => this.response.Version; - /// - /// Releases the unmanaged resources and disposes of unmanaged resources used by the . - /// - public void Dispose() + /// + /// Gets the deserialized content of the original as the specified type. + /// + /// Note, ensure that has been called first, otherwise this value will be default. + /// + /// + public T DeserializedContent { get; private set; } + + /// + /// Allows conversion of a to the without direct casting. + /// + /// + /// The . + /// + /// + /// The . + /// + public static implicit operator HttpResponseMessage(HttpResponseMessage response) + { + return new HttpResponseMessage(response); + } + + /// + /// Deserializes the content of the into the value. + /// + /// A representing the result of the asynchronous operation. + public async Task DeserializeAsync() + { + this.DeserializedContent = JsonSerializer.Deserialize( + await this.Content.ReadAsStringAsync().ConfigureAwait(false), + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + return this.DeserializedContent; + } + + /// + /// Throws an exception if the property for the HTTP response is false. + /// + /// The HTTP response message if the call is successful. + public HttpResponseMessage EnsureSuccessStatusCode() + { + this.response.EnsureSuccessStatusCode(); + return this; + } + + /// + /// Releases the unmanaged resources and disposes of unmanaged resources used by the . + /// + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the and optionally disposes of the managed resources. + /// + /// A value indicating whether to release both managed and unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (this.disposed) { - this.Dispose(disposing: true); - GC.SuppressFinalize(this); + return; } - /// - /// Releases the unmanaged resources used by the and optionally disposes of the managed resources. - /// - /// A value indicating whether to release both managed and unmanaged resources. - protected virtual void Dispose(bool disposing) + if (disposing) { - if (this.disposed) - { - return; - } - - if (disposing) - { - this.response.Dispose(); - } - - this.response = null; - this.DeserializedContent = default; - this.disposed = true; + this.response.Dispose(); } + + this.response = null; + this.DeserializedContent = default; + this.disposed = true; } -} \ No newline at end of file +} diff --git a/src/MADE.Runtime/Actions/Chain.cs b/src/MADE.Runtime/Actions/Chain.cs index 6697d7cc..09af7499 100644 --- a/src/MADE.Runtime/Actions/Chain.cs +++ b/src/MADE.Runtime/Actions/Chain.cs @@ -1,93 +1,92 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime.Actions +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MADE.Runtime.Actions; + +/// +/// Defines an implementation for a chain of objects. +/// +/// The type of object being chained. +public class Chain : IChain + where T : class { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; + private readonly List> chain = new(); /// - /// Defines an implementation for a chain of objects. + /// Initializes a new instance of the class with an instance. /// - /// The type of object being chained. - public class Chain : IChain - where T : class + /// The instance to begin the chain. + public Chain(T instance) { - private readonly List> chain = new(); - - /// - /// Initializes a new instance of the class with an instance. - /// - /// The instance to begin the chain. - public Chain(T instance) - { - this.chain.Add(new WeakReference(instance)); - } + this.chain.Add(new WeakReference(instance)); + } - /// - /// Initializes a new instance of the class with a collection of instances. - /// - /// The instances to begin the chain. - public Chain(IEnumerable instances) - { - this.chain.AddRange(instances.Select(i => new WeakReference(i))); - } + /// + /// Initializes a new instance of the class with a collection of instances. + /// + /// The instances to begin the chain. + public Chain(IEnumerable instances) + { + this.chain.AddRange(instances.Select(i => new WeakReference(i))); + } - /// - /// Concatenates the current instances in the chain with the specified instance. - /// - /// The instance to chain. - /// The updated . - public Chain With(T instance) - { - this.chain.Add(new WeakReference(instance)); - return this; - } + /// + /// Concatenates the current instances in the chain with the specified instance. + /// + /// The instance to chain. + /// The updated . + public Chain With(T instance) + { + this.chain.Add(new WeakReference(instance)); + return this; + } - /// - /// Concatenates the current instances in the chain with the specified instances. - /// - /// The instances to chain. - /// The updated . - public Chain With(IEnumerable instances) - { - this.chain.AddRange(instances.Select(i => new WeakReference(i))); - return this; - } + /// + /// Concatenates the current instances in the chain with the specified instances. + /// + /// The instances to chain. + /// The updated . + public Chain With(IEnumerable instances) + { + this.chain.AddRange(instances.Select(i => new WeakReference(i))); + return this; + } - /// - /// Invokes an action with the chain. - /// - /// The action to invoke. - /// Potential exceptions thrown if delegate callback throws an exception. - public void Invoke(Action func) + /// + /// Invokes an action with the chain. + /// + /// The action to invoke. + /// Potential exceptions thrown if delegate callback throws an exception. + public void Invoke(Action func) + { + foreach (WeakReference instance in this.chain) { - foreach (WeakReference instance in this.chain) + if (instance.TryGetTarget(out T i)) { - if (instance.TryGetTarget(out T i)) - { - func(i); - } + func(i); } } + } - /// - /// Invokes an asynchronous action with the chain. - /// - /// The asynchronous action to invoke. - /// An asynchronous operation. - /// Potential exceptions thrown if delegate callback throws an exception. - public async Task InvokeAsync(Func func) + /// + /// Invokes an asynchronous action with the chain. + /// + /// The asynchronous action to invoke. + /// An asynchronous operation. + /// Potential exceptions thrown if delegate callback throws an exception. + public async Task InvokeAsync(Func func) + { + foreach (WeakReference instance in this.chain) { - foreach (WeakReference instance in this.chain) + if (instance.TryGetTarget(out T i)) { - if (instance.TryGetTarget(out T i)) - { - await func(i); - } + await func(i).ConfigureAwait(false); } } } -} \ No newline at end of file +} diff --git a/src/MADE.Runtime/Actions/IChain.cs b/src/MADE.Runtime/Actions/IChain.cs index 0495e8a2..91beb57f 100644 --- a/src/MADE.Runtime/Actions/IChain.cs +++ b/src/MADE.Runtime/Actions/IChain.cs @@ -1,44 +1,43 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime.Actions -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MADE.Runtime.Actions; +/// +/// Defines an interface for a chain of objects. +/// +/// The type of object being chained. +public interface IChain + where T : class +{ /// - /// Defines an interface for a chain of objects. + /// Concatenates the current instances in the chain with the specified instance. /// - /// The type of object being chained. - public interface IChain - where T : class - { - /// - /// Concatenates the current instances in the chain with the specified instance. - /// - /// The instance to chain. - /// The updated . - Chain With(T instance); + /// The instance to chain. + /// The updated . + Chain With(T instance); - /// - /// Concatenates the current instances in the chain with the specified instances. - /// - /// The instances to chain. - /// The updated . - Chain With(IEnumerable instances); + /// + /// Concatenates the current instances in the chain with the specified instances. + /// + /// The instances to chain. + /// The updated . + Chain With(IEnumerable instances); - /// - /// Invokes an action with the chain. - /// - /// The action to invoke. - void Invoke(Action func); + /// + /// Invokes an action with the chain. + /// + /// The action to invoke. + void Invoke(Action func); - /// - /// Invokes an asynchronous action with the chain. - /// - /// The asynchronous action to invoke. - /// An asynchronous operation. - Task InvokeAsync(Func func); - } -} \ No newline at end of file + /// + /// Invokes an asynchronous action with the chain. + /// + /// The asynchronous action to invoke. + /// An asynchronous operation. + Task InvokeAsync(Func func); +} diff --git a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs index c5f574c7..d8a790bf 100644 --- a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs +++ b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs @@ -1,45 +1,44 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime.Extensions -{ - using System; - using System.Collections.Generic; - using System.Reflection; +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace MADE.Runtime.Extensions; +/// +/// Defines a collection of extensions for object reflection. +/// +public static class ReflectionExtensions +{ /// - /// Defines a collection of extensions for object reflection. + /// Gets a value for a property of the specified object based on the specified property name. /// - public static class ReflectionExtensions + /// The object to retrieve the property value from. + /// The name of the property to retrieve a value for. + /// The type of expected value. + /// The value of the property. + /// More than one property is found with the specified name. + public static T GetPropertyValue(this object obj, string property) + where T : class { - /// - /// Gets a value for a property of the specified object based on the specified property name. - /// - /// The object to retrieve the property value from. - /// The name of the property to retrieve a value for. - /// The type of expected value. - /// The value of the property. - /// More than one property is found with the specified name. - public static T GetPropertyValue(this object obj, string property) - where T : class - { - Type type = obj.GetType(); - PropertyInfo prop = type.GetProperty(property); - return prop?.GetValue(obj) as T; - } + Type type = obj.GetType(); + PropertyInfo prop = type.GetProperty(property); + return prop?.GetValue(obj) as T; + } - /// - /// Gets all the property names declared for the specified object. - /// - /// The object to retrieve property names from. - /// A collection of object property names as a string. - public static IEnumerable GetPropertyNames(this object obj) + /// + /// Gets all the property names declared for the specified object. + /// + /// The object to retrieve property names from. + /// A collection of object property names as a string. + public static IEnumerable GetPropertyNames(this object obj) + { + Type type = obj.GetType(); + foreach (PropertyInfo property in type.GetTypeInfo().DeclaredProperties) { - Type type = obj.GetType(); - foreach (PropertyInfo property in type.GetTypeInfo().DeclaredProperties) - { - yield return property.Name; - } + yield return property.Name; } } -} \ No newline at end of file +} diff --git a/src/MADE.Runtime/WeakReferenceCallback.cs b/src/MADE.Runtime/WeakReferenceCallback.cs index d6858d6a..b296cc64 100644 --- a/src/MADE.Runtime/WeakReferenceCallback.cs +++ b/src/MADE.Runtime/WeakReferenceCallback.cs @@ -1,64 +1,63 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime +using System; +using System.Reflection; + +namespace MADE.Runtime; + +/// +/// Defines a model for providing a weak referenced callback. +/// +public sealed class WeakReferenceCallback { - using System; - using System.Reflection; + private readonly MethodInfo actionInfo; + + private readonly WeakReference weakReference; /// - /// Defines a model for providing a weak referenced callback. + /// Initializes a new instance of the class. /// - public sealed class WeakReferenceCallback + /// + /// The callback action. + /// + /// + /// The expected type for the callback. + /// + /// The callback throws an exception acquiring method info. + public WeakReferenceCallback(Delegate action, Type callbackType) { - private readonly MethodInfo actionInfo; + this.actionInfo = action.GetMethodInfo(); + this.weakReference = new WeakReference(action.Target); + this.Type = callbackType; + } - private readonly WeakReference weakReference; + /// + /// Gets a value indicating whether the callback is alive. + /// + public bool IsAlive => this.weakReference.IsAlive; - /// - /// Initializes a new instance of the class. - /// - /// - /// The callback action. - /// - /// - /// The expected type for the callback. - /// - /// The callback throws an exception acquiring method info. - public WeakReferenceCallback(Delegate action, Type callbackType) + /// + /// Gets the expected type for the callback. + /// + public Type Type { get; } + + /// + /// Invokes the callback with the specified parameter. + /// + /// + /// The parameter to pass to the callback. + /// + /// The associated weak reference is no longer alive. + public void Invoke(object param) + { + if (this.IsAlive) { - this.actionInfo = action.GetMethodInfo(); - this.weakReference = new WeakReference(action.Target); - this.Type = callbackType; + this.actionInfo.Invoke(this.weakReference.Target, new[] { param }); } - - /// - /// Gets a value indicating whether the callback is alive. - /// - public bool IsAlive => this.weakReference.IsAlive; - - /// - /// Gets the expected type for the callback. - /// - public Type Type { get; } - - /// - /// Invokes the callback with the specified parameter. - /// - /// - /// The parameter to pass to the callback. - /// - /// The associated weak reference is no longer alive. - public void Invoke(object param) + else { - if (this.IsAlive) - { - this.actionInfo.Invoke(this.weakReference.Target, new[] { param }); - } - else - { - throw new InvalidOperationException("The callback is no longer alive."); - } + throw new InvalidOperationException("The callback is no longer alive."); } } -} \ No newline at end of file +} diff --git a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs index 7da5cffd..4e7573c6 100644 --- a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs +++ b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs @@ -1,91 +1,87 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime +using System; + +namespace MADE.Runtime; + +/// +/// Defines a model for providing a weak referenced event listener. +/// +/// +/// The instance type for the listener. +/// +/// +/// The source type. +/// +/// +/// The event argument type. +/// +public sealed class WeakReferenceEventListener + where TInstance : class { - using System; + private readonly WeakReference weakInstance; /// - /// Defines a model for providing a weak referenced event listener. + /// Initializes a new instance of the class. /// - /// - /// The instance type for the listener. - /// - /// - /// The source type. - /// - /// - /// The event argument type. - /// - public sealed class WeakReferenceEventListener - where TInstance : class + /// + /// The instance. + /// + /// Thrown if the is . + public WeakReferenceEventListener(TInstance instance) { - private readonly WeakReference weakInstance; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance. - /// - /// Thrown if the is . - public WeakReferenceEventListener(TInstance instance) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } + ArgumentNullException.ThrowIfNull(instance); - this.weakInstance = new WeakReference(instance); - } + this.weakInstance = new WeakReference(instance); + } - /// - /// Gets or sets the action to be fired when the event is triggered. - /// - public Action OnEventAction { get; set; } + /// + /// Gets or sets the action to be fired when the event is triggered. + /// + public Action? OnEventAction { get; set; } - /// - /// Gets or sets the action to be fired when the listener is detached. - /// - public Action> OnDetachAction { get; set; } + /// + /// Gets or sets the action to be fired when the listener is detached. + /// + public Action>? OnDetachAction { get; set; } - /// - /// Called when the event is fired. - /// - /// - /// The source of the event. - /// - /// - /// The event arguments. - /// - /// Potentially thrown by the delegate callback. - public void OnEvent(TSource source, TEventArgs eventArgs) + /// + /// Called when the event is fired. + /// + /// + /// The source of the event. + /// + /// + /// The event arguments. + /// + /// Potentially thrown by the delegate callback. + public void OnEvent(TSource source, TEventArgs eventArgs) + { + var target = (TInstance)this.weakInstance.Target; + if (target != null) { - var target = (TInstance)this.weakInstance.Target; - if (target != null) - { - this.OnEventAction?.Invoke(target, source, eventArgs); - } - else - { - this.Detach(); - } + this.OnEventAction?.Invoke(target, source, eventArgs); } - - /// - /// Called when detaching the event listener. - /// - /// Potentially thrown by the delegate callback. - public void Detach() + else { - var target = (TInstance)this.weakInstance.Target; - if (this.OnDetachAction == null) - { - return; - } + this.Detach(); + } + } - this.OnDetachAction(target, this); - this.OnDetachAction = null; + /// + /// Called when detaching the event listener. + /// + /// Potentially thrown by the delegate callback. + public void Detach() + { + var target = (TInstance)this.weakInstance.Target; + if (this.OnDetachAction == null) + { + return; } + + this.OnDetachAction(target, this); + this.OnDetachAction = null; } -} \ No newline at end of file +} diff --git a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs index ff4a2d40..ef8339ad 100644 --- a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs +++ b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs @@ -1,85 +1,81 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Runtime +using System; + +namespace MADE.Runtime; + +/// +/// Defines a model for providing a weak referenced event listener. +/// +/// +/// The instance type for the listener. +/// +/// +/// The source type. +/// +public sealed class WeakReferenceEventListener + where TInstance : class { - using System; + private readonly WeakReference weakReference; /// - /// Defines a model for providing a weak referenced event listener. + /// Initializes a new instance of the class. /// - /// - /// The instance type for the listener. - /// - /// - /// The source type. - /// - public sealed class WeakReferenceEventListener - where TInstance : class + /// + /// The instance. + /// + /// Thrown if the is . + public WeakReferenceEventListener(TInstance instance) { - private readonly WeakReference weakReference; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The instance. - /// - /// Thrown if the is . - public WeakReferenceEventListener(TInstance instance) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } + ArgumentNullException.ThrowIfNull(instance); - this.weakReference = new WeakReference(instance); - } + this.weakReference = new WeakReference(instance); + } - /// - /// Gets or sets the action to be fired when the event is triggered. - /// - public Action OnEventAction { get; set; } + /// + /// Gets or sets the action to be fired when the event is triggered. + /// + public Action? OnEventAction { get; set; } - /// - /// Gets or sets the action to be fired when the listener is detached. - /// - public Action> OnDetachAction { get; set; } + /// + /// Gets or sets the action to be fired when the listener is detached. + /// + public Action>? OnDetachAction { get; set; } - /// - /// Called when the event is fired. - /// - /// - /// The source of the event. - /// - /// Potentially thrown by the delegate callback. - public void OnEvent(TSource source) + /// + /// Called when the event is fired. + /// + /// + /// The source of the event. + /// + /// Potentially thrown by the delegate callback. + public void OnEvent(TSource source) + { + var target = (TInstance)this.weakReference.Target; + if (target != null) { - var target = (TInstance)this.weakReference.Target; - if (target != null) - { - this.OnEventAction?.Invoke(target, source); - } - else - { - this.Detach(); - } + this.OnEventAction?.Invoke(target, source); } - - /// - /// Called when detaching the event listener. - /// - /// Potentially thrown by the delegate callback. - public void Detach() + else { - var target = (TInstance)this.weakReference.Target; - if (this.OnDetachAction == null) - { - return; - } + this.Detach(); + } + } - this.OnDetachAction(target, this); - this.OnDetachAction = null; + /// + /// Called when detaching the event listener. + /// + /// Potentially thrown by the delegate callback. + public void Detach() + { + var target = (TInstance)this.weakReference.Target; + if (this.OnDetachAction == null) + { + return; } + + this.OnDetachAction(target, this); + this.OnDetachAction = null; } -} \ No newline at end of file +} diff --git a/src/MADE.Testing/CollectionAssertExtensions.cs b/src/MADE.Testing/CollectionAssertExtensions.cs index c2f08ca5..94b5e97c 100644 --- a/src/MADE.Testing/CollectionAssertExtensions.cs +++ b/src/MADE.Testing/CollectionAssertExtensions.cs @@ -1,213 +1,212 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Testing -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace MADE.Testing; +/// +/// Defines a code assertion helper for collection based scenarios. +/// +public static class CollectionAssertExtensions +{ /// - /// Defines a code assertion helper for collection based scenarios. + /// Tests whether two collections contain the same elements and throws an exception if either collection contains an element not in the other collection. /// - public static class CollectionAssertExtensions + /// + /// The type of item in the collection. + /// + /// + /// The first collection to compare. This contains the elements the test expects. + /// + /// + /// The second collection to compare. This is the collection produced by the code under test. + /// + /// Thrown if the condition of equivalency could not be met. + public static void ShouldBeEquivalentTo(this IEnumerable expected, IEnumerable actual) { - /// - /// Tests whether two collections contain the same elements and throws an exception if either collection contains an element not in the other collection. - /// - /// - /// The type of item in the collection. - /// - /// - /// The first collection to compare. This contains the elements the test expects. - /// - /// - /// The second collection to compare. This is the collection produced by the code under test. - /// - /// Thrown if the condition of equivalency could not be met. - public static void ShouldBeEquivalentTo(this IEnumerable expected, IEnumerable actual) + if ((expected == null) != (actual == null)) { - if ((expected == null) != (actual == null)) - { - throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} or {nameof(actual)} provided is null."); - } - - if (Equals(expected, actual)) - { - return; - } - - var expectedList = expected.ToList(); - var actualList = actual.ToList(); - if (expectedList.Count != actualList.Count) - { - throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. The number of elements are different."); - } + throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} or {nameof(actual)} provided is null."); + } - if (expectedList.Count == 0 || - !FindMismatchedElement( - expectedList, - actualList, - out _, - out _, - out object mismatchedElement)) - { - return; - } + if (Equals(expected, actual)) + { + return; + } - throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. The collections contain mismatched elements. {mismatchedElement ?? "Element was null."}"); + var expectedList = expected.ToList(); + var actualList = actual.ToList(); + if (expectedList.Count != actualList.Count) + { + throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. The number of elements are different."); } - /// - /// Tests whether two collections do not contain the same elements. - /// - /// - /// The type of item in the collection. - /// - /// - /// The first collection to compare. This contains the elements the test expects. - /// - /// - /// The second collection to compare. This is the collection produced by the code under test. - /// - /// Thrown if the condition of equivalency could not be met. - public static void ShouldNotBeEquivalentTo(this IEnumerable expected, IEnumerable actual) + if (expectedList.Count == 0 || + !FindMismatchedElement( + expectedList, + actualList, + out _, + out _, + out object mismatchedElement)) { - if ((expected == null) != (actual == null)) - { - throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} or {nameof(actual)} provided is null."); - } + return; + } - if (Equals(expected, actual)) - { - throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} and {nameof(actual)} are equal."); - } + throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. The collections contain mismatched elements. {mismatchedElement ?? "Element was null."}"); + } - var expectedList = expected.ToList(); - var actualList = actual.ToList(); - if (expectedList.Count != actualList.Count) - { - // The counts are different so cannot possibly be the same. - return; - } + /// + /// Tests whether two collections do not contain the same elements. + /// + /// + /// The type of item in the collection. + /// + /// + /// The first collection to compare. This contains the elements the test expects. + /// + /// + /// The second collection to compare. This is the collection produced by the code under test. + /// + /// Thrown if the condition of equivalency could not be met. + public static void ShouldNotBeEquivalentTo(this IEnumerable expected, IEnumerable actual) + { + if ((expected == null) != (actual == null)) + { + throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} or {nameof(actual)} provided is null."); + } - if (expectedList.Count == 0 || - !FindMismatchedElement( - expectedList, - actualList, - out _, - out _, - out _)) - { - throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. The collections do not contain mismatched elements."); - } + if (Equals(expected, actual)) + { + throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} and {nameof(actual)} are equal."); } - /// - /// Finds a mismatched element between the two collections. A mismatched - /// element is one that appears a different number of times in the - /// expected collection than it does in the actual collection. The - /// collections are assumed to be different non-null references with the - /// same number of elements. The caller is responsible for this level of - /// verification. If there is no mismatched element, the function returns - /// false and the out parameters should not be used. - /// - /// The first collection to compare. - /// The second collection to compare. - /// - /// The expected number of occurrences of - /// or 0 if there is no mismatched - /// element. - /// - /// - /// The actual number of occurrences of - /// or 0 if there is no mismatched - /// element. - /// - /// - /// The mismatched element (may be null) or null if there is no - /// mismatched element. - /// - /// - /// True if a mismatched element was found; false otherwise. - /// - private static bool FindMismatchedElement( - IEnumerable expected, - IEnumerable actual, - out int expectedCount, - out int actualCount, - out object mismatchedElement) + var expectedList = expected.ToList(); + var actualList = actual.ToList(); + if (expectedList.Count != actualList.Count) { - Dictionary elementCounts1 = GetElementCounts(expected, out int nullCount1); - Dictionary elementCounts2 = GetElementCounts(actual, out int nullCount2); + // The counts are different so cannot possibly be the same. + return; + } - if (nullCount2 != nullCount1) - { - expectedCount = nullCount1; - actualCount = nullCount2; - mismatchedElement = null; - return true; - } + if (expectedList.Count == 0 || + !FindMismatchedElement( + expectedList, + actualList, + out _, + out _, + out _)) + { + throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. The collections do not contain mismatched elements."); + } + } - foreach (object key in elementCounts1.Keys) - { - elementCounts1.TryGetValue(key, out expectedCount); - elementCounts2.TryGetValue(key, out actualCount); - if (expectedCount == actualCount) - { - continue; - } - - mismatchedElement = key; - return true; - } + /// + /// Finds a mismatched element between the two collections. A mismatched + /// element is one that appears a different number of times in the + /// expected collection than it does in the actual collection. The + /// collections are assumed to be different non-null references with the + /// same number of elements. The caller is responsible for this level of + /// verification. If there is no mismatched element, the function returns + /// false and the out parameters should not be used. + /// + /// The first collection to compare. + /// The second collection to compare. + /// + /// The expected number of occurrences of + /// or 0 if there is no mismatched + /// element. + /// + /// + /// The actual number of occurrences of + /// or 0 if there is no mismatched + /// element. + /// + /// + /// The mismatched element (may be null) or null if there is no + /// mismatched element. + /// + /// + /// True if a mismatched element was found; false otherwise. + /// + private static bool FindMismatchedElement( + IEnumerable expected, + IEnumerable actual, + out int expectedCount, + out int actualCount, + out object mismatchedElement) + { + Dictionary elementCounts1 = GetElementCounts(expected, out int nullCount1); + Dictionary elementCounts2 = GetElementCounts(actual, out int nullCount2); - expectedCount = 0; - actualCount = 0; + if (nullCount2 != nullCount1) + { + expectedCount = nullCount1; + actualCount = nullCount2; mismatchedElement = null; - return false; + return true; } - /// - /// Constructs a dictionary containing the number of occurrences of each - /// element in the specified collection. - /// - /// The collection to process. - /// - /// The number of null elements in the collection. - /// - /// - /// A dictionary containing the number of occurrences of each element - /// in the specified collection. - /// - private static Dictionary GetElementCounts(IEnumerable collection, out int nullCount) + foreach (object key in elementCounts1.Keys) { - var dictionary = new Dictionary(); - nullCount = 0; - foreach (object key in collection) + elementCounts1.TryGetValue(key, out expectedCount); + elementCounts2.TryGetValue(key, out actualCount); + if (expectedCount == actualCount) { - if (key == null) - { - ++nullCount; - } - else - { - dictionary.TryGetValue(key, out int num); - ++num; - dictionary[key] = num; - } + continue; } - return dictionary; + mismatchedElement = key; + return true; } - private class AssertFailedException : Exception + expectedCount = 0; + actualCount = 0; + mismatchedElement = null; + return false; + } + + /// + /// Constructs a dictionary containing the number of occurrences of each + /// element in the specified collection. + /// + /// The collection to process. + /// + /// The number of null elements in the collection. + /// + /// + /// A dictionary containing the number of occurrences of each element + /// in the specified collection. + /// + private static Dictionary GetElementCounts(IEnumerable collection, out int nullCount) + { + var dictionary = new Dictionary(); + nullCount = 0; + foreach (object key in collection) { - public AssertFailedException(string message) - : base(message) + if (key == null) + { + ++nullCount; + } + else { + dictionary.TryGetValue(key, out int num); + ++num; + dictionary[key] = num; } } + + return dictionary; + } + + private class AssertFailedException : Exception + { + public AssertFailedException(string message) + : base(message) + { + } } -} \ No newline at end of file +} diff --git a/src/MADE.Threading/ITimer.cs b/src/MADE.Threading/ITimer.cs index 5f1d28c3..dd66edbe 100644 --- a/src/MADE.Threading/ITimer.cs +++ b/src/MADE.Threading/ITimer.cs @@ -1,59 +1,58 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Threading +using System; + +namespace MADE.Threading; + +/// +/// Defines an interface for a timer to use for performing actions on a tick. +/// +public interface ITimer { - using System; - - /// - /// Defines an interface for a timer to use for performing actions on a tick. - /// - public interface ITimer - { - /// - /// Occurs when the timer ticks over the specified . - /// - event EventHandler Tick; - - /// - /// Gets or sets the interval between initiating the event. - /// - TimeSpan Interval { get; set; } - - /// - /// Gets a value indicating whether the timer is currently running. - /// - bool IsRunning { get; } - - /// - /// Gets or sets the time before initiating the first event. - /// - TimeSpan DueTime { get; set; } - - /// - /// Starts the timer. - /// - void Start(); - - /// - /// Starts the timer after the given . - /// - /// - /// The time before initiating the first event. - /// - void Start(TimeSpan dueTime); - - /// - /// Starts the timer after the given in milliseconds. - /// - /// - /// The time before initiating the first event in milliseconds. - /// - void Start(int dueTime); - - /// - /// Stops the timer. - /// - void Stop(); - } -} \ No newline at end of file + /// + /// Occurs when the timer ticks over the specified . + /// + event EventHandler Tick; + + /// + /// Gets or sets the interval between initiating the event. + /// + TimeSpan Interval { get; set; } + + /// + /// Gets a value indicating whether the timer is currently running. + /// + bool IsRunning { get; } + + /// + /// Gets or sets the time before initiating the first event. + /// + TimeSpan DueTime { get; set; } + + /// + /// Starts the timer. + /// + void Start(); + + /// + /// Starts the timer after the given . + /// + /// + /// The time before initiating the first event. + /// + void Start(TimeSpan dueTime); + + /// + /// Starts the timer after the given in milliseconds. + /// + /// + /// The time before initiating the first event in milliseconds. + /// + void Start(int dueTime); + + /// + /// Stops the timer. + /// + void Stop(); +} diff --git a/src/MADE.Threading/TaskExtensions.cs b/src/MADE.Threading/TaskExtensions.cs index b5aa2114..93733186 100644 --- a/src/MADE.Threading/TaskExtensions.cs +++ b/src/MADE.Threading/TaskExtensions.cs @@ -1,78 +1,77 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Threading -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MADE.Threading; +/// +/// Defines a collection of extensions for tasks. +/// +public static class TaskExtensions +{ /// - /// Defines a collection of extensions for tasks. + /// Observes the exceptions of faulted tasks. /// - public static class TaskExtensions + /// The task to observe for exceptions. + /// An action invoked when an exception is caught. + /// An asynchronous operation. + /// Potentially thrown by the delegate callback. + public static Task AndObserveExceptions(this Task task, Action onException = null) { - /// - /// Observes the exceptions of faulted tasks. - /// - /// The task to observe for exceptions. - /// An action invoked when an exception is caught. - /// An asynchronous operation. - /// Potentially thrown by the delegate callback. - public static Task AndObserveExceptions(this Task task, Action onException = null) - { - task?.ContinueWith( - t => - { - AggregateException aggregateException = t.Exception?.Flatten(); - onException?.Invoke(aggregateException); - }, - TaskContinuationOptions.OnlyOnFaulted); + task?.ContinueWith( + t => + { + AggregateException aggregateException = t.Exception?.Flatten(); + onException?.Invoke(aggregateException); + }, + TaskContinuationOptions.OnlyOnFaulted); - return task; - } + return task; + } - /// - /// Observes the exceptions of faulted tasks. - /// - /// - /// The instance type for the listener. - /// - /// The task to observe for exceptions. - /// An action invoked when an exception is caught. - /// An asynchronous operation. - /// Potentially thrown by the delegate callback. - public static Task AndObserveExceptions(this Task task, Action onException = null) - { - task?.ContinueWith( - t => - { - AggregateException aggregateException = t.Exception?.Flatten(); - onException?.Invoke(aggregateException); - }, - TaskContinuationOptions.OnlyOnFaulted); + /// + /// Observes the exceptions of faulted tasks. + /// + /// + /// The instance type for the listener. + /// + /// The task to observe for exceptions. + /// An action invoked when an exception is caught. + /// An asynchronous operation. + /// Potentially thrown by the delegate callback. + public static Task AndObserveExceptions(this Task task, Action onException = null) + { + task?.ContinueWith( + t => + { + AggregateException aggregateException = t.Exception?.Flatten(); + onException?.Invoke(aggregateException); + }, + TaskContinuationOptions.OnlyOnFaulted); - return task; - } + return task; + } - /// - /// Creates a task that will complete when all of the objects in the collection have completed. - /// - /// The tasks to wait on for completion. - /// A task that represents the completion of all of the supplied tasks. - public static async Task WhenAll(this IEnumerable tasks) - { - await Task.WhenAll(tasks); - } + /// + /// Creates a task that will complete when all of the objects in the collection have completed. + /// + /// The tasks to wait on for completion. + /// A task that represents the completion of all of the supplied tasks. + public static async Task WhenAll(this IEnumerable tasks) + { + await Task.WhenAll(tasks).ConfigureAwait(false); + } - /// - /// Creates a task that will complete when any of the objects in the collection have completed. - /// - /// The tasks to wait on for completion. - /// A task that represents the completion of one of the supplied tasks. - public static async Task WhenAny(this IEnumerable tasks) - { - await Task.WhenAny(tasks); - } + /// + /// Creates a task that will complete when any of the objects in the collection have completed. + /// + /// The tasks to wait on for completion. + /// A task that represents the completion of one of the supplied tasks. + public static async Task WhenAny(this IEnumerable tasks) + { + await Task.WhenAny(tasks).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Threading/Timer.cs b/src/MADE.Threading/Timer.cs index c24299ff..6d45874a 100644 --- a/src/MADE.Threading/Timer.cs +++ b/src/MADE.Threading/Timer.cs @@ -1,148 +1,147 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Threading +using System; +using System.Threading; + +namespace MADE.Threading; + +/// +/// Defines a timer to use for performing actions on a tick. +/// +public class Timer : ITimer, IDisposable { - using System; - using System.Threading; + private System.Threading.Timer? timer; + + /// + /// Occurs when the timer ticks over the specified . + /// + public event EventHandler Tick; + + /// + /// Gets or sets the interval between initiating the event. + /// + public TimeSpan Interval { get; set; } = Timeout.InfiniteTimeSpan; + + /// + /// Gets a value indicating whether the timer is currently running. + /// + public bool IsRunning { get; private set; } /// - /// Defines a timer to use for performing actions on a tick. + /// Gets or sets the time before initiating the first event. /// - public class Timer : ITimer, IDisposable + public TimeSpan DueTime { get; set; } = TimeSpan.FromSeconds(0); + + /// + /// Starts the timer. + /// + public void Start() { - private System.Threading.Timer timer; - - /// - /// Occurs when the timer ticks over the specified . - /// - public event EventHandler Tick; - - /// - /// Gets or sets the interval between initiating the event. - /// - public TimeSpan Interval { get; set; } = Timeout.InfiniteTimeSpan; - - /// - /// Gets a value indicating whether the timer is currently running. - /// - public bool IsRunning { get; private set; } - - /// - /// Gets or sets the time before initiating the first event. - /// - public TimeSpan DueTime { get; set; } = TimeSpan.FromSeconds(0); - - /// - /// Starts the timer. - /// - public void Start() + if (this.timer == null) { - if (this.timer == null) - { - this.timer = new System.Threading.Timer( - c => this.InvokeTick(), - null, - 0, - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - else - { - this.timer.Change( - (int)Math.Ceiling(this.DueTime.TotalMilliseconds), - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - - this.IsRunning = true; + this.timer = new System.Threading.Timer( + c => this.InvokeTick(), + null, + 0, + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - - /// - /// Starts the timer after the given . - /// - /// - /// The time before initiating the first event. - /// - public void Start(TimeSpan dueTime) + else { - if (this.timer == null) - { - this.timer = new System.Threading.Timer( - c => this.InvokeTick(), - null, - dueTime.Milliseconds, - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - else - { - this.timer.Change( - (int)Math.Ceiling(this.DueTime.TotalMilliseconds), - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - - this.IsRunning = true; + this.timer.Change( + (int)Math.Ceiling(this.DueTime.TotalMilliseconds), + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - /// - /// Starts the timer after the given in milliseconds. - /// - /// - /// The time before initiating the first event in milliseconds. - /// - public void Start(int dueTime) + this.IsRunning = true; + } + + /// + /// Starts the timer after the given . + /// + /// + /// The time before initiating the first event. + /// + public void Start(TimeSpan dueTime) + { + if (this.timer == null) { - if (this.timer == null) - { - this.timer = new System.Threading.Timer( - x => this.InvokeTick(), - null, - dueTime, - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - else - { - this.timer.Change( - (int)Math.Ceiling(this.DueTime.TotalMilliseconds), - (int)Math.Ceiling(this.Interval.TotalMilliseconds)); - } - - this.IsRunning = true; + this.timer = new System.Threading.Timer( + c => this.InvokeTick(), + null, + dueTime.Milliseconds, + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - - /// - /// Stops the timer. - /// - public void Stop() + else { - this.timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); - this.IsRunning = false; + this.timer.Change( + (int)Math.Ceiling(this.DueTime.TotalMilliseconds), + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - public void Dispose() + this.IsRunning = true; + } + + /// + /// Starts the timer after the given in milliseconds. + /// + /// + /// The time before initiating the first event in milliseconds. + /// + public void Start(int dueTime) + { + if (this.timer == null) { - this.Dispose(true); - GC.SuppressFinalize(this); + this.timer = new System.Threading.Timer( + x => this.InvokeTick(), + null, + dueTime, + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - - /// - /// Invokes the event, if attached. - /// - /// Potentially thrown by the delegate callback. - protected virtual void InvokeTick() + else { - this.Tick?.Invoke(this, EventArgs.Empty); + this.timer.Change( + (int)Math.Ceiling(this.DueTime.TotalMilliseconds), + (int)Math.Ceiling(this.Interval.TotalMilliseconds)); } - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// A value indicating whether the object is being disposed by the method. - /// - protected virtual void Dispose(bool disposing) + this.IsRunning = true; + } + + /// + /// Stops the timer. + /// + public void Stop() + { + this.timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + this.IsRunning = false; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Invokes the event, if attached. + /// + /// Potentially thrown by the delegate callback. + protected virtual void InvokeTick() + { + this.Tick?.Invoke(this, EventArgs.Empty); + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// A value indicating whether the object is being disposed by the method. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - this.timer?.Dispose(); - } + this.timer?.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs index 8a235f96..8e27c3e9 100644 --- a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs +++ b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs @@ -1,82 +1,68 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Mvc.Extensions -{ - using System; - using System.Net; - using MADE.Web.Mvc.Responses; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ModelBinding; - using System.Text.Json; - using JsonResult = MADE.Web.Mvc.Responses.JsonResult; +using System; +using System.Net; +using MADE.Web.Mvc.Responses; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System.Text.Json; +using JsonResult = MADE.Web.Mvc.Responses.JsonResult; + +namespace MADE.Web.Mvc.Extensions; +/// +/// Defines a collection of extensions for MVC instances. +/// +public static class ControllerBaseExtensions +{ /// - /// Defines a collection of extensions for MVC instances. + /// Creates a object from the specified value for a controller response. /// - public static class ControllerBaseExtensions + /// The controller that is performing the response. + /// The value object to serialize. + /// The expected result HTTP status code. + /// The JSON serializer options for serializing the result. + /// The created for the response. + /// Thrown if the is . + public static IActionResult Json( + this ControllerBase controller, + object value, + HttpStatusCode statusCode = HttpStatusCode.OK, + JsonSerializerOptions serializerOptions = null) { - /// - /// Creates a object from the specified value for a controller response. - /// - /// The controller that is performing the response. - /// The value object to serialize. - /// The expected result HTTP status code. - /// The JSON serializer options for serializing the result. - /// The created for the response. - /// Thrown if the is . - public static IActionResult Json( - this ControllerBase controller, - object value, - HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerOptions serializerOptions = null) - { - if (controller == null) - { - throw new ArgumentNullException(nameof(controller)); - } + ArgumentNullException.ThrowIfNull(controller); - return new JsonResult(value, statusCode, serializerOptions); - } - - /// - /// Creates an that produces a response. - /// - /// The controller that is performing the response. - /// An error object to be returned to the client. - /// The created for the response. - /// Thrown if the is . - public static IActionResult InternalServerError(this ControllerBase controller, object responseContent) - { - if (controller == null) - { - throw new ArgumentNullException(nameof(controller)); - } + return new JsonResult(value, statusCode, serializerOptions); + } - return new InternalServerErrorObjectResult(responseContent); - } + /// + /// Creates an that produces a response. + /// + /// The controller that is performing the response. + /// An error object to be returned to the client. + /// The created for the response. + /// Thrown if the is . + public static IActionResult InternalServerError(this ControllerBase controller, object responseContent) + { + ArgumentNullException.ThrowIfNull(controller); - /// - /// Creates an that produces a response. - /// - /// The controller that is performing the response. - /// The containing errors to be returned to the client. - /// The created for the response. - /// Thrown if the or is . - public static IActionResult InternalServerError(this ControllerBase controller, ModelStateDictionary modelState) - { - if (controller == null) - { - throw new ArgumentNullException(nameof(controller)); - } + return new InternalServerErrorObjectResult(responseContent); + } - if (modelState == null) - { - throw new ArgumentNullException(nameof(modelState)); - } + /// + /// Creates an that produces a response. + /// + /// The controller that is performing the response. + /// The containing errors to be returned to the client. + /// The created for the response. + /// Thrown if the or is . + public static IActionResult InternalServerError(this ControllerBase controller, ModelStateDictionary modelState) + { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(modelState); - return new InternalServerErrorObjectResult(modelState); - } + return new InternalServerErrorObjectResult(modelState); } -} \ No newline at end of file +} diff --git a/src/MADE.Web.Mvc/Responses/InternalServerErrorObjectResult.cs b/src/MADE.Web.Mvc/Responses/InternalServerErrorObjectResult.cs index ad641ffb..eabd4ae9 100644 --- a/src/MADE.Web.Mvc/Responses/InternalServerErrorObjectResult.cs +++ b/src/MADE.Web.Mvc/Responses/InternalServerErrorObjectResult.cs @@ -1,44 +1,40 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Mvc.Responses +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace MADE.Web.Mvc.Responses; + +/// +/// Defines an that when executed will produce a Internal Server Error (500) response. +/// +public class InternalServerErrorObjectResult : ObjectResult { - using System; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.ModelBinding; + private const int DefaultStatusCode = StatusCodes.Status500InternalServerError; /// - /// Defines an that when executed will produce a Internal Server Error (500) response. + /// Initializes a new instance of the class. /// - public class InternalServerErrorObjectResult : ObjectResult + /// Contains the errors to be returned to the client. + public InternalServerErrorObjectResult(object error) + : base(error) { - private const int DefaultStatusCode = StatusCodes.Status500InternalServerError; - - /// - /// Initializes a new instance of the class. - /// - /// Contains the errors to be returned to the client. - public InternalServerErrorObjectResult(object error) - : base(error) - { - this.StatusCode = DefaultStatusCode; - } + this.StatusCode = DefaultStatusCode; + } - /// - /// Initializes a new instance of the class. - /// - /// The containing the validation errors. - /// Thrown if the is . - public InternalServerErrorObjectResult(ModelStateDictionary modelState) - : base(new SerializableError(modelState)) - { - if (modelState == null) - { - throw new ArgumentNullException(nameof(modelState)); - } + /// + /// Initializes a new instance of the class. + /// + /// The containing the validation errors. + /// Thrown if the is . + public InternalServerErrorObjectResult(ModelStateDictionary modelState) + : base(new SerializableError(modelState)) + { + ArgumentNullException.ThrowIfNull(modelState); - this.StatusCode = DefaultStatusCode; - } + this.StatusCode = DefaultStatusCode; } -} \ No newline at end of file +} diff --git a/src/MADE.Web.Mvc/Responses/JsonResult.cs b/src/MADE.Web.Mvc/Responses/JsonResult.cs index a82bc858..e55e7261 100644 --- a/src/MADE.Web.Mvc/Responses/JsonResult.cs +++ b/src/MADE.Web.Mvc/Responses/JsonResult.cs @@ -1,85 +1,81 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Mvc.Responses -{ - using System; - using System.Net; - using System.Runtime.ExceptionServices; - using System.Threading.Tasks; - using MADE.Web.Extensions; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.AspNetCore.Mvc.Infrastructure; - using System.Text.Json; +using System; +using System.Net; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; +using MADE.Web.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using System.Text.Json; + +namespace MADE.Web.Mvc.Responses; +/// +/// Defines a model for a result of a request that is serialized as JSON. +/// +public class JsonResult : ActionResult, IStatusCodeActionResult +{ /// - /// Defines a model for a result of a request that is serialized as JSON. + /// Initializes a new instance of the class with the object to serialize. /// - public class JsonResult : ActionResult, IStatusCodeActionResult + /// The value object to serialize. + /// The expected result HTTP status code. + /// The JSON serializer options for serializing the result. + public JsonResult( + object value, + HttpStatusCode statusCode = HttpStatusCode.OK, + JsonSerializerOptions serializerOptions = default) { - /// - /// Initializes a new instance of the class with the object to serialize. - /// - /// The value object to serialize. - /// The expected result HTTP status code. - /// The JSON serializer options for serializing the result. - public JsonResult( - object value, - HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerOptions serializerOptions = default) - { - this.Value = value; - this.StatusCode = (int)statusCode; - this.SerializerOptions = serializerOptions; - } + this.Value = value; + this.StatusCode = (int)statusCode; + this.SerializerOptions = serializerOptions; + } - /// - /// Gets the value object to serialize. - /// - public object Value { get; } + /// + /// Gets the value object to serialize. + /// + public object Value { get; } - /// - /// Gets the expected result HTTP status code. - /// - public int? StatusCode { get; } + /// + /// Gets the expected result HTTP status code. + /// + public int? StatusCode { get; } - /// - /// Gets the JSON serializer options for serializing the result. - /// - public JsonSerializerOptions SerializerOptions { get; } + /// + /// Gets the JSON serializer options for serializing the result. + /// + public JsonSerializerOptions SerializerOptions { get; } - /// - /// Executes the result operation of the action method asynchronously writing the to the response. - /// - /// The context in which the result is executed. - /// An asynchronous operation. - /// Thrown if is . - public override async Task ExecuteResultAsync(ActionContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + /// + /// Executes the result operation of the action method asynchronously writing the to the response. + /// + /// The context in which the result is executed. + /// An asynchronous operation. + /// Thrown if is . + public override async Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context); - HttpResponse response = context.HttpContext.Response; + HttpResponse response = context.HttpContext.Response; - ExceptionDispatchInfo exceptionDispatchInfo = null; - try - { - await response.WriteJsonAsync( - this.StatusCode.GetValueOrDefault((int)HttpStatusCode.OK), - this.Value, - this.SerializerOptions); - } - catch (Exception ex) - { - exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex); - } - finally - { - exceptionDispatchInfo?.Throw(); - } + ExceptionDispatchInfo? exceptionDispatchInfo = null; + try + { + await response.WriteJsonAsync( + this.StatusCode.GetValueOrDefault((int)HttpStatusCode.OK), + this.Value, + this.SerializerOptions).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex); + } + finally + { + exceptionDispatchInfo?.Throw(); } } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Exceptions/DefaultExceptionHandler.cs b/src/MADE.Web/Exceptions/DefaultExceptionHandler.cs index 8ee04b17..2bec892a 100644 --- a/src/MADE.Web/Exceptions/DefaultExceptionHandler.cs +++ b/src/MADE.Web/Exceptions/DefaultExceptionHandler.cs @@ -1,29 +1,28 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Exceptions -{ - using System; - using System.Net; - using System.Threading.Tasks; - using MADE.Web.Extensions; - using Microsoft.AspNetCore.Http; +using System; +using System.Net; +using System.Threading.Tasks; +using MADE.Web.Extensions; +using Microsoft.AspNetCore.Http; + +namespace MADE.Web.Exceptions; +/// +/// Defines a default exception handler for exceptions thrown which are not explicitly handled. +/// +public class DefaultExceptionHandler : IHttpContextExceptionHandler +{ /// - /// Defines a default exception handler for exceptions thrown which are not explicitly handled. + /// Handles the specified exception for the given context. /// - public class DefaultExceptionHandler : IHttpContextExceptionHandler + /// The request context. + /// The exception thrown. + /// An asynchronous operation. + public async Task HandleAsync(HttpContext context, Exception exception) { - /// - /// Handles the specified exception for the given context. - /// - /// The request context. - /// The exception thrown. - /// An asynchronous operation. - public async Task HandleAsync(HttpContext context, Exception exception) - { - var response = new ExceptionResponse("UnhandledException", "An unhandled exception occurred.", exception); - await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response); - } + var response = new ExceptionResponse("UnhandledException", "An unhandled exception occurred.", exception); + await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Exceptions/ExceptionResponse{TException}.cs b/src/MADE.Web/Exceptions/ExceptionResponse{TException}.cs index 5e7badbd..776ec271 100644 --- a/src/MADE.Web/Exceptions/ExceptionResponse{TException}.cs +++ b/src/MADE.Web/Exceptions/ExceptionResponse{TException}.cs @@ -1,43 +1,42 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Exceptions -{ - using System; +using System; + +namespace MADE.Web.Exceptions; +/// +/// Defines a response to a an exception being thrown. +/// +/// The type of exception thrown. +public class ExceptionResponse + where TException : Exception +{ /// - /// Defines a response to a an exception being thrown. + /// Initializes a new instance of the class with an error code and message. /// - /// The type of exception thrown. - public class ExceptionResponse - where TException : Exception + /// The error code. + /// The error message. + /// The exception thrown. + public ExceptionResponse(string errorCode, string errorMessage, TException exception) { - /// - /// Initializes a new instance of the class with an error code and message. - /// - /// The error code. - /// The error message. - /// The exception thrown. - public ExceptionResponse(string errorCode, string errorMessage, TException exception) - { - this.ErrorCode = errorCode; - this.ErrorMessage = errorMessage; - this.Exception = exception; - } + this.ErrorCode = errorCode; + this.ErrorMessage = errorMessage; + this.Exception = exception; + } - /// - /// Gets the exception thrown. - /// - public TException Exception { get; } + /// + /// Gets the exception thrown. + /// + public TException Exception { get; } - /// - /// Gets the error code. - /// - public string ErrorCode { get; } + /// + /// Gets the error code. + /// + public string ErrorCode { get; } - /// - /// Gets the error message. - /// - public string ErrorMessage { get; } - } -} \ No newline at end of file + /// + /// Gets the error message. + /// + public string ErrorMessage { get; } +} diff --git a/src/MADE.Web/Exceptions/HttpContextExceptionHandlerExtensions.cs b/src/MADE.Web/Exceptions/HttpContextExceptionHandlerExtensions.cs index 54c3dce0..faddcaa8 100644 --- a/src/MADE.Web/Exceptions/HttpContextExceptionHandlerExtensions.cs +++ b/src/MADE.Web/Exceptions/HttpContextExceptionHandlerExtensions.cs @@ -1,55 +1,54 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using System; - using MADE.Web.Exceptions; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; +using System; +using MADE.Web.Exceptions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for objects. +/// +public static class HttpContextExceptionHandlerExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Adds the middleware to the application builder. /// - public static class HttpContextExceptionHandlerExtensions + /// The application builder. + /// The configured application builder. + public static IApplicationBuilder UseHttpContextExceptionHandling(this IApplicationBuilder builder) { - /// - /// Adds the middleware to the application builder. - /// - /// The application builder. - /// The configured application builder. - public static IApplicationBuilder UseHttpContextExceptionHandling(this IApplicationBuilder builder) - { - builder.UseMiddleware(); - return builder; - } + builder.UseMiddleware(); + return builder; + } - /// - /// Adds the default handlers to the service collection. - /// - /// The service collection. - /// The configured service collection. - public static IServiceCollection AddDefaultHttpContextExceptionHandler(this IServiceCollection serviceCollection) - { - serviceCollection.AddHttpContextExceptionHandler(); - return serviceCollection; - } + /// + /// Adds the default handlers to the service collection. + /// + /// The service collection. + /// The configured service collection. + public static IServiceCollection AddDefaultHttpContextExceptionHandler(this IServiceCollection serviceCollection) + { + serviceCollection.AddHttpContextExceptionHandler(); + return serviceCollection; + } - /// - /// Adds a exception handler to the service collection. - /// - /// The type of exception handled. - /// The type of exception handler. - /// The service collection. - /// The configured service collection. - public static IServiceCollection AddHttpContextExceptionHandler( - this IServiceCollection serviceCollection) - where TException : Exception - where THandler : class, IHttpContextExceptionHandler - { - serviceCollection.AddTransient, THandler>(); - return serviceCollection; - } + /// + /// Adds a exception handler to the service collection. + /// + /// The type of exception handled. + /// The type of exception handler. + /// The service collection. + /// The configured service collection. + public static IServiceCollection AddHttpContextExceptionHandler( + this IServiceCollection serviceCollection) + where TException : Exception + where THandler : class, IHttpContextExceptionHandler + { + serviceCollection.AddTransient, THandler>(); + return serviceCollection; } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs b/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs index 85095fa7..cbd30fbe 100644 --- a/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs +++ b/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs @@ -1,162 +1,161 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Exceptions +using System; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Threading.Tasks; +using MADE.Web.Extensions; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace MADE.Web.Exceptions; + +/// +/// Defines a middleware for handling JSON exceptions. +/// +public class HttpContextExceptionsMiddleware { - using System; - using System.Linq; - using System.Net; - using System.Reflection; - using System.Threading.Tasks; - using MADE.Web.Extensions; - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.Hosting; + private static readonly Type ExceptionHandlerInterfaceType; + + private readonly IHttpContextExceptionHandler defaultExceptionHandler; + private readonly IHostEnvironment hostEnvironment; + private readonly RequestDelegate httpRequestDelegate; + private readonly IServiceProvider serviceProvider; + + static HttpContextExceptionsMiddleware() + { + ExceptionHandlerInterfaceType = typeof(IHttpContextExceptionHandler<>); + } /// - /// Defines a middleware for handling JSON exceptions. + /// Initializes a new instance of the class. /// - public class HttpContextExceptionsMiddleware + /// The request delegate for processing a HTTP request. + /// The host environment. + /// The service provider. + /// The default exception handler. + public HttpContextExceptionsMiddleware( + RequestDelegate httpRequestDelegate, + IHostEnvironment hostEnvironment, + IServiceProvider serviceProvider, + IHttpContextExceptionHandler defaultExceptionHandler) { - private static readonly Type ExceptionHandlerInterfaceType; - - private readonly IHttpContextExceptionHandler defaultExceptionHandler; - private readonly IHostEnvironment hostEnvironment; - private readonly RequestDelegate httpRequestDelegate; - private readonly IServiceProvider serviceProvider; + this.httpRequestDelegate = httpRequestDelegate; + this.hostEnvironment = hostEnvironment; + this.serviceProvider = serviceProvider; + this.defaultExceptionHandler = defaultExceptionHandler; + } - static HttpContextExceptionsMiddleware() + /// + /// Invokes the middleware to perform the request and handle any exceptions thrown. + /// + /// The to make a request with. + /// An asynchronous operation. + public async Task Invoke(HttpContext context) + { + try { - ExceptionHandlerInterfaceType = typeof(IHttpContextExceptionHandler<>); + await this.httpRequestDelegate(context).ConfigureAwait(false); } + catch (AggregateException exception) + { + var innerExceptions = exception.InnerExceptions.GroupBy(e => e.GetType()) + .Select(g => g.Last()) + .ToList(); - /// - /// Initializes a new instance of the class. - /// - /// The request delegate for processing a HTTP request. - /// The host environment. - /// The service provider. - /// The default exception handler. - public HttpContextExceptionsMiddleware( - RequestDelegate httpRequestDelegate, - IHostEnvironment hostEnvironment, - IServiceProvider serviceProvider, - IHttpContextExceptionHandler defaultExceptionHandler) + await this.HandleExceptionAsync(context, innerExceptions.Last()).ConfigureAwait(false); + } + catch (Exception exception) { - this.httpRequestDelegate = httpRequestDelegate; - this.hostEnvironment = hostEnvironment; - this.serviceProvider = serviceProvider; - this.defaultExceptionHandler = defaultExceptionHandler; + await this.HandleExceptionAsync(context, exception).ConfigureAwait(false); } + } - /// - /// Invokes the middleware to perform the request and handle any exceptions thrown. - /// - /// The to make a request with. - /// An asynchronous operation. - public async Task Invoke(HttpContext context) + private async Task HandleExceptionAsync(HttpContext context, Exception exception) + { + if (context.Response.HasStarted) { - try - { - await this.httpRequestDelegate(context); - } - catch (AggregateException exception) - { - var innerExceptions = exception.InnerExceptions.GroupBy(e => e.GetType()) - .Select(g => g.Last()) - .ToList(); - - await this.HandleExceptionAsync(context, innerExceptions.Last()); - } - catch (Exception exception) - { - await this.HandleExceptionAsync(context, exception); - } + return; } - private async Task HandleExceptionAsync(HttpContext context, Exception exception) + context.Response.Clear(); + + Type exceptionHandlerType = ExceptionHandlerInterfaceType.MakeGenericType(exception.GetType()); + dynamic exceptionHandler; + + try { - if (context.Response.HasStarted) - { - return; - } + exceptionHandler = this.serviceProvider.GetService(exceptionHandlerType); + } + catch (Exception) + { + await this.HandleWithDefaultHandlerAsync(context, exception).ConfigureAwait(false); + return; + } - context.Response.Clear(); + if (exceptionHandler == null) + { + await this.HandleWithDefaultHandlerAsync(context, exception).ConfigureAwait(false); + return; + } - Type exceptionHandlerType = ExceptionHandlerInterfaceType.MakeGenericType(exception.GetType()); - dynamic exceptionHandler; + MethodInfo handleMethod = exceptionHandlerType.GetTypeInfo().GetMethod("HandleAsync"); - try - { - exceptionHandler = this.serviceProvider.GetService(exceptionHandlerType); - } - catch (Exception) + try + { + if (handleMethod is not null) { - await this.HandleWithDefaultHandlerAsync(context, exception); - return; + await handleMethod.Invoke(exceptionHandler, new object[] { context, exception }).ConfigureAwait(false); } + } + catch (Exception handleException) + { + string exceptionName = handleException.GetType().FullName; + string originalExceptionName = exception.GetType().FullName; - if (exceptionHandler == null) + if (!this.hostEnvironment.IsProduction()) { - await this.HandleWithDefaultHandlerAsync(context, exception); - return; - } - - MethodInfo handleMethod = exceptionHandlerType.GetTypeInfo().GetMethod("HandleAsync"); + var response = new ExceptionResponse( + "ExceptionHandlerThrewException", + $"Exception {exceptionName} thrown with message {handleException.Message} when handling exception {originalExceptionName} with message {exception.Message}", + handleException); - try - { - if (handleMethod is not null) - { - await handleMethod.Invoke(exceptionHandler, new object[] { context, exception }); - } + await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response).ConfigureAwait(false); } - catch (Exception handleException) + else { - string exceptionName = handleException.GetType().FullName; - string originalExceptionName = exception.GetType().FullName; - - if (!this.hostEnvironment.IsProduction()) - { - var response = new ExceptionResponse( - "ExceptionHandlerThrewException", - $"Exception {exceptionName} thrown with message {handleException.Message} when handling exception {originalExceptionName} with message {exception.Message}", - handleException); - - await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - } + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } } + } - private async Task HandleWithDefaultHandlerAsync(HttpContext context, Exception exception) + private async Task HandleWithDefaultHandlerAsync(HttpContext context, Exception exception) + { + string originalExceptionName = exception.GetType().FullName; + + try { - string originalExceptionName = exception.GetType().FullName; + await this.defaultExceptionHandler.HandleAsync(context, exception).ConfigureAwait(false); + } + catch (Exception handlerException) + { + string exceptionName = handlerException.GetType().FullName; - try + if (!this.hostEnvironment.IsProduction()) { - await this.defaultExceptionHandler.HandleAsync(context, exception); + var response = new ExceptionResponse( + "DefaultExceptionHandlerThrewException", + $"Exception {exceptionName} thrown with message {handlerException.Message} when handling exception {originalExceptionName} with message {exception.Message}", + handlerException); + + await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response).ConfigureAwait(false); } - catch (Exception handlerException) + else { - string exceptionName = handlerException.GetType().FullName; - - if (!this.hostEnvironment.IsProduction()) - { - var response = new ExceptionResponse( - "DefaultExceptionHandlerThrewException", - $"Exception {exceptionName} thrown with message {handlerException.Message} when handling exception {originalExceptionName} with message {exception.Message}", - handlerException); - - await context.Response.WriteJsonAsync(HttpStatusCode.InternalServerError, response); - } - else - { - context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; - } + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } } } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Exceptions/IHttpContextExceptionHandler{TException}.cs b/src/MADE.Web/Exceptions/IHttpContextExceptionHandler{TException}.cs index d0d9ce6f..5c67328f 100644 --- a/src/MADE.Web/Exceptions/IHttpContextExceptionHandler{TException}.cs +++ b/src/MADE.Web/Exceptions/IHttpContextExceptionHandler{TException}.cs @@ -1,25 +1,24 @@ -// MADE Apps licenses this file to you under the MIT license. +// MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Exceptions -{ - using System; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace MADE.Web.Exceptions; +/// +/// Defines an interface for a exception handler. +/// +/// The type of exception thrown. +public interface IHttpContextExceptionHandler + where TException : Exception +{ /// - /// Defines an interface for a exception handler. + /// Handles the specified for the given . /// - /// The type of exception thrown. - public interface IHttpContextExceptionHandler - where TException : Exception - { - /// - /// Handles the specified for the given . - /// - /// The request context. - /// The exception that was thrown. - /// An asynchronous operation. - Task HandleAsync(HttpContext context, TException exception); - } -} \ No newline at end of file + /// The request context. + /// The exception that was thrown. + /// An asynchronous operation. + Task HandleAsync(HttpContext context, TException exception); +} diff --git a/src/MADE.Web/Extensions/ApiVersioningExtensions.cs b/src/MADE.Web/Extensions/ApiVersioningExtensions.cs index bd1cd483..27d29c6c 100644 --- a/src/MADE.Web/Extensions/ApiVersioningExtensions.cs +++ b/src/MADE.Web/Extensions/ApiVersioningExtensions.cs @@ -1,65 +1,64 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using Asp.Versioning; - using Microsoft.Extensions.DependencyInjection; +using Asp.Versioning; +using Microsoft.Extensions.DependencyInjection; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for API versioning. +/// +public static class ApiVersioningExtensions +{ /// - /// Defines a collection of extensions for API versioning. + /// Adds request API versioning for controllers and APIs to the specified services collection. /// - public static class ApiVersioningExtensions + /// The services available in the application. + /// The default major version of the API. Default, 1. + /// The default minor version of the API. Default, 0. + /// The configured object. + public static IServiceCollection AddApiVersionSupport( + this IServiceCollection services, + int defaultMajor = 1, + int defaultMinor = 0) { - /// - /// Adds request API versioning for controllers and APIs to the specified services collection. - /// - /// The services available in the application. - /// The default major version of the API. Default, 1. - /// The default minor version of the API. Default, 0. - /// The configured object. - public static IServiceCollection AddApiVersionSupport( - this IServiceCollection services, - int defaultMajor = 1, - int defaultMinor = 0) + var apiVersioningBuilder = services.AddApiVersioning(options => { - var apiVersioningBuilder = services.AddApiVersioning(options => - { - options.DefaultApiVersion = new ApiVersion(defaultMajor, defaultMinor); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ReportApiVersions = true; - }); + options.DefaultApiVersion = new ApiVersion(defaultMajor, defaultMinor); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + }); - apiVersioningBuilder.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); + apiVersioningBuilder.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); - return services; - } + return services; + } - /// - /// Adds request header API versioning for controllers and APIs to the specified services collection. - /// - /// The services available in the application. - /// The name of the header that is required when making requests to API endpoints. Default, x-api-version. - /// The default major version of the API. Default, 1. - /// The default minor version of the API. Default, 0. - /// The configured object. - public static IServiceCollection AddApiVersionHeaderSupport( - this IServiceCollection services, - string apiHeaderName = "x-api-version", - int defaultMajor = 1, - int defaultMinor = 0) + /// + /// Adds request header API versioning for controllers and APIs to the specified services collection. + /// + /// The services available in the application. + /// The name of the header that is required when making requests to API endpoints. Default, x-api-version. + /// The default major version of the API. Default, 1. + /// The default minor version of the API. Default, 0. + /// The configured object. + public static IServiceCollection AddApiVersionHeaderSupport( + this IServiceCollection services, + string apiHeaderName = "x-api-version", + int defaultMajor = 1, + int defaultMinor = 0) + { + var apiVersioningBuilder = services.AddApiVersioning(options => { - var apiVersioningBuilder = services.AddApiVersioning(options => - { - options.DefaultApiVersion = new ApiVersion(defaultMajor, defaultMinor); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ReportApiVersions = true; - options.ApiVersionReader = new HeaderApiVersionReader(apiHeaderName); - }); + options.DefaultApiVersion = new ApiVersion(defaultMajor, defaultMinor); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + options.ApiVersionReader = new HeaderApiVersionReader(apiHeaderName); + }); - apiVersioningBuilder.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); + apiVersioningBuilder.AddApiExplorer(options => options.GroupNameFormat = "'v'VVV"); - return services; - } + return services; } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Extensions/HttpContextExtensions.cs b/src/MADE.Web/Extensions/HttpContextExtensions.cs index cbd13977..13cc094d 100644 --- a/src/MADE.Web/Extensions/HttpContextExtensions.cs +++ b/src/MADE.Web/Extensions/HttpContextExtensions.cs @@ -1,23 +1,22 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for a object. +/// +public static class HttpContextExtensions +{ /// - /// Defines a collection of extensions for a object. + /// Gets the domain name of the requesting context. /// - public static class HttpContextExtensions + /// The requesting . + /// The domain part of the request's host. + public static string GetDomain(this HttpContext context) { - /// - /// Gets the domain name of the requesting context. - /// - /// The requesting . - /// The domain part of the request's host. - public static string GetDomain(this HttpContext context) - { - return context.Request.Host.Host; - } + return context.Request.Host.Host; } } diff --git a/src/MADE.Web/Extensions/HttpResponseExtensions.cs b/src/MADE.Web/Extensions/HttpResponseExtensions.cs index 05e08b9d..a37c2d8d 100644 --- a/src/MADE.Web/Extensions/HttpResponseExtensions.cs +++ b/src/MADE.Web/Extensions/HttpResponseExtensions.cs @@ -1,89 +1,88 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using System.Net; - using System.Text; - using System.Threading.Tasks; - using Microsoft.AspNetCore.Http; - using Microsoft.Net.Http.Headers; - using System.Text.Json; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using System.Text.Json; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for a object. +/// +public static class HttpResponseExtensions +{ /// - /// Defines a collection of extensions for a object. + /// Writes an object value as JSON to the specified . /// - public static class HttpResponseExtensions + /// The HTTP response to write to. + /// The status code of the response. + /// The object to serialize as JSON. + /// An asynchronous operation. + public static async Task WriteJsonAsync( + this HttpResponse response, + HttpStatusCode statusCode, + object value) { - /// - /// Writes an object value as JSON to the specified . - /// - /// The HTTP response to write to. - /// The status code of the response. - /// The object to serialize as JSON. - /// An asynchronous operation. - public static async Task WriteJsonAsync( - this HttpResponse response, - HttpStatusCode statusCode, - object value) - { - await WriteJsonAsync(response, (int)statusCode, value, null); - } + await WriteJsonAsync(response, (int)statusCode, value, null).ConfigureAwait(false); + } - /// - /// Writes an object value as JSON to the specified . - /// - /// The HTTP response to write to. - /// The status code of the response. - /// The object to serialize as JSON. - /// An asynchronous operation. - public static async Task WriteJsonAsync( - this HttpResponse response, - int statusCode, - object value) - { - await WriteJsonAsync(response, statusCode, value, null); - } + /// + /// Writes an object value as JSON to the specified . + /// + /// The HTTP response to write to. + /// The status code of the response. + /// The object to serialize as JSON. + /// An asynchronous operation. + public static async Task WriteJsonAsync( + this HttpResponse response, + int statusCode, + object value) + { + await WriteJsonAsync(response, statusCode, value, null).ConfigureAwait(false); + } - /// - /// Writes an object value as JSON to the specified . - /// - /// The HTTP response to write to. - /// The status code of the response. - /// The object to serialize as JSON. - /// The JSON serializer options. - /// An asynchronous operation. - public static async Task WriteJsonAsync( - this HttpResponse response, - HttpStatusCode statusCode, - object value, - JsonSerializerOptions serializerOptions) - { - await WriteJsonAsync(response, (int)statusCode, value, serializerOptions); - } + /// + /// Writes an object value as JSON to the specified . + /// + /// The HTTP response to write to. + /// The status code of the response. + /// The object to serialize as JSON. + /// The JSON serializer options. + /// An asynchronous operation. + public static async Task WriteJsonAsync( + this HttpResponse response, + HttpStatusCode statusCode, + object value, + JsonSerializerOptions serializerOptions) + { + await WriteJsonAsync(response, (int)statusCode, value, serializerOptions).ConfigureAwait(false); + } - /// - /// Writes an object value as JSON to the specified . - /// - /// The HTTP response to write to. - /// The status code of the response. - /// The object to serialize as JSON. - /// The JSON serializer options. - /// An asynchronous operation. - public static async Task WriteJsonAsync( - this HttpResponse response, - int statusCode, - object value, - JsonSerializerOptions serializerOptions) - { - response.ContentType = new MediaTypeHeaderValue("application/json") { Encoding = Encoding.UTF8 }.ToString(); - response.StatusCode = statusCode; + /// + /// Writes an object value as JSON to the specified . + /// + /// The HTTP response to write to. + /// The status code of the response. + /// The object to serialize as JSON. + /// The JSON serializer options. + /// An asynchronous operation. + public static async Task WriteJsonAsync( + this HttpResponse response, + int statusCode, + object value, + JsonSerializerOptions serializerOptions) + { + response.ContentType = new MediaTypeHeaderValue("application/json") { Encoding = Encoding.UTF8 }.ToString(); + response.StatusCode = statusCode; - var options = serializerOptions ?? new JsonSerializerOptions { WriteIndented = true }; + var options = serializerOptions ?? new JsonSerializerOptions { WriteIndented = true }; - string json = JsonSerializer.Serialize(value, options); + string json = JsonSerializer.Serialize(value, options); - await response.WriteAsync(json, Encoding.UTF8); - } + await response.WriteAsync(json, Encoding.UTF8).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Extensions/IntExtensions.cs b/src/MADE.Web/Extensions/IntExtensions.cs index ec161cd6..664602bf 100644 --- a/src/MADE.Web/Extensions/IntExtensions.cs +++ b/src/MADE.Web/Extensions/IntExtensions.cs @@ -1,25 +1,24 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using System; +using System; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for integer values. +/// +public static class IntExtensions +{ /// - /// Defines a collection of extensions for integer values. + /// Limits the range of a value within a minimum and maximum range. /// - public static class IntExtensions + /// The value to limit. + /// The minimum valid value. + /// The maximum valid value. + /// The valid value within the range. + internal static int LimitRange(this int value, int minimum, int maximum) { - /// - /// Limits the range of a value within a minimum and maximum range. - /// - /// The value to limit. - /// The minimum valid value. - /// The maximum valid value. - /// The valid value within the range. - internal static int LimitRange(this int value, int minimum, int maximum) - { - return Math.Min(maximum, Math.Max(minimum, value)); - } + return Math.Min(maximum, Math.Max(minimum, value)); } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Extensions/QueryCollectionExtensions.cs b/src/MADE.Web/Extensions/QueryCollectionExtensions.cs index 0294b664..7fcd54d7 100644 --- a/src/MADE.Web/Extensions/QueryCollectionExtensions.cs +++ b/src/MADE.Web/Extensions/QueryCollectionExtensions.cs @@ -1,73 +1,72 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Extensions -{ - using System; - using Microsoft.AspNetCore.Http; +using System; +using Microsoft.AspNetCore.Http; + +namespace MADE.Web.Extensions; +/// +/// Defines a collection of extensions for objects. +/// +public static class QueryCollectionExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Gets a string value from the by the specified . /// - public static class QueryCollectionExtensions + /// The query to retrieve a string value from. + /// The key associated with the parameter to retrieve. + /// The default value if the value does not exist. + /// The string value for the specified . + public static string? GetStringValueOrDefault(this IQueryCollection query, string key, string? defaultValue = null) { - /// - /// Gets a string value from the by the specified . - /// - /// The query to retrieve a string value from. - /// The key associated with the parameter to retrieve. - /// The default value if the value does not exist. - /// The string value for the specified . - public static string GetStringValueOrDefault(this IQueryCollection query, string key, string defaultValue = null) + string? value = null; + if (query.ContainsKey(key)) { - string value = null; - if (query.ContainsKey(key)) - { - value = query[key].ToString(); - } - - return string.IsNullOrWhiteSpace(value) ? defaultValue : value; + value = query[key].ToString(); } - /// - /// Gets an integer value from the by the specified . - /// - /// The query to retrieve an integer value from. - /// The key associated with the parameter to retrieve. - /// The default value if the value does not exist. - /// A value indicating whether to treat 0 as empty. True by default. - /// The integer value for the specified . - public static int GetIntValueOrDefault(this IQueryCollection query, string key, int defaultValue, bool treatZeroAsEmpty = true) - { - string stringValue = GetStringValueOrDefault(query, key); + return string.IsNullOrWhiteSpace(value) ? defaultValue : value; + } - if (string.IsNullOrWhiteSpace(stringValue) - || !int.TryParse(stringValue, out int intValue) - || (treatZeroAsEmpty && intValue == 0)) - { - return defaultValue; - } + /// + /// Gets an integer value from the by the specified . + /// + /// The query to retrieve an integer value from. + /// The key associated with the parameter to retrieve. + /// The default value if the value does not exist. + /// A value indicating whether to treat 0 as empty. True by default. + /// The integer value for the specified . + public static int GetIntValueOrDefault(this IQueryCollection query, string key, int defaultValue, bool treatZeroAsEmpty = true) + { + string stringValue = GetStringValueOrDefault(query, key); - return intValue; + if (string.IsNullOrWhiteSpace(stringValue) + || !int.TryParse(stringValue, out int intValue) + || (treatZeroAsEmpty && intValue == 0)) + { + return defaultValue; } - /// - /// Gets a value from the by the specified . - /// - /// The query to retrieve a value from. - /// The key associated with the parameter to retrieve. - /// The default value if the value does not exist. - /// The value for the specified . - public static DateTime GetDateTimeValueOrDefault(this IQueryCollection query, string key, DateTime defaultValue) - { - string stringValue = GetStringValueOrDefault(query, key); + return intValue; + } - if (string.IsNullOrWhiteSpace(stringValue) || !DateTime.TryParse(stringValue, out DateTime dateTimeValue)) - { - return defaultValue; - } + /// + /// Gets a value from the by the specified . + /// + /// The query to retrieve a value from. + /// The key associated with the parameter to retrieve. + /// The default value if the value does not exist. + /// The value for the specified . + public static DateTime GetDateTimeValueOrDefault(this IQueryCollection query, string key, DateTime defaultValue) + { + string stringValue = GetStringValueOrDefault(query, key); - return dateTimeValue; + if (string.IsNullOrWhiteSpace(stringValue) || !DateTime.TryParse(stringValue, out DateTime dateTimeValue)) + { + return defaultValue; } + + return dateTimeValue; } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Identity/AuthenticatedUser.cs b/src/MADE.Web/Identity/AuthenticatedUser.cs index 183498e7..ed9e00b6 100644 --- a/src/MADE.Web/Identity/AuthenticatedUser.cs +++ b/src/MADE.Web/Identity/AuthenticatedUser.cs @@ -1,70 +1,69 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Identity -{ - using System.Collections.Generic; - using System.Collections.Immutable; - using System.Linq; - using System.Security.Claims; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Claims; + +namespace MADE.Web.Identity; +/// +/// Defines a base model for an authenticated user within the application. +/// +public class AuthenticatedUser +{ /// - /// Defines a base model for an authenticated user within the application. + /// The value associated with the authenticated user's identity. /// - public class AuthenticatedUser - { - /// - /// The value associated with the authenticated user's identity. - /// - public const string SubjectClaimType = "sub"; + public const string SubjectClaimType = "sub"; - /// - /// The value associated with the authenticated user's preferred email address. - /// - public const string EmailClaimType = "email"; + /// + /// The value associated with the authenticated user's preferred email address. + /// + public const string EmailClaimType = "email"; - /// - /// The value associated with the authenticated user's assigned role(s). - /// - public const string RoleClaimType = "role"; + /// + /// The value associated with the authenticated user's assigned role(s). + /// + public const string RoleClaimType = "role"; - /// - /// Initializes a new instance of the class with the claims principal associated with the user and configures the properties based on the claims. - /// - /// The claims principal associated with the user. - public AuthenticatedUser(ClaimsPrincipal claimsPrincipal) - { - this.ClaimsPrincipal = claimsPrincipal; + /// + /// Initializes a new instance of the class with the claims principal associated with the user and configures the properties based on the claims. + /// + /// The claims principal associated with the user. + public AuthenticatedUser(ClaimsPrincipal claimsPrincipal) + { + this.ClaimsPrincipal = claimsPrincipal; - this.Subject = claimsPrincipal?.Claims.SingleOrDefault(c => c.Type == SubjectClaimType)?.Value; - this.Email = claimsPrincipal?.Claims.SingleOrDefault(c => c.Type == EmailClaimType)?.Value; - this.Roles = claimsPrincipal?.Claims.Where(x => x.Type == RoleClaimType).Select(x => x.Value); - this.Claims = claimsPrincipal?.Claims.ToImmutableList(); - } + this.Subject = claimsPrincipal?.Claims.SingleOrDefault(c => c.Type == SubjectClaimType)?.Value; + this.Email = claimsPrincipal?.Claims.SingleOrDefault(c => c.Type == EmailClaimType)?.Value; + this.Roles = claimsPrincipal?.Claims.Where(x => x.Type == RoleClaimType).Select(x => x.Value); + this.Claims = claimsPrincipal?.Claims.ToImmutableList(); + } - /// - /// Gets the claims principal associated with the user. - /// - public ClaimsPrincipal ClaimsPrincipal { get; } + /// + /// Gets the claims principal associated with the user. + /// + public ClaimsPrincipal ClaimsPrincipal { get; } - /// - /// Gets the authenticated user's identity. - /// - public string Subject { get; } + /// + /// Gets the authenticated user's identity. + /// + public string Subject { get; } - /// - /// Gets the authenticated user's preferred email address. - /// - public string Email { get; } + /// + /// Gets the authenticated user's preferred email address. + /// + public string Email { get; } - /// - /// Gets the collection of the authenticated user's assigned roles. - /// - public IEnumerable Roles { get; } + /// + /// Gets the collection of the authenticated user's assigned roles. + /// + public IEnumerable Roles { get; } - /// - /// Gets the collection of the authenticated user's claims. - /// - public IImmutableList Claims { get; } - } -} \ No newline at end of file + /// + /// Gets the collection of the authenticated user's claims. + /// + public IImmutableList Claims { get; } +} diff --git a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs index e2410b53..3e9d7dcd 100644 --- a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs +++ b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs @@ -1,35 +1,34 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Identity +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace MADE.Web.Identity; + +/// +/// Defines an accessor for retrieving the authenticated user from a . +/// +public class AuthenticatedUserAccessor : IAuthenticatedUserAccessor { - using System.Security.Claims; - using Microsoft.AspNetCore.Http; + private readonly IHttpContextAccessor httpContextAccessor; /// - /// Defines an accessor for retrieving the authenticated user from a . + /// Initializes a new instance of the class with an instance of the . /// - public class AuthenticatedUserAccessor : IAuthenticatedUserAccessor + /// The . + public AuthenticatedUserAccessor(IHttpContextAccessor httpContextAccessor) { - private readonly IHttpContextAccessor httpContextAccessor; - - /// - /// Initializes a new instance of the class with an instance of the . - /// - /// The . - public AuthenticatedUserAccessor(IHttpContextAccessor httpContextAccessor) - { - this.httpContextAccessor = httpContextAccessor; - } + this.httpContextAccessor = httpContextAccessor; + } - /// - /// Gets the authenticated user's claims principal. - /// - public ClaimsPrincipal ClaimsPrincipal => this.httpContextAccessor?.HttpContext?.User; + /// + /// Gets the authenticated user's claims principal. + /// + public ClaimsPrincipal ClaimsPrincipal => this.httpContextAccessor?.HttpContext?.User; - /// - /// Gets the authenticated user model for the specified / - /// - public AuthenticatedUser AuthenticatedUser => new(this.ClaimsPrincipal); - } -} \ No newline at end of file + /// + /// Gets the authenticated user model for the specified / + /// + public AuthenticatedUser AuthenticatedUser => new(this.ClaimsPrincipal); +} diff --git a/src/MADE.Web/Identity/AuthenticatedUserExtensions.cs b/src/MADE.Web/Identity/AuthenticatedUserExtensions.cs index 1642cbe7..04499ca4 100644 --- a/src/MADE.Web/Identity/AuthenticatedUserExtensions.cs +++ b/src/MADE.Web/Identity/AuthenticatedUserExtensions.cs @@ -1,26 +1,25 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Identity -{ - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace MADE.Web.Identity; +/// +/// Defines a collection of extensions for objects. +/// +public static class AuthenticatedUserExtensions +{ /// - /// Defines a collection of extensions for objects. + /// Adds the and to the specified services collection. /// - public static class AuthenticatedUserExtensions + /// The service collection. + /// The configured service collection. + public static IServiceCollection AddAuthenticatedUserAccessor(this IServiceCollection serviceCollection) { - /// - /// Adds the and to the specified services collection. - /// - /// The service collection. - /// The configured service collection. - public static IServiceCollection AddAuthenticatedUserAccessor(this IServiceCollection serviceCollection) - { - serviceCollection.AddHttpContextAccessor(); - serviceCollection.AddScoped(); - return serviceCollection; - } + serviceCollection.AddHttpContextAccessor(); + serviceCollection.AddScoped(); + return serviceCollection; } -} \ No newline at end of file +} diff --git a/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs b/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs index a704b08f..ed74e7ba 100644 --- a/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs +++ b/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs @@ -1,23 +1,22 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Identity -{ - using System.Security.Claims; +using System.Security.Claims; + +namespace MADE.Web.Identity; +/// +/// Defines an interface for accessing an authenticated user's claims principal. +/// +public interface IAuthenticatedUserAccessor +{ /// - /// Defines an interface for accessing an authenticated user's claims principal. + /// Gets the authenticated user's claims principal. /// - public interface IAuthenticatedUserAccessor - { - /// - /// Gets the authenticated user's claims principal. - /// - ClaimsPrincipal ClaimsPrincipal { get; } + ClaimsPrincipal ClaimsPrincipal { get; } - /// - /// Gets the authenticated user model for the specified / - /// - AuthenticatedUser AuthenticatedUser { get; } - } -} \ No newline at end of file + /// + /// Gets the authenticated user model for the specified / + /// + AuthenticatedUser AuthenticatedUser { get; } +} diff --git a/src/MADE.Web/Requests/IPaginatedRequest{T}.cs b/src/MADE.Web/Requests/IPaginatedRequest{T}.cs index 0bebcf39..b0b89910 100644 --- a/src/MADE.Web/Requests/IPaginatedRequest{T}.cs +++ b/src/MADE.Web/Requests/IPaginatedRequest{T}.cs @@ -1,32 +1,31 @@ -// MADE Apps licenses this file to you under the MIT license. +// MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Requests +namespace MADE.Web.Requests; + +/// +/// Defines an interface for a request with paginated results of the specified response type. +/// +/// The type of item to return. +public interface IPaginatedRequest { /// - /// Defines an interface for a request with paginated results of the specified response type. + /// Gets or sets the page requested. /// - /// The type of item to return. - public interface IPaginatedRequest - { - /// - /// Gets or sets the page requested. - /// - int Page { get; set; } + int Page { get; set; } - /// - /// Gets or sets the number of expected results for the requested page. - /// - int PageSize { get; set; } + /// + /// Gets or sets the number of expected results for the requested page. + /// + int PageSize { get; set; } - /// - /// Gets the number of items to skip. - /// - int Skip { get; } + /// + /// Gets the number of items to skip. + /// + int Skip { get; } - /// - /// Gets the number of items to take. - /// - int Take { get; } - } + /// + /// Gets the number of items to take. + /// + int Take { get; } } diff --git a/src/MADE.Web/Requests/PaginatedRequest{T}.cs b/src/MADE.Web/Requests/PaginatedRequest{T}.cs index bb5de0a8..29304a82 100644 --- a/src/MADE.Web/Requests/PaginatedRequest{T}.cs +++ b/src/MADE.Web/Requests/PaginatedRequest{T}.cs @@ -1,45 +1,44 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Requests -{ - using MADE.Web.Extensions; +using MADE.Web.Extensions; + +namespace MADE.Web.Requests; +/// +/// Defines a request with paginated results of the specified response type. +/// +/// The type of item to return. +public class PaginatedRequest : IPaginatedRequest +{ /// - /// Defines a request with paginated results of the specified response type. + /// Initializes a new instance of the class. /// - /// The type of item to return. - public class PaginatedRequest : IPaginatedRequest + /// The page requested. + /// The number of expected results for the requested page. + public PaginatedRequest(int page = 1, int pageSize = 10) { - /// - /// Initializes a new instance of the class. - /// - /// The page requested. - /// The number of expected results for the requested page. - public PaginatedRequest(int page = 1, int pageSize = 10) - { - this.Page = page; - this.PageSize = pageSize; - } + this.Page = page; + this.PageSize = pageSize; + } - /// - /// Gets or sets the page requested. - /// - public int Page { get; set; } + /// + /// Gets or sets the page requested. + /// + public int Page { get; set; } - /// - /// Gets or sets the number of expected results for the requested page. - /// - public int PageSize { get; set; } + /// + /// Gets or sets the number of expected results for the requested page. + /// + public int PageSize { get; set; } - /// - /// Gets the number of items to skip. - /// - public int Skip => (this.Page.LimitRange(1, 100000) - 1) * this.PageSize.LimitRange(1, 100); + /// + /// Gets the number of items to skip. + /// + public int Skip => (this.Page.LimitRange(1, 100000) - 1) * this.PageSize.LimitRange(1, 100); - /// - /// Gets the number of items to take. - /// - public int Take => this.PageSize; - } + /// + /// Gets the number of items to take. + /// + public int Take => this.PageSize; } diff --git a/src/MADE.Web/Responses/IPaginatedResponse{T}.cs b/src/MADE.Web/Responses/IPaginatedResponse{T}.cs index edff408d..b7a1ef57 100644 --- a/src/MADE.Web/Responses/IPaginatedResponse{T}.cs +++ b/src/MADE.Web/Responses/IPaginatedResponse{T}.cs @@ -1,40 +1,39 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Responses -{ - using System.Collections.Generic; - using MADE.Web.Requests; +using System.Collections.Generic; +using MADE.Web.Requests; + +namespace MADE.Web.Responses; +/// +/// Defines an interface for a response to a request. +/// +/// The type of item to return. +public interface IPaginatedResponse +{ /// - /// Defines an interface for a response to a request. + /// Gets or sets the items associated with the page. /// - /// The type of item to return. - public interface IPaginatedResponse - { - /// - /// Gets or sets the items associated with the page. - /// - IEnumerable Items { get; set; } + IEnumerable Items { get; set; } - /// - /// Gets or sets the page associated with the results. - /// - int Page { get; set; } + /// + /// Gets or sets the page associated with the results. + /// + int Page { get; set; } - /// - /// Gets or sets the number of expected results for the page. - /// - int PageSize { get; set; } + /// + /// Gets or sets the number of expected results for the page. + /// + int PageSize { get; set; } - /// - /// Gets or sets the count of the number of available items. - /// - int AvailableCount { get; set; } + /// + /// Gets or sets the count of the number of available items. + /// + int AvailableCount { get; set; } - /// - /// Gets the total number of pages for the available items based on the page size. - /// - int TotalPages { get; } - } -} \ No newline at end of file + /// + /// Gets the total number of pages for the available items based on the page size. + /// + int TotalPages { get; } +} diff --git a/src/MADE.Web/Responses/PaginatedResponse{T}.cs b/src/MADE.Web/Responses/PaginatedResponse{T}.cs index 75dd417d..9000b95f 100644 --- a/src/MADE.Web/Responses/PaginatedResponse{T}.cs +++ b/src/MADE.Web/Responses/PaginatedResponse{T}.cs @@ -1,56 +1,55 @@ // MADE Apps licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace MADE.Web.Responses -{ - using System; - using System.Collections.Generic; - using MADE.Web.Requests; +using System; +using System.Collections.Generic; +using MADE.Web.Requests; + +namespace MADE.Web.Responses; +/// +/// Defines a response to a request. +/// +/// The type of item to return. +public class PaginatedResponse : IPaginatedResponse +{ /// - /// Defines a response to a request. + /// Initializes a new instance of the class. /// - /// The type of item to return. - public class PaginatedResponse : IPaginatedResponse + /// The items associated with the page. + /// The page associated with the results. + /// The number of expected results for the page. + /// The count of the number of available items. + public PaginatedResponse(IEnumerable items, int page, int pageSize, int availableCount) { - /// - /// Initializes a new instance of the class. - /// - /// The items associated with the page. - /// The page associated with the results. - /// The number of expected results for the page. - /// The count of the number of available items. - public PaginatedResponse(IEnumerable items, int page, int pageSize, int availableCount) - { - this.Items = items; - this.Page = page; - this.PageSize = pageSize; - this.AvailableCount = availableCount; - } - - /// - /// Gets or sets the items associated with the page. - /// - public IEnumerable Items { get; set; } - - /// - /// Gets or sets the page associated with the results. - /// - public int Page { get; set; } - - /// - /// Gets or sets the number of expected results for the page. - /// - public int PageSize { get; set; } - - /// - /// Gets or sets the count of the number of available items. - /// - public int AvailableCount { get; set; } - - /// - /// Gets the total number of pages for the available items based on the page size. - /// - public int TotalPages => this.AvailableCount == 0 || this.PageSize == 0 ? 0 : (int)Math.Ceiling((double)this.AvailableCount / this.PageSize); + this.Items = items; + this.Page = page; + this.PageSize = pageSize; + this.AvailableCount = availableCount; } -} \ No newline at end of file + + /// + /// Gets or sets the items associated with the page. + /// + public IEnumerable Items { get; set; } + + /// + /// Gets or sets the page associated with the results. + /// + public int Page { get; set; } + + /// + /// Gets or sets the number of expected results for the page. + /// + public int PageSize { get; set; } + + /// + /// Gets or sets the count of the number of available items. + /// + public int AvailableCount { get; set; } + + /// + /// Gets the total number of pages for the available items based on the page size. + /// + public int TotalPages => this.AvailableCount == 0 || this.PageSize == 0 ? 0 : (int)Math.Ceiling((double)this.AvailableCount / this.PageSize); +} diff --git a/tests/MADE.Collections.Tests/Fakes/TestEquatableObject.cs b/tests/MADE.Collections.Tests/Fakes/TestEquatableObject.cs index 18285f51..c2d28738 100644 --- a/tests/MADE.Collections.Tests/Fakes/TestEquatableObject.cs +++ b/tests/MADE.Collections.Tests/Fakes/TestEquatableObject.cs @@ -1,53 +1,52 @@ -namespace MADE.Collections.Tests.Fakes +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MADE.Collections.Tests.Fakes; + +[ExcludeFromCodeCoverage] +public class TestEqualityObject : IEquatable { - using System; - using System.Diagnostics.CodeAnalysis; + public string Name { get; set; } - [ExcludeFromCodeCoverage] - public class TestEqualityObject : IEquatable - { - public string Name { get; set; } + public int Count { get; set; } - public int Count { get; set; } + public bool Equals(TestEqualityObject other) + { + if (ReferenceEquals(null, other)) + { + return false; + } - public bool Equals(TestEqualityObject other) + if (ReferenceEquals(this, other)) { - if (ReferenceEquals(null, other)) - { - return false; - } + return true; + } - if (ReferenceEquals(this, other)) - { - return true; - } + return this.Name == other.Name && this.Count == other.Count; + } - return this.Name == other.Name && this.Count == other.Count; + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; } - public override bool Equals(object obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - if (obj.GetType() != this.GetType()) - { - return false; - } - - return this.Equals((TestEqualityObject)obj); + return true; } - public override int GetHashCode() + if (obj.GetType() != this.GetType()) { - return HashCode.Combine(this.Name, this.Count); + return false; } + + return this.Equals((TestEqualityObject)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(this.Name, this.Count); } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Fakes/TestObject.cs b/tests/MADE.Collections.Tests/Fakes/TestObject.cs index 635f15ed..61255c2b 100644 --- a/tests/MADE.Collections.Tests/Fakes/TestObject.cs +++ b/tests/MADE.Collections.Tests/Fakes/TestObject.cs @@ -1,12 +1,11 @@ -namespace MADE.Collections.Tests.Fakes -{ - using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +namespace MADE.Collections.Tests.Fakes; - [ExcludeFromCodeCoverage] - public class TestObject - { - public string Name { get; set; } +[ExcludeFromCodeCoverage] +public class TestObject +{ + public string Name { get; set; } - public int Count { get; set; } - } -} \ No newline at end of file + public int Count { get; set; } +} diff --git a/tests/MADE.Collections.Tests/Fakes/TestObjectFaker.cs b/tests/MADE.Collections.Tests/Fakes/TestObjectFaker.cs index c29c5086..da8b241f 100644 --- a/tests/MADE.Collections.Tests/Fakes/TestObjectFaker.cs +++ b/tests/MADE.Collections.Tests/Fakes/TestObjectFaker.cs @@ -1,17 +1,15 @@ -namespace MADE.Collections.Tests.Fakes -{ - using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using Bogus; - using Bogus; +namespace MADE.Collections.Tests.Fakes; - [ExcludeFromCodeCoverage] - public static class TestObjectFaker +[ExcludeFromCodeCoverage] +public static class TestObjectFaker +{ + public static Faker Create() { - public static Faker Create() - { - return new Faker() - .RuleFor(o => o.Name, faker => faker.Name.FullName()) - .RuleFor(o => o.Count, faker => faker.Random.Int(0, 10)); - } + return new Faker() + .RuleFor(o => o.Name, faker => faker.Name.FullName()) + .RuleFor(o => o.Count, faker => faker.Random.Int(0, 10)); } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Fakes/TestObservableObject.cs b/tests/MADE.Collections.Tests/Fakes/TestObservableObject.cs index 05a22828..74c0ac4c 100644 --- a/tests/MADE.Collections.Tests/Fakes/TestObservableObject.cs +++ b/tests/MADE.Collections.Tests/Fakes/TestObservableObject.cs @@ -1,51 +1,50 @@ -namespace MADE.Collections.Tests.Fakes -{ - using System.ComponentModel; - using System.Diagnostics.CodeAnalysis; - using System.Runtime.CompilerServices; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; - [ExcludeFromCodeCoverage] - public class TestObservableObject : INotifyPropertyChanged - { - private string name; +namespace MADE.Collections.Tests.Fakes; + +[ExcludeFromCodeCoverage] +public class TestObservableObject : INotifyPropertyChanged +{ + private string name; - private int count; + private int count; - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler PropertyChanged; - public string Name + public string Name + { + get => this.name; + set { - get => this.name; - set + if (value == this.name) { - if (value == this.name) - { - return; - } - - this.name = value; - this.OnPropertyChanged(); + return; } + + this.name = value; + this.OnPropertyChanged(); } + } - public int Count + public int Count + { + get => this.count; + set { - get => this.count; - set + if (value == this.count) { - if (value == this.count) - { - return; - } - - this.count = value; - this.OnPropertyChanged(); + return; } - } - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + this.count = value; + this.OnPropertyChanged(); } } + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } diff --git a/tests/MADE.Collections.Tests/Fakes/TestObservableObjectFaker.cs b/tests/MADE.Collections.Tests/Fakes/TestObservableObjectFaker.cs index 170586f6..33249258 100644 --- a/tests/MADE.Collections.Tests/Fakes/TestObservableObjectFaker.cs +++ b/tests/MADE.Collections.Tests/Fakes/TestObservableObjectFaker.cs @@ -1,16 +1,14 @@ -namespace MADE.Collections.Tests.Fakes -{ - using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using Bogus; - using Bogus; +namespace MADE.Collections.Tests.Fakes; - [ExcludeFromCodeCoverage] - public static class TestObservableObjectFaker +[ExcludeFromCodeCoverage] +public static class TestObservableObjectFaker +{ + public static Faker Create() { - public static Faker Create() - { - return new Faker().RuleFor(o => o.Name, faker => faker.Name.FullName()) - .RuleFor(o => o.Count, faker => faker.Random.Int(0, 10)); - } + return new Faker().RuleFor(o => o.Name, faker => faker.Name.FullName()) + .RuleFor(o => o.Count, faker => faker.Random.Int(0, 10)); } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs index 2bfb514b..e4a53ce3 100644 --- a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs +++ b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs @@ -1,408 +1,407 @@ -namespace MADE.Collections.Tests.Tests +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using MADE.Collections.Tests.Fakes; +using MADE.Testing; +using NUnit.Framework; +using Shouldly; +using CollectionExtensions = MADE.Collections.CollectionExtensions; + +namespace MADE.Collections.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class CollectionExtensionsTests { - using System; - using System.Collections.Generic; - using System.Collections.ObjectModel; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using MADE.Collections.Tests.Fakes; - using MADE.Testing; - using NUnit.Framework; - using Shouldly; - using CollectionExtensions = MADE.Collections.CollectionExtensions; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class CollectionExtensionsTests + public class WhenShufflingItems { - public class WhenShufflingItems + [Test] + public void ShouldShuffleItemOrderRandomly() { - [Test] - public void ShouldShuffleItemOrderRandomly() + // Arrange + var items = new List { - // Arrange - var items = new List - { - 1, - 2, - 3, - 4, - 5 - }; - - // Act - var shuffledItems = items.Shuffle(); - - // Assert - shuffledItems.ShouldNotBeSameAs(items); - } + 1, + 2, + 3, + 4, + 5 + }; - [Test] - public void ShouldContainSameItemsAfterShuffle() - { - // Arrange - var items = new List - { - 1, - 2, - 3, - 4, - 5 - }; - - // Act - var shuffledItems = items.Shuffle(); - - // Assert - shuffledItems.ShouldBeEquivalentTo(items); - } + // Act + var shuffledItems = items.Shuffle(); + + // Assert + shuffledItems.ShouldNotBeSameAs(items); } - public class WhenUpdatingACollectionItem + [Test] + public void ShouldContainSameItemsAfterShuffle() { - [Test] - public void ShouldThrowArgumentNullExceptionIfNullCollection() + // Arrange + var items = new List { - // Arrange - List list = null; - string item = "Hello"; + 1, + 2, + 3, + 4, + 5 + }; - // Act & Assert - Assert.Throws(() => list.Update(item, (s, i) => s == i)); - } + // Act + var shuffledItems = items.Shuffle(); - [Test] - public void ShouldThrowArgumentNullExceptionIfNullItem() - { - // Arrange - var list = new List { "Hello" }; - string item = null; + // Assert + shuffledItems.ShouldBeEquivalentTo(items); + } + } - // Act & Assert - Assert.Throws(() => list.Update(item, (s, i) => s == i)); - } + public class WhenUpdatingACollectionItem + { + [Test] + public void ShouldThrowArgumentNullExceptionIfNullCollection() + { + // Arrange + List list = null; + string item = "Hello"; - [Test] - public void ShouldReturnTrueIfItemUpdated() - { - // Arrange - TestObject objectToAdd = TestObjectFaker.Create().Generate(); - TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); + // Act & Assert + Assert.Throws(() => list.Update(item, (s, i) => s == i)); + } - var list = new List { objectToAdd }; + [Test] + public void ShouldThrowArgumentNullExceptionIfNullItem() + { + // Arrange + var list = new List { "Hello" }; + string item = null; - // Act - bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToAdd.Name); + // Act & Assert + Assert.Throws(() => list.Update(item, (s, i) => s == i)); + } - // Assert - updated.ShouldBeTrue(); - } + [Test] + public void ShouldReturnTrueIfItemUpdated() + { + // Arrange + TestObject objectToAdd = TestObjectFaker.Create().Generate(); + TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); - [Test] - public void ShouldReturnFalseIfItemToUpdateDoesNotExist() - { - // Arrange - TestObject objectToAdd = TestObjectFaker.Create().Generate(); - TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); + var list = new List { objectToAdd }; - var list = new List { objectToAdd }; + // Act + bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToAdd.Name); - // Act - bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToUpdateWith.Name); + // Assert + updated.ShouldBeTrue(); + } - // Assert - updated.ShouldBeFalse(); - } + [Test] + public void ShouldReturnFalseIfItemToUpdateDoesNotExist() + { + // Arrange + TestObject objectToAdd = TestObjectFaker.Create().Generate(); + TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); + + var list = new List { objectToAdd }; + + // Act + bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToUpdateWith.Name); + + // Assert + updated.ShouldBeFalse(); } + } - public class WhenUpdatingCollectionEqualToAnother + public class WhenUpdatingCollectionEqualToAnother + { + [Test] + public void ShouldThrowArgumentNullExceptionIfNullCollection() { - [Test] - public void ShouldThrowArgumentNullExceptionIfNullCollection() - { - // Arrange - List list = null; + // Arrange + List list = null; - // Act & Assert - Assert.Throws(() => list.MakeEqualTo(null)); - } + // Act & Assert + Assert.Throws(() => list.MakeEqualTo(null)); + } - [Test] - public void ShouldThrowArgumentNullExceptionIfNullSource() - { - // Arrange - var list = new List { "Hello" }; + [Test] + public void ShouldThrowArgumentNullExceptionIfNullSource() + { + // Arrange + var list = new List { "Hello" }; - // Act & Assert - Assert.Throws(() => list.MakeEqualTo(null)); - } + // Act & Assert + Assert.Throws(() => list.MakeEqualTo(null)); + } - [Test] - public void ShouldUpdateCollectionToBeEqualOther() - { - // Arrange - var list = new List { "Hello" }; - var update = new List { "New", "List" }; + [Test] + public void ShouldUpdateCollectionToBeEqualOther() + { + // Arrange + var list = new List { "Hello" }; + var update = new List { "New", "List" }; - // Act - list.MakeEqualTo(update); + // Act + list.MakeEqualTo(update); - // Assert - list.ShouldBeEquivalentTo(update); - } + // Assert + list.ShouldBeEquivalentTo(update); } + } - public class WhenAddingRangeOfItems + public class WhenAddingRangeOfItems + { + [Test] + public void ShouldAddRangeOfItems() { - [Test] - public void ShouldAddRangeOfItems() - { - // Arrange - List objectsToAdd = TestObjectFaker.Create().Generate(10); + // Arrange + List objectsToAdd = TestObjectFaker.Create().Generate(10); - var collection = new ObservableCollection(); + var collection = new ObservableCollection(); - // Act - collection.AddRange(objectsToAdd); + // Act + collection.AddRange(objectsToAdd); - // Assert - foreach (TestObject item in objectsToAdd) - { - collection.ShouldContain(item); - } + // Assert + foreach (TestObject item in objectsToAdd) + { + collection.ShouldContain(item); } } + } - public class WhenRemovingRangeOfItems + public class WhenRemovingRangeOfItems + { + [Test] + public void ShouldRemoveRangeOfItems() { - [Test] - public void ShouldRemoveRangeOfItems() - { - // Arrange - List items = TestObjectFaker.Create().Generate(10); - var itemsToRemove = items.Take(5).ToList(); + // Arrange + List items = TestObjectFaker.Create().Generate(10); + var itemsToRemove = items.Take(5).ToList(); - var collection = new ObservableCollection(items); + var collection = new ObservableCollection(items); - // Act - collection.RemoveRange(itemsToRemove); + // Act + collection.RemoveRange(itemsToRemove); - // Assert - foreach (TestObject item in itemsToRemove) - { - collection.ShouldNotContain(item); - } + // Assert + foreach (TestObject item in itemsToRemove) + { + collection.ShouldNotContain(item); } } + } - public class WhenDeterminingIfCollectionsAreEquivalent + public class WhenDeterminingIfCollectionsAreEquivalent + { + [TestCaseSource(nameof(ValidCases))] + public void ShouldReturnTrueForValidCases(Collection expected, Collection actual) { - [TestCaseSource(nameof(ValidCases))] - public void ShouldReturnTrueForValidCases(Collection expected, Collection actual) - { - CollectionExtensions.AreEquivalent(expected, actual).ShouldBeTrue(); - } + CollectionExtensions.AreEquivalent(expected, actual).ShouldBeTrue(); + } - [TestCaseSource(nameof(InvalidCases))] - public void ShouldReturnFalseForInvalidCases(Collection expected, Collection actual) - { - CollectionExtensions.AreEquivalent(expected, actual).ShouldBeFalse(); - } + [TestCaseSource(nameof(InvalidCases))] + public void ShouldReturnFalseForInvalidCases(Collection expected, Collection actual) + { + CollectionExtensions.AreEquivalent(expected, actual).ShouldBeFalse(); + } - private static object[] ValidCases = - { - new object[] {null, null}, - new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3}}, - new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {3, 2, 1}}, - }; + private static object[] ValidCases = + { + new object[] {null, null}, + new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3}}, + new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {3, 2, 1}}, + }; - private static object[] InvalidCases = - { - new object[] {null, new ObservableCollection()}, - new object[] {new ObservableCollection(), null}, - new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {4, 5, 6}}, - new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3, 4}}, - }; + private static object[] InvalidCases = + { + new object[] {null, new ObservableCollection()}, + new object[] {new ObservableCollection(), null}, + new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {4, 5, 6}}, + new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3, 4}}, + }; + } + + public class WhenValidatingIfCollectionIsNullOrEmpty + { + [TestCaseSource(nameof(ValidEnumerableCases))] + public void ShouldReturnTrueIfEnumerableIsNullOrEmpty(IEnumerable collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); + + // Assert + isEmpty.ShouldBeTrue(); } - public class WhenValidatingIfCollectionIsNullOrEmpty + [TestCaseSource(nameof(ValidDictionaryCases))] + public void ShouldReturnTrueIfDictionaryIsNullOrEmpty(Dictionary collection) { - [TestCaseSource(nameof(ValidEnumerableCases))] - public void ShouldReturnTrueIfEnumerableIsNullOrEmpty(IEnumerable collection) - { - // Act - bool isEmpty = collection.IsNullOrEmpty(); + // Act + bool isEmpty = collection.IsNullOrEmpty(); - // Assert - isEmpty.ShouldBeTrue(); - } + // Assert + isEmpty.ShouldBeTrue(); + } - [TestCaseSource(nameof(ValidDictionaryCases))] - public void ShouldReturnTrueIfDictionaryIsNullOrEmpty(Dictionary collection) - { - // Act - bool isEmpty = collection.IsNullOrEmpty(); + [TestCaseSource(nameof(InvalidEnumerableCases))] + public void ShouldReturnFalseIfEnumerableIsNotNullOrEmpty(IEnumerable collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); - // Assert - isEmpty.ShouldBeTrue(); - } + // Assert + isEmpty.ShouldBeFalse(); + } - [TestCaseSource(nameof(InvalidEnumerableCases))] - public void ShouldReturnFalseIfEnumerableIsNotNullOrEmpty(IEnumerable collection) - { - // Act - bool isEmpty = collection.IsNullOrEmpty(); + [TestCaseSource(nameof(InvalidDictionaryCases))] + public void ShouldReturnFalseIfDictionaryIsNotNullOrEmpty(Dictionary collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); - // Assert - isEmpty.ShouldBeFalse(); - } + // Assert + isEmpty.ShouldBeFalse(); + } - [TestCaseSource(nameof(InvalidDictionaryCases))] - public void ShouldReturnFalseIfDictionaryIsNotNullOrEmpty(Dictionary collection) - { - // Act - bool isEmpty = collection.IsNullOrEmpty(); + private static object[] ValidEnumerableCases = + { + new object[] {null}, new object[] {new ObservableCollection()}, + }; - // Assert - isEmpty.ShouldBeFalse(); - } + private static object[] ValidDictionaryCases = + { + new object[] {null}, new object[] {new Dictionary()}, + }; - private static object[] ValidEnumerableCases = - { - new object[] {null}, new object[] {new ObservableCollection()}, - }; + private static object[] InvalidEnumerableCases = + { + new object[] {new ObservableCollection {1, 2, 3}}, + }; - private static object[] ValidDictionaryCases = - { - new object[] {null}, new object[] {new Dictionary()}, - }; + private static object[] InvalidDictionaryCases = + { + new object[] {new Dictionary {{1, "A"}, {2, "B"}, {3, "C"}}}, + }; + } + + public class WhenSortingObservableCollections + { + [Test] + public void ShouldSortBySimpleType() + { + // Arrange + var collection = new ObservableCollection { 3, 2, 1 }; + + // Act + collection.Sort(x => x); - private static object[] InvalidEnumerableCases = + // Assert + collection.ShouldBe(new[] { 1, 2, 3 }); + } + + [Test] + public void ShouldSortByComplexType() + { + // Arrange + var collection = new ObservableCollection { - new object[] {new ObservableCollection {1, 2, 3}}, + new() {Id = 0, Name = "James Croft"}, + new() {Id = 1, Name = "Guy Wilmer"}, + new() {Id = 2, Name = "Ben Hartley"}, + new() {Id = 3, Name = "Adam Llewellyn"}, }; - private static object[] InvalidDictionaryCases = + // Act + collection.Sort(x => x.Name); + + // Assert + collection.ShouldBe(new ComplexObject[] { - new object[] {new Dictionary {{1, "A"}, {2, "B"}, {3, "C"}}}, - }; + new() {Id = 3, Name = "Adam Llewellyn"}, new() {Id = 2, Name = "Ben Hartley"}, + new() {Id = 1, Name = "Guy Wilmer"}, new() {Id = 0, Name = "James Croft"}, + }); } - public class WhenSortingObservableCollections + [Test] + public void ShouldSortDescendingBySimpleType() { - [Test] - public void ShouldSortBySimpleType() - { - // Arrange - var collection = new ObservableCollection { 3, 2, 1 }; + // Arrange + var collection = new ObservableCollection { 2, 1, 3 }; - // Act - collection.Sort(x => x); + // Act + collection.SortDescending(x => x); - // Assert - collection.ShouldBe(new[] { 1, 2, 3 }); - } + // Assert + collection.ShouldBe(new[] { 3, 2, 1 }); + } - [Test] - public void ShouldSortByComplexType() + [Test] + public void ShouldSortDescendingByComplexType() + { + // Arrange + var collection = new ObservableCollection { - // Arrange - var collection = new ObservableCollection - { - new() {Id = 0, Name = "James Croft"}, - new() {Id = 1, Name = "Guy Wilmer"}, - new() {Id = 2, Name = "Ben Hartley"}, - new() {Id = 3, Name = "Adam Llewellyn"}, - }; - - // Act - collection.Sort(x => x.Name); + new() {Id = 0, Name = "Ben Hartley"}, + new() {Id = 1, Name = "James Croft"}, + new() {Id = 2, Name = "Adam Llewellyn"}, + new() {Id = 3, Name = "Guy Wilmer"}, + }; - // Assert - collection.ShouldBe(new ComplexObject[] - { - new() {Id = 3, Name = "Adam Llewellyn"}, new() {Id = 2, Name = "Ben Hartley"}, - new() {Id = 1, Name = "Guy Wilmer"}, new() {Id = 0, Name = "James Croft"}, - }); - } + // Act + collection.SortDescending(x => x.Name); - [Test] - public void ShouldSortDescendingBySimpleType() + // Assert + collection.ShouldBe(new ComplexObject[] { - // Arrange - var collection = new ObservableCollection { 2, 1, 3 }; + new() {Id = 1, Name = "James Croft"}, new() {Id = 3, Name = "Guy Wilmer"}, + new() {Id = 0, Name = "Ben Hartley"}, new() {Id = 2, Name = "Adam Llewellyn"}, + }); + } - // Act - collection.SortDescending(x => x); + private class ComplexObject : IEquatable + { + public int Id { get; set; } - // Assert - collection.ShouldBe(new[] { 3, 2, 1 }); - } + public string Name { get; set; } - [Test] - public void ShouldSortDescendingByComplexType() + public bool Equals(ComplexObject other) { - // Arrange - var collection = new ObservableCollection + if (ReferenceEquals(null, other)) { - new() {Id = 0, Name = "Ben Hartley"}, - new() {Id = 1, Name = "James Croft"}, - new() {Id = 2, Name = "Adam Llewellyn"}, - new() {Id = 3, Name = "Guy Wilmer"}, - }; - - // Act - collection.SortDescending(x => x.Name); + return false; + } - // Assert - collection.ShouldBe(new ComplexObject[] + if (ReferenceEquals(this, other)) { - new() {Id = 1, Name = "James Croft"}, new() {Id = 3, Name = "Guy Wilmer"}, - new() {Id = 0, Name = "Ben Hartley"}, new() {Id = 2, Name = "Adam Llewellyn"}, - }); + return true; + } + + return this.Id == other.Id && this.Name == other.Name; } - private class ComplexObject : IEquatable + public override bool Equals(object obj) { - public int Id { get; set; } - - public string Name { get; set; } - - public bool Equals(ComplexObject other) + if (ReferenceEquals(null, obj)) { - if (ReferenceEquals(null, other)) - { - return false; - } - - if (ReferenceEquals(this, other)) - { - return true; - } - - return this.Id == other.Id && this.Name == other.Name; + return false; } - public override bool Equals(object obj) + if (ReferenceEquals(this, obj)) { - if (ReferenceEquals(null, obj)) - { - return false; - } - - if (ReferenceEquals(this, obj)) - { - return true; - } - - return obj.GetType() == this.GetType() && this.Equals((ComplexObject)obj); + return true; } - public override int GetHashCode() - { - return HashCode.Combine(this.Id, this.Name); - } + return obj.GetType() == this.GetType() && this.Equals((ComplexObject)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(this.Id, this.Name); } } } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Tests/DictionaryExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/DictionaryExtensionsTests.cs index 102230e6..71808703 100644 --- a/tests/MADE.Collections.Tests/Tests/DictionaryExtensionsTests.cs +++ b/tests/MADE.Collections.Tests/Tests/DictionaryExtensionsTests.cs @@ -1,52 +1,49 @@ -namespace MADE.Collections.Tests.Tests +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Collections.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DictionaryExtensionsTests { - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; + public class WhenAddingOrUpdatingAnItem + { + [Test] + public void ShouldAddNewKeyValuePairIfNotExisting() + { + // Arrange + string key = "Hello"; + string value = "World"; - using NUnit.Framework; + var dictionary = new Dictionary(); - using Shouldly; + // Act + dictionary.AddOrUpdate(key, value); - [ExcludeFromCodeCoverage] - [TestFixture] - public class DictionaryExtensionsTests - { - public class WhenAddingOrUpdatingAnItem + // Assert + string actualValue = dictionary[key]; + actualValue.ShouldBe(value); + } + + [Test] + public void ShouldUpdateValueForExistingKeyValuePair() { - [Test] - public void ShouldAddNewKeyValuePairIfNotExisting() - { - // Arrange - string key = "Hello"; - string value = "World"; - - var dictionary = new Dictionary(); - - // Act - dictionary.AddOrUpdate(key, value); - - // Assert - string actualValue = dictionary[key]; - actualValue.ShouldBe(value); - } - - [Test] - public void ShouldUpdateValueForExistingKeyValuePair() - { - // Arrange - string key = "Hello"; - string previousValue = "World"; - string newValue = "MADE"; - - var dictionary = new Dictionary { { key, previousValue } }; - - // Act - dictionary.AddOrUpdate(key, newValue); - - // Assert - string actualValue = dictionary[key]; - actualValue.ShouldBe(newValue); - } + // Arrange + string key = "Hello"; + string previousValue = "World"; + string newValue = "MADE"; + + var dictionary = new Dictionary { { key, previousValue } }; + + // Act + dictionary.AddOrUpdate(key, newValue); + + // Assert + string actualValue = dictionary[key]; + actualValue.ShouldBe(newValue); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Tests/GenericEqualityComparerTests.cs b/tests/MADE.Collections.Tests/Tests/GenericEqualityComparerTests.cs index 9c300e91..4d105580 100644 --- a/tests/MADE.Collections.Tests/Tests/GenericEqualityComparerTests.cs +++ b/tests/MADE.Collections.Tests/Tests/GenericEqualityComparerTests.cs @@ -1,180 +1,176 @@ -namespace MADE.Collections.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - - using MADE.Collections.Compare; - using MADE.Collections.Tests.Fakes; - - using NUnit.Framework; +using System.Diagnostics.CodeAnalysis; +using MADE.Collections.Compare; +using MADE.Collections.Tests.Fakes; +using NUnit.Framework; +using Shouldly; - using Shouldly; +namespace MADE.Collections.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class GenericEqualityComparerTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class GenericEqualityComparerTests +{ + public class WhenComparingObjects { - public class WhenComparingObjects + [Test] + public void ShouldReturnTrueIfObjectsSameReferenceAndComparingOnObject() { - [Test] - public void ShouldReturnTrueIfObjectsSameReferenceAndComparingOnObject() - { - // Arrange - string objectName = "Hello, World!"; - int objectCount = 10; + // Arrange + string objectName = "Hello, World!"; + int objectCount = 10; - var first = new TestObject { Name = objectName, Count = objectCount }; - TestObject second = first; + var first = new TestObject { Name = objectName, Count = objectCount }; + TestObject second = first; - var comparer = new GenericEqualityComparer(o => o); - - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o); - // Assert - areEqual.ShouldBeTrue(); - } + // Act + bool areEqual = comparer.Equals(first, second); - [Test] - public void ShouldReturnTrueIfObjectsSimilarImplementIEquatableAndComparingOnObject() - { - // Arrange - string objectName = "Hello, World!"; - int objectCount = 10; + // Assert + areEqual.ShouldBeTrue(); + } - var first = new TestEqualityObject { Name = objectName, Count = objectCount }; - var second = new TestEqualityObject { Name = objectName, Count = objectCount }; + [Test] + public void ShouldReturnTrueIfObjectsSimilarImplementIEquatableAndComparingOnObject() + { + // Arrange + string objectName = "Hello, World!"; + int objectCount = 10; - var comparer = new GenericEqualityComparer(o => o); + var first = new TestEqualityObject { Name = objectName, Count = objectCount }; + var second = new TestEqualityObject { Name = objectName, Count = objectCount }; - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o); - // Assert - areEqual.ShouldBeTrue(); - } + // Act + bool areEqual = comparer.Equals(first, second); - [Test] - public void ShouldReturnTrueIfObjectsSimilarAndComparingOnPropertyWithSameValue() - { - // Arrange - string objectName = "Hello, World!"; - int objectCount = 10; + // Assert + areEqual.ShouldBeTrue(); + } - var first = new TestObject { Name = objectName, Count = objectCount }; - var second = new TestObject { Name = objectName, Count = objectCount }; + [Test] + public void ShouldReturnTrueIfObjectsSimilarAndComparingOnPropertyWithSameValue() + { + // Arrange + string objectName = "Hello, World!"; + int objectCount = 10; - var comparer = new GenericEqualityComparer(o => o.Name); + var first = new TestObject { Name = objectName, Count = objectCount }; + var second = new TestObject { Name = objectName, Count = objectCount }; - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o.Name); - // Assert - areEqual.ShouldBeTrue(); - } + // Act + bool areEqual = comparer.Equals(first, second); - [Test] - public void ShouldReturnTrueIfObjectsDifferentAndComparingOnPropertyWithSameValue() - { - // Arrange - int count = 10; - var first = new TestObject { Name = "Hello, World", Count = count }; - var second = new TestObject { Name = "World, Hello", Count = count }; + // Assert + areEqual.ShouldBeTrue(); + } - var comparer = new GenericEqualityComparer(o => o.Count); + [Test] + public void ShouldReturnTrueIfObjectsDifferentAndComparingOnPropertyWithSameValue() + { + // Arrange + int count = 10; + var first = new TestObject { Name = "Hello, World", Count = count }; + var second = new TestObject { Name = "World, Hello", Count = count }; - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o.Count); - // Assert - areEqual.ShouldBeTrue(); - } + // Act + bool areEqual = comparer.Equals(first, second); - [Test] - public void ShouldReturnFaleIfObjectsDifferentAndComparingOnObject() - { - // Arrange - var first = new TestObject { Name = "Hello, World", Count = 10 }; - var second = new TestObject { Name = "World, Hello", Count = 5 }; + // Assert + areEqual.ShouldBeTrue(); + } - var comparer = new GenericEqualityComparer(o => o); + [Test] + public void ShouldReturnFaleIfObjectsDifferentAndComparingOnObject() + { + // Arrange + var first = new TestObject { Name = "Hello, World", Count = 10 }; + var second = new TestObject { Name = "World, Hello", Count = 5 }; - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o); - // Assert - areEqual.ShouldBeFalse(); - } + // Act + bool areEqual = comparer.Equals(first, second); - [Test] - public void ShouldReturnFalseIfObjectsDifferentImplementIEquatableAndComparingOnObject() - { - // Arrange - var first = new TestEqualityObject { Name = "Hello, World", Count = 10 }; - var second = new TestEqualityObject { Name = "World, Hello", Count = 5 }; + // Assert + areEqual.ShouldBeFalse(); + } - var comparer = new GenericEqualityComparer(o => o); + [Test] + public void ShouldReturnFalseIfObjectsDifferentImplementIEquatableAndComparingOnObject() + { + // Arrange + var first = new TestEqualityObject { Name = "Hello, World", Count = 10 }; + var second = new TestEqualityObject { Name = "World, Hello", Count = 5 }; - // Act - bool areEqual = comparer.Equals(first, second); + var comparer = new GenericEqualityComparer(o => o); - // Assert - areEqual.ShouldBeFalse(); - } + // Act + bool areEqual = comparer.Equals(first, second); + + // Assert + areEqual.ShouldBeFalse(); + } - [Test] - public void ShouldReturnFalseIfObjectsDifferentAndComparingOnPropertyWithDifferentValue() - { - // Arrange - var first = new TestObject { Name = "Hello, World", Count = 10 }; - var second = new TestObject { Name = "World, Hello", Count = 5 }; + [Test] + public void ShouldReturnFalseIfObjectsDifferentAndComparingOnPropertyWithDifferentValue() + { + // Arrange + var first = new TestObject { Name = "Hello, World", Count = 10 }; + var second = new TestObject { Name = "World, Hello", Count = 5 }; - var comparer = new GenericEqualityComparer(o => o.Name); + var comparer = new GenericEqualityComparer(o => o.Name); - // Act - bool areEqual = comparer.Equals(first, second); + // Act + bool areEqual = comparer.Equals(first, second); - // Assert - areEqual.ShouldBeFalse(); - } + // Assert + areEqual.ShouldBeFalse(); } + } - public class WhenRetrievingHashCode + public class WhenRetrievingHashCode + { + [Test] + public void ShouldReturnSameHashCodeWhenComparingOnObject() { - [Test] - public void ShouldReturnSameHashCodeWhenComparingOnObject() - { - // Arrange - string objectName = "Hello, World!"; - int objectCount = 10; + // Arrange + string objectName = "Hello, World!"; + int objectCount = 10; - var obj = new TestObject { Name = objectName, Count = objectCount }; + var obj = new TestObject { Name = objectName, Count = objectCount }; - var comparer = new GenericEqualityComparer(o => o); + var comparer = new GenericEqualityComparer(o => o); - // Act - int hashCode = comparer.GetHashCode(obj); + // Act + int hashCode = comparer.GetHashCode(obj); - // Assert - hashCode.ShouldBe(obj.GetHashCode()); - } + // Assert + hashCode.ShouldBe(obj.GetHashCode()); + } - [Test] - public void ShouldReturnSameHashCodeWhenComparingOnProperty() - { - // Arrange - string objectName = "Hello, World!"; - int objectCount = 10; + [Test] + public void ShouldReturnSameHashCodeWhenComparingOnProperty() + { + // Arrange + string objectName = "Hello, World!"; + int objectCount = 10; - var obj = new TestObject { Name = objectName, Count = objectCount }; + var obj = new TestObject { Name = objectName, Count = objectCount }; - var comparer = new GenericEqualityComparer(o => o.Name); + var comparer = new GenericEqualityComparer(o => o.Name); - // Act - int hashCode = comparer.GetHashCode(obj); + // Act + int hashCode = comparer.GetHashCode(obj); - // Assert - hashCode.ShouldBe(obj.Name.GetHashCode()); - } + // Assert + hashCode.ShouldBe(obj.Name.GetHashCode()); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Collections.Tests/Tests/ObservableItemCollectionTests.cs b/tests/MADE.Collections.Tests/Tests/ObservableItemCollectionTests.cs index b86e8c44..ec3f81b5 100644 --- a/tests/MADE.Collections.Tests/Tests/ObservableItemCollectionTests.cs +++ b/tests/MADE.Collections.Tests/Tests/ObservableItemCollectionTests.cs @@ -1,267 +1,263 @@ -namespace MADE.Collections.Tests.Tests +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using MADE.Collections.ObjectModel; +using MADE.Collections.Tests.Fakes; +using MADE.Testing; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Collections.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ObservableItemCollectionTests { - using System.Collections.Generic; - using System.Collections.Specialized; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading.Tasks; - - using MADE.Collections.ObjectModel; - using MADE.Collections.Tests.Fakes; - using MADE.Testing; - - using NUnit.Framework; - - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class ObservableItemCollectionTests + public class WhenInitializing { - public class WhenInitializing + [Test] + public void ShouldBeEmptyIfDefaultConstructor() { - [Test] - public void ShouldBeEmptyIfDefaultConstructor() - { - // Act - var collection = new ObservableItemCollection(); + // Act + var collection = new ObservableItemCollection(); - // Assert - collection.Count.ShouldBe(0); - } + // Assert + collection.Count.ShouldBe(0); + } - [Test] - public void ShouldContainItemsIfInitializedAsEnumerable() - { - // Arrange - IEnumerable objects = TestObservableObjectFaker.Create().Generate(10); + [Test] + public void ShouldContainItemsIfInitializedAsEnumerable() + { + // Arrange + IEnumerable objects = TestObservableObjectFaker.Create().Generate(10); - // Act - var collection = new ObservableItemCollection(objects); + // Act + var collection = new ObservableItemCollection(objects); - // Assert - collection.Count.ShouldBe(objects.Count()); - collection.ShouldBeEquivalentTo(objects); - } + // Assert + collection.Count.ShouldBe(objects.Count()); + collection.ShouldBeEquivalentTo(objects); + } - [Test] - public void ShouldContainItemsIfInitializedAsList() - { - // Arrange - List objects = TestObservableObjectFaker.Create().Generate(10); + [Test] + public void ShouldContainItemsIfInitializedAsList() + { + // Arrange + List objects = TestObservableObjectFaker.Create().Generate(10); - // Act - var collection = new ObservableItemCollection(objects); + // Act + var collection = new ObservableItemCollection(objects); - // Assert - collection.Count.ShouldBe(objects.Count()); - collection.ShouldBeEquivalentTo(objects); - } + // Assert + collection.Count.ShouldBe(objects.Count()); + collection.ShouldBeEquivalentTo(objects); } + } - public class WhenAddingItems + public class WhenAddingItems + { + [Test] + public void ShouldAddRangeOfItems() { - [Test] - public void ShouldAddRangeOfItems() - { - // Arrange - List objectsToAdd = TestObservableObjectFaker.Create().Generate(10); + // Arrange + List objectsToAdd = TestObservableObjectFaker.Create().Generate(10); - var collection = new ObservableItemCollection(); + var collection = new ObservableItemCollection(); - // Act - collection.AddRange(objectsToAdd); + // Act + collection.AddRange(objectsToAdd); - // Assert - foreach (TestObservableObject item in objectsToAdd) - { - collection.ShouldContain(item); - } + // Assert + foreach (TestObservableObject item in objectsToAdd) + { + collection.ShouldContain(item); } + } - [Test] - public void ShouldAddSingleItem() - { - // Arrange - TestObservableObject objectToAdd = TestObservableObjectFaker.Create().Generate(); + [Test] + public void ShouldAddSingleItem() + { + // Arrange + TestObservableObject objectToAdd = TestObservableObjectFaker.Create().Generate(); - // Act - var collection = new ObservableItemCollection { objectToAdd }; + // Act + var collection = new ObservableItemCollection { objectToAdd }; - // Assert - collection.ShouldContain(objectToAdd); - } + // Assert + collection.ShouldContain(objectToAdd); + } - [Test] - public async Task ShouldRaiseCollectionChangedEventForAdd() - { - // Arrange - TestObservableObject objectToAdd = TestObservableObjectFaker.Create().Generate(); + [Test] + public async Task ShouldRaiseCollectionChangedEventForAdd() + { + // Arrange + TestObservableObject objectToAdd = TestObservableObjectFaker.Create().Generate(); - var collection = new ObservableItemCollection(); + var collection = new ObservableItemCollection(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - collection.CollectionChanged += (sender, args) => - { - tcs.SetResult(args); - }; + collection.CollectionChanged += (sender, args) => + { + tcs.SetResult(args); + }; - // Act - collection.Add(objectToAdd); + // Act + collection.Add(objectToAdd); - // Assert - NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; + // Assert + NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; - collectionChanged.ShouldNotBeNull(); - collectionChanged.NewItems.Contains(objectToAdd).ShouldBeTrue(); - } + collectionChanged.ShouldNotBeNull(); + collectionChanged.NewItems.Contains(objectToAdd).ShouldBeTrue(); + } - [Test] - public async Task ShouldRaiseCollectionChangedEventForAddRange() - { - // Arrange - List objectsToAdd = TestObservableObjectFaker.Create().Generate(10); + [Test] + public async Task ShouldRaiseCollectionChangedEventForAddRange() + { + // Arrange + List objectsToAdd = TestObservableObjectFaker.Create().Generate(10); - var collection = new ObservableItemCollection(); + var collection = new ObservableItemCollection(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - collection.CollectionChanged += (sender, args) => - { - tcs.SetResult(args); - }; + collection.CollectionChanged += (sender, args) => + { + tcs.SetResult(args); + }; - // Act - collection.AddRange(objectsToAdd); + // Act + collection.AddRange(objectsToAdd); - // Assert - NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; - collectionChanged.ShouldNotBeNull(); - collectionChanged.NewItems.Count.ShouldBe(objectsToAdd.Count); - foreach (object item in collectionChanged.NewItems) - { - objectsToAdd.ShouldContain(item); - } + // Assert + NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; + collectionChanged.ShouldNotBeNull(); + collectionChanged.NewItems.Count.ShouldBe(objectsToAdd.Count); + foreach (object item in collectionChanged.NewItems) + { + objectsToAdd.ShouldContain(item); } } + } - public class WhenRemovingItems + public class WhenRemovingItems + { + [Test] + public void ShouldRemoveRangeOfItems() { - [Test] - public void ShouldRemoveRangeOfItems() - { - // Arrange - List items = TestObservableObjectFaker.Create().Generate(10); - var itemsToRemove = items.Take(5).ToList(); + // Arrange + List items = TestObservableObjectFaker.Create().Generate(10); + var itemsToRemove = items.Take(5).ToList(); - var collection = new ObservableItemCollection(items); + var collection = new ObservableItemCollection(items); - // Act - collection.RemoveRange(itemsToRemove); + // Act + collection.RemoveRange(itemsToRemove); - // Assert - foreach (TestObservableObject item in itemsToRemove) - { - collection.ShouldNotContain(item); - } + // Assert + foreach (TestObservableObject item in itemsToRemove) + { + collection.ShouldNotContain(item); } + } - [Test] - public void ShouldRemoveSingleItem() - { - // Arrange - TestObservableObject objectToRemove = TestObservableObjectFaker.Create().Generate(); - var collection = new ObservableItemCollection { objectToRemove }; + [Test] + public void ShouldRemoveSingleItem() + { + // Arrange + TestObservableObject objectToRemove = TestObservableObjectFaker.Create().Generate(); + var collection = new ObservableItemCollection { objectToRemove }; - // Act - collection.Remove(objectToRemove); + // Act + collection.Remove(objectToRemove); - // Assert - collection.ShouldNotContain(objectToRemove); - } + // Assert + collection.ShouldNotContain(objectToRemove); + } - [Test] - public async Task ShouldRaiseCollectionChangedEventForRemove() - { - // Arrange - TestObservableObject objectToRemove = TestObservableObjectFaker.Create().Generate(); - var collection = new ObservableItemCollection { objectToRemove }; + [Test] + public async Task ShouldRaiseCollectionChangedEventForRemove() + { + // Arrange + TestObservableObject objectToRemove = TestObservableObjectFaker.Create().Generate(); + var collection = new ObservableItemCollection { objectToRemove }; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - collection.CollectionChanged += (sender, args) => - { - tcs.SetResult(args); - }; + collection.CollectionChanged += (sender, args) => + { + tcs.SetResult(args); + }; - // Act - collection.Remove(objectToRemove); + // Act + collection.Remove(objectToRemove); - // Assert - NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; + // Assert + NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; - collectionChanged.ShouldNotBeNull(); - collectionChanged.OldItems.Contains(objectToRemove).ShouldBeTrue(); - } + collectionChanged.ShouldNotBeNull(); + collectionChanged.OldItems.Contains(objectToRemove).ShouldBeTrue(); + } - [Test] - public async Task ShouldRaiseCollectionChangedEventForRemoveRange() - { - // Arrange - List objectsToRemove = TestObservableObjectFaker.Create().Generate(10); + [Test] + public async Task ShouldRaiseCollectionChangedEventForRemoveRange() + { + // Arrange + List objectsToRemove = TestObservableObjectFaker.Create().Generate(10); - var collection = new ObservableItemCollection(); + var collection = new ObservableItemCollection(); - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - collection.CollectionChanged += (sender, args) => - { - tcs.SetResult(args); - }; + collection.CollectionChanged += (sender, args) => + { + tcs.SetResult(args); + }; - // Act - collection.RemoveRange(objectsToRemove); + // Act + collection.RemoveRange(objectsToRemove); - // Assert - NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; - collectionChanged.ShouldNotBeNull(); - collectionChanged.OldItems.Count.ShouldBe(objectsToRemove.Count); - foreach (object item in collectionChanged.OldItems) - { - objectsToRemove.ShouldContain(item); - } + // Assert + NotifyCollectionChangedEventArgs collectionChanged = await tcs.Task; + collectionChanged.ShouldNotBeNull(); + collectionChanged.OldItems.Count.ShouldBe(objectsToRemove.Count); + foreach (object item in collectionChanged.OldItems) + { + objectsToRemove.ShouldContain(item); } } + } - public class WhenItemPropertyChanges + public class WhenItemPropertyChanges + { + [Test] + public async Task ShouldRaisePropertyChangedEvent() { - [Test] - public async Task ShouldRaisePropertyChangedEvent() - { - // Arrange - TestObservableObject obj = TestObservableObjectFaker.Create().Generate(); + // Arrange + TestObservableObject obj = TestObservableObjectFaker.Create().Generate(); - var collection = new ObservableItemCollection { obj }; + var collection = new ObservableItemCollection { obj }; - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); - collection.ItemPropertyChanged += (sender, args) => - { - tcs.SetResult(args); - }; + collection.ItemPropertyChanged += (sender, args) => + { + tcs.SetResult(args); + }; - // Act - obj.Name = "Hello, World!"; + // Act + obj.Name = "Hello, World!"; - // Assert - ObservableItemCollectionPropertyChangedEventArgs result = await tcs.Task; + // Assert + ObservableItemCollectionPropertyChangedEventArgs result = await tcs.Task; - result.ShouldNotBeNull(); - result.Sender.ShouldBe(obj); - result.EventArgs.ShouldNotBeNull(); - result.EventArgs.PropertyName.ShouldBe(nameof(TestObservableObject.Name)); - } + result.ShouldNotBeNull(); + result.Sender.ShouldBe(obj); + result.EventArgs.ShouldNotBeNull(); + result.EventArgs.PropertyName.ShouldBe(nameof(TestObservableObject.Name)); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/BooleanExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/BooleanExtensionsTests.cs index c2d26776..efa7ab58 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/BooleanExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/BooleanExtensionsTests.cs @@ -1,88 +1,87 @@ -namespace MADE.Data.Converters.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class BooleanExtensionsTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class BooleanExtensionsTests + public class WhenConvertingBooleanToFormattedString { - public class WhenConvertingBooleanToFormattedString + [Test] + public void ShouldReturnTrueValueIfTrue() + { + // Arrange + const bool boolean = true; + const string expected = "Yes"; + + // Act + string formatted = boolean.ToFormattedString(expected, "No"); + + // Assert + formatted.ShouldBe(expected); + } + + [Test] + public void ShouldReturnFalseValueIfFalse() { - [Test] - public void ShouldReturnTrueValueIfTrue() - { - // Arrange - const bool boolean = true; - const string expected = "Yes"; - - // Act - string formatted = boolean.ToFormattedString(expected, "No"); - - // Assert - formatted.ShouldBe(expected); - } - - [Test] - public void ShouldReturnFalseValueIfFalse() - { - // Arrange - const bool boolean = false; - const string expected = "No"; - - // Act - string formatted = boolean.ToFormattedString("Yes", expected); - - // Assert - formatted.ShouldBe(expected); - } + // Arrange + const bool boolean = false; + const string expected = "No"; + + // Act + string formatted = boolean.ToFormattedString("Yes", expected); + + // Assert + formatted.ShouldBe(expected); } + } - public class WhenConvertingNullableBooleanToFormattedString + public class WhenConvertingNullableBooleanToFormattedString + { + [Test] + public void ShouldReturnTrueValueIfTrue() { - [Test] - public void ShouldReturnTrueValueIfTrue() - { - // Arrange - bool? boolean = true; - const string expected = "Yes"; - - // Act - string formatted = boolean.ToFormattedString(expected, "No", "N/A"); - - // Assert - formatted.ShouldBe(expected); - } - - [Test] - public void ShouldReturnFalseValueIfFalse() - { - // Arrange - bool? boolean = false; - const string expected = "No"; - - // Act - string formatted = boolean.ToFormattedString("Yes", expected, "N/A"); - - // Assert - formatted.ShouldBe(expected); - } - - [Test] - public void ShouldReturnNullValueIfNull() - { - // Arrange - bool? boolean = null; - const string expected = "N/A"; - - // Act - string formatted = boolean.ToFormattedString("Yes", "No", expected); - - // Assert - formatted.ShouldBe(expected); - } + // Arrange + bool? boolean = true; + const string expected = "Yes"; + + // Act + string formatted = boolean.ToFormattedString(expected, "No", "N/A"); + + // Assert + formatted.ShouldBe(expected); + } + + [Test] + public void ShouldReturnFalseValueIfFalse() + { + // Arrange + bool? boolean = false; + const string expected = "No"; + + // Act + string formatted = boolean.ToFormattedString("Yes", expected, "N/A"); + + // Assert + formatted.ShouldBe(expected); + } + + [Test] + public void ShouldReturnNullValueIfNull() + { + // Arrange + bool? boolean = null; + const string expected = "N/A"; + + // Act + string formatted = boolean.ToFormattedString("Yes", "No", expected); + + // Assert + formatted.ShouldBe(expected); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/BooleanToStringValueConverterTests.cs b/tests/MADE.Data.Converters.Tests/Tests/BooleanToStringValueConverterTests.cs index 4d298df8..d3ae34c2 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/BooleanToStringValueConverterTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/BooleanToStringValueConverterTests.cs @@ -1,94 +1,92 @@ -namespace MADE.Data.Converters.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Exceptions; - using NUnit.Framework; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Exceptions; +using NUnit.Framework; +using Shouldly; - using Shouldly; +namespace MADE.Data.Converters.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class BooleanToStringValueConverterTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class BooleanToStringValueConverterTests +{ + public class WhenConverting { - public class WhenConverting + [Test] + public void ShouldConvertToTrueValueWhenTrue() { - [Test] - public void ShouldConvertToTrueValueWhenTrue() - { - // Arrange - const bool boolean = true; - const string expected = "Yes"; + // Arrange + const bool boolean = true; + const string expected = "Yes"; - var converter = new BooleanToStringValueConverter {TrueValue = expected, FalseValue = "No"}; + var converter = new BooleanToStringValueConverter {TrueValue = expected, FalseValue = "No"}; - // Act - string converted = converter.Convert(boolean); + // Act + string converted = converter.Convert(boolean); - // Assert - converted.ShouldBe(expected); - } + // Assert + converted.ShouldBe(expected); + } - [Test] - public void ShouldConvertToFalseValueWhenFalse() - { - const bool boolean = false; - const string expected = "No"; + [Test] + public void ShouldConvertToFalseValueWhenFalse() + { + const bool boolean = false; + const string expected = "No"; - var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = expected}; + var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = expected}; - // Act - string converted = converter.Convert(boolean); + // Act + string converted = converter.Convert(boolean); - // Assert - converted.ShouldBe(expected); - } + // Assert + converted.ShouldBe(expected); } + } - public class WhenConvertingBack + public class WhenConvertingBack + { + [Test] + public void ShouldConvertToTrueWhenTrueValue() { - [Test] - public void ShouldConvertToTrueWhenTrueValue() - { - // Arrange - const string booleanString = "Yes"; - const bool expected = true; + // Arrange + const string booleanString = "Yes"; + const bool expected = true; - var converter = new BooleanToStringValueConverter {TrueValue = booleanString, FalseValue = "No"}; + var converter = new BooleanToStringValueConverter {TrueValue = booleanString, FalseValue = "No"}; - // Act - bool converted = converter.ConvertBack(booleanString); + // Act + bool converted = converter.ConvertBack(booleanString); - // Assert - converted.ShouldBe(expected); - } + // Assert + converted.ShouldBe(expected); + } - [Test] - public void ShouldConvertToFalseWhenFalseValue() - { - // Arrange - const string booleanString = "No"; - const bool expected = false; + [Test] + public void ShouldConvertToFalseWhenFalseValue() + { + // Arrange + const string booleanString = "No"; + const bool expected = false; - var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = booleanString}; + var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = booleanString}; - // Act - bool converted = converter.ConvertBack(booleanString); + // Act + bool converted = converter.ConvertBack(booleanString); - // Assert - converted.ShouldBe(expected); - } + // Assert + converted.ShouldBe(expected); + } - [Test] - public void ShouldThrowInvalidDataConversionExceptionIfNotTrueOrFalseValue() - { - // Arrange - const string booleanString = "Not valid"; + [Test] + public void ShouldThrowInvalidDataConversionExceptionIfNotTrueOrFalseValue() + { + // Arrange + const string booleanString = "Not valid"; - var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = "No"}; + var converter = new BooleanToStringValueConverter {TrueValue = "Yes", FalseValue = "No"}; - // Act & Assert - Should.Throw(() => converter.ConvertBack(booleanString)); - } + // Act & Assert + Should.Throw(() => converter.ConvertBack(booleanString)); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs index 446c522d..37c5e6bc 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs @@ -1,41 +1,40 @@ -namespace MADE.Data.Converters.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Extensions; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class CollectionExtensionsTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class CollectionExtensionsTests +{ + public class WhenConvertingToDelimitedString { - public class WhenConvertingToDelimitedString + [Test] + public void ShouldReturnItemsWithCommaDelimiterByDefault() { - [Test] - public void ShouldReturnItemsWithCommaDelimiterByDefault() - { - // Arrange - var items = new[] { "item1", "item2", "item3" }; + // Arrange + var items = new[] { "item1", "item2", "item3" }; - // Act - var result = items.ToDelimitedString(); + // Act + var result = items.ToDelimitedString(); - // Assert - result.ShouldBe("item1,item2,item3"); - } + // Assert + result.ShouldBe("item1,item2,item3"); + } - [Test] - public void ShouldReturnItemsWithCustomDelimiter() - { - // Arrange - var items = new[] { "item1", "item2", "item3" }; + [Test] + public void ShouldReturnItemsWithCustomDelimiter() + { + // Arrange + var items = new[] { "item1", "item2", "item3" }; - // Act - var result = items.ToDelimitedString(delimiter: " | "); + // Act + var result = items.ToDelimitedString(delimiter: " | "); - // Assert - result.ShouldBe("item1 | item2 | item3"); - } + // Assert + result.ShouldBe("item1 | item2 | item3"); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/DateTimeExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/DateTimeExtensionsTests.cs index 4e12dfd3..ba71c43a 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/DateTimeExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/DateTimeExtensionsTests.cs @@ -1,351 +1,350 @@ -namespace MADE.Data.Converters.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Constants; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DateTimeExtensionsTests { - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Constants; - using MADE.Data.Converters.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class DateTimeExtensionsTests + public class WhenConvertingDateToDaySuffix { - public class WhenConvertingDateToDaySuffix + private static object[] TestCases = { - private static object[] TestCases = - { - new object[] {new DateTime(2022, 3, 1), "st"}, new object[] {new DateTime(2022, 3, 2), "nd"}, - new object[] {new DateTime(2022, 3, 3), "rd"}, new object[] {new DateTime(2022, 3, 4), "th"}, - new object[] {new DateTime(2022, 3, 5), "th"}, new object[] {new DateTime(2022, 3, 6), "th"}, - new object[] {new DateTime(2022, 3, 7), "th"}, new object[] {new DateTime(2022, 3, 8), "th"}, - new object[] {new DateTime(2022, 3, 9), "th"}, new object[] {new DateTime(2022, 3, 10), "th"}, - new object[] {new DateTime(2022, 3, 11), "th"}, new object[] {new DateTime(2022, 3, 12), "th"}, - new object[] {new DateTime(2022, 3, 13), "th"}, new object[] {new DateTime(2022, 3, 14), "th"}, - new object[] {new DateTime(2022, 3, 15), "th"}, new object[] {new DateTime(2022, 3, 16), "th"}, - new object[] {new DateTime(2022, 3, 17), "th"}, new object[] {new DateTime(2022, 3, 18), "th"}, - new object[] {new DateTime(2022, 3, 19), "th"}, new object[] {new DateTime(2022, 3, 20), "th"}, - new object[] {new DateTime(2022, 3, 21), "st"}, new object[] {new DateTime(2022, 3, 22), "nd"}, - new object[] {new DateTime(2022, 3, 23), "rd"}, new object[] {new DateTime(2022, 3, 24), "th"}, - new object[] {new DateTime(2022, 3, 25), "th"}, new object[] {new DateTime(2022, 3, 26), "th"}, - new object[] {new DateTime(2022, 3, 27), "th"}, new object[] {new DateTime(2022, 3, 28), "th"}, - new object[] {new DateTime(2022, 3, 29), "th"}, new object[] {new DateTime(2022, 3, 30), "th"}, - new object[] {new DateTime(2022, 3, 31), "st"}, - }; - - [TestCaseSource(nameof(TestCases))] - public void ShouldReturnCorrectDaySuffix(DateTime dateTime, string expected) - { - // Act - var result = dateTime.ToDaySuffix(); - - // Assert - result.ShouldBe(expected); - } + new object[] {new DateTime(2022, 3, 1), "st"}, new object[] {new DateTime(2022, 3, 2), "nd"}, + new object[] {new DateTime(2022, 3, 3), "rd"}, new object[] {new DateTime(2022, 3, 4), "th"}, + new object[] {new DateTime(2022, 3, 5), "th"}, new object[] {new DateTime(2022, 3, 6), "th"}, + new object[] {new DateTime(2022, 3, 7), "th"}, new object[] {new DateTime(2022, 3, 8), "th"}, + new object[] {new DateTime(2022, 3, 9), "th"}, new object[] {new DateTime(2022, 3, 10), "th"}, + new object[] {new DateTime(2022, 3, 11), "th"}, new object[] {new DateTime(2022, 3, 12), "th"}, + new object[] {new DateTime(2022, 3, 13), "th"}, new object[] {new DateTime(2022, 3, 14), "th"}, + new object[] {new DateTime(2022, 3, 15), "th"}, new object[] {new DateTime(2022, 3, 16), "th"}, + new object[] {new DateTime(2022, 3, 17), "th"}, new object[] {new DateTime(2022, 3, 18), "th"}, + new object[] {new DateTime(2022, 3, 19), "th"}, new object[] {new DateTime(2022, 3, 20), "th"}, + new object[] {new DateTime(2022, 3, 21), "st"}, new object[] {new DateTime(2022, 3, 22), "nd"}, + new object[] {new DateTime(2022, 3, 23), "rd"}, new object[] {new DateTime(2022, 3, 24), "th"}, + new object[] {new DateTime(2022, 3, 25), "th"}, new object[] {new DateTime(2022, 3, 26), "th"}, + new object[] {new DateTime(2022, 3, 27), "th"}, new object[] {new DateTime(2022, 3, 28), "th"}, + new object[] {new DateTime(2022, 3, 29), "th"}, new object[] {new DateTime(2022, 3, 30), "th"}, + new object[] {new DateTime(2022, 3, 31), "st"}, + }; + + [TestCaseSource(nameof(TestCases))] + public void ShouldReturnCorrectDaySuffix(DateTime dateTime, string expected) + { + // Act + var result = dateTime.ToDaySuffix(); + + // Assert + result.ShouldBe(expected); + } + } + + public class WhenSettingTime + { + [Test] + public void ShouldSetNullableDateTimeIfProvidedTimeAsTimeSpan() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 30); + DateTime? dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime); + + // Assert + dateTime.Value.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetDateTimeIfProvidedTimeAsTimeSpan() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 30); + var dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime); + + // Assert + dateTime.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursAndMinutes() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 0); + DateTime? dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes); + + // Assert + dateTime.Value.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetDateTimeIfProvidedTimeAsHoursAndMinutes() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 0); + var dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes); + + // Assert + dateTime.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursMinutesAndSeconds() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 30); + DateTime? dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes, expectedTime.Seconds); + + // Assert + dateTime.Value.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetDateTimeIfProvidedTimeAsHoursMinutesAndSeconds() + { + // Arrange + var expectedTime = new TimeSpan(9, 10, 30); + var dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes, expectedTime.Seconds); + + // Assert + dateTime.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursMinutesSecondsAndMilliseconds() + { + // Arrange + var expectedTime = new TimeSpan(0, 9, 10, 30, 10); + DateTime? dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime( + expectedTime.Hours, + expectedTime.Minutes, + expectedTime.Seconds, + expectedTime.Milliseconds); + + // Assert + dateTime.Value.TimeOfDay.ShouldBe(expectedTime); + } + + [Test] + public void ShouldSetDateTimeIfProvidedTimeAsHoursMinutesSecondsAndMilliseconds() + { + // Arrange + var expectedTime = new TimeSpan(0, 9, 10, 30, 10); + var dateTime = new DateTime(2020, 11, 15); + + // Act + dateTime = dateTime.SetTime( + expectedTime.Hours, + expectedTime.Minutes, + expectedTime.Seconds, + expectedTime.Milliseconds); + + // Assert + dateTime.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenSettingTime + public class WhenRoundingToNearestHour + { + [Test] + public void ShouldRoundUpIfAfterHalfHour() { - [Test] - public void ShouldSetNullableDateTimeIfProvidedTimeAsTimeSpan() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 30); - DateTime? dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime); - - // Assert - dateTime.Value.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetDateTimeIfProvidedTimeAsTimeSpan() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 30); - var dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime); - - // Assert - dateTime.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursAndMinutes() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 0); - DateTime? dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes); - - // Assert - dateTime.Value.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetDateTimeIfProvidedTimeAsHoursAndMinutes() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 0); - var dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes); - - // Assert - dateTime.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursMinutesAndSeconds() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 30); - DateTime? dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes, expectedTime.Seconds); - - // Assert - dateTime.Value.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetDateTimeIfProvidedTimeAsHoursMinutesAndSeconds() - { - // Arrange - var expectedTime = new TimeSpan(9, 10, 30); - var dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime(expectedTime.Hours, expectedTime.Minutes, expectedTime.Seconds); - - // Assert - dateTime.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetNullableDateTimeIfProvidedTimeAsHoursMinutesSecondsAndMilliseconds() - { - // Arrange - var expectedTime = new TimeSpan(0, 9, 10, 30, 10); - DateTime? dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime( - expectedTime.Hours, - expectedTime.Minutes, - expectedTime.Seconds, - expectedTime.Milliseconds); - - // Assert - dateTime.Value.TimeOfDay.ShouldBe(expectedTime); - } - - [Test] - public void ShouldSetDateTimeIfProvidedTimeAsHoursMinutesSecondsAndMilliseconds() - { - // Arrange - var expectedTime = new TimeSpan(0, 9, 10, 30, 10); - var dateTime = new DateTime(2020, 11, 15); - - // Act - dateTime = dateTime.SetTime( - expectedTime.Hours, - expectedTime.Minutes, - expectedTime.Seconds, - expectedTime.Milliseconds); - - // Assert - dateTime.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + int expectedHour = 10; + var dateTime = new DateTime(2021, 5, 12, 9, 31, 0); + + // Act + DateTime nearestHour = dateTime.ToNearestHour(); + + // Assert + nearestHour.Hour.ShouldBe(expectedHour); } - public class WhenRoundingToNearestHour + [Test] + public void ShouldRoundDownIfBeforeHalfHour() { - [Test] - public void ShouldRoundUpIfAfterHalfHour() - { - // Arrange - int expectedHour = 10; - var dateTime = new DateTime(2021, 5, 12, 9, 31, 0); - - // Act - DateTime nearestHour = dateTime.ToNearestHour(); - - // Assert - nearestHour.Hour.ShouldBe(expectedHour); - } - - [Test] - public void ShouldRoundDownIfBeforeHalfHour() - { - // Arrange - int expectedHour = 9; - var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); - - // Act - DateTime nearestHour = dateTime.ToNearestHour(); - - // Assert - nearestHour.Hour.ShouldBe(expectedHour); - } + // Arrange + int expectedHour = 9; + var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); + + // Act + DateTime nearestHour = dateTime.ToNearestHour(); + + // Assert + nearestHour.Hour.ShouldBe(expectedHour); } + } - public class WhenGettingStartOfDay + public class WhenGettingStartOfDay + { + [Test] + public void ShouldReturnDateTimeAtMidnight() { - [Test] - public void ShouldReturnDateTimeAtMidnight() - { - // Arrange - var expectedTime = new TimeSpan(0, 0, 0); - var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); - - // Act - DateTime startOfDay = dateTime.StartOfDay(); - - // Assert - startOfDay.Date.ShouldBe(dateTime.Date); - startOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedTime = new TimeSpan(0, 0, 0); + var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); + + // Act + DateTime startOfDay = dateTime.StartOfDay(); + + // Assert + startOfDay.Date.ShouldBe(dateTime.Date); + startOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingEndOfDay + public class WhenGettingEndOfDay + { + [Test] + public void ShouldReturnDateTimeAtJustBeforeMidnight() { - [Test] - public void ShouldReturnDateTimeAtJustBeforeMidnight() - { - // Arrange - TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; - var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); - - // Act - DateTime endOfDay = dateTime.EndOfDay(); - - // Assert - endOfDay.Date.ShouldBe(dateTime.Date); - endOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; + var dateTime = new DateTime(2021, 5, 12, 9, 29, 0); + + // Act + DateTime endOfDay = dateTime.EndOfDay(); + + // Assert + endOfDay.Date.ShouldBe(dateTime.Date); + endOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingStartOfWeek + public class WhenGettingStartOfWeek + { + [Test] + public void ShouldReturnFirstDayOfWeekAtMidnight() { - [Test] - public void ShouldReturnFirstDayOfWeekAtMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 5, 9); - var expectedTime = new TimeSpan(0, 0, 0); - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime startOfDay = dateTime.StartOfWeek(); - - // Assert - startOfDay.Date.ShouldBe(expectedDate.Date); - startOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 5, 9); + var expectedTime = new TimeSpan(0, 0, 0); + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime startOfDay = dateTime.StartOfWeek(); + + // Assert + startOfDay.Date.ShouldBe(expectedDate.Date); + startOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingEndOfWeek + public class WhenGettingEndOfWeek + { + [Test] + public void ShouldReturnLastDayOfWeekAtJustBeforeMidnight() { - [Test] - public void ShouldReturnLastDayOfWeekAtJustBeforeMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 5, 16); - TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime endOfDay = dateTime.EndOfWeek(); - - // Assert - endOfDay.Date.ShouldBe(expectedDate.Date); - endOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 5, 16); + TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime endOfDay = dateTime.EndOfWeek(); + + // Assert + endOfDay.Date.ShouldBe(expectedDate.Date); + endOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingStartOfMonth + public class WhenGettingStartOfMonth + { + [Test] + public void ShouldReturnFirstDayOfMonthAtMidnight() { - [Test] - public void ShouldReturnFirstDayOfMonthAtMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 5, 1); - var expectedTime = new TimeSpan(0, 0, 0); - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime startOfDay = dateTime.StartOfMonth(); - - // Assert - startOfDay.Date.ShouldBe(expectedDate.Date); - startOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 5, 1); + var expectedTime = new TimeSpan(0, 0, 0); + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime startOfDay = dateTime.StartOfMonth(); + + // Assert + startOfDay.Date.ShouldBe(expectedDate.Date); + startOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingEndOfMonth + public class WhenGettingEndOfMonth + { + [Test] + public void ShouldReturnLastDayOfMonthAtJustBeforeMidnight() { - [Test] - public void ShouldReturnLastDayOfMonthAtJustBeforeMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 5, 31); - TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime endOfDay = dateTime.EndOfMonth(); - - // Assert - endOfDay.Date.ShouldBe(expectedDate.Date); - endOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 5, 31); + TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime endOfDay = dateTime.EndOfMonth(); + + // Assert + endOfDay.Date.ShouldBe(expectedDate.Date); + endOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingStartOfYear + public class WhenGettingStartOfYear + { + [Test] + public void ShouldReturnFirstDayOfYearAtMidnight() { - [Test] - public void ShouldReturnFirstDayOfYearAtMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 1, 1); - var expectedTime = new TimeSpan(0, 0, 0); - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime startOfDay = dateTime.StartOfYear(); - - // Assert - startOfDay.Date.ShouldBe(expectedDate.Date); - startOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 1, 1); + var expectedTime = new TimeSpan(0, 0, 0); + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime startOfDay = dateTime.StartOfYear(); + + // Assert + startOfDay.Date.ShouldBe(expectedDate.Date); + startOfDay.TimeOfDay.ShouldBe(expectedTime); } + } - public class WhenGettingEndOfYear + public class WhenGettingEndOfYear + { + [Test] + public void ShouldReturnLastDayOfYearAtJustBeforeMidnight() { - [Test] - public void ShouldReturnLastDayOfYearAtJustBeforeMidnight() - { - // Arrange - var expectedDate = new DateTime(2021, 12, 31); - TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; - var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); - - // Act - DateTime endOfDay = dateTime.EndOfYear(); - - // Assert - endOfDay.Date.ShouldBe(expectedDate.Date); - endOfDay.TimeOfDay.ShouldBe(expectedTime); - } + // Arrange + var expectedDate = new DateTime(2021, 12, 31); + TimeSpan expectedTime = DateTimeConstants.EndOfDayTime; + var dateTime = new DateTime(2021, 05, 12, 9, 29, 0); + + // Act + DateTime endOfDay = dateTime.EndOfYear(); + + // Assert + endOfDay.Date.ShouldBe(expectedDate.Date); + endOfDay.TimeOfDay.ShouldBe(expectedTime); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/DateTimeToStringValueConverterTests.cs b/tests/MADE.Data.Converters.Tests/Tests/DateTimeToStringValueConverterTests.cs index bbee3b1f..6b392e74 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/DateTimeToStringValueConverterTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/DateTimeToStringValueConverterTests.cs @@ -1,104 +1,101 @@ -namespace MADE.Data.Converters.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DateTimeToStringValueConverterTests { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; + public class WhenConverting + { + [Test] + public void ShouldConvertToDefaultDateTimeStringWithoutParameter() + { + // Arrange + var dateTime = new DateTime(2020, 11, 8, 9, 49, 0); + string expected = dateTime.ToString(CultureInfo.InvariantCulture); - using NUnit.Framework; + var converter = new DateTimeToStringValueConverter(); + + // Act + string converted = converter.Convert(dateTime); + + // Assert + converted.ShouldBe(expected); + } - using Shouldly; + [TestCase("G")] + [TestCase("g")] + public void ShouldConvertToFormattedDateTimeStringWithParameter(string format) + { + // Arrange + var dateTime = new DateTime(2020, 11, 8, 9, 49, 0); + string expected = dateTime.ToString(format, CultureInfo.InvariantCulture); + + var converter = new DateTimeToStringValueConverter(); + + // Act + string converted = converter.Convert(dateTime, format); + + // Assert + converted.ShouldBe(expected); + } + } - [ExcludeFromCodeCoverage] - [TestFixture] - public class DateTimeToStringValueConverterTests + public class WhenConvertingBack { - public class WhenConverting + [TestCase(null)] + [TestCase("G")] + [TestCase("g")] + public void ShouldConvertToDateTime(string format) { - [Test] - public void ShouldConvertToDefaultDateTimeStringWithoutParameter() - { - // Arrange - var dateTime = new DateTime(2020, 11, 8, 9, 49, 0); - string expected = dateTime.ToString(CultureInfo.InvariantCulture); - - var converter = new DateTimeToStringValueConverter(); - - // Act - string converted = converter.Convert(dateTime); - - // Assert - converted.ShouldBe(expected); - } - - [TestCase("G")] - [TestCase("g")] - public void ShouldConvertToFormattedDateTimeStringWithParameter(string format) - { - // Arrange - var dateTime = new DateTime(2020, 11, 8, 9, 49, 0); - string expected = dateTime.ToString(format, CultureInfo.InvariantCulture); - - var converter = new DateTimeToStringValueConverter(); - - // Act - string converted = converter.Convert(dateTime, format); - - // Assert - converted.ShouldBe(expected); - } + // Arrange + var expected = new DateTime(2020, 11, 8, 9, 49, 0); + string dateTimeString = expected.ToString(format, CultureInfo.InvariantCulture); + + var converter = new DateTimeToStringValueConverter(); + + // Act + DateTime converted = converter.ConvertBack(dateTimeString); + + // Assert + converted.ShouldBe(expected); + } + + [Test] + public void ShouldReturnDateTimeMinValueIfNull() + { + // Arrange + DateTime expected = DateTime.MinValue; + string dateTimeString = null; + + var converter = new DateTimeToStringValueConverter(); + + // Act + DateTime converted = converter.ConvertBack(dateTimeString); + + // Assert + converted.ShouldBe(expected); } - public class WhenConvertingBack + [Test] + public void ShouldReturnDateTimeMinValueIfNotDate() { - [TestCase(null)] - [TestCase("G")] - [TestCase("g")] - public void ShouldConvertToDateTime(string format) - { - // Arrange - var expected = new DateTime(2020, 11, 8, 9, 49, 0); - string dateTimeString = expected.ToString(format, CultureInfo.InvariantCulture); - - var converter = new DateTimeToStringValueConverter(); - - // Act - DateTime converted = converter.ConvertBack(dateTimeString); - - // Assert - converted.ShouldBe(expected); - } - - [Test] - public void ShouldReturnDateTimeMinValueIfNull() - { - // Arrange - DateTime expected = DateTime.MinValue; - string dateTimeString = null; - - var converter = new DateTimeToStringValueConverter(); - - // Act - DateTime converted = converter.ConvertBack(dateTimeString); - - // Assert - converted.ShouldBe(expected); - } - - [Test] - public void ShouldReturnDateTimeMinValueIfNotDate() - { - // Arrange - DateTime expected = DateTime.MinValue; - string dateTimeString = "Hello, World!"; - - var converter = new DateTimeToStringValueConverter(); - - // Act - DateTime converted = converter.ConvertBack(dateTimeString); - - // Assert - converted.ShouldBe(expected); - } + // Arrange + DateTime expected = DateTime.MinValue; + string dateTimeString = "Hello, World!"; + + var converter = new DateTimeToStringValueConverter(); + + // Act + DateTime converted = converter.ConvertBack(dateTimeString); + + // Assert + converted.ShouldBe(expected); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs index cdf78102..3ed289a0 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs @@ -1,32 +1,31 @@ -namespace MADE.Data.Converters.Tests.Tests -{ - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Extensions; - using NUnit.Framework; - using Shouldly; +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class MathExtensionsTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class MathExtensionsTests +{ + public class WhenConvertingToRadians { - public class WhenConvertingToRadians + private static readonly object[] ToRadiansTestCases = { - private static readonly object[] ToRadiansTestCases = - { - new object[] { 0, 0 }, new object[] { 90, Math.PI / 2 }, new object[] { 180, Math.PI }, - new object[] { 360, Math.PI * 2 }, - }; + new object[] { 0, 0 }, new object[] { 90, Math.PI / 2 }, new object[] { 180, Math.PI }, + new object[] { 360, Math.PI * 2 }, + }; - [TestCaseSource(nameof(ToRadiansTestCases))] - public void ShouldConvertToRadians(double degrees, double expected) - { - // Act - double actual = degrees.ToRadians(); + [TestCaseSource(nameof(ToRadiansTestCases))] + public void ShouldConvertToRadians(double degrees, double expected) + { + // Act + double actual = degrees.ToRadians(); - // Assert - actual.ShouldBe(expected); - } + // Assert + actual.ShouldBe(expected); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs index 9a87ca96..b10d5778 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs @@ -1,189 +1,188 @@ -namespace MADE.Data.Converters.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class StringExtensionsTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Converters.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class StringExtensionsTests + public class WhenTruncatingStrings { - public class WhenTruncatingStrings + [Test] + public void ShouldReturnTruncatedIfGreaterThanMaxLength() { - [Test] - public void ShouldReturnTruncatedIfGreaterThanMaxLength() - { - // Arrange - const string input = "Hello, World!"; - const int maxLength = 8; - - // Act - var result = input.Truncate(maxLength); - - // Assert - result.ShouldBe("Hello..."); - } - - [Test] - public void ShouldReturnOriginalIfLessThanMaxLength() - { - // Arrange - const string input = "Hello, World!"; - int maxLength = input.Length; - - // Act - var result = input.Truncate(maxLength); - - // Assert - result.ShouldBe(input); - } + // Arrange + const string input = "Hello, World!"; + const int maxLength = 8; + + // Act + var result = input.Truncate(maxLength); + + // Assert + result.ShouldBe("Hello..."); + } + + [Test] + public void ShouldReturnOriginalIfLessThanMaxLength() + { + // Arrange + const string input = "Hello, World!"; + int maxLength = input.Length; + + // Act + var result = input.Truncate(maxLength); + + // Assert + result.ShouldBe(input); } + } - public class WhenConvertingToTitleCase + public class WhenConvertingToTitleCase + { + [TestCase("", "")] + [TestCase("HELLO, WORLD", "Hello, World")] + [TestCase("HeLlO, WoRlD", "Hello, World")] + [TestCase("hello, world", "Hello, World")] + public void ShouldConvert(string value, string expected) { - [TestCase("", "")] - [TestCase("HELLO, WORLD", "Hello, World")] - [TestCase("HeLlO, WoRlD", "Hello, World")] - [TestCase("hello, world", "Hello, World")] - public void ShouldConvert(string value, string expected) - { - // Act - string actual = value.ToTitleCase(); - - // Assert - actual.ShouldBe(expected); - } + // Act + string actual = value.ToTitleCase(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToDefaultCase + public class WhenConvertingToDefaultCase + { + [TestCase("", "")] + [TestCase("HELLO, WORLD", "Hello, world")] + [TestCase("HeLlO, WoRlD", "Hello, world")] + [TestCase("hello, world", "Hello, world")] + public void ShouldConvert(string value, string expected) { - [TestCase("", "")] - [TestCase("HELLO, WORLD", "Hello, world")] - [TestCase("HeLlO, WoRlD", "Hello, world")] - [TestCase("hello, world", "Hello, world")] - public void ShouldConvert(string value, string expected) - { - // Act - string actual = value.ToDefaultCase(); - - // Assert - actual.ShouldBe(expected); - } + // Act + string actual = value.ToDefaultCase(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToInt + public class WhenConvertingToInt + { + [TestCase(null, 0)] + [TestCase("", 0)] + [TestCase("10", 10)] + [TestCase("-10", -10)] + public void ShouldConvert(string value, int expected) { - [TestCase(null, 0)] - [TestCase("", 0)] - [TestCase("10", 10)] - [TestCase("-10", -10)] - public void ShouldConvert(string value, int expected) - { - // Act - int actual = value.ToInt(); - - // Assert - actual.ShouldBe(expected); - } + // Act + int actual = value.ToInt(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToNullableInt + public class WhenConvertingToNullableInt + { + [TestCase(null, null)] + [TestCase("", null)] + [TestCase("10", 10)] + [TestCase("-10", -10)] + public void ShouldConvert(string value, int? expected) { - [TestCase(null, null)] - [TestCase("", null)] - [TestCase("10", 10)] - [TestCase("-10", -10)] - public void ShouldConvert(string value, int? expected) - { - // Act - int? actual = value.ToNullableInt(); - - // Assert - actual.ShouldBe(expected); - } + // Act + int? actual = value.ToNullableInt(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToBoolean + public class WhenConvertingToBoolean + { + [TestCase(null, false)] + [TestCase("", false)] + [TestCase("True", true)] + [TestCase("False", false)] + [TestCase("true", true)] + [TestCase("false", false)] + public void ShouldConvert(string value, bool expected) { - [TestCase(null, false)] - [TestCase("", false)] - [TestCase("True", true)] - [TestCase("False", false)] - [TestCase("true", true)] - [TestCase("false", false)] - public void ShouldConvert(string value, bool expected) - { - // Act - bool actual = value.ToBoolean(); - - // Assert - actual.ShouldBe(expected); - } + // Act + bool actual = value.ToBoolean(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToFloat + public class WhenConvertingToFloat + { + [TestCase(null, 0f)] + [TestCase("", 0f)] + [TestCase("10.5", 10.5f)] + [TestCase("-10.5", -10.5f)] + public void ShouldConvert(string value, float expected) { - [TestCase(null, 0f)] - [TestCase("", 0f)] - [TestCase("10.5", 10.5f)] - [TestCase("-10.5", -10.5f)] - public void ShouldConvert(string value, float expected) - { - // Act - float actual = value.ToFloat(); - - // Assert - actual.ShouldBe(expected); - } + // Act + float actual = value.ToFloat(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToNullableFloat + public class WhenConvertingToNullableFloat + { + [TestCase(null, null)] + [TestCase("", null)] + [TestCase("10.5", 10.5f)] + [TestCase("-10.5", -10.5f)] + public void ShouldConvert(string value, float? expected) { - [TestCase(null, null)] - [TestCase("", null)] - [TestCase("10.5", 10.5f)] - [TestCase("-10.5", -10.5f)] - public void ShouldConvert(string value, float? expected) - { - // Act - float? actual = value.ToNullableFloat(); - - // Assert - actual.ShouldBe(expected); - } + // Act + float? actual = value.ToNullableFloat(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToDouble + public class WhenConvertingToDouble + { + [TestCase(null, 0d)] + [TestCase("", 0d)] + [TestCase("10.5", 10.5d)] + [TestCase("-10.5", -10.5d)] + public void ShouldConvert(string value, double expected) { - [TestCase(null, 0d)] - [TestCase("", 0d)] - [TestCase("10.5", 10.5d)] - [TestCase("-10.5", -10.5d)] - public void ShouldConvert(string value, double expected) - { - // Act - double actual = value.ToDouble(); - - // Assert - actual.ShouldBe(expected); - } + // Act + double actual = value.ToDouble(); + + // Assert + actual.ShouldBe(expected); } + } - public class WhenConvertingToNullableDouble + public class WhenConvertingToNullableDouble + { + [TestCase(null, null)] + [TestCase("", null)] + [TestCase("10.5", 10.5d)] + [TestCase("-10.5", -10.5d)] + public void ShouldConvert(string value, double? expected) { - [TestCase(null, null)] - [TestCase("", null)] - [TestCase("10.5", 10.5d)] - [TestCase("-10.5", -10.5d)] - public void ShouldConvert(string value, double? expected) - { - // Act - double? actual = value.ToNullableDouble(); - - // Assert - actual.ShouldBe(expected); - } + // Act + double? actual = value.ToNullableDouble(); + + // Assert + actual.ShouldBe(expected); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs b/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs index 47034b4f..8a7e894c 100644 --- a/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs +++ b/tests/MADE.Data.EFCore.Tests/Data/TestDbContext.cs @@ -1,72 +1,71 @@ -namespace MADE.Data.EFCore.Tests.Data -{ - using System.Diagnostics.CodeAnalysis; - using System.Threading; - using System.Threading.Tasks; - using Converters; - using Extensions; - using Microsoft.EntityFrameworkCore; - using Microsoft.EntityFrameworkCore.Metadata.Builders; - - [ExcludeFromCodeCoverage] - public class TestDbContext : DbContext - { - public DbSet Entities { get; set; } +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using MADE.Data.EFCore.Converters; +using MADE.Data.EFCore.Extensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; - public DbSet KeyEntities { get; set; } +namespace MADE.Data.EFCore.Tests.Data; - public TestDbContext(DbContextOptions options) : base(options) { } +[ExcludeFromCodeCoverage] +public class TestDbContext : DbContext +{ + public DbSet Entities { get; set; } - public static TestDbContext CreateInMemoryContext(string dbName) - { - var optionsBuilder = new DbContextOptionsBuilder(); - DbContextOptions options = optionsBuilder.UseInMemoryDatabase(dbName).Options; - return new TestDbContext(options); - } + public DbSet KeyEntities { get; set; } - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - this.SetEntityDates(); - return base.SaveChangesAsync(cancellationToken); - } + public TestDbContext(DbContextOptions options) : base(options) { } - public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) - { - this.SetEntityDates(); - return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - } + public static TestDbContext CreateInMemoryContext(string dbName) + { + var optionsBuilder = new DbContextOptionsBuilder(); + DbContextOptions options = optionsBuilder.UseInMemoryDatabase(dbName).Options; + return new TestDbContext(options); + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.HasDefaultSchema("dbo"); - modelBuilder.ApplyConfigurationsFromAssembly(typeof(TestDbContext).Assembly); - modelBuilder.ApplyUtcDateTimeConverter(); - } + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + this.SetEntityDates(); + return base.SaveChangesAsync(cancellationToken); } - public class TestEntity : EntityBase + public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { - public string Name { get; set; } + this.SetEntityDates(); + return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); } - public class TestKeyedEntity : EntityBase + protected override void OnModelCreating(ModelBuilder modelBuilder) { - public string Name { get; set; } + modelBuilder.HasDefaultSchema("dbo"); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(TestDbContext).Assembly); + modelBuilder.ApplyUtcDateTimeConverter(); } +} - public class TestEntityTypeConfiguration : IEntityTypeConfiguration +public class TestEntity : EntityBase +{ + public string Name { get; set; } +} + +public class TestKeyedEntity : EntityBase +{ + public string Name { get; set; } +} + +public class TestEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - builder.Configure(); - } + builder.Configure(); } +} - public class TestKeyedEntityTypeConfiguration : IEntityTypeConfiguration +public class TestKeyedEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - builder.ConfigureWithKey(); - } + builder.ConfigureWithKey(); } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs index e5bd47ce..d8caab15 100644 --- a/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs +++ b/tests/MADE.Data.EFCore.Tests/Tests/EntityBaseTests.cs @@ -1,56 +1,59 @@ -namespace MADE.Data.EFCore.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using MADE.Data.EFCore.Tests.Data; +using MADE.Data.EFCore.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.EFCore.Tests.Tests; + +[TestFixture] +[ExcludeFromCodeCoverage] +public class EntityBaseTests { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Threading.Tasks; - using Data; - using Extensions; - using NUnit.Framework; - using Shouldly; - - [TestFixture] - [ExcludeFromCodeCoverage] - public class EntityBaseTests + public class WhenSavingToDbContext { - public class WhenSavingToDbContext + [Test] + public async Task ShouldSetEntityBaseDates() { - [Test] - public async Task ShouldSetEntityBaseDates() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetEntityBaseDates"); + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetEntityBaseDates"); - var entity = new TestEntity { Id = Guid.NewGuid(), Name = "Test" }; + var entity = new TestEntity { Id = Guid.NewGuid(), Name = "Test" }; - await dbContext.AddAsync(entity); + await dbContext.AddAsync(entity); - // Act - await dbContext.TrySaveChangesAsync(); + // Act + var before = DateTime.UtcNow; + await dbContext.TrySaveChangesAsync(); + var after = DateTime.UtcNow; - // Assert - entity.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1)); - entity.UpdatedDate.ShouldNotBeNull(); - entity.UpdatedDate.Value.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1)); - } + // Assert + entity.CreatedDate.ShouldBeInRange(before.AddSeconds(-1), after.AddSeconds(1)); + entity.UpdatedDate.ShouldNotBeNull(); + entity.UpdatedDate.Value.ShouldBeInRange(before.AddSeconds(-1), after.AddSeconds(1)); + } - [Test] - public async Task ShouldSetKeyedEntityBaseDates() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetKeyedEntityBaseDates"); + [Test] + public async Task ShouldSetKeyedEntityBaseDates() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("ShouldSetKeyedEntityBaseDates"); - var entity = new TestKeyedEntity { Id = 1, Name = "Test" }; + var entity = new TestKeyedEntity { Id = 1, Name = "Test" }; - await dbContext.AddAsync(entity); + await dbContext.AddAsync(entity); - // Act - await dbContext.TrySaveChangesAsync(); + // Act + var before = DateTime.UtcNow; + await dbContext.TrySaveChangesAsync(); + var after = DateTime.UtcNow; - // Assert - entity.CreatedDate.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1)); - entity.UpdatedDate.ShouldNotBeNull(); - entity.UpdatedDate.Value.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow.AddSeconds(1)); - } + // Assert + entity.CreatedDate.ShouldBeInRange(before.AddSeconds(-1), after.AddSeconds(1)); + entity.UpdatedDate.ShouldNotBeNull(); + entity.UpdatedDate.Value.ShouldBeInRange(before.AddSeconds(-1), after.AddSeconds(1)); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs index 3e846375..aa9a02ae 100644 --- a/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs +++ b/tests/MADE.Data.EFCore.Tests/Tests/QueryableExtensionsTests.cs @@ -1,179 +1,178 @@ -namespace MADE.Data.EFCore.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; +using MADE.Data.EFCore.Tests.Data; +using MADE.Data.EFCore.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.EFCore.Tests.Tests; + +[TestFixture] +[ExcludeFromCodeCoverage] +public class QueryableExtensionsTests { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading.Tasks; - using Data; - using Extensions; - using NUnit.Framework; - using Shouldly; - - [TestFixture] - [ExcludeFromCodeCoverage] - public class QueryableExtensionsTests + public class WhenOrderingByPropertyName { - public class WhenOrderingByPropertyName + [Test] + public async Task ShouldOrderByNameAscending() { - [Test] - public async Task ShouldOrderByNameAscending() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameAsc"); - - await dbContext.Entities.AddRangeAsync( - new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); - await dbContext.SaveChangesAsync(); - - // Act - var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: false).ToList(); - - // Assert - result.Count.ShouldBe(3); - result[0].Name.ShouldBe("Alice"); - result[1].Name.ShouldBe("Bob"); - result[2].Name.ShouldBe("Charlie"); - } + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameAsc"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: false).ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Alice"); + result[1].Name.ShouldBe("Bob"); + result[2].Name.ShouldBe("Charlie"); + } - [Test] - public async Task ShouldOrderByNameDescending() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameDesc"); - - await dbContext.Entities.AddRangeAsync( - new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); - await dbContext.SaveChangesAsync(); - - // Act - var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: true).ToList(); - - // Assert - result.Count.ShouldBe(3); - result[0].Name.ShouldBe("Charlie"); - result[1].Name.ShouldBe("Bob"); - result[2].Name.ShouldBe("Alice"); - } + [Test] + public async Task ShouldOrderByNameDescending() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNameDesc"); + + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Charlie" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }); + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities.OrderBy(nameof(TestEntity.Name), sortDesc: true).ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Charlie"); + result[1].Name.ShouldBe("Bob"); + result[2].Name.ShouldBe("Alice"); + } - [Test] - public async Task ShouldReturnUnorderedQueryWhenSortNameIsEmpty() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("OrderByEmpty"); + [Test] + public async Task ShouldReturnUnorderedQueryWhenSortNameIsEmpty() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByEmpty"); - await dbContext.Entities.AddRangeAsync( - new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); - await dbContext.SaveChangesAsync(); + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); + await dbContext.SaveChangesAsync(); - // Act - var result = dbContext.Entities.OrderBy("", sortDesc: false).ToList(); + // Act + var result = dbContext.Entities.OrderBy("", sortDesc: false).ToList(); - // Assert - result.Count.ShouldBe(2); - } + // Assert + result.Count.ShouldBe(2); + } - [Test] - public async Task ShouldReturnUnorderedQueryWhenSortNameIsNull() - { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("OrderByNull"); + [Test] + public async Task ShouldReturnUnorderedQueryWhenSortNameIsNull() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("OrderByNull"); - await dbContext.Entities.AddRangeAsync( - new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, - new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); - await dbContext.SaveChangesAsync(); + await dbContext.Entities.AddRangeAsync( + new TestEntity { Id = Guid.NewGuid(), Name = "Bob" }, + new TestEntity { Id = Guid.NewGuid(), Name = "Alice" }); + await dbContext.SaveChangesAsync(); - // Act - var result = dbContext.Entities.OrderBy(null, sortDesc: false).ToList(); + // Act + var result = dbContext.Entities.OrderBy(null, sortDesc: false).ToList(); - // Assert - result.Count.ShouldBe(2); - } + // Assert + result.Count.ShouldBe(2); } + } - public class WhenPaging + public class WhenPaging + { + [Test] + public async Task ShouldReturnFirstPage() { - [Test] - public async Task ShouldReturnFirstPage() + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageFirst"); + + for (int i = 0; i < 10; i++) { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("PageFirst"); - - for (int i = 0; i < 10; i++) - { - await dbContext.Entities.AddAsync( - new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); - } - await dbContext.SaveChangesAsync(); - - // Act - var result = dbContext.Entities - .OrderBy(nameof(TestEntity.Name), sortDesc: false) - .Page(page: 1, pageSize: 3) - .ToList(); - - // Assert - result.Count.ShouldBe(3); - result[0].Name.ShouldBe("Entity00"); - result[1].Name.ShouldBe("Entity01"); - result[2].Name.ShouldBe("Entity02"); + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 1, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Entity00"); + result[1].Name.ShouldBe("Entity01"); + result[2].Name.ShouldBe("Entity02"); + } - [Test] - public async Task ShouldReturnSecondPage() + [Test] + public async Task ShouldReturnSecondPage() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageSecond"); + + for (int i = 0; i < 10; i++) { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("PageSecond"); - - for (int i = 0; i < 10; i++) - { - await dbContext.Entities.AddAsync( - new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); - } - await dbContext.SaveChangesAsync(); - - // Act - var result = dbContext.Entities - .OrderBy(nameof(TestEntity.Name), sortDesc: false) - .Page(page: 2, pageSize: 3) - .ToList(); - - // Assert - result.Count.ShouldBe(3); - result[0].Name.ShouldBe("Entity03"); - result[1].Name.ShouldBe("Entity04"); - result[2].Name.ShouldBe("Entity05"); + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 2, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(3); + result[0].Name.ShouldBe("Entity03"); + result[1].Name.ShouldBe("Entity04"); + result[2].Name.ShouldBe("Entity05"); + } + + [Test] + public async Task ShouldReturnPartialLastPage() + { + // Arrange + var dbContext = TestDbContext.CreateInMemoryContext("PageLast"); - [Test] - public async Task ShouldReturnPartialLastPage() + for (int i = 0; i < 5; i++) { - // Arrange - var dbContext = TestDbContext.CreateInMemoryContext("PageLast"); - - for (int i = 0; i < 5; i++) - { - await dbContext.Entities.AddAsync( - new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); - } - await dbContext.SaveChangesAsync(); - - // Act - var result = dbContext.Entities - .OrderBy(nameof(TestEntity.Name), sortDesc: false) - .Page(page: 2, pageSize: 3) - .ToList(); - - // Assert - result.Count.ShouldBe(2); - result[0].Name.ShouldBe("Entity03"); - result[1].Name.ShouldBe("Entity04"); + await dbContext.Entities.AddAsync( + new TestEntity { Id = Guid.NewGuid(), Name = $"Entity{i:D2}" }); } + await dbContext.SaveChangesAsync(); + + // Act + var result = dbContext.Entities + .OrderBy(nameof(TestEntity.Name), sortDesc: false) + .Page(page: 2, pageSize: 3) + .ToList(); + + // Assert + result.Count.ShouldBe(2); + result[0].Name.ShouldBe("Entity03"); + result[1].Name.ShouldBe("Entity04"); } } } diff --git a/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs b/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs index 51d9ecab..7f7e8811 100644 --- a/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs +++ b/tests/MADE.Data.Serialization.Tests/Tests/JsonTypeMigrationSerializationBinderTests.cs @@ -1,102 +1,100 @@ -namespace MADE.Data.Serialization.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using MADE.Data.Serialization.Json; +using MADE.Data.Serialization.Json.Converters; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Serialization.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonTypeMigrationSerializationBinderTests { - using System.Diagnostics.CodeAnalysis; - using System.Text.Json; - using System.Threading.Tasks; - using MADE.Data.Serialization.Json; - using MADE.Data.Serialization.Json.Converters; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonTypeMigrationSerializationBinderTests + public class WhenMigratingFromOneTypeToAnother { - public class WhenMigratingFromOneTypeToAnother + [Test] + public void ShouldMigrateFromTypeToType() { - [Test] - public async Task ShouldMigrateFromTypeToType() + // Arrange + var converter = new JsonTypeMigrationConverter(); + converter.AddTypeMigration(new JsonTypeMigration(typeof(OldType), typeof(NewType))); + + var oldType = new OldType(); + + // Simulate JSON with $type metadata (as previously serialized by Newtonsoft.Json with TypeNameHandling.All) + string serialized = JsonSerializer.Serialize(new { - // Arrange - var converter = new JsonTypeMigrationConverter(); - await converter.AddTypeMigrationAsync(new JsonTypeMigration(typeof(OldType), typeof(NewType))); + @__type = typeof(OldType).FullName + ", " + typeof(OldType).Assembly.GetName().Name, + oldType.Name, + oldType.Number + }); - var oldType = new OldType(); + // Replace __type with $type since $ isn't valid in anonymous type member names + serialized = serialized.Replace("\"__type\"", "\"$type\""); - // Simulate JSON with $type metadata (as previously serialized by Newtonsoft.Json with TypeNameHandling.All) - string serialized = JsonSerializer.Serialize(new - { - @__type = typeof(OldType).FullName + ", " + typeof(OldType).Assembly.GetName().Name, - oldType.Name, - oldType.Number - }); + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); - // Replace __type with $type since $ isn't valid in anonymous type member names - serialized = serialized.Replace("\"__type\"", "\"$type\""); + // Act + var deserialized = JsonSerializer.Deserialize(serialized, options); - var options = new JsonSerializerOptions(); - options.Converters.Add(converter); + // Assert + deserialized.ShouldBeOfType(typeof(NewType)); - // Act - var deserialized = JsonSerializer.Deserialize(serialized, options); + var newType = (NewType)deserialized; + newType.Name.ShouldBe(oldType.Name); + newType.Number.ShouldBe((double)oldType.Number); + } - // Assert - deserialized.ShouldBeOfType(typeof(NewType)); + [Test] + public void ShouldMigrateFromAssemblyAndTypeNameToType() + { + // Arrange + var converter = new JsonTypeMigrationConverter(); + converter.AddTypeMigration(new JsonTypeMigration( + "MADE.Data.Serialization.Tests", + "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType", + typeof(NewType))); - var newType = (NewType)deserialized; - newType.Name.ShouldBe(oldType.Name); - newType.Number.ShouldBe((double)oldType.Number); - } + var oldType = new OldType(); - [Test] - public async Task ShouldMigrateFromAssemblyAndTypeNameToType() + // Simulate JSON with $type metadata + string serialized = JsonSerializer.Serialize(new { - // Arrange - var converter = new JsonTypeMigrationConverter(); - await converter.AddTypeMigrationAsync(new JsonTypeMigration( - "MADE.Data.Serialization.Tests", - "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType", - typeof(NewType))); - - var oldType = new OldType(); - - // Simulate JSON with $type metadata - string serialized = JsonSerializer.Serialize(new - { - @__type = "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType, MADE.Data.Serialization.Tests", - oldType.Name, - oldType.Number - }); - - serialized = serialized.Replace("\"__type\"", "\"$type\""); - - var options = new JsonSerializerOptions(); - options.Converters.Add(converter); - - // Act - var deserialized = JsonSerializer.Deserialize(serialized, options); - - // Assert - deserialized.ShouldBeOfType(typeof(NewType)); - - var newType = (NewType)deserialized; - newType.Name.ShouldBe(oldType.Name); - newType.Number.ShouldBe((double)oldType.Number); - } - } + @__type = "MADE.Data.Serialization.Tests.Tests.JsonTypeMigrationSerializationBinderTests+OldType, MADE.Data.Serialization.Tests", + oldType.Name, + oldType.Number + }); - private class OldType - { - public string Name { get; set; } + serialized = serialized.Replace("\"__type\"", "\"$type\""); - public int Number { get; set; } - } + var options = new JsonSerializerOptions(); + options.Converters.Add(converter); - private class NewType - { - public string Name { get; set; } + // Act + var deserialized = JsonSerializer.Deserialize(serialized, options); + + // Assert + deserialized.ShouldBeOfType(typeof(NewType)); - public double Number { get; set; } + var newType = (NewType)deserialized; + newType.Name.ShouldBe(oldType.Name); + newType.Number.ShouldBe((double)oldType.Number); } } -} \ No newline at end of file + + private class OldType + { + public string Name { get; set; } + + public int Number { get; set; } + } + + private class NewType + { + public string Name { get; set; } + + public double Number { get; set; } + } +} diff --git a/tests/MADE.Data.Validation.FluentValidation.Tests/Tests/FluentValidatorCollectionTests.cs b/tests/MADE.Data.Validation.FluentValidation.Tests/Tests/FluentValidatorCollectionTests.cs index 534024f3..231680fc 100644 --- a/tests/MADE.Data.Validation.FluentValidation.Tests/Tests/FluentValidatorCollectionTests.cs +++ b/tests/MADE.Data.Validation.FluentValidation.Tests/Tests/FluentValidatorCollectionTests.cs @@ -1,234 +1,233 @@ -namespace MADE.Data.Validation.FluentValidation.Tests.Tests +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using global::FluentValidation; +using global::FluentValidation.Results; +using MADE.Testing; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.FluentValidation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class FluentValidatorCollectionTests { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using global::FluentValidation; - using global::FluentValidation.Results; - using MADE.Testing; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class FluentValidatorCollectionTests + public class WhenInitializing { - public class WhenInitializing + [Test] + public void ShouldBeEmptyIfDefaultConstructor() { - [Test] - public void ShouldBeEmptyIfDefaultConstructor() - { - // Act - var collection = new FluentValidatorCollection(); + // Act + var collection = new FluentValidatorCollection(); - // Assert - collection.Count.ShouldBe(0); - } + // Assert + collection.Count.ShouldBe(0); + } - [Test] - public void ShouldContainItemsIfInitializedAsEnumerable() + [Test] + public void ShouldContainItemsIfInitializedAsEnumerable() + { + // Arrange + IEnumerable> validators = new List> { - // Arrange - IEnumerable> validators = new List> - { - new PersonValidator() - }; - - // Act - var collection = new FluentValidatorCollection(validators); - - // Assert - collection.Count.ShouldBe(validators.Count()); - collection.ToList().ShouldBeEquivalentTo(validators); - } + new PersonValidator() + }; + + // Act + var collection = new FluentValidatorCollection(validators); + + // Assert + collection.Count.ShouldBe(validators.Count()); + collection.ToList().ShouldBeEquivalentTo(validators); } + } - public class WhenAddingItems + public class WhenAddingItems + { + [Test] + public void ShouldAddRangeOfItems() { - [Test] - public void ShouldAddRangeOfItems() + // Arrange + IEnumerable> validators = new List> { - // Arrange - IEnumerable> validators = new List> - { - new PersonValidator() - }; - - var collection = new FluentValidatorCollection(); - - // Act - collection.AddRange(validators); - - // Assert - foreach (AbstractValidator item in validators) - { - collection.ShouldContain(item); - } - } + new PersonValidator() + }; - [Test] - public void ShouldAddSingleItem() - { - // Arrange - var objectToAdd = new PersonValidator(); + var collection = new FluentValidatorCollection(); - // Act - var collection = new FluentValidatorCollection { objectToAdd }; + // Act + collection.AddRange(validators); - // Assert - collection.ShouldContain(objectToAdd); + // Assert + foreach (AbstractValidator item in validators) + { + collection.ShouldContain(item); } } - public class WhenValidating + [Test] + public void ShouldAddSingleItem() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - var value = new Person(); + // Arrange + var objectToAdd = new PersonValidator(); - var collection = new FluentValidatorCollection { new PersonValidator() }; + // Act + var collection = new FluentValidatorCollection { objectToAdd }; - // Act - collection.Validate(value); + // Assert + collection.ShouldContain(objectToAdd); + } + } - // Assert - collection.IsDirty.ShouldBe(true); - } + public class WhenValidating + { + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + var value = new Person(); - [Test] - public void ShouldBeValidIfValidValue() - { - // Arrange - var value = new Person { Name = "Joe Bloggs", DateOfBirth = new DateTime(1992, 01, 01) }; + var collection = new FluentValidatorCollection { new PersonValidator() }; - var collection = new FluentValidatorCollection { new PersonValidator() }; + // Act + collection.Validate(value); - // Act - collection.Validate(value); + // Assert + collection.IsDirty.ShouldBe(true); + } - // Assert - collection.IsInvalid.ShouldBe(false); - } + [Test] + public void ShouldBeValidIfValidValue() + { + // Arrange + var value = new Person { Name = "Joe Bloggs", DateOfBirth = new DateTime(1992, 01, 01) }; - [Test] - public void ShouldBeInvalidIfInvalidValue() - { - // Arrange - var value = new Person(); + var collection = new FluentValidatorCollection { new PersonValidator() }; - var collection = new FluentValidatorCollection { new PersonValidator() }; + // Act + collection.Validate(value); - // Act - collection.Validate(value); + // Assert + collection.IsInvalid.ShouldBe(false); + } - // Assert - collection.IsInvalid.ShouldBe(true); - } + [Test] + public void ShouldBeInvalidIfInvalidValue() + { + // Arrange + var value = new Person(); + + var collection = new FluentValidatorCollection { new PersonValidator() }; + + // Act + collection.Validate(value); - [Test] - public void ShouldHaveFeedbackMessagesIfInvalidValue() + // Assert + collection.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldHaveFeedbackMessagesIfInvalidValue() + { + // Arrange + var value = new Person { - // Arrange - var value = new Person - { - Name = "Joe Bloggs", - DateOfBirth = DateTime.UtcNow.AddDays(1) // Invalid birth date - }; - - var collection = new FluentValidatorCollection { new PersonValidator() }; - - // Act - collection.Validate(value); - - // Assert - collection.FeedbackMessages.ShouldNotBeEmpty(); - collection.FeedbackMessages.Count().ShouldBe(1); - collection.FeedbackMessages.FirstOrDefault().ShouldBe(PersonValidator.DateOfBirthValidationMessage); - } + Name = "Joe Bloggs", + DateOfBirth = DateTime.UtcNow.AddDays(1) // Invalid birth date + }; + + var collection = new FluentValidatorCollection { new PersonValidator() }; + + // Act + collection.Validate(value); + + // Assert + collection.FeedbackMessages.ShouldNotBeEmpty(); + collection.FeedbackMessages.Count().ShouldBe(1); + collection.FeedbackMessages.FirstOrDefault().ShouldBe(PersonValidator.DateOfBirthValidationMessage); + } - [Test] - public void ShouldValidateComplexObjectWithMultipleValidators() + [Test] + public void ShouldValidateComplexObjectWithMultipleValidators() + { + // Arrange + var value = new Staff { - // Arrange - var value = new Staff - { - Name = "Joe Bloggs", - JobTitle = null, // Invalid job title - Department = "Build", - DateOfBirth = DateTime.UtcNow.AddDays(1) // Invalid birth date - }; - - var collection = new FluentValidatorCollection { new PersonValidator(), new StaffValidator() }; - - // Act - collection.Validate(value); - - // Assert - collection.FeedbackMessages.ShouldNotBeEmpty(); - collection.FeedbackMessages.Count().ShouldBe(2); - collection.FeedbackMessages.FirstOrDefault(x => x.Equals(PersonValidator.DateOfBirthValidationMessage, - StringComparison.InvariantCultureIgnoreCase)).ShouldNotBeNull(); - collection.FeedbackMessages.FirstOrDefault(x => x.Equals(StaffValidator.JobTitleValidationMessage, - StringComparison.InvariantCultureIgnoreCase)).ShouldNotBeNull(); - } + Name = "Joe Bloggs", + JobTitle = null, // Invalid job title + Department = "Build", + DateOfBirth = DateTime.UtcNow.AddDays(1) // Invalid birth date + }; + + var collection = new FluentValidatorCollection { new PersonValidator(), new StaffValidator() }; + + // Act + collection.Validate(value); + + // Assert + collection.FeedbackMessages.ShouldNotBeEmpty(); + collection.FeedbackMessages.Count().ShouldBe(2); + collection.FeedbackMessages.FirstOrDefault(x => x.Equals(PersonValidator.DateOfBirthValidationMessage, + StringComparison.InvariantCultureIgnoreCase)).ShouldNotBeNull(); + collection.FeedbackMessages.FirstOrDefault(x => x.Equals(StaffValidator.JobTitleValidationMessage, + StringComparison.InvariantCultureIgnoreCase)).ShouldNotBeNull(); } } +} - [ExcludeFromCodeCoverage] - public class Person - { - public string Name { get; set; } +[ExcludeFromCodeCoverage] +public class Person +{ + public string Name { get; set; } - public DateTime? DateOfBirth { get; set; } - } + public DateTime? DateOfBirth { get; set; } +} - [ExcludeFromCodeCoverage] - public class Staff : Person - { - public string JobTitle { get; set; } +[ExcludeFromCodeCoverage] +public class Staff : Person +{ + public string JobTitle { get; set; } - public string Department { get; set; } - } + public string Department { get; set; } +} - [ExcludeFromCodeCoverage] - public class PersonValidator : AbstractValidator - { - public const string DateOfBirthValidationMessage = "Please specify a valid date of birth"; +[ExcludeFromCodeCoverage] +public class PersonValidator : AbstractValidator +{ + public const string DateOfBirthValidationMessage = "Please specify a valid date of birth"; - public PersonValidator() - { - this.RuleFor(x => x.Name).NotEmpty(); - this.RuleFor(x => x.DateOfBirth) - .NotNull() - .LessThanOrEqualTo(DateTime.UtcNow) - .WithMessage(DateOfBirthValidationMessage); - } + public PersonValidator() + { + this.RuleFor(x => x.Name).NotEmpty(); + this.RuleFor(x => x.DateOfBirth) + .NotNull() + .LessThanOrEqualTo(DateTime.UtcNow) + .WithMessage(DateOfBirthValidationMessage); } +} - [ExcludeFromCodeCoverage] - public class StaffValidator : AbstractValidator, IValidator - { - public const string JobTitleValidationMessage = "Please specify a job title"; +[ExcludeFromCodeCoverage] +public class StaffValidator : AbstractValidator, IValidator +{ + public const string JobTitleValidationMessage = "Please specify a job title"; - public StaffValidator() - { - this.RuleFor(x => x.JobTitle).NotEmpty().WithMessage(JobTitleValidationMessage); - this.RuleFor(x => x.Department).NotEmpty(); - } + public StaffValidator() + { + this.RuleFor(x => x.JobTitle).NotEmpty().WithMessage(JobTitleValidationMessage); + this.RuleFor(x => x.Department).NotEmpty(); + } - public ValidationResult Validate(Person instance) - { - return base.Validate(instance as Staff); - } + public ValidationResult Validate(Person instance) + { + return base.Validate(instance as Staff); + } - public Task ValidateAsync(Person instance, CancellationToken cancellation = new()) - { - return base.ValidateAsync(instance as Staff, cancellation); - } + public Task ValidateAsync(Person instance, CancellationToken cancellation = new()) + { + return base.ValidateAsync(instance as Staff, cancellation); } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/AlphaNumericValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/AlphaNumericValidatorTests.cs index fa8c5126..ea399466 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/AlphaNumericValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/AlphaNumericValidatorTests.cs @@ -1,56 +1,55 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class AlphaNumericValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class AlphaNumericValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + string value = "Test"; + var validator = new AlphaNumericValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase("Test")] + [TestCase("Test1")] + public void ShouldBeValidIfContainsAlphaNumericCharacters(string value) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new AlphaNumericValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase("Test")] - [TestCase("Test1")] - public void ShouldBeValidIfContainsAlphaNumericCharacters(string value) - { - // Arrange - var validator = new AlphaNumericValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase("Test!")] - public void ShouldBeInvalidIfContainsNonAlphaNumericCharacters(string value) - { - // Arrange - var validator = new AlphaNumericValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new AlphaNumericValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase("Test!")] + public void ShouldBeInvalidIfContainsNonAlphaNumericCharacters(string value) + { + // Arrange + var validator = new AlphaNumericValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/AlphaValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/AlphaValidatorTests.cs index 8c95d3d7..79f6ee7d 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/AlphaValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/AlphaValidatorTests.cs @@ -1,57 +1,56 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class AlphaValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class AlphaValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + string value = "Test"; + var validator = new AlphaValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [Test] + public void ShouldBeValidIfContainsOnlyAlphaCharacters() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new AlphaValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [Test] - public void ShouldBeValidIfContainsOnlyAlphaCharacters() - { - // Arrange - string value = "Test"; - var validator = new AlphaValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase("Test1")] - [TestCase("Test!")] - public void ShouldBeInvalidIfContainsNonAlphaCharacters(string value) - { - // Arrange - var validator = new AlphaValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + string value = "Test"; + var validator = new AlphaValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase("Test1")] + [TestCase("Test!")] + public void ShouldBeInvalidIfContainsNonAlphaCharacters(string value) + { + // Arrange + var validator = new AlphaValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/Base64ValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/Base64ValidatorTests.cs index 14de8c05..e3326aae 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/Base64ValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/Base64ValidatorTests.cs @@ -1,73 +1,72 @@ -namespace MADE.Data.Validation.Tests.Tests -{ - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class Base64ValidatorTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class Base64ValidatorTests +{ + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - const string value = "Test"; - var validator = new Base64Validator(); + // Arrange + const string value = "Test"; + var validator = new Base64Validator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [Test] - public void ShouldBeValidIfDefinedBase64String() - { - // Arrange - const string value = "VGVzdA=="; - var validator = new Base64Validator(); + [Test] + public void ShouldBeValidIfDefinedBase64String() + { + // Arrange + const string value = "VGVzdA=="; + var validator = new Base64Validator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [Test] - public void ShouldBeValidIfConvertedToBase64String() - { - // Arrange - const string decodedValue = "Test"; - string value = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(decodedValue)); - var validator = new Base64Validator(); + [Test] + public void ShouldBeValidIfConvertedToBase64String() + { + // Arrange + const string decodedValue = "Test"; + string value = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(decodedValue)); + var validator = new Base64Validator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [Test] - public void ShouldBeInvalidIfNotBase64() - { - // Arrange - const string value = "Tes"; - var validator = new Base64Validator(); + [Test] + public void ShouldBeInvalidIfNotBase64() + { + // Arrange + const string value = "Tes"; + var validator = new Base64Validator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/BetweenValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/BetweenValidatorTests.cs index 4a0f971c..a11077cb 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/BetweenValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/BetweenValidatorTests.cs @@ -1,125 +1,124 @@ -namespace MADE.Data.Validation.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class BetweenValidatorTests { - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class BetweenValidatorTests + public class WhenValidatingInclusive + { + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + int value = 1; + var validator = new BetweenValidator(0, 2); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase(0, 0, 2)] + [TestCase(1, 0, 2)] + [TestCase(2, 0, 2)] + [TestCase(0.0, 0.0, 2.0)] + [TestCase(1.0, 0.0, 2.0)] + [TestCase(2.0, 0.0, 2.0)] + [TestCase(0.0f, 0.0f, 2.0f)] + [TestCase(1.0f, 0.0f, 2.0f)] + [TestCase(2.0f, 0.0f, 2.0f)] + public void ShouldBeValidIfValueWithinRange(IComparable value, IComparable min, IComparable max) + { + // Arrange + var validator = new BetweenValidator(min, max); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase(-1, 0, 2)] + [TestCase(3, 0, 2)] + [TestCase(-1.0, 0.0, 2.0)] + [TestCase(3.0, 0.0, 2.0)] + [TestCase(-1.0f, 0.0f, 2.0f)] + [TestCase(3.0f, 0.0f, 2.0f)] + public void ShouldBeInvalidIfValueOutsideRange(IComparable value, IComparable min, IComparable max) + { + // Arrange + var validator = new BetweenValidator(min, max); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + } + + public class WhenValidatingExclusive { - public class WhenValidatingInclusive + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + int value = 1; + var validator = new BetweenValidator(0, 2) { Inclusive = false }; + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase(1, 0, 2)] + [TestCase(1.0, 0.0, 2.0)] + [TestCase(1.0f, 0.0f, 2.0f)] + public void ShouldBeValidIfValueWithinRange(IComparable value, IComparable min, IComparable max) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - int value = 1; - var validator = new BetweenValidator(0, 2); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase(0, 0, 2)] - [TestCase(1, 0, 2)] - [TestCase(2, 0, 2)] - [TestCase(0.0, 0.0, 2.0)] - [TestCase(1.0, 0.0, 2.0)] - [TestCase(2.0, 0.0, 2.0)] - [TestCase(0.0f, 0.0f, 2.0f)] - [TestCase(1.0f, 0.0f, 2.0f)] - [TestCase(2.0f, 0.0f, 2.0f)] - public void ShouldBeValidIfValueWithinRange(IComparable value, IComparable min, IComparable max) - { - // Arrange - var validator = new BetweenValidator(min, max); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase(-1, 0, 2)] - [TestCase(3, 0, 2)] - [TestCase(-1.0, 0.0, 2.0)] - [TestCase(3.0, 0.0, 2.0)] - [TestCase(-1.0f, 0.0f, 2.0f)] - [TestCase(3.0f, 0.0f, 2.0f)] - public void ShouldBeInvalidIfValueOutsideRange(IComparable value, IComparable min, IComparable max) - { - // Arrange - var validator = new BetweenValidator(min, max); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new BetweenValidator(min, max) { Inclusive = false }; + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); } - public class WhenValidatingExclusive + [TestCase(0, 0, 2)] + [TestCase(-1, 0, 2)] + [TestCase(2, 0, 2)] + [TestCase(3, 0, 2)] + [TestCase(0.0, 0.0, 2.0)] + [TestCase(-1.0, 0.0, 2.0)] + [TestCase(2.0, 0.0, 2.0)] + [TestCase(3.0, 0.0, 2.0)] + [TestCase(0.0f, 0.0f, 2.0f)] + [TestCase(-1.0f, 0.0f, 2.0f)] + [TestCase(2.0f, 0.0f, 2.0f)] + [TestCase(3.0f, 0.0f, 2.0f)] + public void ShouldBeInvalidIfValueOutsideRange(IComparable value, IComparable min, IComparable max) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - int value = 1; - var validator = new BetweenValidator(0, 2) { Inclusive = false }; - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase(1, 0, 2)] - [TestCase(1.0, 0.0, 2.0)] - [TestCase(1.0f, 0.0f, 2.0f)] - public void ShouldBeValidIfValueWithinRange(IComparable value, IComparable min, IComparable max) - { - // Arrange - var validator = new BetweenValidator(min, max) { Inclusive = false }; - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase(0, 0, 2)] - [TestCase(-1, 0, 2)] - [TestCase(2, 0, 2)] - [TestCase(3, 0, 2)] - [TestCase(0.0, 0.0, 2.0)] - [TestCase(-1.0, 0.0, 2.0)] - [TestCase(2.0, 0.0, 2.0)] - [TestCase(3.0, 0.0, 2.0)] - [TestCase(0.0f, 0.0f, 2.0f)] - [TestCase(-1.0f, 0.0f, 2.0f)] - [TestCase(2.0f, 0.0f, 2.0f)] - [TestCase(3.0f, 0.0f, 2.0f)] - public void ShouldBeInvalidIfValueOutsideRange(IComparable value, IComparable min, IComparable max) - { - // Arrange - var validator = new BetweenValidator(min, max) { Inclusive = false }; - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new BetweenValidator(min, max) { Inclusive = false }; + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/DateTimeExtensionsTests.cs b/tests/MADE.Data.Validation.Tests/Tests/DateTimeExtensionsTests.cs index 082543fe..22371d64 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/DateTimeExtensionsTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/DateTimeExtensionsTests.cs @@ -1,117 +1,116 @@ -namespace MADE.Data.Validation.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using MADE.Data.Validation.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DateTimeExtensionsTests { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using MADE.Data.Validation.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class DateTimeExtensionsTests + public class WhenDeterminingWhetherDateIsInRange { - public class WhenDeterminingWhetherDateIsInRange + [TestCase("07/01/2021", "01/01/2021", "12/31/2021")] + [TestCase("07/01/2021 12:00:00", "07/01/2021 09:00:00", "07/01/2021 15:00:00")] + public void ShouldBeTrueIfInRange(string dateVal, string fromVal, string toVal) { - [TestCase("07/01/2021", "01/01/2021", "12/31/2021")] - [TestCase("07/01/2021 12:00:00", "07/01/2021 09:00:00", "07/01/2021 15:00:00")] - public void ShouldBeTrueIfInRange(string dateVal, string fromVal, string toVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - var from = DateTime.Parse(fromVal, new CultureInfo("en-US")); - var to = DateTime.Parse(toVal, new CultureInfo("en-US")); - - // Act - bool isInRange = date.IsInRange(from, to); - - // Assert - isInRange.ShouldBeTrue(); - } - - [TestCase("07/01/2021", "01/01/2020", "12/31/2020")] - [TestCase("07/01/2021 00:00:00", "07/01/2021 09:00:00", "07/01/2021 15:00:00")] - public void ShouldBeFalseIfNotInRange(string dateVal, string fromVal, string toVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - var from = DateTime.Parse(fromVal, new CultureInfo("en-US")); - var to = DateTime.Parse(toVal, new CultureInfo("en-US")); - - // Act - bool isInRange = date.IsInRange(from, to); - - // Assert - isInRange.ShouldBeFalse(); - } + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + var from = DateTime.Parse(fromVal, new CultureInfo("en-US")); + var to = DateTime.Parse(toVal, new CultureInfo("en-US")); + + // Act + bool isInRange = date.IsInRange(from, to); + + // Assert + isInRange.ShouldBeTrue(); } - public class WhenDeterminingWeekdays + [TestCase("07/01/2021", "01/01/2020", "12/31/2020")] + [TestCase("07/01/2021 00:00:00", "07/01/2021 09:00:00", "07/01/2021 15:00:00")] + public void ShouldBeFalseIfNotInRange(string dateVal, string fromVal, string toVal) { - [TestCase("05/10/2021")] - [TestCase("05/11/2021")] - [TestCase("05/12/2021")] - [TestCase("05/13/2021")] - [TestCase("05/14/2021")] - public void ShouldBeTrueIfWeekday(string dateVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - - // Act - bool isWeekday = date.IsWeekday(); - - // Assert - isWeekday.ShouldBeTrue(); - } - - [TestCase("05/15/2021")] - [TestCase("05/16/2021")] - public void ShouldBeFalseIfNotWeekday(string dateVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - - // Act - bool isWeekday = date.IsWeekday(); - - // Assert - isWeekday.ShouldBeFalse(); - } + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + var from = DateTime.Parse(fromVal, new CultureInfo("en-US")); + var to = DateTime.Parse(toVal, new CultureInfo("en-US")); + + // Act + bool isInRange = date.IsInRange(from, to); + + // Assert + isInRange.ShouldBeFalse(); } + } - public class WhenDeterminingWeekends + public class WhenDeterminingWeekdays + { + [TestCase("05/10/2021")] + [TestCase("05/11/2021")] + [TestCase("05/12/2021")] + [TestCase("05/13/2021")] + [TestCase("05/14/2021")] + public void ShouldBeTrueIfWeekday(string dateVal) { - [TestCase("05/15/2021")] - [TestCase("05/16/2021")] - public void ShouldBeTrueIfWeekend(string dateVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - - // Act - bool isWeekend = date.IsWeekend(); - - // Assert - isWeekend.ShouldBeTrue(); - } - - [TestCase("05/10/2021")] - [TestCase("05/11/2021")] - [TestCase("05/12/2021")] - [TestCase("05/13/2021")] - [TestCase("05/14/2021")] - public void ShouldBeFalseIfNotWeekend(string dateVal) - { - // Arrange - var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); - - // Act - bool isWeekend = date.IsWeekend(); - - // Assert - isWeekend.ShouldBeFalse(); - } + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + + // Act + bool isWeekday = date.IsWeekday(); + + // Assert + isWeekday.ShouldBeTrue(); + } + + [TestCase("05/15/2021")] + [TestCase("05/16/2021")] + public void ShouldBeFalseIfNotWeekday(string dateVal) + { + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + + // Act + bool isWeekday = date.IsWeekday(); + + // Assert + isWeekday.ShouldBeFalse(); + } + } + + public class WhenDeterminingWeekends + { + [TestCase("05/15/2021")] + [TestCase("05/16/2021")] + public void ShouldBeTrueIfWeekend(string dateVal) + { + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + + // Act + bool isWeekend = date.IsWeekend(); + + // Assert + isWeekend.ShouldBeTrue(); + } + + [TestCase("05/10/2021")] + [TestCase("05/11/2021")] + [TestCase("05/12/2021")] + [TestCase("05/13/2021")] + [TestCase("05/14/2021")] + public void ShouldBeFalseIfNotWeekend(string dateVal) + { + // Arrange + var date = DateTime.Parse(dateVal, new CultureInfo("en-US")); + + // Act + bool isWeekend = date.IsWeekend(); + + // Assert + isWeekend.ShouldBeFalse(); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/EmailValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/EmailValidatorTests.cs index 448e41e0..2bf9104b 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/EmailValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/EmailValidatorTests.cs @@ -1,71 +1,70 @@ -namespace MADE.Data.Validation.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class EmailValidatorTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class EmailValidatorTests +{ + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new EmailValidator(); + // Arrange + string value = "Test"; + var validator = new EmailValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [TestCase("email@example.com")] - [TestCase("firstname.lastname@example.com")] - [TestCase("email@subdomain.example.com")] - [TestCase("firstname+lastname@example.com")] - [TestCase("1234567890@example.com")] - [TestCase("email@example-example.com")] - [TestCase("email@example.name")] - public void ShouldBeValidIfValidEmailAddress(string value) - { - // Arrange - var validator = new EmailValidator(); + [TestCase("email@example.com")] + [TestCase("firstname.lastname@example.com")] + [TestCase("email@subdomain.example.com")] + [TestCase("firstname+lastname@example.com")] + [TestCase("1234567890@example.com")] + [TestCase("email@example-example.com")] + [TestCase("email@example.name")] + public void ShouldBeValidIfValidEmailAddress(string value) + { + // Arrange + var validator = new EmailValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [TestCase("emailaddress")] - [TestCase("#@%^%#$@#$@#.com")] - [TestCase("Joe Bloggs ")] - [TestCase("email.example.com")] - [TestCase("email@example@example.com")] - [TestCase(".email@example.com")] - [TestCase("email.@example.com")] - [TestCase("email..email@example.com")] - [TestCase("email@example.com (Joe Bloggs)")] - [TestCase("email@example")] - [TestCase("email@111.222.333.44444")] - public void ShouldBeInvalidIfInvalidEmailAddress(string value) - { - // Arrange - var validator = new EmailValidator(); + [TestCase("emailaddress")] + [TestCase("#@%^%#$@#$@#.com")] + [TestCase("Joe Bloggs ")] + [TestCase("email.example.com")] + [TestCase("email@example@example.com")] + [TestCase(".email@example.com")] + [TestCase("email.@example.com")] + [TestCase("email..email@example.com")] + [TestCase("email@example.com (Joe Bloggs)")] + [TestCase("email@example")] + [TestCase("email@111.222.333.44444")] + public void ShouldBeInvalidIfInvalidEmailAddress(string value) + { + // Arrange + var validator = new EmailValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/GuidValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/GuidValidatorTests.cs index 14312d3f..73523d0f 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/GuidValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/GuidValidatorTests.cs @@ -1,72 +1,71 @@ -namespace MADE.Data.Validation.Tests.Tests -{ - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class GuidValidatorTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class GuidValidatorTests +{ + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new GuidValidator(); + // Arrange + string value = "Test"; + var validator = new GuidValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [Test] - public void ShouldBeValidIfGuidType() - { - // Arrange - var value = Guid.NewGuid(); - var validator = new GuidValidator(); + [Test] + public void ShouldBeValidIfGuidType() + { + // Arrange + var value = Guid.NewGuid(); + var validator = new GuidValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [Test] - public void ShouldBeValidIfStringGuid() - { - // Arrange - var value = "f39bc65d-dcb5-47f1-a3ba-51fb5f584fd9"; - var validator = new GuidValidator(); + [Test] + public void ShouldBeValidIfStringGuid() + { + // Arrange + var value = "f39bc65d-dcb5-47f1-a3ba-51fb5f584fd9"; + var validator = new GuidValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [Test] - public void ShouldBeInvalidIfNotGuid() - { - // Arrange - const string value = "Test"; - var validator = new GuidValidator(); + [Test] + public void ShouldBeInvalidIfNotGuid() + { + // Arrange + const string value = "Test"; + var validator = new GuidValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/IpAddressValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/IpAddressValidatorTests.cs index e40c8d1f..bac94d2f 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/IpAddressValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/IpAddressValidatorTests.cs @@ -1,67 +1,66 @@ -namespace MADE.Data.Validation.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class IpAddressValidatorTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class IpAddressValidatorTests +{ + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new IpAddressValidator(); + // Arrange + string value = "Test"; + var validator = new IpAddressValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [TestCase("127.0.0.1")] - [TestCase("8.8.8.8")] - [TestCase("123.41.12.168")] - [TestCase("10.0.0.1")] - [TestCase("10.0.0.0")] - public void ShouldBeValidIfValidIpAddress(string value) - { - // Arrange - var validator = new IpAddressValidator(); + [TestCase("127.0.0.1")] + [TestCase("8.8.8.8")] + [TestCase("123.41.12.168")] + [TestCase("10.0.0.1")] + [TestCase("10.0.0.0")] + public void ShouldBeValidIfValidIpAddress(string value) + { + // Arrange + var validator = new IpAddressValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [TestCase("Test")] - [TestCase("123123123123")] - [TestCase(" 127.0.0.1 ")] - [TestCase("127.0.00.1")] - [TestCase("127.0.1")] - [TestCase("10.0.1.2.3")] - [TestCase("1.2.3.-4")] - [TestCase("1.256.3.4")] - [TestCase("10.0.0.1/24")] - public void ShouldBeInvalidIfInvalidIpAddress(string value) - { - // Arrange - var validator = new IpAddressValidator(); + [TestCase("Test")] + [TestCase("123123123123")] + [TestCase(" 127.0.0.1 ")] + [TestCase("127.0.00.1")] + [TestCase("127.0.1")] + [TestCase("10.0.1.2.3")] + [TestCase("1.2.3.-4")] + [TestCase("1.256.3.4")] + [TestCase("10.0.0.1/24")] + public void ShouldBeInvalidIfInvalidIpAddress(string value) + { + // Arrange + var validator = new IpAddressValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/LatitudeValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/LatitudeValidatorTests.cs index a88dc485..0ed2eb59 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/LatitudeValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/LatitudeValidatorTests.cs @@ -1,57 +1,56 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class LatitudeValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class LatitudeValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new LatitudeValidator(); + // Arrange + string value = "Test"; + var validator = new LatitudeValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [Test] - public void ShouldBeValidIfInLatitudeRange() + [Test] + public void ShouldBeValidIfInLatitudeRange() + { + // Arrange + var validator = new LatitudeValidator(); + + // Act & Assert + for (var i = -90; i <= 90; i++) { - // Arrange - var validator = new LatitudeValidator(); - - // Act & Assert - for (var i = -90; i <= 90; i++) - { - validator.Validate(i); - validator.IsInvalid.ShouldBe(false); - } + validator.Validate(i); + validator.IsInvalid.ShouldBe(false); } + } - [TestCase(-90.5)] - [TestCase(90.5)] - public void ShouldBeInvalidIfOutLatitudeRange(object value) - { - // Arrange - var validator = new LatitudeValidator(); + [TestCase(-90.5)] + [TestCase(90.5)] + public void ShouldBeInvalidIfOutLatitudeRange(object value) + { + // Arrange + var validator = new LatitudeValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/LongitudeValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/LongitudeValidatorTests.cs index b29da2e1..df87ace0 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/LongitudeValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/LongitudeValidatorTests.cs @@ -1,57 +1,56 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class LongitudeValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class LongitudeValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new LongitudeValidator(); + // Arrange + string value = "Test"; + var validator = new LongitudeValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [Test] - public void ShouldBeValidIfInLongitudeRange() + [Test] + public void ShouldBeValidIfInLongitudeRange() + { + // Arrange + var validator = new LongitudeValidator(); + + // Act & Assert + for (var i = -180; i <= 180; i++) { - // Arrange - var validator = new LongitudeValidator(); - - // Act & Assert - for (var i = -180; i <= 180; i++) - { - validator.Validate(i); - validator.IsInvalid.ShouldBe(false); - } + validator.Validate(i); + validator.IsInvalid.ShouldBe(false); } + } - [TestCase(-180.5)] - [TestCase(180.5)] - public void ShouldBeInvalidIfOutLongitudeRange(object value) - { - // Arrange - var validator = new LongitudeValidator(); + [TestCase(-180.5)] + [TestCase(180.5)] + public void ShouldBeInvalidIfOutLongitudeRange(object value) + { + // Arrange + var validator = new LongitudeValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/MacAddressValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/MacAddressValidatorTests.cs index ce7a75c8..b3e5275f 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/MacAddressValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/MacAddressValidatorTests.cs @@ -1,61 +1,60 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class MacAddressValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class MacAddressValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + string value = "Test"; + var validator = new MacAddressValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase("001122334455")] + [TestCase("00-11-22-33-44-55")] + [TestCase("0011.2233.4455")] + [TestCase("00:11:22:33:44:55")] + [TestCase("F0-E1-D2-C3-B4-A5")] + [TestCase("f0-e1-d2-c3-b4-a5")] + public void ShouldBeValidIfValidMacAddress(string value) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Test"; - var validator = new MacAddressValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase("001122334455")] - [TestCase("00-11-22-33-44-55")] - [TestCase("0011.2233.4455")] - [TestCase("00:11:22:33:44:55")] - [TestCase("F0-E1-D2-C3-B4-A5")] - [TestCase("f0-e1-d2-c3-b4-a5")] - public void ShouldBeValidIfValidMacAddress(string value) - { - // Arrange - var validator = new MacAddressValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase("Test")] - [TestCase("00/11/22/33/44/55")] - public void ShouldBeInvalidIfInvalidMacAddress(string value) - { - // Arrange - var validator = new MacAddressValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new MacAddressValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase("Test")] + [TestCase("00/11/22/33/44/55")] + public void ShouldBeInvalidIfInvalidMacAddress(string value) + { + // Arrange + var validator = new MacAddressValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/MathExtensionsTests.cs b/tests/MADE.Data.Validation.Tests/Tests/MathExtensionsTests.cs index 16161b24..9f727406 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/MathExtensionsTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/MathExtensionsTests.cs @@ -1,327 +1,326 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Exceptions; +using MADE.Data.Validation.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class MathExtensionsTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Exceptions; - using MADE.Data.Validation.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class MathExtensionsTests + public class WhenCheckingIfValueIsZero + { + [Test] + public void ShouldReturnTrueIfDoubleZero() + { + // Act + bool isZero = 0d.IsZero(); + + // Assert + isZero.ShouldBeTrue(); + } + + [TestCase(0.05d)] + [TestCase(-0.05d)] + public void ShouldReturnFalseIfDoubleNotZero(double value) + { + // Act + bool isZero = value.IsZero(); + + // Assert + isZero.ShouldBeFalse(); + } + + [Test] + public void ShouldReturnTrueIfFloatZero() + { + // Act + bool isZero = 0f.IsZero(); + + // Assert + isZero.ShouldBeTrue(); + } + + [TestCase(0.05f)] + [TestCase(-0.05f)] + public void ShouldReturnFalseIfFloatNotZero(float value) + { + // Act + bool isZero = value.IsZero(); + + // Assert + isZero.ShouldBeFalse(); + } + } + + public class WhenCheckingIfValueIsCloseToAnother + { + [TestCase(1, 1)] + public void ShouldReturnTrueIfIntClose(int value, int compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeTrue(); + } + + [TestCase(1, 2)] + public void ShouldReturnFalseIfIntNotClose(int value, int compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeFalse(); + } + + [TestCase(0.005d, 0.005d)] + [TestCase(0.00000005d, 0.00000005d)] + public void ShouldReturnTrueIfDoubleClose(double value, double compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeTrue(); + } + + [TestCase(1.005d, 0.005d)] + [TestCase(1.00000005d, 0.00000005d)] + public void ShouldReturnFalseIfDoubleNotClose(double value, double compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeFalse(); + } + + [TestCase(0.005d, 0.005d)] + [TestCase(0.00000005d, 0.00000005d)] + public void ShouldReturnTrueIfNullableDoubleClose(double? value, double? compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeTrue(); + } + + [TestCase(null, 0.005d)] + [TestCase(1.005d, null)] + [TestCase(1.005d, 0.005d)] + [TestCase(1.00000005d, 0.00000005d)] + public void ShouldReturnFalseIfNullableDoubleNotClose(double? value, double? compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeFalse(); + } + + [TestCase(0.005f, 0.005f)] + [TestCase(0.00000005f, 0.00000005f)] + public void ShouldReturnTrueIfFloatClose(float value, float compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeTrue(); + } + + [TestCase(1.005f, 0.005f)] + [TestCase(1.00000005f, 0.00000005f)] + public void ShouldReturnFalseIfFloatNotClose(float value, float compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeFalse(); + } + + [TestCase(0.005f, 0.005f)] + [TestCase(0.00000005f, 0.00000005f)] + public void ShouldReturnTrueIfNullableFloatClose(float? value, float? compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeTrue(); + } + + [TestCase(null, 0.005f)] + [TestCase(1.005f, null)] + [TestCase(1.005f, 0.005f)] + [TestCase(1.00000005f, 0.00000005f)] + public void ShouldReturnFalseIfNullableFloatNotClose(float? value, float? compare) + { + // Act + bool isCloseTo = value.IsCloseTo(compare); + + // Assert + isCloseTo.ShouldBeFalse(); + } + } + + public class WhenCheckingIfValueIsGreaterThan + { + [TestCase(0.05d, 0.000005d)] + [TestCase(1d, 0.99999999d)] + public void ShouldReturnTrueIfGreaterThan(double value, double compare) + { + // Act + bool isGreaterThan = value.IsGreaterThan(compare); + + // Assert + isGreaterThan.ShouldBeTrue(); + } + + [TestCase(0.000005d, 0.05d)] + [TestCase(0.99999999d, 1d)] + public void ShouldReturnFalseIfNotGreaterThan(double value, double compare) + { + // Act + bool isGreaterThan = value.IsGreaterThan(compare); + + // Assert + isGreaterThan.ShouldBeFalse(); + } + } + + public class WhenCheckingIfValueIsLessThan + { + [TestCase(0.000005d, 0.05d)] + [TestCase(0.99999999d, 1d)] + public void ShouldReturnTrueIfLessThan(double value, double compare) + { + // Act + bool isGreaterThan = value.IsLessThan(compare); + + // Assert + isGreaterThan.ShouldBeTrue(); + } + + [TestCase(0.05d, 0.000005d)] + [TestCase(1d, 0.99999999d)] + public void ShouldReturnFalseIfNotLessThan(double value, double compare) + { + // Act + bool isGreaterThan = value.IsLessThan(compare); + + // Assert + isGreaterThan.ShouldBeFalse(); + } + } + + public class WhenCheckingIfValueIsInRange { - public class WhenCheckingIfValueIsZero + [TestCase(0, 0, 2)] + [TestCase(1, 0, 2)] + [TestCase(2, 0, 2)] + public void ShouldReturnTrueIfIntInRange(int value, int lower, int upper) { - [Test] - public void ShouldReturnTrueIfDoubleZero() - { - // Act - bool isZero = 0d.IsZero(); - - // Assert - isZero.ShouldBeTrue(); - } - - [TestCase(0.05d)] - [TestCase(-0.05d)] - public void ShouldReturnFalseIfDoubleNotZero(double value) - { - // Act - bool isZero = value.IsZero(); - - // Assert - isZero.ShouldBeFalse(); - } - - [Test] - public void ShouldReturnTrueIfFloatZero() - { - // Act - bool isZero = 0f.IsZero(); - - // Assert - isZero.ShouldBeTrue(); - } - - [TestCase(0.05f)] - [TestCase(-0.05f)] - public void ShouldReturnFalseIfFloatNotZero(float value) - { - // Act - bool isZero = value.IsZero(); - - // Assert - isZero.ShouldBeFalse(); - } + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeTrue(); + } + + [TestCase(-1, 0, 1)] + [TestCase(2, 0, 1)] + public void ShouldReturnFalseIfIntNotInRange(int value, int lower, int upper) + { + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeFalse(); } - public class WhenCheckingIfValueIsCloseToAnother + [Test] + public void ShouldThrowInvalidRangeExceptionIfIntRangeInvalid() { - [TestCase(1, 1)] - public void ShouldReturnTrueIfIntClose(int value, int compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeTrue(); - } - - [TestCase(1, 2)] - public void ShouldReturnFalseIfIntNotClose(int value, int compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeFalse(); - } - - [TestCase(0.005d, 0.005d)] - [TestCase(0.00000005d, 0.00000005d)] - public void ShouldReturnTrueIfDoubleClose(double value, double compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeTrue(); - } - - [TestCase(1.005d, 0.005d)] - [TestCase(1.00000005d, 0.00000005d)] - public void ShouldReturnFalseIfDoubleNotClose(double value, double compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeFalse(); - } - - [TestCase(0.005d, 0.005d)] - [TestCase(0.00000005d, 0.00000005d)] - public void ShouldReturnTrueIfNullableDoubleClose(double? value, double? compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeTrue(); - } - - [TestCase(null, 0.005d)] - [TestCase(1.005d, null)] - [TestCase(1.005d, 0.005d)] - [TestCase(1.00000005d, 0.00000005d)] - public void ShouldReturnFalseIfNullableDoubleNotClose(double? value, double? compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeFalse(); - } - - [TestCase(0.005f, 0.005f)] - [TestCase(0.00000005f, 0.00000005f)] - public void ShouldReturnTrueIfFloatClose(float value, float compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeTrue(); - } - - [TestCase(1.005f, 0.005f)] - [TestCase(1.00000005f, 0.00000005f)] - public void ShouldReturnFalseIfFloatNotClose(float value, float compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeFalse(); - } - - [TestCase(0.005f, 0.005f)] - [TestCase(0.00000005f, 0.00000005f)] - public void ShouldReturnTrueIfNullableFloatClose(float? value, float? compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeTrue(); - } - - [TestCase(null, 0.005f)] - [TestCase(1.005f, null)] - [TestCase(1.005f, 0.005f)] - [TestCase(1.00000005f, 0.00000005f)] - public void ShouldReturnFalseIfNullableFloatNotClose(float? value, float? compare) - { - // Act - bool isCloseTo = value.IsCloseTo(compare); - - // Assert - isCloseTo.ShouldBeFalse(); - } + Assert.Throws( + () => + { + bool isInRange = 0.IsInRange(3, 1); + }); } - public class WhenCheckingIfValueIsGreaterThan + [TestCase(0, 0, 1)] + [TestCase(1, 0, 1)] + [TestCase(0.0000001f, 0, 1)] + [TestCase(0.9999999f, 0, 1)] + public void ShouldReturnTrueIfFloatInRange(float value, float lower, float upper) { - [TestCase(0.05d, 0.000005d)] - [TestCase(1d, 0.99999999d)] - public void ShouldReturnTrueIfGreaterThan(double value, double compare) - { - // Act - bool isGreaterThan = value.IsGreaterThan(compare); - - // Assert - isGreaterThan.ShouldBeTrue(); - } - - [TestCase(0.000005d, 0.05d)] - [TestCase(0.99999999d, 1d)] - public void ShouldReturnFalseIfNotGreaterThan(double value, double compare) - { - // Act - bool isGreaterThan = value.IsGreaterThan(compare); - - // Assert - isGreaterThan.ShouldBeFalse(); - } + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeTrue(); } - public class WhenCheckingIfValueIsLessThan + [TestCase(-0.0000001f, 0, 1)] + [TestCase(1.0000001f, 0, 1)] + public void ShouldReturnFalseIfFloatNotInRange(float value, float lower, float upper) { - [TestCase(0.000005d, 0.05d)] - [TestCase(0.99999999d, 1d)] - public void ShouldReturnTrueIfLessThan(double value, double compare) - { - // Act - bool isGreaterThan = value.IsLessThan(compare); - - // Assert - isGreaterThan.ShouldBeTrue(); - } - - [TestCase(0.05d, 0.000005d)] - [TestCase(1d, 0.99999999d)] - public void ShouldReturnFalseIfNotLessThan(double value, double compare) - { - // Act - bool isGreaterThan = value.IsLessThan(compare); - - // Assert - isGreaterThan.ShouldBeFalse(); - } + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeFalse(); + } + + [Test] + public void ShouldThrowInvalidRangeExceptionIfFloatRangeInvalid() + { + Assert.Throws( + () => + { + bool isInRange = 0f.IsInRange(3, 1); + }); + } + + [TestCase(0, 0, 1)] + [TestCase(1, 0, 1)] + [TestCase(0.0000001d, 0, 1)] + [TestCase(0.9999999d, 0, 1)] + public void ShouldReturnTrueIfDoubleInRange(double value, double lower, double upper) + { + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeTrue(); + } + + [TestCase(-0.0000001d, 0, 1)] + [TestCase(1.0000001d, 0, 1)] + public void ShouldReturnFalseIfDoubleNotInRange(double value, double lower, double upper) + { + // Act + bool isInRange = value.IsInRange(lower, upper); + + // Assert + isInRange.ShouldBeFalse(); } - public class WhenCheckingIfValueIsInRange + [Test] + public void ShouldThrowInvalidRangeExceptionIfDoubleRangeInvalid() { - [TestCase(0, 0, 2)] - [TestCase(1, 0, 2)] - [TestCase(2, 0, 2)] - public void ShouldReturnTrueIfIntInRange(int value, int lower, int upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeTrue(); - } - - [TestCase(-1, 0, 1)] - [TestCase(2, 0, 1)] - public void ShouldReturnFalseIfIntNotInRange(int value, int lower, int upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeFalse(); - } - - [Test] - public void ShouldThrowInvalidRangeExceptionIfIntRangeInvalid() - { - Assert.Throws( - () => - { - bool isInRange = 0.IsInRange(3, 1); - }); - } - - [TestCase(0, 0, 1)] - [TestCase(1, 0, 1)] - [TestCase(0.0000001f, 0, 1)] - [TestCase(0.9999999f, 0, 1)] - public void ShouldReturnTrueIfFloatInRange(float value, float lower, float upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeTrue(); - } - - [TestCase(-0.0000001f, 0, 1)] - [TestCase(1.0000001f, 0, 1)] - public void ShouldReturnFalseIfFloatNotInRange(float value, float lower, float upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeFalse(); - } - - [Test] - public void ShouldThrowInvalidRangeExceptionIfFloatRangeInvalid() - { - Assert.Throws( - () => - { - bool isInRange = 0f.IsInRange(3, 1); - }); - } - - [TestCase(0, 0, 1)] - [TestCase(1, 0, 1)] - [TestCase(0.0000001d, 0, 1)] - [TestCase(0.9999999d, 0, 1)] - public void ShouldReturnTrueIfDoubleInRange(double value, double lower, double upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeTrue(); - } - - [TestCase(-0.0000001d, 0, 1)] - [TestCase(1.0000001d, 0, 1)] - public void ShouldReturnFalseIfDoubleNotInRange(double value, double lower, double upper) - { - // Act - bool isInRange = value.IsInRange(lower, upper); - - // Assert - isInRange.ShouldBeFalse(); - } - - [Test] - public void ShouldThrowInvalidRangeExceptionIfDoubleRangeInvalid() - { - Assert.Throws( - () => - { - bool isInRange = 0d.IsInRange(3, 1); - }); - } + Assert.Throws( + () => + { + bool isInRange = 0d.IsInRange(3, 1); + }); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/MaxValueIndicatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/MaxValueIndicatorTests.cs index ae7c9a19..dbab8d5a 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/MaxValueIndicatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/MaxValueIndicatorTests.cs @@ -1,63 +1,62 @@ -namespace MADE.Data.Validation.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class MaxValueValidatorTests { - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class MaxValueValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + int value = 1; + var validator = new MaxValueValidator(0); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase(0, 1)] + [TestCase(1, 1)] + [TestCase(0.0, 1.0)] + [TestCase(1.0, 1.0)] + [TestCase(0.0f, 1.0f)] + [TestCase(1.0f, 1.0f)] + public void ShouldBeValidIfValueBelowMax(IComparable value, IComparable max) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - int value = 1; - var validator = new MaxValueValidator(0); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase(0, 1)] - [TestCase(1, 1)] - [TestCase(0.0, 1.0)] - [TestCase(1.0, 1.0)] - [TestCase(0.0f, 1.0f)] - [TestCase(1.0f, 1.0f)] - public void ShouldBeValidIfValueBelowMax(IComparable value, IComparable max) - { - // Arrange - var validator = new MaxValueValidator(max); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase(2, 1)] - [TestCase(2.0, 1.0)] - [TestCase(2.0f, 1.0f)] - public void ShouldBeInvalidIfValueAboveMax(IComparable value, IComparable max) - { - // Arrange - var validator = new MaxValueValidator(max); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new MaxValueValidator(max); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase(2, 1)] + [TestCase(2.0, 1.0)] + [TestCase(2.0f, 1.0f)] + public void ShouldBeInvalidIfValueAboveMax(IComparable value, IComparable max) + { + // Arrange + var validator = new MaxValueValidator(max); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/MinValueValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/MinValueValidatorTests.cs index 7fa568f0..75cc8849 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/MinValueValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/MinValueValidatorTests.cs @@ -1,63 +1,62 @@ -namespace MADE.Data.Validation.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class MinValueValidatorTests { - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class MinValueValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + int value = 1; + var validator = new MinValueValidator(0); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [TestCase(0, 0)] + [TestCase(1, 0)] + [TestCase(0.0, 0.0)] + [TestCase(1.0, 0.0)] + [TestCase(0.0f, 0.0f)] + [TestCase(1.0f, 0.0f)] + public void ShouldBeValidIfValueAboveMin(IComparable value, IComparable min) { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - int value = 1; - var validator = new MinValueValidator(0); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [TestCase(0, 0)] - [TestCase(1, 0)] - [TestCase(0.0, 0.0)] - [TestCase(1.0, 0.0)] - [TestCase(0.0f, 0.0f)] - [TestCase(1.0f, 0.0f)] - public void ShouldBeValidIfValueAboveMin(IComparable value, IComparable min) - { - // Arrange - var validator = new MinValueValidator(min); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [TestCase(-1, 0)] - [TestCase(-1.0, 0.0)] - [TestCase(-1.0f, 0.0f)] - public void ShouldBeInvalidIfValueBelowMin(IComparable value, IComparable min) - { - // Arrange - var validator = new MinValueValidator(min); - - // Act - validator.Validate(value); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new MinValueValidator(min); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [TestCase(-1, 0)] + [TestCase(-1.0, 0.0)] + [TestCase(-1.0f, 0.0f)] + public void ShouldBeInvalidIfValueBelowMin(IComparable value, IComparable min) + { + // Arrange + var validator = new MinValueValidator(min); + + // Act + validator.Validate(value); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/PredicateValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/PredicateValidatorTests.cs index 9e0474d0..9bb8aff4 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/PredicateValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/PredicateValidatorTests.cs @@ -1,54 +1,53 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class PredicateValidatorTests { - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class PredicateValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + var validator = new PredicateValidator(i => i > 0); + + // Act + validator.Validate(1); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [Test] + public void ShouldBeValidIfPredicateIsTrue() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - var validator = new PredicateValidator(i => i > 0); - - // Act - validator.Validate(1); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [Test] - public void ShouldBeValidIfPredicateIsTrue() - { - // Arrange - var validator = new PredicateValidator(i => i > 0); - - // Act - validator.Validate(1); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [Test] - public void ShouldBeInvalidIfPredicateIsFalse() - { - // Arrange - var validator = new PredicateValidator(i => i > 0); - - // Act - validator.Validate(0); - - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Arrange + var validator = new PredicateValidator(i => i > 0); + + // Act + validator.Validate(1); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [Test] + public void ShouldBeInvalidIfPredicateIsFalse() + { + // Arrange + var validator = new PredicateValidator(i => i > 0); + + // Act + validator.Validate(0); + + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/RequiredValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/RequiredValidatorTests.cs index da3842aa..0873aa88 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/RequiredValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/RequiredValidatorTests.cs @@ -1,148 +1,147 @@ -namespace MADE.Data.Validation.Tests.Tests +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class RequiredValidatorTests { - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class RequiredValidatorTests + public class WhenValidating { - public class WhenValidating + [Test] + public void ShouldBeDirtyOnceValidated() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - int value = 1; - var validator = new RequiredValidator(); - - // Act - validator.Validate(value); - - // Assert - validator.IsDirty.ShouldBe(true); - } - - [Test] - public void ShouldNotValidateEmptyString() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(string.Empty); - - // Assert - validator.IsInvalid.ShouldBe(true); - } - - [Test] - public void ShouldNotValidateStringOnlyWithSpaces() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(" "); - - // Assert - validator.IsInvalid.ShouldBe(true); - } - - [Test] - public void ShouldValidateStringWithCharacters() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate("Hello, World"); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [Test] - public void ShouldNotValidateEmptyCollection() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(new List()); - - // Assert - validator.IsInvalid.ShouldBe(true); - } - - [Test] - public void ShouldValidatePopulatedCollection() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(new List { "Hello", "World" }); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [Test] - public void ShouldNotValidateNullObject() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(null); - - // Assert - validator.IsInvalid.ShouldBe(true); - } - - [Test] - public void ShouldValidateInitializedObject() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(new EventArgs()); - - // Assert - validator.IsInvalid.ShouldBe(false); - } - - [Test] - public void ShouldNotValidateFalse() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(false); - - // Assert - validator.IsInvalid.ShouldBe(true); - } - - [Test] - public void ShouldValidateTrue() - { - // Arrange - var validator = new RequiredValidator(); - - // Act - validator.Validate(true); - - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Arrange + int value = 1; + var validator = new RequiredValidator(); + + // Act + validator.Validate(value); + + // Assert + validator.IsDirty.ShouldBe(true); + } + + [Test] + public void ShouldNotValidateEmptyString() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(string.Empty); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldNotValidateStringOnlyWithSpaces() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(" "); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldValidateStringWithCharacters() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate("Hello, World"); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [Test] + public void ShouldNotValidateEmptyCollection() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(new List()); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldValidatePopulatedCollection() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(new List { "Hello", "World" }); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [Test] + public void ShouldNotValidateNullObject() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(null); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldValidateInitializedObject() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(new EventArgs()); + + // Assert + validator.IsInvalid.ShouldBe(false); + } + + [Test] + public void ShouldNotValidateFalse() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(false); + + // Assert + validator.IsInvalid.ShouldBe(true); + } + + [Test] + public void ShouldValidateTrue() + { + // Arrange + var validator = new RequiredValidator(); + + // Act + validator.Validate(true); + + // Assert + validator.IsInvalid.ShouldBe(false); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/StringExtensionsTests.cs b/tests/MADE.Data.Validation.Tests/Tests/StringExtensionsTests.cs index 9b7756f7..214ce6c7 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/StringExtensionsTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/StringExtensionsTests.cs @@ -1,165 +1,164 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using MADE.Data.Validation.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class StringExtensionsTests { - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using MADE.Data.Validation.Extensions; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class StringExtensionsTests + public class WhenValidatingIfStringIsLike + { + [TestCase("*", "abc", true)] + [TestCase("a*", "abc", true)] + [TestCase("a?c", "abc", true)] + [TestCase("[a-z][a-z][a-z]", "abc", true)] + [TestCase("###", "123", true)] + [TestCase("###", "abc", false)] + [TestCase("*###", "123abc", false)] + [TestCase("[a-z][a-z][a-z]", "ABC", false)] + [TestCase("a?c", "aba", false)] + public void ShouldMatchPattern(string pattern, string input, bool expected) + { + // Act + var actual = input.IsLike(pattern); + + // Assert + actual.ShouldBe(expected); + } + } + + public class WhenCheckingIfStringContainsValue + { + [TestCase("Hello, World", "ello", CompareOptions.None)] + [TestCase("Hello, World", "hello", CompareOptions.IgnoreCase)] + public void ShouldReturnTrueIfDoesContain(string phrase, string value, CompareOptions compare) + { + // Act + bool contains = phrase.Contains(value, compare); + + // Assert + contains.ShouldBeTrue(); + } + + [TestCase("Hello, World", "Hey", CompareOptions.None)] + [TestCase("Hello, World", "hello", CompareOptions.None)] + public void ShouldReturnFalseIfDoesNotContain(string phrase, string value, CompareOptions compare) + { + // Act + bool contains = phrase.Contains(value, compare); + + // Assert + contains.ShouldBeFalse(); + } + } + + public class WhenCheckingIfStringIsInt + { + [TestCase("10")] + [TestCase("-10")] + public void ShouldReturnTrueIfInt(string value) + { + // Act + bool actual = value.IsInt(); + + // Assert + actual.ShouldBeTrue(); + } + + [TestCase(null)] + [TestCase("")] + [TestCase("Hello, World")] + public void ShouldReturnFalseIfNotInt(string value) + { + // Act + bool actual = value.IsInt(); + + // Assert + actual.ShouldBeFalse(); + } + } + + public class WhenCheckingIfStringIsDouble { - public class WhenValidatingIfStringIsLike + [TestCase("10.5")] + [TestCase("-10.5")] + public void ShouldReturnTrueIfDouble(string value) { - [TestCase("*", "abc", true)] - [TestCase("a*", "abc", true)] - [TestCase("a?c", "abc", true)] - [TestCase("[a-z][a-z][a-z]", "abc", true)] - [TestCase("###", "123", true)] - [TestCase("###", "abc", false)] - [TestCase("*###", "123abc", false)] - [TestCase("[a-z][a-z][a-z]", "ABC", false)] - [TestCase("a?c", "aba", false)] - public void ShouldMatchPattern(string pattern, string input, bool expected) - { - // Act - var actual = input.IsLike(pattern); - - // Assert - actual.ShouldBe(expected); - } + // Act + bool actual = value.IsDouble(); + + // Assert + actual.ShouldBeTrue(); } - public class WhenCheckingIfStringContainsValue + [TestCase(null)] + [TestCase("")] + [TestCase("Hello, World")] + public void ShouldReturnFalseIfNotDouble(string value) { - [TestCase("Hello, World", "ello", CompareOptions.None)] - [TestCase("Hello, World", "hello", CompareOptions.IgnoreCase)] - public void ShouldReturnTrueIfDoesContain(string phrase, string value, CompareOptions compare) - { - // Act - bool contains = phrase.Contains(value, compare); - - // Assert - contains.ShouldBeTrue(); - } - - [TestCase("Hello, World", "Hey", CompareOptions.None)] - [TestCase("Hello, World", "hello", CompareOptions.None)] - public void ShouldReturnFalseIfDoesNotContain(string phrase, string value, CompareOptions compare) - { - // Act - bool contains = phrase.Contains(value, compare); - - // Assert - contains.ShouldBeFalse(); - } + // Act + bool actual = value.IsDouble(); + + // Assert + actual.ShouldBeFalse(); } + } - public class WhenCheckingIfStringIsInt + public class WhenCheckingIfStringIsBoolean + { + [TestCase("True")] + [TestCase("true")] + [TestCase("False")] + [TestCase("false")] + public void ShouldReturnTrueIfBoolean(string value) { - [TestCase("10")] - [TestCase("-10")] - public void ShouldReturnTrueIfInt(string value) - { - // Act - bool actual = value.IsInt(); - - // Assert - actual.ShouldBeTrue(); - } - - [TestCase(null)] - [TestCase("")] - [TestCase("Hello, World")] - public void ShouldReturnFalseIfNotInt(string value) - { - // Act - bool actual = value.IsInt(); - - // Assert - actual.ShouldBeFalse(); - } + // Act + bool actual = value.IsBoolean(); + + // Assert + actual.ShouldBeTrue(); } - public class WhenCheckingIfStringIsDouble + [TestCase(null)] + [TestCase("")] + [TestCase("Hello, World")] + public void ShouldReturnFalseIfNotBoolean(string value) { - [TestCase("10.5")] - [TestCase("-10.5")] - public void ShouldReturnTrueIfDouble(string value) - { - // Act - bool actual = value.IsDouble(); - - // Assert - actual.ShouldBeTrue(); - } - - [TestCase(null)] - [TestCase("")] - [TestCase("Hello, World")] - public void ShouldReturnFalseIfNotDouble(string value) - { - // Act - bool actual = value.IsDouble(); - - // Assert - actual.ShouldBeFalse(); - } + // Act + bool actual = value.IsBoolean(); + + // Assert + actual.ShouldBeFalse(); } + } - public class WhenCheckingIfStringIsBoolean + public class WhenCheckingIfStringIsFloat + { + [TestCase("10.5")] + [TestCase("-10.5")] + public void ShouldReturnTrueIfFloat(string value) { - [TestCase("True")] - [TestCase("true")] - [TestCase("False")] - [TestCase("false")] - public void ShouldReturnTrueIfBoolean(string value) - { - // Act - bool actual = value.IsBoolean(); - - // Assert - actual.ShouldBeTrue(); - } - - [TestCase(null)] - [TestCase("")] - [TestCase("Hello, World")] - public void ShouldReturnFalseIfNotBoolean(string value) - { - // Act - bool actual = value.IsBoolean(); - - // Assert - actual.ShouldBeFalse(); - } + // Act + bool actual = value.IsFloat(); + + // Assert + actual.ShouldBeTrue(); } - public class WhenCheckingIfStringIsFloat + [TestCase(null)] + [TestCase("")] + [TestCase("Hello, World")] + public void ShouldReturnFalseIfNotFloat(string value) { - [TestCase("10.5")] - [TestCase("-10.5")] - public void ShouldReturnTrueIfFloat(string value) - { - // Act - bool actual = value.IsFloat(); - - // Assert - actual.ShouldBeTrue(); - } - - [TestCase(null)] - [TestCase("")] - [TestCase("Hello, World")] - public void ShouldReturnFalseIfNotFloat(string value) - { - // Act - bool actual = value.IsFloat(); - - // Assert - actual.ShouldBeFalse(); - } + // Act + bool actual = value.IsFloat(); + + // Assert + actual.ShouldBeFalse(); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/ValidatorCollectionTests.cs b/tests/MADE.Data.Validation.Tests/Tests/ValidatorCollectionTests.cs index 77c25acf..07cb0142 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/ValidatorCollectionTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/ValidatorCollectionTests.cs @@ -1,144 +1,143 @@ -namespace MADE.Data.Validation.Tests.Tests +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using MADE.Data.Validation.Validators; +using MADE.Testing; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ValidatorCollectionTests { - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using System.Linq; - using MADE.Data.Validation.Validators; - using MADE.Testing; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class ValidatorCollectionTests + public class WhenInitializing { - public class WhenInitializing + [Test] + public void ShouldBeEmptyIfDefaultConstructor() { - [Test] - public void ShouldBeEmptyIfDefaultConstructor() - { - // Act - var collection = new ValidatorCollection(); + // Act + var collection = new ValidatorCollection(); - // Assert - collection.Count.ShouldBe(0); - } + // Assert + collection.Count.ShouldBe(0); + } - [Test] - public void ShouldContainItemsIfInitializedAsEnumerable() + [Test] + public void ShouldContainItemsIfInitializedAsEnumerable() + { + // Arrange + IEnumerable validators = new List { - // Arrange - IEnumerable validators = new List - { - new AlphaValidator { Key = "AlphaOnly" }, - new RequiredValidator { Key = "Required" }, - }; - - // Act - var collection = new ValidatorCollection(validators); - - // Assert - collection.Count.ShouldBe(validators.Count()); - collection.ToList().ShouldBeEquivalentTo(validators); - } + new AlphaValidator { Key = "AlphaOnly" }, + new RequiredValidator { Key = "Required" }, + }; + + // Act + var collection = new ValidatorCollection(validators); + + // Assert + collection.Count.ShouldBe(validators.Count()); + collection.ToList().ShouldBeEquivalentTo(validators); } + } - public class WhenAddingItems + public class WhenAddingItems + { + [Test] + public void ShouldAddRangeOfItems() { - [Test] - public void ShouldAddRangeOfItems() + // Arrange + IEnumerable objectsToAdd = new List { - // Arrange - IEnumerable objectsToAdd = new List - { - new AlphaValidator { Key = "AlphaOnly" }, - new RequiredValidator { Key = "Required" }, - }; - - var collection = new ValidatorCollection(); - - // Act - collection.AddRange(objectsToAdd); - - // Assert - foreach (IValidator item in objectsToAdd) - { - collection.ShouldContain(item); - } - } + new AlphaValidator { Key = "AlphaOnly" }, + new RequiredValidator { Key = "Required" }, + }; - [Test] - public void ShouldAddSingleItem() - { - // Arrange - var objectToAdd = new AlphaValidator { Key = "AlphaOnly" }; + var collection = new ValidatorCollection(); - // Act - var collection = new ValidatorCollection { objectToAdd }; + // Act + collection.AddRange(objectsToAdd); - // Assert - collection.ShouldContain(objectToAdd); + // Assert + foreach (IValidator item in objectsToAdd) + { + collection.ShouldContain(item); } } - public class WhenValidating + [Test] + public void ShouldAddSingleItem() { - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "Hello"; + // Arrange + var objectToAdd = new AlphaValidator { Key = "AlphaOnly" }; - var collection = new ValidatorCollection - { - new AlphaValidator { Key = "AlphaOnly" }, - new RequiredValidator { Key = "Required" }, - }; + // Act + var collection = new ValidatorCollection { objectToAdd }; - // Act - collection.Validate(value); + // Assert + collection.ShouldContain(objectToAdd); + } + } - // Assert - collection.IsDirty.ShouldBe(true); - } + public class WhenValidating + { + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + string value = "Hello"; - [Test] - public void ShouldBeValidIfValidValue() + var collection = new ValidatorCollection { - // Arrange - string value = "Hello"; + new AlphaValidator { Key = "AlphaOnly" }, + new RequiredValidator { Key = "Required" }, + }; - var collection = new ValidatorCollection - { - new AlphaValidator { Key = "AlphaOnly" }, - new RequiredValidator { Key = "Required" }, - }; + // Act + collection.Validate(value); - // Act - collection.Validate(value); + // Assert + collection.IsDirty.ShouldBe(true); + } - // Assert - collection.IsInvalid.ShouldBe(false); - } + [Test] + public void ShouldBeValidIfValidValue() + { + // Arrange + string value = "Hello"; - [Test] - public void ShouldBeInvalidIfInvalidValue() + var collection = new ValidatorCollection { - // Arrange - string value = string.Empty; + new AlphaValidator { Key = "AlphaOnly" }, + new RequiredValidator { Key = "Required" }, + }; - var collection = new ValidatorCollection - { - new AlphaValidator { Key = "AlphaOnly" }, - new RequiredValidator { Key = "Required" }, - }; + // Act + collection.Validate(value); - // Act - collection.Validate(value); + // Assert + collection.IsInvalid.ShouldBe(false); + } - // Assert - collection.IsInvalid.ShouldBe(true); - } + [Test] + public void ShouldBeInvalidIfInvalidValue() + { + // Arrange + string value = string.Empty; + + var collection = new ValidatorCollection + { + new AlphaValidator { Key = "AlphaOnly" }, + new RequiredValidator { Key = "Required" }, + }; + + // Act + collection.Validate(value); + + // Assert + collection.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/WellFormedUrlValidatorTests.cs b/tests/MADE.Data.Validation.Tests/Tests/WellFormedUrlValidatorTests.cs index b2d398a0..231964cf 100644 --- a/tests/MADE.Data.Validation.Tests/Tests/WellFormedUrlValidatorTests.cs +++ b/tests/MADE.Data.Validation.Tests/Tests/WellFormedUrlValidatorTests.cs @@ -1,78 +1,77 @@ -namespace MADE.Data.Validation.Tests.Tests -{ - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Data.Validation.Validators; - using NUnit.Framework; - using Shouldly; +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Validation.Validators; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class WellFormedUrlValidatorTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class WellFormedUrlValidatorTests +{ + public class WhenValidating { - public class WhenValidating + private static readonly object[] ValidUrls = { - private static readonly object[] ValidUrls = - { - "https://www.wellformed.com", "http://www.wellformed.com", "ftp://wellformed.com", - "https://www.wellformed.com/slug" - }; + "https://www.wellformed.com", "http://www.wellformed.com", "ftp://wellformed.com", + "https://www.wellformed.com/slug" + }; - [Test] - public void ShouldBeDirtyOnceValidated() - { - // Arrange - string value = "www.website.com"; - var validator = new WellFormedUrlValidator(); + [Test] + public void ShouldBeDirtyOnceValidated() + { + // Arrange + string value = "www.website.com"; + var validator = new WellFormedUrlValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsDirty.ShouldBe(true); - } + // Assert + validator.IsDirty.ShouldBe(true); + } - [TestCaseSource(nameof(ValidUrls))] - public void ShouldBeValidIfWellFormedUrlString(string value) - { - // Arrange - var validator = new WellFormedUrlValidator(); + [TestCaseSource(nameof(ValidUrls))] + public void ShouldBeValidIfWellFormedUrlString(string value) + { + // Arrange + var validator = new WellFormedUrlValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [TestCaseSource(nameof(ValidUrls))] - public void ShouldBeValidIfWellFormedUri(string value) - { - // Arrange - var uri = new Uri(value); - var validator = new WellFormedUrlValidator(); + [TestCaseSource(nameof(ValidUrls))] + public void ShouldBeValidIfWellFormedUri(string value) + { + // Arrange + var uri = new Uri(value); + var validator = new WellFormedUrlValidator(); - // Act - validator.Validate(uri); + // Act + validator.Validate(uri); - // Assert - validator.IsInvalid.ShouldBe(false); - } + // Assert + validator.IsInvalid.ShouldBe(false); + } - [TestCase("NotUrl")] - [TestCase("www.notwellformed.com")] - [TestCase("www.notwellformed.com/slug")] - public void ShouldBeInvalidIfNotWellFormedUrlString(string value) - { - // Arrange - var validator = new WellFormedUrlValidator(); + [TestCase("NotUrl")] + [TestCase("www.notwellformed.com")] + [TestCase("www.notwellformed.com/slug")] + public void ShouldBeInvalidIfNotWellFormedUrlString(string value) + { + // Arrange + var validator = new WellFormedUrlValidator(); - // Act - validator.Validate(value); + // Act + validator.Validate(value); - // Assert - validator.IsInvalid.ShouldBe(true); - } + // Assert + validator.IsInvalid.ShouldBe(true); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Diagnostics.Tests/Tests/StopwatchHelperTests.cs b/tests/MADE.Diagnostics.Tests/Tests/StopwatchHelperTests.cs index 1f9d6cdc..e147a7d9 100644 --- a/tests/MADE.Diagnostics.Tests/Tests/StopwatchHelperTests.cs +++ b/tests/MADE.Diagnostics.Tests/Tests/StopwatchHelperTests.cs @@ -1,94 +1,93 @@ -namespace MADE.Diagnostics.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Diagnostics.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class StopwatchHelperTests { - using System; - using System.Diagnostics.CodeAnalysis; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class StopwatchHelperTests + public class WhenRunning { - public class WhenRunning + [Test] + public void ShouldReturnMessageForStopwatchUsingFilePathAndMember() + { + // Act + string startMessage = StopwatchHelper.Start(); + (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(); + + // Assert + startMessage.ShouldContain(nameof(this.ShouldReturnMessageForStopwatchUsingFilePathAndMember)); + stopMessage.ShouldContain(nameof(this.ShouldReturnMessageForStopwatchUsingFilePathAndMember)); + stopMessage.ShouldContain(time.ToString()); + time.TotalMilliseconds.ShouldBeGreaterThan(0); + } + + [Test] + public void ShouldReturnMessageForStopwatchUsingProvidedPathAndMember() + { + // Act + string caller = nameof(StopwatchHelperTests); + string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); + + string startMessage = StopwatchHelper.Start(caller, name); + (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(caller, name); + + // Assert + startMessage.ShouldContain(caller); + startMessage.ShouldContain(name); + stopMessage.ShouldContain(caller); + stopMessage.ShouldContain(name); + stopMessage.ShouldContain(time.ToString()); + time.TotalMilliseconds.ShouldBeGreaterThan(0); + } + + [Test] + public void ShouldNotStartMultipleStopwatchForSameKey() + { + // Act + string caller = nameof(StopwatchHelperTests); + string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); + + StopwatchHelper.Start(caller, name); + string duplicateStartMessage = StopwatchHelper.Start(caller, name); + + StopwatchHelper.Stop(caller, name); + + // Assert + duplicateStartMessage.ShouldBeNull(); + } + + [Test] + public void ShouldNotStopNonExistentStopwatch() + { + // Act + string caller = nameof(StopwatchHelperTests); + string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); + + (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(caller, name); + + // Assert + stopMessage.ShouldBeNull(); + time.TotalMilliseconds.ShouldBe(0); + } + + [Test] + public void ShouldNotStopAlreadyStoppedStopwatch() { - [Test] - public void ShouldReturnMessageForStopwatchUsingFilePathAndMember() - { - // Act - string startMessage = StopwatchHelper.Start(); - (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(); - - // Assert - startMessage.ShouldContain(nameof(this.ShouldReturnMessageForStopwatchUsingFilePathAndMember)); - stopMessage.ShouldContain(nameof(this.ShouldReturnMessageForStopwatchUsingFilePathAndMember)); - stopMessage.ShouldContain(time.ToString()); - time.TotalMilliseconds.ShouldBeGreaterThan(0); - } - - [Test] - public void ShouldReturnMessageForStopwatchUsingProvidedPathAndMember() - { - // Act - string caller = nameof(StopwatchHelperTests); - string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); - - string startMessage = StopwatchHelper.Start(caller, name); - (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(caller, name); - - // Assert - startMessage.ShouldContain(caller); - startMessage.ShouldContain(name); - stopMessage.ShouldContain(caller); - stopMessage.ShouldContain(name); - stopMessage.ShouldContain(time.ToString()); - time.TotalMilliseconds.ShouldBeGreaterThan(0); - } - - [Test] - public void ShouldNotStartMultipleStopwatchForSameKey() - { - // Act - string caller = nameof(StopwatchHelperTests); - string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); - - StopwatchHelper.Start(caller, name); - string duplicateStartMessage = StopwatchHelper.Start(caller, name); - - StopwatchHelper.Stop(caller, name); - - // Assert - duplicateStartMessage.ShouldBeNull(); - } - - [Test] - public void ShouldNotStopNonExistentStopwatch() - { - // Act - string caller = nameof(StopwatchHelperTests); - string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); - - (string stopMessage, TimeSpan time) = StopwatchHelper.Stop(caller, name); - - // Assert - stopMessage.ShouldBeNull(); - time.TotalMilliseconds.ShouldBe(0); - } - - [Test] - public void ShouldNotStopAlreadyStoppedStopwatch() - { - // Act - string caller = nameof(StopwatchHelperTests); - string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); - - StopwatchHelper.Start(caller, name); - StopwatchHelper.Stop(caller, name); - (string duplicateStopMessage, TimeSpan duplicateStopTime) = StopwatchHelper.Stop(caller, name); - - // Assert - duplicateStopMessage.ShouldBeNull(); - duplicateStopTime.TotalMilliseconds.ShouldBe(0); - } + // Act + string caller = nameof(StopwatchHelperTests); + string name = nameof(this.ShouldReturnMessageForStopwatchUsingProvidedPathAndMember); + + StopwatchHelper.Start(caller, name); + StopwatchHelper.Stop(caller, name); + (string duplicateStopMessage, TimeSpan duplicateStopTime) = StopwatchHelper.Stop(caller, name); + + // Assert + duplicateStopMessage.ShouldBeNull(); + duplicateStopTime.TotalMilliseconds.ShouldBe(0); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs index 7d844f15..aaec7e7f 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs @@ -1,62 +1,61 @@ -namespace MADE.Networking.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonDeleteNetworkRequestTests { - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonDeleteNetworkRequestTests + public class WhenExecutingRequest { - public class WhenExecutingRequest + [Test] + public async Task ShouldReturnSuccessFromDeleteEndpointWithResponse() { - [Test] - public async Task ShouldReturnSuccessFromDeleteEndpointWithResponse() - { - // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/delete?{query}={queryValue}"; - var request = new JsonDeleteNetworkRequest(new HttpClient(), requestUrl); - - // Act - var response = await request.ExecuteAsync(); - - // Assert - response.ShouldNotBeNull(); - response.Url.ShouldBe(requestUrl); - bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); - } - - [Test] - public async Task ShouldReturnErrorFromGetEndpoint() - { - // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonDeleteNetworkRequest(new HttpClient(), requestUrl); - - // Act - var exception = await request.ExecuteAsync().ShouldThrowAsync(); - - // Assert - exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); - } + // Arrange + const string query = "test"; + const bool queryValue = true; + + var requestUrl = $"https://httpbin.org/delete?{query}={queryValue}"; + var request = new JsonDeleteNetworkRequest(new HttpClient(), requestUrl); + + // Act + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe(requestUrl); + bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); } - public class RequestResponse + [Test] + public async Task ShouldReturnErrorFromGetEndpoint() { - public JsonObject Args { get; set; } + // Arrange + const string query = "test"; + const bool queryValue = true; + + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonDeleteNetworkRequest(new HttpClient(), requestUrl); + + // Act + var exception = await request.ExecuteAsync().ShouldThrowAsync(); - public string Url { get; set; } + // Assert + exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); } } -} \ No newline at end of file + + public class RequestResponse + { + public JsonObject Args { get; set; } + + public string Url { get; set; } + } +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs index 7eb3460c..ba9ad2bc 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs @@ -1,62 +1,61 @@ -namespace MADE.Networking.Tests.Tests +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonGetNetworkRequestTests { - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; - - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonGetNetworkRequestTests + public class WhenExecutingRequest { - public class WhenExecutingRequest + [Test] + public async Task ShouldReturnSuccessFromGetEndpointWithResponse() { - [Test] - public async Task ShouldReturnSuccessFromGetEndpointWithResponse() - { - // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - - // Act - var response = await request.ExecuteAsync(); - - // Assert - response.ShouldNotBeNull(); - response.Url.ShouldBe(requestUrl); - bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); - } - - [Test] - public async Task ShouldReturnErrorFromDeleteEndpoint() - { - // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/delete?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - - // Act - var exception = await request.ExecuteAsync().ShouldThrowAsync(); - - // Assert - exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); - } + // Arrange + const string query = "test"; + const bool queryValue = true; + + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + + // Act + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe(requestUrl); + bool.Parse(response.Args[query].ToString()).ShouldBe(queryValue); } - public class RequestResponse + [Test] + public async Task ShouldReturnErrorFromDeleteEndpoint() { - public JsonObject Args { get; set; } + // Arrange + const string query = "test"; + const bool queryValue = true; + + var requestUrl = $"https://httpbin.org/delete?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + + // Act + var exception = await request.ExecuteAsync().ShouldThrowAsync(); - public string Url { get; set; } + // Assert + exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); } } -} \ No newline at end of file + + public class RequestResponse + { + public JsonObject Args { get; set; } + + public string Url { get; set; } + } +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs index 87e02eec..2127d63f 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs @@ -1,81 +1,80 @@ -namespace MADE.Networking.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonPatchNetworkRequestTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonPatchNetworkRequestTests +{ + public class WhenExecutingRequest { - public class WhenExecutingRequest + [Test] + public async Task ShouldReturnSuccessFromPatchEndpointWithResponse() { - [Test] - public async Task ShouldReturnSuccessFromPatchEndpointWithResponse() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/patch"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/patch"; + var request = new JsonPatchNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var response = await request.ExecuteAsync(); + // Act + var response = await request.ExecuteAsync(); - // Assert - response.ShouldNotBeNull(); - response.Url.ShouldBe(requestUrl); - response.Data.ShouldNotBeNull(); + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe(requestUrl); + response.Data.ShouldNotBeNull(); - var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - responseData.ShouldNotBeNull(); - responseData.Key.ShouldBe(requestData.Key); - responseData.Enabled.ShouldBe(requestData.Enabled); - } + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + responseData.ShouldNotBeNull(); + responseData.Key.ShouldBe(requestData.Key); + responseData.Enabled.ShouldBe(requestData.Enabled); + } - [Test] - public async Task ShouldReturnErrorFromGetEndpoint() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + [Test] + public async Task ShouldReturnErrorFromGetEndpoint() + { + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/get"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/get"; + var request = new JsonPatchNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var exception = await request.ExecuteAsync().ShouldThrowAsync(); + // Act + var exception = await request.ExecuteAsync().ShouldThrowAsync(); - // Assert - exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); - } + // Assert + exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); } + } - public class RequestData - { - public string Key { get; set; } + public class RequestData + { + public string Key { get; set; } - public bool Enabled { get; set; } - } + public bool Enabled { get; set; } + } - public class RequestResponse - { - public JsonObject Args { get; set; } + public class RequestResponse + { + public JsonObject Args { get; set; } - public string Data { get; set; } + public string Data { get; set; } - public string Url { get; set; } - } + public string Url { get; set; } } -} \ No newline at end of file +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs index 6fb519a1..720b8f2f 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs @@ -1,81 +1,80 @@ -namespace MADE.Networking.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonPostNetworkRequestTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonPostNetworkRequestTests +{ + public class WhenExecutingRequest { - public class WhenExecutingRequest + [Test] + public async Task ShouldReturnSuccessFromPostEndpointWithResponse() { - [Test] - public async Task ShouldReturnSuccessFromPostEndpointWithResponse() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/post"; - var request = new JsonPostNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/post"; + var request = new JsonPostNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var response = await request.ExecuteAsync(); + // Act + var response = await request.ExecuteAsync(); - // Assert - response.ShouldNotBeNull(); - response.Url.ShouldBe(requestUrl); - response.Data.ShouldNotBeNull(); + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe(requestUrl); + response.Data.ShouldNotBeNull(); - var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - responseData.ShouldNotBeNull(); - responseData.Key.ShouldBe(requestData.Key); - responseData.Enabled.ShouldBe(requestData.Enabled); - } + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + responseData.ShouldNotBeNull(); + responseData.Key.ShouldBe(requestData.Key); + responseData.Enabled.ShouldBe(requestData.Enabled); + } - [Test] - public async Task ShouldReturnErrorFromGetEndpoint() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + [Test] + public async Task ShouldReturnErrorFromGetEndpoint() + { + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/get"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/get"; + var request = new JsonPatchNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var exception = await request.ExecuteAsync().ShouldThrowAsync(); + // Act + var exception = await request.ExecuteAsync().ShouldThrowAsync(); - // Assert - exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); - } + // Assert + exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); } + } - public class RequestData - { - public string Key { get; set; } + public class RequestData + { + public string Key { get; set; } - public bool Enabled { get; set; } - } + public bool Enabled { get; set; } + } - public class RequestResponse - { - public JsonObject Args { get; set; } + public class RequestResponse + { + public JsonObject Args { get; set; } - public string Data { get; set; } + public string Data { get; set; } - public string Url { get; set; } - } + public string Url { get; set; } } -} \ No newline at end of file +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs index ee4b6af5..eece3488 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs @@ -1,81 +1,80 @@ -namespace MADE.Networking.Tests.Tests -{ - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Net.Http; - using System.Threading.Tasks; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class JsonPutNetworkRequestTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class JsonPutNetworkRequestTests +{ + public class WhenExecutingRequest { - public class WhenExecutingRequest + [Test] + public async Task ShouldReturnSuccessFromPutEndpointWithResponse() { - [Test] - public async Task ShouldReturnSuccessFromPutEndpointWithResponse() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/put"; - var request = new JsonPutNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/put"; + var request = new JsonPutNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var response = await request.ExecuteAsync(); + // Act + var response = await request.ExecuteAsync(); - // Assert - response.ShouldNotBeNull(); - response.Url.ShouldBe(requestUrl); - response.Data.ShouldNotBeNull(); + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe(requestUrl); + response.Data.ShouldNotBeNull(); - var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - responseData.ShouldNotBeNull(); - responseData.Key.ShouldBe(requestData.Key); - responseData.Enabled.ShouldBe(requestData.Enabled); - } + var responseData = JsonSerializer.Deserialize(response.Data, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + responseData.ShouldNotBeNull(); + responseData.Key.ShouldBe(requestData.Key); + responseData.Enabled.ShouldBe(requestData.Enabled); + } - [Test] - public async Task ShouldReturnErrorFromGetEndpoint() - { - // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; + [Test] + public async Task ShouldReturnErrorFromGetEndpoint() + { + // Arrange + var requestData = new RequestData { Key = "test", Enabled = true }; - const string requestUrl = "https://httpbin.org/get"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + const string requestUrl = "https://httpbin.org/get"; + var request = new JsonPatchNetworkRequest( + new HttpClient(), + requestUrl, + JsonSerializer.Serialize(requestData)); - // Act - var exception = await request.ExecuteAsync().ShouldThrowAsync(); + // Act + var exception = await request.ExecuteAsync().ShouldThrowAsync(); - // Assert - exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); - } + // Assert + exception.StatusCode.ShouldBe(HttpStatusCode.MethodNotAllowed); } + } - public class RequestData - { - public string Key { get; set; } + public class RequestData + { + public string Key { get; set; } - public bool Enabled { get; set; } - } + public bool Enabled { get; set; } + } - public class RequestResponse - { - public JsonObject Args { get; set; } + public class RequestResponse + { + public JsonObject Args { get; set; } - public string Data { get; set; } + public string Data { get; set; } - public string Url { get; set; } - } + public string Url { get; set; } } -} \ No newline at end of file +} diff --git a/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs b/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs index 5c54bcb6..9c275423 100644 --- a/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs +++ b/tests/MADE.Networking.Tests/Tests/NetworkRequestManagerTests.cs @@ -1,200 +1,199 @@ -namespace MADE.Networking.Tests.Tests +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using MADE.Networking.Http; +using MADE.Networking.Http.Requests.Json; +using System.Text.Json.Nodes; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; + +[TestFixture] +[ExcludeFromCodeCoverage] +public class NetworkRequestManagerTests { - using System; - using System.Diagnostics.CodeAnalysis; - using System.Net.Http; - using System.Threading; - using MADE.Networking.Http; - using MADE.Networking.Http.Requests.Json; - using System.Text.Json.Nodes; - using NUnit.Framework; - using Shouldly; - - [TestFixture] - [ExcludeFromCodeCoverage] - public class NetworkRequestManagerTests + public class WhenAddingOrUpdatingQueueRequests { - public class WhenAddingOrUpdatingQueueRequests + [Test] + public void ShouldAddToQueue() { - [Test] - public void ShouldAddToQueue() - { - // Arrange - const string query = "test"; - const bool queryValue = true; + // Arrange + const string query = "test"; + const bool queryValue = true; - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - var manager = new NetworkRequestManager(); + var manager = new NetworkRequestManager(); - // Act - manager.AddOrUpdate( - request, - _ => { }); + // Act + manager.AddOrUpdate( + request, + _ => { }); - // Assert - manager.CurrentQueue.Count.ShouldBe(1); - manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); - } + // Assert + manager.CurrentQueue.Count.ShouldBe(1); + manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); + } - [Test] - public void ShouldUpdateExistingInQueue() - { - // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - - var manager = new NetworkRequestManager(); - manager.AddOrUpdate( - request, - _ => { }); - - // Act - request.Url = $"https://httpbin.org/get?{query}={!queryValue}"; - manager.AddOrUpdate( - request, - _ => { }); - - // Assert - manager.CurrentQueue.Count.ShouldBe(1); - manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); - } + [Test] + public void ShouldUpdateExistingInQueue() + { + // Arrange + const string query = "test"; + const bool queryValue = true; + + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + + var manager = new NetworkRequestManager(); + manager.AddOrUpdate( + request, + _ => { }); + + // Act + request.Url = $"https://httpbin.org/get?{query}={!queryValue}"; + manager.AddOrUpdate( + request, + _ => { }); + + // Assert + manager.CurrentQueue.Count.ShouldBe(1); + manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); } + } - public class WhenRemovingQueueRequests + public class WhenRemovingQueueRequests + { + [Test] + public void ShouldRemoveByRequest() { - [Test] - public void ShouldRemoveByRequest() - { - // Arrange - const string query = "test"; - const bool queryValue = true; + // Arrange + const string query = "test"; + const bool queryValue = true; - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - var manager = new NetworkRequestManager(); + var manager = new NetworkRequestManager(); - manager.AddOrUpdate( - request, - _ => { }); + manager.AddOrUpdate( + request, + _ => { }); - // Act - manager.Remove(request); + // Act + manager.Remove(request); - // Assert - manager.CurrentQueue.Count.ShouldBe(0); - } + // Assert + manager.CurrentQueue.Count.ShouldBe(0); + } - [Test] - public void ShouldRemoveByRequestId() - { - // Arrange - const string query = "test"; - const bool queryValue = true; + [Test] + public void ShouldRemoveByRequestId() + { + // Arrange + const string query = "test"; + const bool queryValue = true; - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - var manager = new NetworkRequestManager(); + var manager = new NetworkRequestManager(); - manager.AddOrUpdate( - request, - _ => { }); + manager.AddOrUpdate( + request, + _ => { }); - // Act - manager.RemoveByKey(request.Identifier.ToString()); + // Act + manager.RemoveByKey(request.Identifier.ToString()); - // Assert - manager.CurrentQueue.Count.ShouldBe(0); - } + // Assert + manager.CurrentQueue.Count.ShouldBe(0); } + } - public class WhenProcessingQueueRequests + public class WhenProcessingQueueRequests + { + [Test] + public void ShouldProcessQueue() { - [Test] - public void ShouldProcessQueue() - { - // Arrange - AutoResetEvent autoResetEvent = new AutoResetEvent(false); + // Arrange + AutoResetEvent autoResetEvent = new AutoResetEvent(false); - const string query = "test"; - const bool queryValue = true; + const string query = "test"; + const bool queryValue = true; - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - var manager = new NetworkRequestManager(); + var manager = new NetworkRequestManager(); - RequestResponse actualResponse = null; + RequestResponse actualResponse = null; - manager.AddOrUpdate(request, response => - { - actualResponse = response; - autoResetEvent.Set(); - }); + manager.AddOrUpdate(request, response => + { + actualResponse = response; + autoResetEvent.Set(); + }); - // Act - manager.Start(); + // Act + manager.Start(); - // Assert - autoResetEvent.WaitOne(TimeSpan.FromSeconds(60)); + // Assert + autoResetEvent.WaitOne(TimeSpan.FromSeconds(60)); - actualResponse.ShouldNotBeNull(); - actualResponse.Url.ShouldBe(requestUrl); - bool.Parse(actualResponse.Args[query].ToString()).ShouldBe(queryValue); - } + actualResponse.ShouldNotBeNull(); + actualResponse.Url.ShouldBe(requestUrl); + bool.Parse(actualResponse.Args[query].ToString()).ShouldBe(queryValue); } + } - public class WhenProcessingQueueRequestsStopped + public class WhenProcessingQueueRequestsStopped + { + [Test] + public void ShouldStopProcessingQueue() { - [Test] - public void ShouldStopProcessingQueue() - { - // Arrange - AutoResetEvent autoResetEvent = new AutoResetEvent(false); + // Arrange + AutoResetEvent autoResetEvent = new AutoResetEvent(false); - const string query = "test"; - const bool queryValue = true; + const string query = "test"; + const bool queryValue = true; - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; + var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); - var manager = new NetworkRequestManager(); + var manager = new NetworkRequestManager(); - manager.AddOrUpdate( - request, - _ => - { - autoResetEvent.Set(); - }); + manager.AddOrUpdate( + request, + _ => + { + autoResetEvent.Set(); + }); - manager.Start(); + manager.Start(); - autoResetEvent.WaitOne(TimeSpan.FromSeconds(60)); + autoResetEvent.WaitOne(TimeSpan.FromSeconds(60)); - // Act - manager.Stop(); + // Act + manager.Stop(); - manager.AddOrUpdate( - request, - _ => { }); + manager.AddOrUpdate( + request, + _ => { }); - // Assert - manager.CurrentQueue.Count.ShouldBe(1); - manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); - } + // Assert + manager.CurrentQueue.Count.ShouldBe(1); + manager.CurrentQueue.Keys.ShouldContain(request.Identifier.ToString()); } + } - public class RequestResponse - { - public JsonObject Args { get; set; } + public class RequestResponse + { + public JsonObject Args { get; set; } - public string Url { get; set; } - } + public string Url { get; set; } } -} \ No newline at end of file +} diff --git a/tests/MADE.Networking.Tests/Tests/UriExtensionsTests.cs b/tests/MADE.Networking.Tests/Tests/UriExtensionsTests.cs index d6e040b5..ed93eef2 100644 --- a/tests/MADE.Networking.Tests/Tests/UriExtensionsTests.cs +++ b/tests/MADE.Networking.Tests/Tests/UriExtensionsTests.cs @@ -1,43 +1,42 @@ -namespace MADE.Networking.Tests.Tests -{ - using System; - using System.Diagnostics.CodeAnalysis; - using MADE.Networking.Extensions; - using NUnit.Framework; - using Shouldly; +using System; +using System.Diagnostics.CodeAnalysis; +using MADE.Networking.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class UriExtensionsTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class UriExtensionsTests +{ + public class WhenRetrievingQueryValues { - public class WhenRetrievingQueryValues + [TestCase("https://www.jamescroft.co.uk/api/profile?name=jamesmcroft", "name", "jamesmcroft")] + [TestCase("https://www.jamescroft.co.uk/api/profile?name=jamescroft&age=24", "age", "24")] + public void ShouldGetQueryValue(string url, string queryParam, string expectedValue) { - [TestCase("https://www.jamescroft.co.uk/api/profile?name=jamesmcroft", "name", "jamesmcroft")] - [TestCase("https://www.jamescroft.co.uk/api/profile?name=jamescroft&age=24", "age", "24")] - public void ShouldGetQueryValue(string url, string queryParam, string expectedValue) - { - // Arrange - var uri = new Uri(url); + // Arrange + var uri = new Uri(url); - // Act - string value = uri.GetQueryValue(queryParam); + // Act + string value = uri.GetQueryValue(queryParam); - // Assert - value.ShouldBe(expectedValue); - } + // Assert + value.ShouldBe(expectedValue); + } - [Test] - public void ShouldReturnNullIfQueryParamDoesNotExist() - { - // Arrange - var uri = new Uri("https://www.jamescroft.co.uk/api/profile?name=jamesmcroft"); + [Test] + public void ShouldReturnNullIfQueryParamDoesNotExist() + { + // Arrange + var uri = new Uri("https://www.jamescroft.co.uk/api/profile?name=jamesmcroft"); - // Act - string value = uri.GetQueryValue("age"); + // Act + string value = uri.GetQueryValue("age"); - // Assert - value.ShouldBeNull(); - } + // Assert + value.ShouldBeNull(); } } -} \ No newline at end of file +} diff --git a/tests/MADE.Web.Tests/Tests/PaginatedResponseTests.cs b/tests/MADE.Web.Tests/Tests/PaginatedResponseTests.cs index bedaa861..581f7115 100644 --- a/tests/MADE.Web.Tests/Tests/PaginatedResponseTests.cs +++ b/tests/MADE.Web.Tests/Tests/PaginatedResponseTests.cs @@ -1,41 +1,40 @@ -namespace MADE.Web.Tests.Tests -{ - using System; - using System.Collections.Generic; - using System.Diagnostics.CodeAnalysis; - using MADE.Testing; - using MADE.Web.Responses; - using NUnit.Framework; - using Shouldly; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using MADE.Testing; +using MADE.Web.Responses; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Web.Tests.Tests; - [ExcludeFromCodeCoverage] - [TestFixture] - public class PaginatedResponseTests +[ExcludeFromCodeCoverage] +[TestFixture] +public class PaginatedResponseTests +{ + public class WhenReturningPaginatedResponse { - public class WhenReturningPaginatedResponse + [Test] + public void ShouldReturnPaginatedResultsWhenCountIsGreaterThanRequest() { - [Test] - public void ShouldReturnPaginatedResultsWhenCountIsGreaterThanRequest() - { - // Arrange - int page = 1; - int pageSize = 3; - int totalItemCount = 10; + // Arrange + int page = 1; + int pageSize = 3; + int totalItemCount = 10; - int expectedPageCount = (int)Math.Ceiling((double)totalItemCount / pageSize); + int expectedPageCount = (int)Math.Ceiling((double)totalItemCount / pageSize); - var items = new List { "Hello", "World", "Test" }; + var items = new List { "Hello", "World", "Test" }; - // Act - var result = new PaginatedResponse(items, page, pageSize, totalItemCount); + // Act + var result = new PaginatedResponse(items, page, pageSize, totalItemCount); - // Assert - result.AvailableCount.ShouldBe(totalItemCount); - result.Page.ShouldBe(page); - result.PageSize.ShouldBe(pageSize); - result.TotalPages.ShouldBe(expectedPageCount); - result.Items.ShouldBeEquivalentTo(items); - } + // Assert + result.AvailableCount.ShouldBe(totalItemCount); + result.Page.ShouldBe(page); + result.PageSize.ShouldBe(pageSize); + result.TotalPages.ShouldBe(expectedPageCount); + result.Items.ShouldBeEquivalentTo(items); } } } From 76d5e016b890f55d2a7c1239cf297d92e3e6181d Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 19:51:16 +0100 Subject: [PATCH 04/12] refactor: add nullable annotations to improve code safety and clarity --- src/MADE.Collections/CollectionExtensions.cs | 2 +- .../ObjectModel/ObservableItemCollection{T}.cs | 4 ++-- .../BooleanToStringValueConverter.cs | 8 ++++---- .../DateTimeToStringValueConverter.cs | 4 ++-- src/MADE.Data.Converters/IValueConverter.cs | 4 ++-- .../StringToBase64StringValueConverter.cs | 4 ++-- .../FluentValidatorCollection{T}.cs | 2 +- .../ValidatorCollection.cs | 2 +- .../Validators/AlphaNumericValidator.cs | 2 +- .../Validators/AlphaValidator.cs | 2 +- .../Validators/Base64Validator.cs | 2 +- .../Validators/BetweenValidator.cs | 2 +- .../Validators/EmailValidator.cs | 2 +- .../Validators/GuidValidator.cs | 2 +- .../Validators/IpAddressValidator.cs | 2 +- .../Validators/LatitudeValidator.cs | 2 +- .../Validators/LongitudeValidator.cs | 2 +- .../Validators/MacAddressValidator.cs | 2 +- .../Validators/MaxLengthValidator.cs | 2 +- .../Validators/MaxValueValidator.cs | 2 +- .../Validators/MinLengthValidator.cs | 2 +- .../Validators/MinValueValidator.cs | 2 +- .../Validators/PredicateValidator{T}.cs | 2 +- .../Validators/RegexValidator.cs | 2 +- .../Validators/RequiredValidator.cs | 2 +- .../Validators/WellFormedUrlValidator.cs | 2 +- src/MADE.Diagnostics/AppDiagnostics.cs | 2 +- src/MADE.Threading/TaskExtensions.cs | 18 ++++++++++++------ src/MADE.Threading/Timer.cs | 2 +- .../Extensions/ControllerBaseExtensions.cs | 2 +- src/MADE.Web.Mvc/Responses/JsonResult.cs | 4 ++-- .../HttpContextExceptionsMiddleware.cs | 12 ++++++------ .../Extensions/HttpResponseExtensions.cs | 8 +++++--- .../Extensions/QueryCollectionExtensions.cs | 4 ++-- src/MADE.Web/Identity/AuthenticatedUser.cs | 8 ++++---- .../Identity/AuthenticatedUserAccessor.cs | 2 +- .../Identity/IAuthenticatedUserAccessor.cs | 2 +- tests/Directory.Build.props | 1 + 38 files changed, 70 insertions(+), 61 deletions(-) diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index d212a0af..22664ac6 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -78,7 +78,7 @@ public static bool Update(this IList collection, T item, Func ArgumentNullException.ThrowIfNull(item); ArgumentNullException.ThrowIfNull(collection); - T existing = collection.FirstOrDefault(x => predicate.Invoke(x, item)); + T? existing = collection.FirstOrDefault(x => predicate.Invoke(x, item)); if (existing == null) { return false; diff --git a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs index 92c99e9e..723370b0 100644 --- a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs +++ b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs @@ -83,12 +83,12 @@ public ObservableItemCollection(List list) /// /// Occurs when an item is added, removed, changed, moved, or the entire list is refreshed. /// - public override event NotifyCollectionChangedEventHandler CollectionChanged; + public override event NotifyCollectionChangedEventHandler? CollectionChanged; /// /// Occurs when an item's event is invoked. /// - public event ObservableItemCollectionPropertyChangedEventHandler ItemPropertyChanged; + public event ObservableItemCollectionPropertyChangedEventHandler? ItemPropertyChanged; /// /// Adds a range of objects to the end of the collection. diff --git a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs index aac6c11b..dbc907a0 100644 --- a/src/MADE.Data.Converters/BooleanToStringValueConverter.cs +++ b/src/MADE.Data.Converters/BooleanToStringValueConverter.cs @@ -14,12 +14,12 @@ public class BooleanToStringValueConverter : IValueConverter /// /// Gets or sets the positive/true value. /// - public string TrueValue { get; set; } + public string TrueValue { get; set; } = string.Empty; /// /// Gets or sets the negative/false value. /// - public string FalseValue { get; set; } + public string FalseValue { get; set; } = string.Empty; /// /// Converts the value to the type. @@ -33,7 +33,7 @@ public class BooleanToStringValueConverter : IValueConverter /// /// The converted object. /// - public string Convert(bool value, object parameter = default) + public string Convert(bool value, object? parameter = default) { return value.ToFormattedString(this.TrueValue, this.FalseValue); } @@ -50,7 +50,7 @@ public string Convert(bool value, object parameter = default) /// /// The converted object. /// - public bool ConvertBack(string value, object parameter = default) + public bool ConvertBack(string value, object? parameter = default) { if (value == this.TrueValue) { diff --git a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs index daacba5d..394168f8 100644 --- a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs +++ b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs @@ -23,7 +23,7 @@ public class DateTimeToStringValueConverter : IValueConverter /// /// The converted object. /// - public string Convert(DateTime value, object parameter = default) + public string Convert(DateTime value, object? parameter = default) { string format = parameter?.ToString(); return !string.IsNullOrWhiteSpace(format) @@ -43,7 +43,7 @@ public string Convert(DateTime value, object parameter = default) /// /// The converted object. /// - public DateTime ConvertBack(string value, object parameter = default) + public DateTime ConvertBack(string value, object? parameter = default) { if (string.IsNullOrWhiteSpace(value)) { diff --git a/src/MADE.Data.Converters/IValueConverter.cs b/src/MADE.Data.Converters/IValueConverter.cs index ad5f41de..b89c9c94 100644 --- a/src/MADE.Data.Converters/IValueConverter.cs +++ b/src/MADE.Data.Converters/IValueConverter.cs @@ -26,7 +26,7 @@ public interface IValueConverter /// /// The converted object. /// - TTo Convert(TFrom value, object parameter = default); + TTo Convert(TFrom value, object? parameter = default); /// /// Converts the value back to the type. @@ -40,5 +40,5 @@ public interface IValueConverter /// /// The converted object. /// - TFrom ConvertBack(TTo value, object parameter = default); + TFrom ConvertBack(TTo value, object? parameter = default); } diff --git a/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs b/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs index 98009097..bbf48a05 100644 --- a/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs +++ b/src/MADE.Data.Converters/StringToBase64StringValueConverter.cs @@ -23,7 +23,7 @@ public partial class StringToBase64StringValueConverter : IValueConverter /// The converted Base64 object. /// - public string Convert(string value, object parameter = default) + public string Convert(string value, object? parameter = default) { return value.ToBase64(parameter as Encoding ?? Encoding.UTF8); } @@ -40,7 +40,7 @@ public string Convert(string value, object parameter = default) /// /// The converted object. /// - public string ConvertBack(string value, object parameter = default) + public string ConvertBack(string value, object? parameter = default) { return value.FromBase64(parameter as Encoding ?? Encoding.UTF8); } diff --git a/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs b/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs index 14226b07..b26f8c8a 100644 --- a/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs +++ b/src/MADE.Data.Validation.FluentValidation/FluentValidatorCollection{T}.cs @@ -40,7 +40,7 @@ public FluentValidatorCollection(int capacity) /// /// Occurs when the input value is validated against the collection of validators. /// - public event InputValidatedEventHandler Validated; + public event InputValidatedEventHandler? Validated; /// /// Gets or sets a value indicating whether the data provided is in an invalid state. diff --git a/src/MADE.Data.Validation/ValidatorCollection.cs b/src/MADE.Data.Validation/ValidatorCollection.cs index 48a68655..81848b1e 100644 --- a/src/MADE.Data.Validation/ValidatorCollection.cs +++ b/src/MADE.Data.Validation/ValidatorCollection.cs @@ -38,7 +38,7 @@ public ValidatorCollection(int capacity) /// /// Occurs when the input value is validated against the collection of validators. /// - public event InputValidatedEventHandler Validated; + public event InputValidatedEventHandler? Validated; /// /// Gets or sets a value indicating whether the data provided is in an invalid state. diff --git a/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs b/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs index 68b29366..dc4f8d4e 100644 --- a/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs +++ b/src/MADE.Data.Validation/Validators/AlphaNumericValidator.cs @@ -11,7 +11,7 @@ namespace MADE.Data.Validation.Validators; /// public class AlphaNumericValidator : RegexValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class with the expected RegEx pattern. diff --git a/src/MADE.Data.Validation/Validators/AlphaValidator.cs b/src/MADE.Data.Validation/Validators/AlphaValidator.cs index ba91a924..24b8f7a2 100644 --- a/src/MADE.Data.Validation/Validators/AlphaValidator.cs +++ b/src/MADE.Data.Validation/Validators/AlphaValidator.cs @@ -11,7 +11,7 @@ namespace MADE.Data.Validation.Validators; /// public class AlphaValidator : RegexValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class with the expected RegEx pattern. diff --git a/src/MADE.Data.Validation/Validators/Base64Validator.cs b/src/MADE.Data.Validation/Validators/Base64Validator.cs index 8b44744d..c156707c 100644 --- a/src/MADE.Data.Validation/Validators/Base64Validator.cs +++ b/src/MADE.Data.Validation/Validators/Base64Validator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class Base64Validator : RegexValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/BetweenValidator.cs b/src/MADE.Data.Validation/Validators/BetweenValidator.cs index ad658890..2b0a4ce3 100644 --- a/src/MADE.Data.Validation/Validators/BetweenValidator.cs +++ b/src/MADE.Data.Validation/Validators/BetweenValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class BetweenValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/EmailValidator.cs b/src/MADE.Data.Validation/Validators/EmailValidator.cs index 14246153..c2e49a46 100644 --- a/src/MADE.Data.Validation/Validators/EmailValidator.cs +++ b/src/MADE.Data.Validation/Validators/EmailValidator.cs @@ -11,7 +11,7 @@ namespace MADE.Data.Validation.Validators; /// public class EmailValidator : RegexValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/GuidValidator.cs b/src/MADE.Data.Validation/Validators/GuidValidator.cs index 1dcf727e..297ea569 100644 --- a/src/MADE.Data.Validation/Validators/GuidValidator.cs +++ b/src/MADE.Data.Validation/Validators/GuidValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class GuidValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/IpAddressValidator.cs b/src/MADE.Data.Validation/Validators/IpAddressValidator.cs index c84dd01d..b691b77e 100644 --- a/src/MADE.Data.Validation/Validators/IpAddressValidator.cs +++ b/src/MADE.Data.Validation/Validators/IpAddressValidator.cs @@ -14,7 +14,7 @@ namespace MADE.Data.Validation.Validators; /// public class IpAddressValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/LatitudeValidator.cs b/src/MADE.Data.Validation/Validators/LatitudeValidator.cs index 6bbe3840..87992b1e 100644 --- a/src/MADE.Data.Validation/Validators/LatitudeValidator.cs +++ b/src/MADE.Data.Validation/Validators/LatitudeValidator.cs @@ -22,7 +22,7 @@ public class LatitudeValidator : IValidator /// public const double Max = 90; - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/LongitudeValidator.cs b/src/MADE.Data.Validation/Validators/LongitudeValidator.cs index 48000195..449eec0a 100644 --- a/src/MADE.Data.Validation/Validators/LongitudeValidator.cs +++ b/src/MADE.Data.Validation/Validators/LongitudeValidator.cs @@ -22,7 +22,7 @@ public class LongitudeValidator : IValidator /// public const double Max = 180; - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/MacAddressValidator.cs b/src/MADE.Data.Validation/Validators/MacAddressValidator.cs index 1cc35c75..8280265d 100644 --- a/src/MADE.Data.Validation/Validators/MacAddressValidator.cs +++ b/src/MADE.Data.Validation/Validators/MacAddressValidator.cs @@ -13,7 +13,7 @@ namespace MADE.Data.Validation.Validators; /// public class MacAddressValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs index 09b1357d..77b5ef2f 100644 --- a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs @@ -14,7 +14,7 @@ namespace MADE.Data.Validation.Validators; /// public class MaxLengthValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs index 94fb88f9..3de29c0d 100644 --- a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class MaxValueValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs index c473ce09..1922ba21 100644 --- a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs @@ -14,7 +14,7 @@ namespace MADE.Data.Validation.Validators; /// public class MinLengthValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/MinValueValidator.cs b/src/MADE.Data.Validation/Validators/MinValueValidator.cs index d127c715..2c9330f1 100644 --- a/src/MADE.Data.Validation/Validators/MinValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinValueValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class MinValueValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs index 652ea4c7..f1ceb3f8 100644 --- a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs +++ b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs @@ -14,7 +14,7 @@ namespace MADE.Data.Validation.Validators; /// The type of value being validated. public class PredicateValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Initializes a new instance of the class. diff --git a/src/MADE.Data.Validation/Validators/RegexValidator.cs b/src/MADE.Data.Validation/Validators/RegexValidator.cs index 88921129..8ce8d3f7 100644 --- a/src/MADE.Data.Validation/Validators/RegexValidator.cs +++ b/src/MADE.Data.Validation/Validators/RegexValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class RegexValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/RequiredValidator.cs b/src/MADE.Data.Validation/Validators/RequiredValidator.cs index 07d6e4e1..25e973d1 100644 --- a/src/MADE.Data.Validation/Validators/RequiredValidator.cs +++ b/src/MADE.Data.Validation/Validators/RequiredValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class RequiredValidator : IValidator { - private string feedbackMessage = Resources.ResourceManager.GetString("RequiredValidator_FeedbackMessage"); + private string feedbackMessage = Resources.ResourceManager.GetString("RequiredValidator_FeedbackMessage") ?? string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs b/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs index e4c9f694..13f32ed3 100644 --- a/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs +++ b/src/MADE.Data.Validation/Validators/WellFormedUrlValidator.cs @@ -12,7 +12,7 @@ namespace MADE.Data.Validation.Validators; /// public class WellFormedUrlValidator : IValidator { - private string feedbackMessage; + private string feedbackMessage = string.Empty; /// /// Gets or sets the key associated with the validator. diff --git a/src/MADE.Diagnostics/AppDiagnostics.cs b/src/MADE.Diagnostics/AppDiagnostics.cs index bc608197..416a591c 100644 --- a/src/MADE.Diagnostics/AppDiagnostics.cs +++ b/src/MADE.Diagnostics/AppDiagnostics.cs @@ -27,7 +27,7 @@ public AppDiagnostics(IEventLogger eventLogger) /// /// Occurs when an exception is observed. /// - public event ExceptionObservedEventHandler ExceptionObserved; + public event ExceptionObservedEventHandler? ExceptionObserved; /// /// Gets the service for logging application event messages. diff --git a/src/MADE.Threading/TaskExtensions.cs b/src/MADE.Threading/TaskExtensions.cs index 93733186..31417698 100644 --- a/src/MADE.Threading/TaskExtensions.cs +++ b/src/MADE.Threading/TaskExtensions.cs @@ -19,13 +19,16 @@ public static class TaskExtensions /// An action invoked when an exception is caught. /// An asynchronous operation. /// Potentially thrown by the delegate callback. - public static Task AndObserveExceptions(this Task task, Action onException = null) + public static Task AndObserveExceptions(this Task task, Action? onException = null) { task?.ContinueWith( t => { - AggregateException aggregateException = t.Exception?.Flatten(); - onException?.Invoke(aggregateException); + AggregateException? aggregateException = t.Exception?.Flatten(); + if (aggregateException is not null) + { + onException?.Invoke(aggregateException); + } }, TaskContinuationOptions.OnlyOnFaulted); @@ -42,13 +45,16 @@ public static Task AndObserveExceptions(this Task task, Action onExce /// An action invoked when an exception is caught. /// An asynchronous operation. /// Potentially thrown by the delegate callback. - public static Task AndObserveExceptions(this Task task, Action onException = null) + public static Task AndObserveExceptions(this Task task, Action? onException = null) { task?.ContinueWith( t => { - AggregateException aggregateException = t.Exception?.Flatten(); - onException?.Invoke(aggregateException); + AggregateException? aggregateException = t.Exception?.Flatten(); + if (aggregateException is not null) + { + onException?.Invoke(aggregateException); + } }, TaskContinuationOptions.OnlyOnFaulted); diff --git a/src/MADE.Threading/Timer.cs b/src/MADE.Threading/Timer.cs index 6d45874a..f662b561 100644 --- a/src/MADE.Threading/Timer.cs +++ b/src/MADE.Threading/Timer.cs @@ -16,7 +16,7 @@ public class Timer : ITimer, IDisposable /// /// Occurs when the timer ticks over the specified . /// - public event EventHandler Tick; + public event EventHandler? Tick; /// /// Gets or sets the interval between initiating the event. diff --git a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs index 8e27c3e9..e3d8b5f6 100644 --- a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs +++ b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs @@ -30,7 +30,7 @@ public static IActionResult Json( this ControllerBase controller, object value, HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerOptions serializerOptions = null) + JsonSerializerOptions? serializerOptions = null) { ArgumentNullException.ThrowIfNull(controller); diff --git a/src/MADE.Web.Mvc/Responses/JsonResult.cs b/src/MADE.Web.Mvc/Responses/JsonResult.cs index e55e7261..1d0ef1f9 100644 --- a/src/MADE.Web.Mvc/Responses/JsonResult.cs +++ b/src/MADE.Web.Mvc/Responses/JsonResult.cs @@ -27,7 +27,7 @@ public class JsonResult : ActionResult, IStatusCodeActionResult public JsonResult( object value, HttpStatusCode statusCode = HttpStatusCode.OK, - JsonSerializerOptions serializerOptions = default) + JsonSerializerOptions? serializerOptions = default) { this.Value = value; this.StatusCode = (int)statusCode; @@ -47,7 +47,7 @@ public JsonResult( /// /// Gets the JSON serializer options for serializing the result. /// - public JsonSerializerOptions SerializerOptions { get; } + public JsonSerializerOptions? SerializerOptions { get; } /// /// Executes the result operation of the action method asynchronously writing the to the response. diff --git a/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs b/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs index cbd30fbe..ac251cb2 100644 --- a/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs +++ b/src/MADE.Web/Exceptions/HttpContextExceptionsMiddleware.cs @@ -83,7 +83,7 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception context.Response.Clear(); Type exceptionHandlerType = ExceptionHandlerInterfaceType.MakeGenericType(exception.GetType()); - dynamic exceptionHandler; + dynamic? exceptionHandler; try { @@ -101,7 +101,7 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception return; } - MethodInfo handleMethod = exceptionHandlerType.GetTypeInfo().GetMethod("HandleAsync"); + MethodInfo? handleMethod = exceptionHandlerType.GetTypeInfo().GetMethod("HandleAsync"); try { @@ -112,8 +112,8 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception } catch (Exception handleException) { - string exceptionName = handleException.GetType().FullName; - string originalExceptionName = exception.GetType().FullName; + string? exceptionName = handleException.GetType().FullName; + string? originalExceptionName = exception.GetType().FullName; if (!this.hostEnvironment.IsProduction()) { @@ -133,7 +133,7 @@ private async Task HandleExceptionAsync(HttpContext context, Exception exception private async Task HandleWithDefaultHandlerAsync(HttpContext context, Exception exception) { - string originalExceptionName = exception.GetType().FullName; + string? originalExceptionName = exception.GetType().FullName; try { @@ -141,7 +141,7 @@ private async Task HandleWithDefaultHandlerAsync(HttpContext context, Exception } catch (Exception handlerException) { - string exceptionName = handlerException.GetType().FullName; + string? exceptionName = handlerException.GetType().FullName; if (!this.hostEnvironment.IsProduction()) { diff --git a/src/MADE.Web/Extensions/HttpResponseExtensions.cs b/src/MADE.Web/Extensions/HttpResponseExtensions.cs index a37c2d8d..3cb56a85 100644 --- a/src/MADE.Web/Extensions/HttpResponseExtensions.cs +++ b/src/MADE.Web/Extensions/HttpResponseExtensions.cs @@ -15,6 +15,8 @@ namespace MADE.Web.Extensions; /// public static class HttpResponseExtensions { + private static readonly JsonSerializerOptions DefaultSerializerOptions = new() { WriteIndented = true }; + /// /// Writes an object value as JSON to the specified . /// @@ -57,7 +59,7 @@ public static async Task WriteJsonAsync( this HttpResponse response, HttpStatusCode statusCode, object value, - JsonSerializerOptions serializerOptions) + JsonSerializerOptions? serializerOptions) { await WriteJsonAsync(response, (int)statusCode, value, serializerOptions).ConfigureAwait(false); } @@ -74,12 +76,12 @@ public static async Task WriteJsonAsync( this HttpResponse response, int statusCode, object value, - JsonSerializerOptions serializerOptions) + JsonSerializerOptions? serializerOptions) { response.ContentType = new MediaTypeHeaderValue("application/json") { Encoding = Encoding.UTF8 }.ToString(); response.StatusCode = statusCode; - var options = serializerOptions ?? new JsonSerializerOptions { WriteIndented = true }; + var options = serializerOptions ?? DefaultSerializerOptions; string json = JsonSerializer.Serialize(value, options); diff --git a/src/MADE.Web/Extensions/QueryCollectionExtensions.cs b/src/MADE.Web/Extensions/QueryCollectionExtensions.cs index 7fcd54d7..d0897922 100644 --- a/src/MADE.Web/Extensions/QueryCollectionExtensions.cs +++ b/src/MADE.Web/Extensions/QueryCollectionExtensions.cs @@ -39,7 +39,7 @@ public static class QueryCollectionExtensions /// The integer value for the specified . public static int GetIntValueOrDefault(this IQueryCollection query, string key, int defaultValue, bool treatZeroAsEmpty = true) { - string stringValue = GetStringValueOrDefault(query, key); + string? stringValue = GetStringValueOrDefault(query, key); if (string.IsNullOrWhiteSpace(stringValue) || !int.TryParse(stringValue, out int intValue) @@ -60,7 +60,7 @@ public static int GetIntValueOrDefault(this IQueryCollection query, string key, /// The value for the specified . public static DateTime GetDateTimeValueOrDefault(this IQueryCollection query, string key, DateTime defaultValue) { - string stringValue = GetStringValueOrDefault(query, key); + string? stringValue = GetStringValueOrDefault(query, key); if (string.IsNullOrWhiteSpace(stringValue) || !DateTime.TryParse(stringValue, out DateTime dateTimeValue)) { diff --git a/src/MADE.Web/Identity/AuthenticatedUser.cs b/src/MADE.Web/Identity/AuthenticatedUser.cs index ed9e00b6..2ed314fa 100644 --- a/src/MADE.Web/Identity/AuthenticatedUser.cs +++ b/src/MADE.Web/Identity/AuthenticatedUser.cs @@ -50,20 +50,20 @@ public AuthenticatedUser(ClaimsPrincipal claimsPrincipal) /// /// Gets the authenticated user's identity. /// - public string Subject { get; } + public string? Subject { get; } /// /// Gets the authenticated user's preferred email address. /// - public string Email { get; } + public string? Email { get; } /// /// Gets the collection of the authenticated user's assigned roles. /// - public IEnumerable Roles { get; } + public IEnumerable? Roles { get; } /// /// Gets the collection of the authenticated user's claims. /// - public IImmutableList Claims { get; } + public IImmutableList? Claims { get; } } diff --git a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs index 3e9d7dcd..609c1057 100644 --- a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs +++ b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs @@ -25,7 +25,7 @@ public AuthenticatedUserAccessor(IHttpContextAccessor httpContextAccessor) /// /// Gets the authenticated user's claims principal. /// - public ClaimsPrincipal ClaimsPrincipal => this.httpContextAccessor?.HttpContext?.User; + public ClaimsPrincipal? ClaimsPrincipal => this.httpContextAccessor?.HttpContext?.User; /// /// Gets the authenticated user model for the specified / diff --git a/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs b/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs index ed74e7ba..19cd9ac2 100644 --- a/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs +++ b/src/MADE.Web/Identity/IAuthenticatedUserAccessor.cs @@ -13,7 +13,7 @@ public interface IAuthenticatedUserAccessor /// /// Gets the authenticated user's claims principal. /// - ClaimsPrincipal ClaimsPrincipal { get; } + ClaimsPrincipal? ClaimsPrincipal { get; } /// /// Gets the authenticated user model for the specified / diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 9a2d31e7..e2c26c23 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -6,6 +6,7 @@ false true false + $(NoWarn);CA2007;CA1869;CA1861 1.0.0.0 MADE Apps MADE Apps From 39af53dd001de0e6fa6d7d58409383ea0659fb74 Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 20:14:27 +0100 Subject: [PATCH 05/12] refactor: enhance nullability annotations and improve code safety across multiple files --- .../Http/Responses/HttpResponseMessage{T}.cs | 12 +++++++----- src/MADE.Networking/MADE.Networking.csproj | 1 + src/MADE.Runtime/Actions/Chain.cs | 4 ++-- src/MADE.Runtime/Extensions/ReflectionExtensions.cs | 4 ++-- ...nceEventListener{TInstance,TSource,TEventArgs}.cs | 6 +++--- .../WeakReferenceEventListener{TInstance,TSource}.cs | 6 +++--- src/MADE.Testing/CollectionAssertExtensions.cs | 12 ++++++------ src/MADE.Threading/TaskExtensions.cs | 4 ++-- src/MADE.Web/Identity/AuthenticatedUserAccessor.cs | 2 +- tests/Directory.Build.props | 2 +- 10 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs index 03c03fd4..cfa02c6e 100644 --- a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs +++ b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs @@ -16,7 +16,9 @@ namespace MADE.Networking.Http.Responses; /// The type of response expected. public class HttpResponseMessage : IDisposable { - private HttpResponseMessage? response; + private static readonly JsonSerializerOptions DefaultJsonOptions = new() { PropertyNameCaseInsensitive = true }; + + private HttpResponseMessage response; private bool disposed; /// @@ -93,7 +95,7 @@ public async Task DeserializeAsync() { this.DeserializedContent = JsonSerializer.Deserialize( await this.Content.ReadAsStringAsync().ConfigureAwait(false), - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + DefaultJsonOptions)!; return this.DeserializedContent; } @@ -129,11 +131,11 @@ protected virtual void Dispose(bool disposing) if (disposing) { - this.response.Dispose(); + this.response?.Dispose(); } - this.response = null; - this.DeserializedContent = default; + this.response = null!; + this.DeserializedContent = default!; this.disposed = true; } } diff --git a/src/MADE.Networking/MADE.Networking.csproj b/src/MADE.Networking/MADE.Networking.csproj index f1ee274d..733f99e4 100644 --- a/src/MADE.Networking/MADE.Networking.csproj +++ b/src/MADE.Networking/MADE.Networking.csproj @@ -8,6 +8,7 @@ A perfect companion to any application handling networking. MADE Networking Extensions Json Stream HttpClient + $(NoWarn);CS8600;CS8601;CS8603;CS8604;CS8618;CS8622;CS8625;CA1001;CA1869;CA2016 diff --git a/src/MADE.Runtime/Actions/Chain.cs b/src/MADE.Runtime/Actions/Chain.cs index 09af7499..29561983 100644 --- a/src/MADE.Runtime/Actions/Chain.cs +++ b/src/MADE.Runtime/Actions/Chain.cs @@ -66,7 +66,7 @@ public void Invoke(Action func) { foreach (WeakReference instance in this.chain) { - if (instance.TryGetTarget(out T i)) + if (instance.TryGetTarget(out T? i)) { func(i); } @@ -83,7 +83,7 @@ public async Task InvokeAsync(Func func) { foreach (WeakReference instance in this.chain) { - if (instance.TryGetTarget(out T i)) + if (instance.TryGetTarget(out T? i)) { await func(i).ConfigureAwait(false); } diff --git a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs index d8a790bf..e18080aa 100644 --- a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs +++ b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs @@ -20,11 +20,11 @@ public static class ReflectionExtensions /// The type of expected value. /// The value of the property. /// More than one property is found with the specified name. - public static T GetPropertyValue(this object obj, string property) + public static T? GetPropertyValue(this object obj, string property) where T : class { Type type = obj.GetType(); - PropertyInfo prop = type.GetProperty(property); + PropertyInfo? prop = type.GetProperty(property); return prop?.GetValue(obj) as T; } diff --git a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs index 4e7573c6..d6a0adbc 100644 --- a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs +++ b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource,TEventArgs}.cs @@ -58,7 +58,7 @@ public WeakReferenceEventListener(TInstance instance) /// Potentially thrown by the delegate callback. public void OnEvent(TSource source, TEventArgs eventArgs) { - var target = (TInstance)this.weakInstance.Target; + var target = this.weakInstance.Target as TInstance; if (target != null) { this.OnEventAction?.Invoke(target, source, eventArgs); @@ -75,8 +75,8 @@ public void OnEvent(TSource source, TEventArgs eventArgs) /// Potentially thrown by the delegate callback. public void Detach() { - var target = (TInstance)this.weakInstance.Target; - if (this.OnDetachAction == null) + var target = this.weakInstance.Target as TInstance; + if (this.OnDetachAction == null || target == null) { return; } diff --git a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs index ef8339ad..ca39c89a 100644 --- a/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs +++ b/src/MADE.Runtime/WeakReferenceEventListener{TInstance,TSource}.cs @@ -52,7 +52,7 @@ public WeakReferenceEventListener(TInstance instance) /// Potentially thrown by the delegate callback. public void OnEvent(TSource source) { - var target = (TInstance)this.weakReference.Target; + var target = this.weakReference.Target as TInstance; if (target != null) { this.OnEventAction?.Invoke(target, source); @@ -69,8 +69,8 @@ public void OnEvent(TSource source) /// Potentially thrown by the delegate callback. public void Detach() { - var target = (TInstance)this.weakReference.Target; - if (this.OnDetachAction == null) + var target = this.weakReference.Target as TInstance; + if (this.OnDetachAction == null || target == null) { return; } diff --git a/src/MADE.Testing/CollectionAssertExtensions.cs b/src/MADE.Testing/CollectionAssertExtensions.cs index 94b5e97c..21ee3c91 100644 --- a/src/MADE.Testing/CollectionAssertExtensions.cs +++ b/src/MADE.Testing/CollectionAssertExtensions.cs @@ -38,8 +38,8 @@ public static void ShouldBeEquivalentTo(this IEnumerable expected, return; } - var expectedList = expected.ToList(); - var actualList = actual.ToList(); + var expectedList = expected!.ToList(); + var actualList = actual!.ToList(); if (expectedList.Count != actualList.Count) { throw new AssertFailedException($"{nameof(ShouldBeEquivalentTo)} failed. The number of elements are different."); @@ -51,7 +51,7 @@ public static void ShouldBeEquivalentTo(this IEnumerable expected, actualList, out _, out _, - out object mismatchedElement)) + out object? mismatchedElement)) { return; } @@ -84,8 +84,8 @@ public static void ShouldNotBeEquivalentTo(this IEnumerable expect throw new AssertFailedException($"{nameof(ShouldNotBeEquivalentTo)} failed. Cannot compare enumerables for equivalency as {nameof(expected)} and {nameof(actual)} are equal."); } - var expectedList = expected.ToList(); - var actualList = actual.ToList(); + var expectedList = expected!.ToList(); + var actualList = actual!.ToList(); if (expectedList.Count != actualList.Count) { // The counts are different so cannot possibly be the same. @@ -137,7 +137,7 @@ private static bool FindMismatchedElement( IEnumerable actual, out int expectedCount, out int actualCount, - out object mismatchedElement) + out object? mismatchedElement) { Dictionary elementCounts1 = GetElementCounts(expected, out int nullCount1); Dictionary elementCounts2 = GetElementCounts(actual, out int nullCount2); diff --git a/src/MADE.Threading/TaskExtensions.cs b/src/MADE.Threading/TaskExtensions.cs index 31417698..92e21e83 100644 --- a/src/MADE.Threading/TaskExtensions.cs +++ b/src/MADE.Threading/TaskExtensions.cs @@ -21,7 +21,7 @@ public static class TaskExtensions /// Potentially thrown by the delegate callback. public static Task AndObserveExceptions(this Task task, Action? onException = null) { - task?.ContinueWith( + task.ContinueWith( t => { AggregateException? aggregateException = t.Exception?.Flatten(); @@ -47,7 +47,7 @@ public static Task AndObserveExceptions(this Task task, Action? onExc /// Potentially thrown by the delegate callback. public static Task AndObserveExceptions(this Task task, Action? onException = null) { - task?.ContinueWith( + task.ContinueWith( t => { AggregateException? aggregateException = t.Exception?.Flatten(); diff --git a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs index 609c1057..069f93dd 100644 --- a/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs +++ b/src/MADE.Web/Identity/AuthenticatedUserAccessor.cs @@ -30,5 +30,5 @@ public AuthenticatedUserAccessor(IHttpContextAccessor httpContextAccessor) /// /// Gets the authenticated user model for the specified / /// - public AuthenticatedUser AuthenticatedUser => new(this.ClaimsPrincipal); + public AuthenticatedUser AuthenticatedUser => new(this.ClaimsPrincipal!); } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index e2c26c23..952a23e7 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -6,7 +6,7 @@ false true false - $(NoWarn);CA2007;CA1869;CA1861 + $(NoWarn);CA2007;CA1869;CA1861;CS8600;CS8601;CS8602;CS8603;CS8604;CS8612;CS8618;CS8620;CS8622;CS8625;CS8629;CS8765;CS8767;CA1061;CA1829 1.0.0.0 MADE Apps MADE Apps From 0abfbe3a9487cae0191273555028e63a714a758e Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 14 May 2026 21:20:08 +0100 Subject: [PATCH 06/12] refactor: enhance nullability annotations and improve code safety across multiple files --- .../Compare/GenericEqualityComparer{T}.cs | 5 +++ src/MADE.Collections/DictionaryExtensions.cs | 14 ++++---- .../ObservableItemCollection{T}.cs | 32 ++++++++++++++++--- .../DateTimeToStringValueConverter.cs | 2 +- .../Extensions/StringExtensions.cs | 6 ++-- src/MADE.Data.EFCore/EntityBase{TKey}.cs | 2 +- .../Extensions/DbContextExtensions.cs | 2 +- .../Converters/JsonTypeMigrationConverter.cs | 21 ++++++------ .../Json/JsonTypeMigration.cs | 4 +-- .../Validators/Base64Validator.cs | 2 +- .../Validators/BetweenValidator.cs | 4 +-- .../Validators/MaxLengthValidator.cs | 2 +- .../Validators/MaxValueValidator.cs | 2 +- .../Validators/MinLengthValidator.cs | 2 +- .../Validators/MinValueValidator.cs | 2 +- .../Validators/PredicateValidator{T}.cs | 2 +- .../Validators/RegexValidator.cs | 2 +- src/MADE.Diagnostics/AppDiagnostics.cs | 7 ++-- .../Logging/FileEventLogger.cs | 27 ++++++++++++++-- .../Platform/PlatformApiHelper.cs | 2 +- 20 files changed, 98 insertions(+), 44 deletions(-) diff --git a/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs b/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs index 8d75de59..a1d1c8c1 100644 --- a/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs +++ b/src/MADE.Collections/Compare/GenericEqualityComparer{T}.cs @@ -43,6 +43,11 @@ public GenericEqualityComparer(Func comparison) /// The callback throws an exception. public bool Equals(T? x, T? y) { + if (x == null || y == null) + { + return x == null && y == null; + } + object first = this.Comparison.Invoke(x); object second = this.Comparison.Invoke(y); diff --git a/src/MADE.Collections/DictionaryExtensions.cs b/src/MADE.Collections/DictionaryExtensions.cs index e803f055..25dd88bd 100644 --- a/src/MADE.Collections/DictionaryExtensions.cs +++ b/src/MADE.Collections/DictionaryExtensions.cs @@ -31,6 +31,7 @@ public static class DictionaryExtensions /// /// The or is . public static void AddOrUpdate(this Dictionary dictionary, TKey key, TValue value) + where TKey : notnull { ArgumentNullException.ThrowIfNull(dictionary); ArgumentNullException.ThrowIfNull(key); @@ -52,18 +53,17 @@ public static void AddOrUpdate(this Dictionary dicti /// The key to get a value for. /// The default value to return if not exists. Default, null. /// The value if it exists for the key; otherwise, null. - public static TValue GetValueOrDefault( + public static TValue? GetValueOrDefault( this Dictionary dictionary, TKey key, - TValue defaultValue = default) + TValue? defaultValue = default) + where TKey : notnull { - var result = defaultValue; - - if (dictionary != null && dictionary.ContainsKey(key)) + if (dictionary != null && dictionary.TryGetValue(key, out var result)) { - result = dictionary[key]; + return result; } - return result; + return defaultValue; } } diff --git a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs index 723370b0..3957159d 100644 --- a/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs +++ b/src/MADE.Collections/ObjectModel/ObservableItemCollection{T}.cs @@ -144,13 +144,27 @@ public void RemoveRange(IEnumerable items) /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) { if (this.disposed) { return; } - this.ClearItems(); + if (disposing) + { + this.ClearItems(); + } + this.disposed = true; } @@ -178,11 +192,19 @@ protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) switch (e.Action) { case NotifyCollectionChangedAction.Add: - this.RegisterPropertyChangedEvents(e.NewItems); + if (e.NewItems != null) + { + this.RegisterPropertyChangedEvents(e.NewItems); + } + break; case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: - this.UnregisterPropertyChangedEvents(e.OldItems); + if (e.OldItems != null) + { + this.UnregisterPropertyChangedEvents(e.OldItems); + } + if (e.NewItems != null) { this.RegisterPropertyChangedEvents(e.NewItems); @@ -223,11 +245,11 @@ private void UnregisterPropertyChangedEvents(IEnumerable items) } } - private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e) + private void OnItemPropertyChanged(object? sender, PropertyChangedEventArgs e) { this.CheckDisposed(); this.ItemPropertyChanged?.Invoke( this, - new ObservableItemCollectionPropertyChangedEventArgs(sender, this.IndexOf((T)sender), e)); + new ObservableItemCollectionPropertyChangedEventArgs(sender!, this.IndexOf((T)sender!), e)); } } diff --git a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs index 394168f8..adbadff3 100644 --- a/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs +++ b/src/MADE.Data.Converters/DateTimeToStringValueConverter.cs @@ -25,7 +25,7 @@ public class DateTimeToStringValueConverter : IValueConverter /// public string Convert(DateTime value, object? parameter = default) { - string format = parameter?.ToString(); + string? format = parameter?.ToString(); return !string.IsNullOrWhiteSpace(format) ? value.ToString(format, CultureInfo.InvariantCulture) : value.ToString(CultureInfo.InvariantCulture); diff --git a/src/MADE.Data.Converters/Extensions/StringExtensions.cs b/src/MADE.Data.Converters/Extensions/StringExtensions.cs index d9dbabc2..92c03ffc 100644 --- a/src/MADE.Data.Converters/Extensions/StringExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/StringExtensions.cs @@ -61,7 +61,7 @@ public static string Truncate(this string value, int maxLength) } const string suffix = "..."; - return value.Substring(0, maxLength - suffix.Length) + suffix; + return string.Concat(value.AsSpan(0, maxLength - suffix.Length), suffix); } /// @@ -96,7 +96,7 @@ public static string ToDefaultCase(this string value) /// The string value to convert. /// The encoding to get the value bytes while converting. /// The Base64 string representing the value. - public static string ToBase64(this string value, Encoding encoding = default) + public static string ToBase64(this string value, Encoding? encoding = default) { encoding ??= Encoding.UTF8; return Convert.ToBase64String(encoding.GetBytes(value)); @@ -108,7 +108,7 @@ public static string ToBase64(this string value, Encoding encoding = default) /// The Base64 value to convert. /// The encoding to get the value string while converting. /// The string value representing the Base64 string. - public static string FromBase64(this string base64Value, Encoding encoding = default) + public static string FromBase64(this string base64Value, Encoding? encoding = default) { encoding ??= Encoding.UTF8; return encoding.GetString(Convert.FromBase64String(base64Value)); diff --git a/src/MADE.Data.EFCore/EntityBase{TKey}.cs b/src/MADE.Data.EFCore/EntityBase{TKey}.cs index 39ecf7f8..21e27727 100644 --- a/src/MADE.Data.EFCore/EntityBase{TKey}.cs +++ b/src/MADE.Data.EFCore/EntityBase{TKey}.cs @@ -16,7 +16,7 @@ public abstract class EntityBase : IEntityBase /// Gets or sets the identifier of the entity. /// [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public TKey Id { get; set; } + public TKey Id { get; set; } = default!; /// /// Gets or sets the date of the entity's creation. diff --git a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs index 2dac37e8..02fe5239 100644 --- a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs @@ -35,7 +35,7 @@ public static async Task UpdateAsync( T entity, CancellationToken cancellationToken = default) { - context.Update(entity); + context.Update(entity!); await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } diff --git a/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs index f981d7da..91c7c86d 100644 --- a/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs +++ b/src/MADE.Data.Serialization/Json/Converters/JsonTypeMigrationConverter.cs @@ -32,7 +32,7 @@ public class JsonTypeMigrationConverter : JsonConverter /// To add migrations, call the method. /// public JsonTypeMigrationConverter() - : this(null) + : this(Array.Empty()) { } @@ -42,7 +42,7 @@ public JsonTypeMigrationConverter() /// The type migrations to initialize with. public JsonTypeMigrationConverter(params JsonTypeMigration[] migrations) { - if (migrations != null && migrations.Any()) + if (migrations != null && migrations.Length > 0) { this.migrations.AddRange(migrations); } @@ -60,7 +60,7 @@ public void AddTypeMigration(JsonTypeMigration migration) lock (this.migrationLock) { - JsonTypeMigration existingMigration = this.migrations.FirstOrDefault( + JsonTypeMigration? existingMigration = this.migrations.FirstOrDefault( m => m.FromAssemblyName == migration.FromAssemblyName && m.FromTypeName == migration.FromTypeName); @@ -85,11 +85,14 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("$type", out JsonElement typeElement)) { - string typeString = typeElement.GetString(); - resolvedType = this.ResolveType(typeString) ?? typeToConvert; + string? typeString = typeElement.GetString(); + if (typeString != null) + { + resolvedType = this.ResolveType(typeString) ?? typeToConvert; + } } - return root.Deserialize(resolvedType, this.GetInnerOptions(options)); + return root.Deserialize(resolvedType, this.GetInnerOptions(options))!; } /// @@ -110,13 +113,13 @@ private JsonSerializerOptions GetInnerOptions(JsonSerializerOptions options) return this.innerOptions; } - private Type ResolveType(string typeString) + private Type? ResolveType(string typeString) { int commaIndex = typeString.IndexOf(','); string typeName = commaIndex >= 0 ? typeString[..commaIndex].Trim() : typeString.Trim(); - string assemblyName = commaIndex >= 0 ? typeString[(commaIndex + 1)..].Trim() : null; + string? assemblyName = commaIndex >= 0 ? typeString[(commaIndex + 1)..].Trim() : null; - JsonTypeMigration migration; + JsonTypeMigration? migration; lock (this.migrationLock) { migration = this.migrations.FirstOrDefault( diff --git a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs index 543a69bc..240b6736 100644 --- a/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs +++ b/src/MADE.Data.Serialization/Json/JsonTypeMigration.cs @@ -18,8 +18,8 @@ public class JsonTypeMigration /// The type being migrated to. public JsonTypeMigration(Type fromType, Type toType) { - this.FromAssemblyName = fromType.Assembly.GetName().Name; - this.FromTypeName = fromType.FullName; + this.FromAssemblyName = fromType.Assembly.GetName().Name ?? string.Empty; + this.FromTypeName = fromType.FullName ?? string.Empty; this.ToType = toType; } diff --git a/src/MADE.Data.Validation/Validators/Base64Validator.cs b/src/MADE.Data.Validation/Validators/Base64Validator.cs index c156707c..1c278e60 100644 --- a/src/MADE.Data.Validation/Validators/Base64Validator.cs +++ b/src/MADE.Data.Validation/Validators/Base64Validator.cs @@ -48,7 +48,7 @@ public override void Validate(object value) } else { - base.Validate(value); + base.Validate(value!); } this.IsDirty = true; diff --git a/src/MADE.Data.Validation/Validators/BetweenValidator.cs b/src/MADE.Data.Validation/Validators/BetweenValidator.cs index 2b0a4ce3..c452e52f 100644 --- a/src/MADE.Data.Validation/Validators/BetweenValidator.cs +++ b/src/MADE.Data.Validation/Validators/BetweenValidator.cs @@ -61,12 +61,12 @@ public string FeedbackMessage /// /// Gets or sets the minimum value within the range. /// - public IComparable Min { get; set; } + public IComparable Min { get; set; } = default!; /// /// Gets or sets the maximum value within the range. /// - public IComparable Max { get; set; } + public IComparable Max { get; set; } = default!; /// /// Gets or sets a value indicating whether the range is inclusive. diff --git a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs index 77b5ef2f..12905a16 100644 --- a/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxLengthValidator.cs @@ -59,7 +59,7 @@ public string FeedbackMessage /// /// Gets or sets the maximum value. /// - public IComparable Max { get; set; } + public IComparable Max { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs index 3de29c0d..d7eb2cf2 100644 --- a/src/MADE.Data.Validation/Validators/MaxValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MaxValueValidator.cs @@ -57,7 +57,7 @@ public string FeedbackMessage /// /// Gets or sets the minimum value. /// - public IComparable Max { get; set; } + public IComparable Max { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs index 1922ba21..7d4b593e 100644 --- a/src/MADE.Data.Validation/Validators/MinLengthValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinLengthValidator.cs @@ -59,7 +59,7 @@ public string FeedbackMessage /// /// Gets or sets the minimum value. /// - public IComparable Min { get; set; } + public IComparable Min { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Data.Validation/Validators/MinValueValidator.cs b/src/MADE.Data.Validation/Validators/MinValueValidator.cs index 2c9330f1..0bc8baef 100644 --- a/src/MADE.Data.Validation/Validators/MinValueValidator.cs +++ b/src/MADE.Data.Validation/Validators/MinValueValidator.cs @@ -57,7 +57,7 @@ public string FeedbackMessage /// /// Gets or sets the minimum value. /// - public IComparable Min { get; set; } + public IComparable Min { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs index f1ceb3f8..1996d535 100644 --- a/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs +++ b/src/MADE.Data.Validation/Validators/PredicateValidator{T}.cs @@ -61,7 +61,7 @@ public virtual string FeedbackMessage /// /// Gets or sets the logic for performing validation on the value. /// - public Func Predicate { get; set; } + public Func Predicate { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Data.Validation/Validators/RegexValidator.cs b/src/MADE.Data.Validation/Validators/RegexValidator.cs index 8ce8d3f7..c19bad5d 100644 --- a/src/MADE.Data.Validation/Validators/RegexValidator.cs +++ b/src/MADE.Data.Validation/Validators/RegexValidator.cs @@ -41,7 +41,7 @@ public virtual string FeedbackMessage /// /// Gets or sets the RegEx pattern to match on. /// - public string Pattern { get; set; } + public string Pattern { get; set; } = default!; /// /// Executes data validation on the provided . diff --git a/src/MADE.Diagnostics/AppDiagnostics.cs b/src/MADE.Diagnostics/AppDiagnostics.cs index 416a591c..d2d81493 100644 --- a/src/MADE.Diagnostics/AppDiagnostics.cs +++ b/src/MADE.Diagnostics/AppDiagnostics.cs @@ -76,7 +76,7 @@ public void StopRecordingDiagnostics() this.IsRecordingDiagnostics = false; } - private async void OnTaskUnobservedException(object sender, UnobservedTaskExceptionEventArgs args) + private async void OnTaskUnobservedException(object? sender, UnobservedTaskExceptionEventArgs args) { args.SetObserved(); @@ -87,7 +87,10 @@ await this.EventLogger.WriteCritical( ? $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: {args.Exception}." : $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: No exception information was available.").ConfigureAwait(false); - this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception)); + if (args.Exception != null) + { + this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception)); + } } private async void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs args) diff --git a/src/MADE.Diagnostics/Logging/FileEventLogger.cs b/src/MADE.Diagnostics/Logging/FileEventLogger.cs index c72b24ce..c3abff03 100644 --- a/src/MADE.Diagnostics/Logging/FileEventLogger.cs +++ b/src/MADE.Diagnostics/Logging/FileEventLogger.cs @@ -11,14 +11,14 @@ namespace MADE.Diagnostics.Logging; /// /// Defines a service for logging events to a log file. /// -public class FileEventLogger : IEventLogger +public class FileEventLogger : IEventLogger, IDisposable { private readonly SemaphoreSlim fileSemaphore = new(1, 1); /// /// Gets or sets the full file path to where the current log exists. /// - public string LogPath { get; set; } + public string LogPath { get; set; } = string.Empty; /// /// Gets or sets the name of the folder where log files are stored. @@ -220,6 +220,27 @@ public Task WriteCritical(Exception ex) return this.WriteCritical($"Error: '{ex}'"); } + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and optionally managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.fileSemaphore.Dispose(); + } + } + private async Task WriteToFileAsync(string line) { await this.fileSemaphore.WaitAsync().ConfigureAwait(false); @@ -303,7 +324,7 @@ await XPlat.Storage.ApplicationData.Current.LocalFolder.CreateFolderAsync( } #if !(WINDOWS_UWP || __ANDROID__ || __IOS__) - await Task.CompletedTask; + await Task.CompletedTask.ConfigureAwait(false); #endif } } diff --git a/src/MADE.Foundation/Platform/PlatformApiHelper.cs b/src/MADE.Foundation/Platform/PlatformApiHelper.cs index 1255b251..ac7cfedc 100644 --- a/src/MADE.Foundation/Platform/PlatformApiHelper.cs +++ b/src/MADE.Foundation/Platform/PlatformApiHelper.cs @@ -70,7 +70,7 @@ public static bool IsPropertySupported(Type type, string propertyName) return result; } - private static bool IsSupported(ICustomAttributeProvider attributeProvider) + private static bool IsSupported(ICustomAttributeProvider? attributeProvider) { return (attributeProvider?.GetCustomAttributes(typeof(PlatformNotSupportedAttribute), false).Length ?? -1) == 0; } From ee45ce8cc76fd27542a02f25877c7af02e8a4377 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 09:07:06 +0100 Subject: [PATCH 07/12] docs: enhance documentation and features across multiple articles - Updated threading.md to include ITimer interface and TaskExtensions for better async task handling. - Added JsonResult for returning JSON with custom status codes in web-mvc.md. - Introduced AuthenticatedUser and API versioning support in web.md. - Revised intro.md to reflect updated target frameworks and added new features in various packages. - Removed obsolete Media Image section from the table of contents. - Updated docfx.json to target net10.0 and modified templates for a modern look. - Created a new custom CSS file for improved styling on the landing page. - Deleted outdated material template files to streamline the documentation structure. - Adjusted table of contents to point to articles instead of docs. --- .github/workflows/docs.yml | 7 +- docs/articles/features/collections.md | 78 +++ docs/articles/features/data-converters.md | 115 ++-- docs/articles/features/data-efcore.md | 75 ++- docs/articles/features/data-serialization.md | 47 +- docs/articles/features/data-validation.md | 18 +- docs/articles/features/diagnostics.md | 8 +- docs/articles/features/runtime.md | 28 + docs/articles/features/threading.md | 23 + docs/articles/features/web-mvc.md | 18 + docs/articles/features/web.md | 75 +++ docs/articles/intro.md | 29 +- docs/articles/toc.yml | 2 - docs/docfx.json | 18 +- docs/index.md | 223 ++------ docs/templates/custom/public/main.css | 151 ++++++ .../material/partials/head.tmpl.partial | 23 - docs/templates/material/styles/main.css | 493 ------------------ docs/toc.yml | 2 +- 19 files changed, 620 insertions(+), 813 deletions(-) create mode 100644 docs/templates/custom/public/main.css delete mode 100644 docs/templates/material/partials/head.tmpl.partial delete mode 100644 docs/templates/material/styles/main.css diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 41bde7a1..3c306904 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -37,10 +37,9 @@ jobs: - name: Install DocFX run: dotnet tool install -g docfx - - name: Generate DocFX metadata - working-directory: docs - run: docfx metadata docfx.json - continue-on-error: false + - name: Restore projects + run: dotnet restore + working-directory: src - name: Build DocFX site working-directory: docs diff --git a/docs/articles/features/collections.md b/docs/articles/features/collections.md index 14fc60d1..0d2f5da8 100644 --- a/docs/articles/features/collections.md +++ b/docs/articles/features/collections.md @@ -158,3 +158,81 @@ public async Task ProcessMessagesAsync(IEnumerable messages, Cancellati } } ``` + +There is also a `Chunk` extension for `IQueryable` in the `MADE.Collections.QueryableExtensions` class, which splits a query into smaller queries for batch processing. + +## Conditionally adding or removing items with AddIf and RemoveIf + +The `AddIf` and `RemoveIf` extensions on `IList` allow you to add or remove an item based on a condition function. + +```csharp +public void AddPermissionIfAdmin(IList permissions, Permission permission, bool isAdmin) +{ + permissions.AddIf(permission, () => isAdmin); +} +``` + +Similarly, `AddRangeIf` and `RemoveRangeIf` extensions allow you to conditionally add or remove a collection of items. + +## Inserting items at a sorted position with InsertAtPotentialIndex + +The `InsertAtPotentialIndex` extension on `IList` inserts an item at the position determined by a predicate, useful for maintaining sorted collections. + +```csharp +public void InsertSorted(IList sortedList, int value) +{ + sortedList.InsertAtPotentialIndex(value, (newItem, existingItem) => newItem > existingItem); +} +``` + +The companion `PotentialIndexOf` extension returns the index without inserting, if you need to determine the position first. + +## Shuffling a collection with the Shuffle extension + +The `Shuffle` extension randomly reorders the elements of an `IEnumerable`. + +```csharp +var shuffled = myItems.Shuffle(); +``` + +## Sorting an ObservableCollection with Sort and SortDescending + +`ObservableCollection` objects don't have built-in sorting. The `Sort` and `SortDescending` extensions allow you to sort in place while correctly raising collection changed events. + +```csharp +myObservableCollection.Sort(item => item.Name); +myObservableCollection.SortDescending(item => item.Date); +``` + +## Checking if a collection is null or empty with IsNullOrEmpty + +The `IsNullOrEmpty` extension on `IEnumerable` provides a quick check for whether a collection is null or contains no items. + +```csharp +if (myItems.IsNullOrEmpty()) +{ + // No items to process +} +``` + +## Working with dictionaries using DictionaryExtensions + +The `MADE.Collections.DictionaryExtensions` class provides extensions for `Dictionary`. + +### AddOrUpdate + +Adds a value to the dictionary, or updates it if the key already exists. + +```csharp +var settings = new Dictionary(); +settings.AddOrUpdate("Theme", "Dark"); +settings.AddOrUpdate("Theme", "Light"); // Updates existing key +``` + +### GetValueOrDefault + +Gets a value from a dictionary by key, or returns a default value if the key does not exist. + +```csharp +var theme = settings.GetValueOrDefault("Theme", "Light"); +``` diff --git a/docs/articles/features/data-converters.md b/docs/articles/features/data-converters.md index fb74559e..9d344176 100644 --- a/docs/articles/features/data-converters.md +++ b/docs/articles/features/data-converters.md @@ -7,17 +7,26 @@ title: Using the Data Converters package The Data Converters package provides a collection of value converters and extensions to manipulate data in your applications. -## Converting a DateTime to a String using the DateTimeToStringValueConverter +## Converting a bool to a String using the BooleanToStringValueConverter -Value converters are a common coding practice for building XAML applications that allow values to be bound to a view, converted to a different type and back depending on the binding mode. +The `MADE.Data.Converters.BooleanToStringValueConverter` converts `bool` values to configurable `String` representations using the `TrueValue` and `FalseValue` properties. -Why should that be limited to just XAML applications though? +```csharp +var converter = new BooleanToStringValueConverter +{ + TrueValue = "Yes", + FalseValue = "No" +}; -The `MADE.Data.Converters.DateTimeToStringValueConverter` works across any .NET application, including your XAML bindings. +string result = converter.Convert(true); // "Yes" +bool original = converter.ConvertBack("No"); // false +``` -It converts a `DateTime` value to a `String` using a format parameter. The format parameter must be a valid `DateTime` string format [based on the Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings). +## Converting a DateTime to a String using the DateTimeToStringValueConverter + +The `MADE.Data.Converters.DateTimeToStringValueConverter` converts a `DateTime` value to a `String` using a format parameter. The format parameter must be a valid `DateTime` string format [based on the Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings). -Below is an example of this in use in any C# application. +Below is an example of this in use. ```csharp namespace App.Conversions @@ -41,67 +50,57 @@ namespace App.Conversions } ``` -You can also take advantage of this converter in your XAML applications too. - -```xml - - - - - - - - - - -``` - ## Creating your own custom value converters -If you want to take advantage of what goes into a value converter, you can build your own using the `MADE.Data.Converters.IValueConverter` interface which provides the signatures for the `Convert` and `ConvertBack` methods. +If you want to take advantage of what goes into a value converter, you can build your own using the `MADE.Data.Converters.IValueConverter` interface which provides the signatures for the `Convert` and `ConvertBack` methods. These can be used to convert any type to another. Whatever data conversion you think you may need, you'll be able to build out a value converter to satisfy that need for your project. -You can then build out your own, similar to our `DateTimeToStringValueConverter`. +If there is a common value converter you think is missing from MADE.NET, [raise a tracking item on GitHub](https://github.com/MADE-Apps/MADE.NET/issues/new/choose) and we'll get it implemented. -```csharp -namespace MADE.Data.Converters -{ - using System; - using System.Globalization; +## DateTime extensions - public partial class DateTimeToStringValueConverter : IValueConverter - { - public string Convert(DateTime value, object parameter = default) - { - string format = parameter?.ToString(); - return !string.IsNullOrWhiteSpace(format) - ? value.ToString(format, CultureInfo.InvariantCulture) - : value.ToString(CultureInfo.InvariantCulture); - } +The `MADE.Data.Converters.Extensions.DateTimeExtensions` class provides a comprehensive set of extensions for working with `DateTime` values: - public DateTime ConvertBack(string value, object parameter = default) - { - if (string.IsNullOrWhiteSpace(value)) - { - return DateTime.MinValue; - } +- `ToCurrentAge()` - Calculates an age in years from a date to today. +- `ToDaySuffix()` - Returns the day suffix (st, nd, rd, th) for a date. +- `ToNearestHour()` - Rounds a date to the nearest hour. +- `StartOfDay()` / `EndOfDay()` - Gets the start or end of the day. +- `StartOfWeek()` / `EndOfWeek()` - Gets the start or end of the week. +- `StartOfMonth()` / `EndOfMonth()` - Gets the start or end of the month. +- `StartOfYear()` / `EndOfYear()` - Gets the start or end of the year. +- `SetTime()` - Overrides the time part of a `DateTime` value (multiple overloads). - bool parsed = DateTime.TryParse(value, out DateTime dateTime); - return parsed ? dateTime : DateTime.MinValue; - } - } -} +## String extensions + +The `MADE.Data.Converters.Extensions.StringExtensions` class provides extensions for manipulating `String` values: + +- `ToTitleCase()` - Converts a string to title case. +- `ToDefaultCase()` - Converts a string to default (lower) case. +- `Truncate()` - Truncates a string to a specified length. +- `ToBase64()` / `FromBase64()` - Converts to and from Base64 encoding. +- `ToMemoryStreamAsync()` - Converts a string to a `MemoryStream`. +- `ToInt()` / `ToNullableInt()` - Parses a string to an integer. +- `ToFloat()` / `ToNullableFloat()` - Parses a string to a float. +- `ToDouble()` / `ToNullableDouble()` - Parses a string to a double. +- `ToBoolean()` - Parses a string to a boolean. + +## Boolean extensions + +The `MADE.Data.Converters.Extensions.BooleanExtensions` class provides the `ToFormattedString` extension for formatting `bool` values to custom string representations. + +```csharp +bool isActive = true; +string result = isActive.ToFormattedString("Active", "Inactive"); // "Active" ``` -If you want to build a XAML specific value converter, you can also apply the `Windows.UI.Xaml.Data.IValueConverter` to your class and implement the additional methods calling directly into your `Convert` and `ConvertBack` methods. +## Math extensions -If there is a common value converter you think is missing from MADE.NET, [raise a tracking item on GitHub](https://github.com/MADE-Apps/MADE.NET/issues/new/choose) and we'll get it implemented. +The `MADE.Data.Converters.Extensions.MathExtensions` class provides extensions for common mathematic expressions including `ToRadians` to convert a degrees value to radians. + +## Length extensions + +The `MADE.Data.Converters.Extensions.LengthExtensions` class provides extensions for converting length values: + +- `ToMeters()` - Converts a value from miles to meters. +- `ToMiles()` - Converts a value from meters to miles. diff --git a/docs/articles/features/data-efcore.md b/docs/articles/features/data-efcore.md index fd40f261..ab98c33f 100644 --- a/docs/articles/features/data-efcore.md +++ b/docs/articles/features/data-efcore.md @@ -1,6 +1,6 @@ --- -uid: package-data-converters -title: Using the Data Converters package +uid: package-data-efcore +title: Using the Data EF Core package --- # Using the Data Entity Framework Core package @@ -17,9 +17,9 @@ These are: - A date the entity was created - A date the entity was last updated -This is what the `MADE.Data.EFCore.EntityBase` type provides for you. It even goes as far as to initialize your created and last updated date values for you when you create your object. +This is what the `MADE.Data.EFCore.EntityBase` type provides for you. It initializes your created and last updated date values when you create your object. -To use it for your own entities, it's as simple as inheriting from the `EntityBase` type. +To use it for your own entities, inherit from the `EntityBase` type. By default, it uses a `Guid` identifier. ```csharp namespace MyApp.Data @@ -36,3 +36,70 @@ namespace MyApp.Data } } ``` + +### Using a custom key type with EntityBase + +If you need a different identifier type, use the generic `EntityBase`: + +```csharp +public class Product : EntityBase +{ + public string Name { get; set; } + + public decimal Price { get; set; } +} +``` + +### Entity interfaces + +The following interfaces are available for implementing custom entity types: + +- `IDatedEntity` - Defines `CreatedDate` and `UpdatedDate` properties. +- `IEntityBase` - Extends `IDatedEntity` with a typed `Id` property. +- `IEntityBase` - A convenience interface that uses `Guid` as the key type. + +## Configuring entities with EntityBaseExtensions + +The `MADE.Data.EFCore.Extensions.EntityBaseExtensions` class provides extensions for configuring entity types in your `DbContext` model builder. + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + // Configures the entity key and UTC date properties for a Guid-based entity + modelBuilder.Entity().Configure(); + + // For entities with a custom key type + modelBuilder.Entity().ConfigureWithKey(); +} +``` + +The `ConfigureDateProperties` extension can be used independently to configure UTC date properties on any entity implementing `IDatedEntity`. + +## Storing dates in UTC with UtcDateTimeConverter + +The `MADE.Data.EFCore.Converters.UtcDateTimeConverter` helps ensure that entity model dates are stored and read in UTC format. + +Use the `IsUtc()` annotation on date properties in your entity configuration, then apply the converter to the model builder: + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.ApplyUtcDateTimeConverter(); +} +``` + +## DbContext extensions + +The `MADE.Data.EFCore.Extensions.DbContextExtensions` class provides additional helpers: + +- `UpdateAsync` - Updates an entity and saves changes in a single call. +- `RemoveWhere` - Removes entities from a `DbSet` matching a predicate. +- `SetEntityDates` - Automatically sets `CreatedDate` and `UpdatedDate` on tracked entities. Best called from an override of `SaveChangesAsync`. +- `TrySaveChangesAsync` - Attempts to save changes to the database and handles concurrency exceptions. + +## Query extensions + +The `MADE.Data.EFCore.Extensions.QueryableExtensions` class provides helpers for querying: + +- `Page` - Applies skip and take pagination to a query based on page number and page size. +- `OrderBy` - Dynamically orders query results by a property name string, with optional descending sort. diff --git a/docs/articles/features/data-serialization.md b/docs/articles/features/data-serialization.md index a3e3028c..5649cf84 100644 --- a/docs/articles/features/data-serialization.md +++ b/docs/articles/features/data-serialization.md @@ -7,37 +7,54 @@ title: Using the Data Serialization package The Data Serialization package provides a collection of helpers and extensions for data serialization in different types, e.g. JSON. -## Handling type changes in JSON objects serialized with JSON.NET with TypeNameHandling set to All +## Handling type changes in JSON objects with JsonTypeMigrationConverter -There are many ways to use JSON.NET in your applications to serialize and deserialize data. This includes the ability to set the `TypeNameHandling` property to `All` include your .NET type information within your serialized data. +When working with serialized JSON data that includes .NET type information (e.g., a `$type` metadata property), type refactoring or restructuring in your codebase can cause deserialization to fail. -This can come with challenges when you want to use the same data in different solutions, or when you want to perform refactors or data restructures in your codebase. +The `MADE.Data.Serialization.Json.Converters.JsonTypeMigrationConverter` is a `System.Text.Json` converter that reads `$type` metadata from JSON objects and resolves the target type using registered type migrations. It is designed to deserialize JSON that was previously serialized with type metadata (e.g., from Newtonsoft.Json's `TypeNameHandling.All`). -The `JsonTypeMigrationSerializationBinder` class provides a way to handle type changes in JSON objects serialized with JSON.NET, migrating from one type to another (whether known within your codebase or not). - -Here's how to setup your application for migrating JSON objects from one type to another. +Here's how to set up your application for migrating JSON objects from one type to another. ```csharp namespace App.Migrations { + using System.Text.Json; using MADE.Data.Serialization.Json; - using MADE.Data.Serialization.Json.Binders; + using MADE.Data.Serialization.Json.Converters; - public class JsonSerializer + public class JsonMigrationSerializer { - public JsonSerializer() + private readonly JsonSerializerOptions options; + + public JsonMigrationSerializer() { - JsonSerializerSettings.Default.TypeNameHandling = TypeNameHandling.All; - JsonSerializerSettings.Default.Binder = new JsonTypeMigrationSerializationBinder( - new JsonTypeMigration(typeof(OldType), typeof(NewType)), - new JsonTypeMigration("App.Migrations", "App.Migrations.Data.OldDataType", typeof(NewType)) + var converter = new JsonTypeMigrationConverter( + new JsonTypeMigration(typeof(OldType), typeof(NewType)), + new JsonTypeMigration("App.Migrations", "App.Migrations.Data.OldDataType", typeof(NewType)) ); + + this.options = new JsonSerializerOptions(); + this.options.Converters.Add(converter); } - public T Deserialize(string serializedJson) + public T? Deserialize(string serializedJson) { - return JsonConvert.DeserializeObject(serializedJson); + return JsonSerializer.Deserialize(serializedJson, this.options); } } } ``` + +### Adding migrations dynamically + +You can also add migrations after construction using the `AddTypeMigration` method: + +```csharp +var converter = new JsonTypeMigrationConverter(); +converter.AddTypeMigration(new JsonTypeMigration(typeof(LegacyOrder), typeof(Order))); +``` + +The `JsonTypeMigration` class supports two constructor overloads: + +- `JsonTypeMigration(Type fromType, Type toType)` - Migrates from one known type to another. +- `JsonTypeMigration(string fromAssemblyName, string fromTypeName, Type toType)` - Migrates from a type that may no longer exist in the codebase, identified by its original assembly and type name. diff --git a/docs/articles/features/data-validation.md b/docs/articles/features/data-validation.md index 188c5742..8fdf9601 100644 --- a/docs/articles/features/data-validation.md +++ b/docs/articles/features/data-validation.md @@ -142,9 +142,21 @@ The minimum can be configured by setting the `Min` value. The in-box `System` types which implement the `IComparable` interface can be [found in the Microsoft documentation](https://docs.microsoft.com/en-us/dotnet/api/system.icomparable?view=net-5.0). +### MinLengthValidator + +The `MinLengthValidator` validates that a string value meets a minimum length requirement. + +The minimum length can be configured by setting the `MinLength` value. + +### MaxLengthValidator + +The `MaxLengthValidator` validates that a string value does not exceed a maximum length. + +The maximum length can be configured by setting the `MaxLength` value. + ### PredicateValidator -The `PredicateValidator` validates a value using a custom predicate to ensure that a condition is met. +The `PredicateValidator` validates a value using a custom predicate function to ensure that a condition is met. This is useful for one-off validation logic that doesn't warrant its own validator class. ### RegexValidator @@ -163,6 +175,10 @@ This is determined based on the following criteria: - The value is a boolean and is true - The value is a string and is not null or whitespace +### WellFormedUrlValidator + +The `WellFormedUrlValidator` validates that a string value is a well-formed URL using the `Uri.IsWellFormedUriString` method. + ## Creating your own custom data validators There are likely to be more advanced, custom scenarios for your own applications that need to extend the capabilities past the in-box `IValidator` types. diff --git a/docs/articles/features/diagnostics.md b/docs/articles/features/diagnostics.md index 81b21213..aa676d92 100644 --- a/docs/articles/features/diagnostics.md +++ b/docs/articles/features/diagnostics.md @@ -17,8 +17,6 @@ The `LogFileNameFormat` has a `DateTime` parameter that can used to ensure logs By default, the logs are stored in the application's root directory, however, this can also be overridden completely using the `LogPath` property which requires a full directory path including log file name. -The implementation also has platform-specific code, so if you're building applications for Windows, Android, or iOS, the `FileEventLogger` will ensure the logs are stored within the application's data store. - Here's an example of the output using the default configuration. ```log @@ -53,11 +51,7 @@ There are occasions when you want to build an application that will not crash wh The `MADE.Diagnostics.AppDiagnostics` helper provides you with the means to ensure your users can keep using your application with no worry about minor issues causing crashes. -Taking advantage of the `IEventLogger` interface available in the Diagnostics package, the `AppDiagnostics` helper can track exceptions thrown by the `AppDomain.UnhandledException`, `TaskScheduler.UnobservedTaskException`, `Windows.UI.Xaml.Application.UnhandledException`, `Android.Runtime.UnhandledExceptionRaiser`, and `Java.Lang.Thread.DefaultUncaughtException` handlers, depending on your platform usage. - -In the unfortunate event that an exception thrown is application terminating, the `AppDiagnostics` helper will ensure that the error is logged before closing the application. - -Logs will be created for each handle of an unhandled exception from one or more of these handlers at the **Critical** level with a custom message which also includes a correlation ID. +Taking advantage of the `IEventLogger` interface available in the Diagnostics package, the `AppDiagnostics` helper can track exceptions thrown by the `AppDomain.UnhandledException` and `TaskScheduler.UnobservedTaskException` handlers, logging them at the **Critical** level with a correlation ID. ### Observing the unhandled exceptions diff --git a/docs/articles/features/runtime.md b/docs/articles/features/runtime.md index efa044c6..58f8fc1d 100644 --- a/docs/articles/features/runtime.md +++ b/docs/articles/features/runtime.md @@ -7,6 +7,24 @@ title: Using the Runtime package The Runtime package provides additional types for .NET to provide extensibility over existing `System` types. +## Chaining actions on multiple instances with Chain + +The `MADE.Runtime.Actions.Chain` type provides a fluent API for invoking an action across one or more instances of a type. This is useful when you need to perform the same operation on a collection of objects without writing boilerplate loops. + +```csharp +var handler1 = new MessageHandler(); +var handler2 = new MessageHandler(); + +new Chain(handler1) + .With(handler2) + .Invoke(handler => handler.Initialize()); + +// Or asynchronously +await new Chain(handler1) + .With(handler2) + .InvokeAsync(handler => handler.InitializeAsync()); +``` + ## Improving callback memory management with WeakReferenceCallback The `MADE.Runtime.WeakReferenceCallback` is a wrapper type for a `WeakReference`. It is capable of taking a delegate action and ensuring that it is available to be garbage collected if the referring object is disposed. @@ -32,3 +50,13 @@ public void AddOrUpdate( ``` In the above example, an action can be passed from a requesting object for when a network request is a success or errors. By using a `WeakReferenceCallback` instead of keeping a hold on the action reference, the requesting object can be disposed when it is no longer required. This is useful in application development scenarios, for example, a page view-model. + +## Retrieving property names with ReflectionExtensions + +The `MADE.Runtime.Extensions.ReflectionExtensions` class provides the `GetPropertyNames` extension method, which returns the names of all public properties on an object using reflection. + +```csharp +var user = new User { FirstName = "James", LastName = "Croft" }; +IEnumerable propertyNames = user.GetPropertyNames(); +// Returns: ["FirstName", "LastName"] +``` diff --git a/docs/articles/features/threading.md b/docs/articles/features/threading.md index e3e3e371..8d0adcc5 100644 --- a/docs/articles/features/threading.md +++ b/docs/articles/features/threading.md @@ -85,3 +85,26 @@ public class TimerJob ``` As you can see, the MADE implementation performs the same actions, but is much more concise and a lot easier to understand. + +The `ITimer` interface is also available if you need to define your own timer implementation or use it for dependency injection and testing. + +## Task extensions + +The `MADE.Threading.TaskExtensions` class provides extensions for working with asynchronous tasks: + +### AndObserveExceptions + +Observes the exceptions of faulted tasks, allowing you to handle errors without causing unobserved task exceptions. + +```csharp +await myTask.AndObserveExceptions(ex => logger.LogError(ex, "Task faulted")); +``` + +### WhenAll and WhenAny for Task IEnumerable + +Convenience extensions that call `Task.WhenAll` and `Task.WhenAny` directly on an `IEnumerable` collection. + +```csharp +var tasks = myItems.Select(item => ProcessAsync(item)); +await tasks.WhenAll(); +``` diff --git a/docs/articles/features/web-mvc.md b/docs/articles/features/web-mvc.md index 901dcb02..0192393d 100644 --- a/docs/articles/features/web-mvc.md +++ b/docs/articles/features/web-mvc.md @@ -12,3 +12,21 @@ The Web MVC library is a complementary extension package to ASP.NET Core MVC app Out-of-the-box, the ASP.NET Core MVC packages don't contain a way of returning an internal server error (500) `ObjectResult` if an error occurs in your application. The `InternalServerErrorObjectResult` can be used to achieve this. It contains two constructors, one for providing the error, and another for providing a `ModelStateDictionary` model state containing validation errors. + +## Returning JSON with a custom status code using JsonResult + +The `MADE.Web.Mvc.Responses.JsonResult` is a custom `ActionResult` that serializes a value as JSON using `System.Text.Json` and returns it with a configurable HTTP status code. + +```csharp +return new MADE.Web.Mvc.Responses.JsonResult(myObject, HttpStatusCode.Created); +``` + +You can also pass custom `JsonSerializerOptions` to control serialization behavior. + +## Controller extensions + +The `MADE.Web.Mvc.Extensions.ControllerBaseExtensions` class provides helper methods for returning common action results from controllers: + +- `Json(object, HttpStatusCode, JsonSerializerOptions?)` - Returns a `JsonResult` with a custom status code. +- `InternalServerError(object)` - Returns an `InternalServerErrorObjectResult` with an error value. +- `InternalServerError(ModelStateDictionary)` - Returns an `InternalServerErrorObjectResult` with model state validation errors. diff --git a/docs/articles/features/web.md b/docs/articles/features/web.md index bc85f631..fd08ef4a 100644 --- a/docs/articles/features/web.md +++ b/docs/articles/features/web.md @@ -73,3 +73,78 @@ The `PaginatedRequest` and `PaginatedResponse` can be used together to help you The `PaginatedRequest` takes a page and page size parameter when constructed, and it automatically provides you with the `Skip` and `Take` properties that you can provide to your data queries. When you have your data from your request, you can construct a response using the `PaginatedResponse` which takes the data, the original page and page size parameters, and the number of available items. It will also provide the `TotalPages` that are available to allow a UI to generate a pagination user experience. + +## Accessing authenticated user identity with AuthenticatedUser + +The `MADE.Web.Identity.AuthenticatedUser` class extracts claims-based identity information from a `ClaimsPrincipal`. It provides easy access to common claims: + +- `Subject` - The user's identity (`sub` claim). +- `Email` - The user's preferred email address (`email` claim). +- `Roles` - The user's assigned roles (`role` claims). +- `Claims` - All claims as an immutable list. + +```csharp +var authenticatedUser = new AuthenticatedUser(httpContext.User); +string userId = authenticatedUser.Subject; +string email = authenticatedUser.Email; +``` + +### Using IAuthenticatedUserAccessor with dependency injection + +Register the `AuthenticatedUserAccessor` with your service collection to inject authenticated user information: + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddAuthenticatedUserAccessor(); +} +``` + +Then inject `IAuthenticatedUserAccessor` into your services: + +```csharp +public class MyService +{ + private readonly IAuthenticatedUserAccessor userAccessor; + + public MyService(IAuthenticatedUserAccessor userAccessor) + { + this.userAccessor = userAccessor; + } + + public string GetCurrentUserId() + { + return this.userAccessor.AuthenticatedUser.Subject; + } +} +``` + +## Adding API versioning support with ApiVersioningExtensions + +The `MADE.Web.Extensions.ApiVersioningExtensions` class provides extensions for adding API versioning to your ASP.NET Core application. + +### URL-based versioning + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddApiVersionSupport(defaultMajor: 1, defaultMinor: 0); +} +``` + +### Header-based versioning + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddApiVersionHeaderSupport(apiHeaderName: "x-api-version", defaultMajor: 1, defaultMinor: 0); +} +``` + +## Query collection extensions + +The `MADE.Web.Extensions.QueryCollectionExtensions` class provides helpers for reading query string values from an `IQueryCollection`: + +- `GetStringValueOrDefault` - Gets a string value from the query collection, or a default. +- `GetIntValueOrDefault` - Gets an integer value from the query collection, or a default. +- `GetDateTimeValueOrDefault` - Gets a DateTime value from the query collection, or a default. diff --git a/docs/articles/intro.md b/docs/articles/intro.md index d4af2b98..679e0d31 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -11,7 +11,7 @@ Whether you're building an ASP.NET Core Web API or a native UI application with MADE.NET has been built on common code from projects built by the MADE team, and is now a home for all those bits of code that you know you will reuse in another project! -Most packages support the `netstandard2.0` target framework, with some packages opting to support platform specific frameworks such as .NET 6/7/8 and UWP. +All packages target `net8.0` and `net10.0`. ## Installation @@ -48,7 +48,9 @@ It includes features such as: - GenericEqualityComparer, a `IEqualityComparer` implementation for comparing two objects using a simple comparison function. - ObservableItemCollection, a `ObservableCollection` implementation that takes a `INotifyPropertyChanged` item type which manages and surfaces up property changed events. -- CollectionExtensions, a collection of extensions for enumerable objects including `Update` (to update an existing item), `MakeEqualTo` (to update a collection's items to be equal to another), and `AreEquivalent` (to compare two collections contain the same items ignoring order). +- CollectionExtensions, a collection of extensions for enumerable objects including `Update` (to update an existing item), `MakeEqualTo` (to update a collection's items to be equal to another), `AreEquivalent` (to compare two collections contain the same items ignoring order), `AddIf`/`RemoveIf` (conditional add/remove), `Shuffle`, `Sort`/`SortDescending` for `ObservableCollection`, and `IsNullOrEmpty`. +- DictionaryExtensions, providing `AddOrUpdate` and `GetValueOrDefault` for dictionaries. +- QueryableExtensions, providing `Chunk` for splitting `IQueryable` sources into smaller queries. @@ -62,10 +64,13 @@ The Data Converters package provides a collection of value converters and extens It includes features such as: +- BooleanToStringValueConverter, a value converter for converting `bool` values to configurable `String` representations, with the capability to convert back. - DateTimeToStringValueConverter, a value converter that takes a `DateTime` string format parameter to convert a `DateTime` value to a `String`, with the capability to convert back. -- DateTimeExtensions, a collection of useful extensions for interacting with `DateTime` values including `ToCurrentAge` (to get an age in years based on a given date from today) and `SetTime` (to override the time part of a `DateTime` value). +- DateTimeExtensions, a collection of useful extensions for interacting with `DateTime` values including `ToCurrentAge` (to get an age in years based on a given date from today), `StartOfDay`/`EndOfDay`, `StartOfWeek`/`EndOfWeek`, `StartOfMonth`/`EndOfMonth`, `StartOfYear`/`EndOfYear`, and `ToNearestHour`. - MathExtensions, a collection of extensions for common mathematic expressions including `ToRadians` (to convert a degrees value to radians). -- StringExtensions, a collection of extensions for manipulating `String` values such as `ToTitleCase`, `ToDefaultCase`, `ToInt`, `ToBoolean`, `ToFloat`, and `ToDouble`. +- StringExtensions, a collection of extensions for manipulating `String` values such as `ToTitleCase`, `ToDefaultCase`, `Truncate`, `ToBase64`, `FromBase64`, `ToInt`, `ToBoolean`, `ToFloat`, and `ToDouble`. +- LengthExtensions, for converting length values such as `ToMeters` and `ToMiles`. +- BooleanExtensions, for formatting `bool` values to custom string representations with `ToFormattedString`. @@ -79,8 +84,11 @@ The Data Entity Framework Core package provides a collection of helpers, extensi It includes features such as: -- DbContextExtensions, for additional helpers to EF data contexts such as asynchronous update & save. -- EntityBase, for providing a base definition for entities including a GUID identifier, created, and updated dates. +- IEntityBase, interfaces for defining entities with a typed identifier and date tracking. +- EntityBase, providing a base definition for entities including a typed identifier, created, and updated dates. +- DbContextExtensions, for additional helpers to EF data contexts such as asynchronous update and save, and automatic entity date management. +- EntityBaseExtensions, for configuring entity types with EF Core model builders including UTC date property configuration. +- QueryableExtensions, for pagination and dynamic ordering of queries. - UtcDateTimeConverter, to help with the storing of entity model dates in a UTC format. @@ -95,7 +103,7 @@ The Data Serialization package provides a collection of helpers and extensions f It includes features such as: -- JsonTypeMigrationSerializationBinder, for migrating type names within a serialized JSON object. +- JsonTypeMigrationConverter, a `System.Text.Json` converter for migrating type names within a serialized JSON object that contains `$type` metadata. @@ -116,8 +124,11 @@ It provides easy-to-use validation helpers such as: - IpAddressValidator, for validating whether a value is a valid IP address. - MaxValueValidator, for validating whether a value is below a maximum value. - MinValueValidator, for validating whether a value is above a minimum value. +- MinLengthValidator, for validating whether a string meets a minimum length requirement. +- MaxLengthValidator, for validating whether a string is within a maximum length limit. - RegexValidator, for validating a value based on a specified regular expression. - RequiredValidator, for validating a value is not null, false, whitespace, or empty. +- WellFormedUrlValidator, for validating a value is a well-formed URL. @@ -212,6 +223,8 @@ This includes features such as: - PaginatedRequest, a simple request object that provides the expected return type, with properties for the current `Page`, the `PageSize`, and the number of items to `Skip` and `Take`. - PaginatedResponse, a complementary response return type for the `PaginatedRequest`, with properties including the `Items` collection, the current `Page` and `PageSize`, plus the `AvailableCount` of requestable items, and the `TotalPages` based on the available count and page size requested. - HttpContextExceptionsMiddleware, a middleware that manages the handling of exceptions thrown within a `HttpContext` to serve up meaningful exception details to the requesting client using exception handlers. +- AuthenticatedUser, a model for extracting claims-based identity information such as Subject, Email, and Roles from a `ClaimsPrincipal`. +- ApiVersioningExtensions, for adding API versioning support to ASP.NET Core applications using URL or header-based versioning. @@ -226,6 +239,8 @@ The Web MVC library is a complementary extension package to ASP.NET Core MVC app Included in this package is: - InternalServerErrorObjectResult, an `ObjectResult` type that returns an Internal Server Error (500) with the optional `ModelStateDictionary` of validation errors. +- JsonResult, a custom `ActionResult` that serializes a value as JSON with a configurable HTTP status code. +- ControllerBaseExtensions, providing `Json` and `InternalServerError` helper methods for controller actions. diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml index 39300c62..15c3cd55 100644 --- a/docs/articles/toc.yml +++ b/docs/articles/toc.yml @@ -14,8 +14,6 @@ href: features/data-validation.md - name: Diagnostics href: features/diagnostics.md - - name: Media Image - href: features/media-image.md - name: Networking href: features/networking.md - name: Runtime diff --git a/docs/docfx.json b/docs/docfx.json index d49cfd9f..20e58706 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -15,10 +15,8 @@ } ], "dest": "api", - "disableGitFeatures": false, - "disableDefaultFilter": false, "properties": { - "TargetFramework": "netstandard2.0" + "TargetFramework": "net10.0" } } ], @@ -58,25 +56,17 @@ } ], "dest": "_site", - "globalMetadataFiles": [], - "fileMetadataFiles": [], "template": [ "default", - "templates/material" + "modern", + "templates/custom" ], - "postProcessors": [], - "markdownEngineName": "markdig", - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, - "disableGitFeatures": false, "globalMetadata": { "_appLogoPath": "images/Logo.png", "_appFaviconPath": "images/Logo.png", "_appTitle": "MADE.NET", "_appFooter": "Copyright (c) MADE Apps", - "_enableSearch": true, - "_enableNewTab": true + "_enableSearch": true } } } \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 72cc7378..87ac77c6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,203 +1,58 @@ --- uid: front-page -title: Make App Development Easier +title: MADE.NET +_layout: landing --- -MADE project banner - -
-

- Make app development easier with reusable code -

- -
- MADE.NET is a toolkit of reusable code libraries building applications. It streamlines the approach to get projects going. +
+ MADE.NET +

Make app development easier with reusable code

+

A toolkit of lightweight .NET libraries that streamline common development tasks, from data validation and serialization to networking and diagnostics.

+ +
-
- - -[Learn more](articles/intro.md) +
- -
+``` +dotnet add package MADE.Collections +```
-
- - Supports - -
-
- -
-
- - .NET - -
-
- - Standard 2.0+, 6, 7, 8 - -
-
-
-
-
- -
-
- - Windows - -
-
- - WinUI, UWP, WPF, WinForms - -
-
-
-
-
- -
-
- - MAUI - -
-
- - Android, iOS - -
-
-
-
-
- -
-
- - Uno Platform - -
-
- - WebAssembly, Android, iOS, macOS, Linux - -
-
-
-
+
+
+

Data validation

+

A lightweight, extensible validation framework for input values in any .NET application.

-
- ---- - -
-
-
- A validation icon -

- Get data validation in seconds -

-
- MADE provides a lightweight, extensible data validation framework for input values in your applications -
-
-
- A database icon -

- Improve data access with EF Core helpers -

-
- Simple, easy-to-use Entity Framework Core helpers for common data modelling, querying, and data access functions -
-
+
+

EF Core helpers

+

Common data modelling, querying, and data access patterns for Entity Framework Core.

-
-
- A network icon -

- Simplify your API requests -

-
- Save time making API requests in your apps using MADE's network request handlers -
-
-
- A model icon -

- Cross-platform by default -

-
- No matter what platform your app is running on, MADE.NET provides a consistent set of APIs for any .NET application -
-
+
+

Networking

+

Typed HTTP request handlers that simplify API calls with built-in serialization.

-
- ---- - -
-
-

- MADE.NET is simple to install and it's easy to get started. -

+
+

Cross-platform

+

Targets .NET 8 and .NET 10. Works everywhere .NET runs, from server to mobile.

-
- - - -[Learn more](articles/intro.md) - - - +
+

Collections

+

Extension methods for lists, dictionaries, and queryables that reduce boilerplate.

+
+
+

Diagnostics

+

Stopwatch-style helpers for measuring and logging performance in your code.

---- - -
-
-
- Hands holding a heart icon -

- Support MADE.NET -

-
- If you'd like to support MADE.NET, considering donating to the project -
- -
-
-
-
- A GitHub icon -

- Contribute on GitHub -

-
- MADE.NET is open source and you can contribute on GitHub -
-
+
+

Open source and ready to use

+
diff --git a/docs/templates/custom/public/main.css b/docs/templates/custom/public/main.css new file mode 100644 index 00000000..b2606c9a --- /dev/null +++ b/docs/templates/custom/public/main.css @@ -0,0 +1,151 @@ +:root { + --bs-link-color-rgb: 44, 119, 204; + --bs-link-hover-color-rgb: 59, 117, 187; + --brand-blue: #2c77cc; + --brand-blue-light: #e8f1fb; + --brand-blue-dark: #1a5a9e; +} + +/* Landing page */ + +.landing-hero { + text-align: center; + padding: 4rem 1.5rem 3rem; + max-width: 720px; + margin: 0 auto; +} + +.landing-hero img { + max-width: 400px; + width: 100%; + margin-bottom: 1.5rem; +} + +.landing-hero h1 { + font-size: 2rem; + font-weight: 300; + line-height: 1.4; + margin-bottom: 1rem; + color: var(--bs-body-color); +} + +.landing-hero p { + font-size: 1.125rem; + line-height: 1.7; + color: var(--bs-secondary-color); + margin-bottom: 2rem; +} + +.landing-actions { + display: flex; + gap: 0.75rem; + justify-content: center; + flex-wrap: wrap; +} + +.landing-actions a { + display: inline-block; + padding: 0.6rem 1.5rem; + border-radius: 6px; + font-weight: 500; + font-size: 0.95rem; + text-decoration: none; + transition: background-color 0.15s, color 0.15s; +} + +.btn-primary-landing { + background-color: var(--brand-blue); + color: #fff !important; +} + +.btn-primary-landing:hover { + background-color: var(--brand-blue-dark); + color: #fff !important; +} + +.btn-outline-landing { + border: 1px solid var(--bs-border-color); + color: var(--bs-body-color) !important; + background: transparent; +} + +.btn-outline-landing:hover { + border-color: var(--brand-blue); + color: var(--brand-blue) !important; +} + +/* Quick install */ + +.quick-install { + max-width: 520px; + margin: 0 auto 4rem; + text-align: center; +} + +.quick-install code { + display: block; + padding: 0.75rem 1.25rem; + border-radius: 6px; + font-size: 0.9rem; +} + +/* Feature grid */ + +.feature-grid { + display: grid; + grid-template-columns: 1fr; + gap: 2rem; + max-width: 900px; + margin: 0 auto 4rem; + padding: 0 1.5rem; +} + +@media (min-width: 768px) { + .feature-grid { + grid-template-columns: 1fr 1fr; + } +} + +.feature-card { + padding: 1.5rem; + border: 1px solid var(--bs-border-color); + border-radius: 8px; + transition: border-color 0.15s; +} + +.feature-card:hover { + border-color: var(--brand-blue); +} + +.feature-card h3 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.feature-card p { + font-size: 0.95rem; + line-height: 1.6; + color: var(--bs-secondary-color); + margin-bottom: 0; +} + +/* Bottom CTA */ + +.landing-cta { + text-align: center; + padding: 3rem 1.5rem 4rem; + max-width: 600px; + margin: 0 auto; +} + +.landing-cta h2 { + font-size: 1.5rem; + font-weight: 400; + margin-bottom: 1.5rem; +} + +.landing-cta .landing-actions { + margin-top: 1rem; +} + diff --git a/docs/templates/material/partials/head.tmpl.partial b/docs/templates/material/partials/head.tmpl.partial deleted file mode 100644 index 02298e35..00000000 --- a/docs/templates/material/partials/head.tmpl.partial +++ /dev/null @@ -1,23 +0,0 @@ -{{!Copyright (c) Oscar Vasquez. All rights reserved. Licensed under the MIT license. See LICENSE file in the project root for full license information.}} - - - - - {{#title}}{{title}}{{/title}}{{^title}}{{>partials/title}}{{/title}} {{#_appTitle}}| {{_appTitle}} {{/_appTitle}} - - - - {{#_description}}{{/_description}} - - - - - - - - - - {{#_noindex}}{{/_noindex}} - {{#_enableSearch}}{{/_enableSearch}} - {{#_enableNewTab}}{{/_enableNewTab}} - \ No newline at end of file diff --git a/docs/templates/material/styles/main.css b/docs/templates/material/styles/main.css deleted file mode 100644 index a4ebcf5b..00000000 --- a/docs/templates/material/styles/main.css +++ /dev/null @@ -1,493 +0,0 @@ -/* COLOR VARIABLES*/ -:root { - --header-bg-color: #fff; - --header-ft-color: #3b75bb; - --highlight-light: #8aa1cf; - --highlight-dark: #2c77cc; - --accent-dim: #e0e0e0; - --accent-super-dim: #f3f3f3; - --font-color: #34393e; - --card-box-shadow: 0 1px 2px 0 rgba(61, 65, 68, 0.06), 0 1px 3px 1px rgba(61, 65, 68, 0.16); - --search-box-shadow: 0 1px 2px 0 rgba(41, 45, 48, 0.36), 0 1px 3px 1px rgba(41, 45, 48, 0.46); - --transition: 350ms; - --light-border: 1px solid rgba(0,0,0,.1); -} - -body { - color: var(--font-color); - font-family: "Roboto", sans-serif; - line-height: 1.5; - font-size: 16px; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; - word-wrap: break-word; -} - -/* HIGHLIGHT COLOR */ - -button, -.button, -a { - color: var(--highlight-dark); - cursor: pointer; -} - -button:hover, -button:focus, -a:hover, -a:focus { - color: var(--highlight-light); - text-decoration: none; -} - -.toc .nav > li.active > a { - color: var(--highlight-dark); -} - -.toc .nav > li.active > a:hover, -.toc .nav > li.active > a:focus { - color: var(--highlight-light); -} - -.pagination > .active > a { - background-color: var(--header-bg-color); - border-color: var(--header-bg-color); -} - -.pagination > .active > a, -.pagination > .active > a:focus, -.pagination > .active > a:hover, -.pagination > .active > span, -.pagination > .active > span:focus, -.pagination > .active > span:hover { - background-color: var(--highlight-light); - border-color: var(--highlight-light); -} - -/* HEADINGS */ - -h1 { - font-weight: 600; - font-size: 32px; -} - -h2 { - font-weight: 600; - font-size: 24px; - line-height: 1.8; -} - -h3 { - font-weight: 600; - font-size: 20px; - line-height: 1.8; -} - -h5 { - font-size: 14px; - padding: 10px 0px; -} - -article h1, -article h2, -article h3, -article h4 { - margin-top: 35px; - margin-bottom: 15px; -} - -article h4 { - padding-bottom: 8px; - border-bottom: 2px solid #ddd; -} - -/* NAVBAR */ - -.navbar-brand > img { - color: var(--header-ft-color); -} - -.navbar { - border: none; - /* Both navbars use box-shadow */ - -webkit-box-shadow: var(--card-box-shadow); - -moz-box-shadow: var(--card-box-shadow); - box-shadow: var(--card-box-shadow); -} - -.subnav { - border-top: 1px solid #ddd; - background-color: #fff; -} - -.navbar-inverse { - background-color: var(--header-bg-color); - z-index: 100; -} - -.navbar-inverse .navbar-nav > li > a, -.navbar-inverse .navbar-text { - color: var(--header-ft-color); - background-color: var(--header-bg-color); - border-bottom: 3px solid transparent; - padding-bottom: 12px; - transition: 350ms; -} - -.navbar-inverse .navbar-nav > li > a:focus, -.navbar-inverse .navbar-nav > li > a:hover { - color: var(--header-ft-color); - background-color: var(--header-bg-color); - border-bottom: 3px solid white; -} - -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:focus, -.navbar-inverse .navbar-nav > .active > a:hover { - color: var(--header-ft-color); - background-color: var(--header-bg-color); - border-bottom: 3px solid white; -} - -.navbar-form .form-control { - border: 0; - border-radius: 4px; - box-shadow: var(--search-box-shadow); - transition:var(--transition); -} - -.navbar-form .form-control:hover { - background-color: var(--accent-dim); -} - -/* NAVBAR TOGGLED (small screens) */ - -.navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form { - border: none; -} -.navbar-inverse .navbar-toggle { - box-shadow: var(--card-box-shadow); - border: none; -} - -.navbar-inverse .navbar-toggle:focus, -.navbar-inverse .navbar-toggle:hover { - background-color: var(--highlight-dark); -} - -/* SIDEBAR */ - -.toc .level1 > li { - font-weight: 400; -} - -.toc .nav > li > a { - color: var(--font-color); -} - -.sidefilter { - background-color: #fff; - border-left: none; - border-right: none; -} - -.sidefilter { - background-color: #fff; - border-left: none; - border-right: none; -} - -.toc-filter { - padding: 5px; - margin: 0; - box-shadow: var(--card-box-shadow); - transition:var(--transition); -} - -.toc-filter:hover { - background-color: var(--accent-super-dim); -} - -.toc-filter > input { - border: none; - background-color: inherit; - transition: inherit; -} - -.toc-filter > .filter-icon { - display: none; -} - -.sidetoc > .toc { - background-color: #fff; - overflow-x: hidden; -} - -.sidetoc { - background-color: #fff; - border: none; -} - -/* ALERTS */ - -.alert { - padding: 0px 0px 5px 0px; - color: inherit; - background-color: inherit; - border: none; - box-shadow: var(--card-box-shadow); -} - -.alert > p { - margin-bottom: 0; - padding: 5px 10px; -} - -.alert > ul { - margin-bottom: 0; - padding: 5px 40px; -} - -.alert > h5 { - padding: 10px 15px; - margin-top: 0; - text-transform: uppercase; - font-weight: bold; - border-radius: 4px 4px 0 0; -} - -.alert-info > h5 { - color: #1976d2; - border-bottom: 4px solid #1976d2; - background-color: #e3f2fd; -} - -.alert-warning > h5 { - color: #f57f17; - border-bottom: 4px solid #f57f17; - background-color: #fff3e0; -} - -.alert-danger > h5 { - color: #d32f2f; - border-bottom: 4px solid #d32f2f; - background-color: #ffebee; -} - -/* CODE HIGHLIGHT */ -pre { - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - word-break: break-all; - word-wrap: break-word; - background-color: #fffaef; - border-radius: 4px; - border: none; - box-shadow: var(--card-box-shadow); -} - -/* STYLE FOR IMAGES */ - -.article .small-image { - margin-top: 15px; - box-shadow: var(--card-box-shadow); - max-width: 350px; -} - -.article .medium-image { - margin-top: 15px; - box-shadow: var(--card-box-shadow); - max-width: 550px; -} - -.article .large-image { - margin-top: 15px; - box-shadow: var(--card-box-shadow); - max-width: 700px; -} - -/* STYLE FOR BUTTONS */ - -.button { - display: inline-block; -} - -.button a { - display: inline-block; - padding: 12px 20px; - box-shadow: 0 2px 4px 0 rgba(31, 31, 31, 0.25); - border-radius: 3em; - transition: color 0.2s ease-in-out, - background-color 0.2s ease-in-out; -} - -.accent-button a { - background-color: var(--header-bg-color); - color: var(--header-ft-color); -} - -.sponsor-button .octicon { - vertical-align: text-top; - overflow: visible; - transition: transform .15s cubic-bezier(.2,0,.13,2); -} - -.text-pink { - color: #ea4aaa; -} - -.octicon { - display: inline-block; - fill: currentcolor; -} - -/* SITE STYLES */ - -hr { - margin-top: 0; - margin-bottom: 7rem; -} - -.article { - margin-top: 120px !important; -} - -.highlight-section { - max-width: 640px; - margin: 0 auto 8px; - display: block; -} - -.hero-layout { - text-align: center; -} - -.hero-image { - width: 100%; -} - -.hero-title { - font-size: 48px; - line-height: 52px; - margin-bottom: 1rem; -} - -.hero-subtitle { - font-size: 20px; - line-height: 32px; - margin-bottom: 1.5rem; -} - -.small-heading { - font-weight: 900; - letter-spacing: 0.35rem; - text-transform: uppercase; - font-size: 12px; - margin-bottom: 1rem; - display: block; -} - -.stat { - color: var(--font-color); - text-decoration: none; - padding: 0; - display: block; - margin-right: 8px; -} - -a.stat { - color: var(--header-bg-color); -} - -.stats-container > * { - flex: 1 1 0; - margin-bottom: 1.25rem; -} - -.stat-container { - text-align: left; - vertical-align: top; - border-radius: 4px; - padding: 0; -} - -.stat-header { - font-weight: 700; - font-size: 14px; - line-height: 16px; - margin-bottom: 2px; -} - -.stat-content { - color: var(--font-color); - font-size: 12px; - line-height: 14px; - clear: both; -} - -.md-header { - font-size: 32px; - line-height: 40px; - margin-bottom: 0.75rem; - font-weight: 800; -} - -.md-content { - font-size: 20px; - line-height: 32px; - color: var(--font-color); -} - -.sm-header { - font-size: 20px; - line-height: 24px; - margin-bottom: 0.5rem; -} - -.highlight-section .sm-header { - margin-top: 0; -} - -.sm-content { - font-size: 16px; - line-height: 1.5; - color: var(--font-color); -} - -.grid-item { - flex: 0 1 50%; - padding: 4rem 70px 4rem; -} - -.grid-item img { - max-height: 120px; - max-width: 120px; -} - -.grid-col { - margin: 0; - padding: 0; -} - -.home-row { - margin-bottom: 7rem; -} - -.home-row-centered-sm { - text-align: center; -} - -@media (min-width: 992px) { - .grid-border-bottom { - border-bottom: var(--light-border); - } - - .grid-border-right { - border-right: var(--light-border); - } - - .home-row-centered-sm { - text-align: unset; - } -} diff --git a/docs/toc.yml b/docs/toc.yml index 046f4b01..891cc4eb 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,5 +1,5 @@ - name: Docs - href: docs/ + href: articles/ homepage: articles/intro.md - name: APIs href: api/ \ No newline at end of file From d912e22d89aa25d554cd132c3a849904d17e87c8 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 10:22:42 +0100 Subject: [PATCH 08/12] feat: add assertion helpers, async primitives, and EFCore extensions New features across multiple packages: - Testing: assertion extensions for objects, booleans, comparables, strings, and exceptions (ShouldBeNull, ShouldBeTrue, ShouldThrow, etc.) - Threading: AsyncLazy, Debouncer, and Throttler - Data.EFCore: soft-delete (ISoftDeletable) and audit trail (IAuditableEntity) with DbContext extensions - Data.Validation: IAsyncValidator and AsyncValidatorCollection - Networking: MultipartFormDataPostNetworkRequest and RetryDelegatingHandler with exponential backoff - Web.Mvc: ForbiddenObjectResult (403) with controller extensions Also adds CancellationToken support to async methods that were missing it, and updates documentation for all new and existing features. --- assets/Logo.afdesign | Bin 35597 -> 34121 bytes assets/Logo.png | Bin 5629 -> 5427 bytes assets/ProjectBanner.afdesign | Bin 20199 -> 128991 bytes assets/ProjectBanner.png | Bin 27717 -> 16373 bytes assets/ProjectIcon.jpg | Bin 14042 -> 17465 bytes assets/ProjectIcon.png | Bin 10331 -> 14411 bytes docs/articles/features/data-efcore.md | 78 ++++++++++ docs/articles/features/data-validation.md | 54 +++++++ docs/articles/features/networking.md | 36 +++++ docs/articles/features/testing.md | 93 ++++++++++++ docs/articles/features/threading.md | 51 +++++++ docs/articles/features/web-mvc.md | 6 + docs/articles/intro.md | 17 ++- src/Directory.Build.props | 2 +- .../Extensions/StringExtensions.cs | 7 +- .../Extensions/DbContextExtensions.cs | 35 ++++- .../Extensions/SoftDeleteExtensions.cs | 91 ++++++++++++ src/MADE.Data.EFCore/IAuditableEntity.cs | 20 +++ src/MADE.Data.EFCore/ISoftDeletable.cs | 20 +++ src/MADE.Data.EFCore/MADE.Data.EFCore.csproj | 4 +- ...DE.Data.Validation.FluentValidation.csproj | 2 +- .../AsyncValidatorCollection.cs | 81 ++++++++++ src/MADE.Data.Validation/IAsyncValidator.cs | 38 +++++ .../HttpResponseMessageExtensions.cs | 10 +- .../MultipartFormDataPostNetworkRequest.cs | 138 ++++++++++++++++++ .../Http/Responses/HttpResponseMessage{T}.cs | 5 +- .../Http/RetryDelegatingHandler.cs | 104 +++++++++++++ src/MADE.Runtime/Actions/Chain.cs | 6 +- src/MADE.Runtime/Actions/IChain.cs | 3 +- src/MADE.Testing/AssertFailedException.cs | 19 +++ src/MADE.Testing/BooleanAssertExtensions.cs | 36 +++++ .../CollectionAssertExtensions.cs | 8 - .../ComparableAssertExtensions.cs | 74 ++++++++++ src/MADE.Testing/ExceptionAssertExtensions.cs | 96 ++++++++++++ src/MADE.Testing/ObjectAssertExtensions.cs | 36 +++++ src/MADE.Testing/StringAssertExtensions.cs | 70 +++++++++ src/MADE.Threading/AsyncLazy{T}.cs | 57 ++++++++ src/MADE.Threading/Debouncer.cs | 107 ++++++++++++++ src/MADE.Threading/Throttler.cs | 100 +++++++++++++ .../Extensions/ControllerBaseExtensions.cs | 29 ++++ .../Responses/ForbiddenObjectResult.cs | 39 +++++ .../Extensions/HttpResponseExtensions.cs | 24 ++- src/MADE.Web/MADE.Web.csproj | 8 +- tests/Directory.Build.props | 12 +- .../MADE.Collections.Tests.csproj | 5 - .../MADE.Data.Converters.Tests.csproj | 7 +- .../MADE.Data.EFCore.Tests.csproj | 13 +- .../MADE.Data.Serialization.Tests.csproj | 5 - ...a.Validation.FluentValidation.Tests.csproj | 5 - .../MADE.Data.Validation.Tests.csproj | 5 - .../MADE.Diagnostics.Tests.csproj | 5 - .../MADE.Networking.Tests.csproj | 5 - tests/MADE.Web.Tests/MADE.Web.Tests.csproj | 5 - 53 files changed, 1578 insertions(+), 93 deletions(-) create mode 100644 src/MADE.Data.EFCore/Extensions/SoftDeleteExtensions.cs create mode 100644 src/MADE.Data.EFCore/IAuditableEntity.cs create mode 100644 src/MADE.Data.EFCore/ISoftDeletable.cs create mode 100644 src/MADE.Data.Validation/AsyncValidatorCollection.cs create mode 100644 src/MADE.Data.Validation/IAsyncValidator.cs create mode 100644 src/MADE.Networking/Http/Requests/MultipartFormDataPostNetworkRequest.cs create mode 100644 src/MADE.Networking/Http/RetryDelegatingHandler.cs create mode 100644 src/MADE.Testing/AssertFailedException.cs create mode 100644 src/MADE.Testing/BooleanAssertExtensions.cs create mode 100644 src/MADE.Testing/ComparableAssertExtensions.cs create mode 100644 src/MADE.Testing/ExceptionAssertExtensions.cs create mode 100644 src/MADE.Testing/ObjectAssertExtensions.cs create mode 100644 src/MADE.Testing/StringAssertExtensions.cs create mode 100644 src/MADE.Threading/AsyncLazy{T}.cs create mode 100644 src/MADE.Threading/Debouncer.cs create mode 100644 src/MADE.Threading/Throttler.cs create mode 100644 src/MADE.Web.Mvc/Responses/ForbiddenObjectResult.cs diff --git a/assets/Logo.afdesign b/assets/Logo.afdesign index 5454309926c7774d2566c74f89a8e69e7917389d..4e332576eebdc2e9b6bb6f5cfac334389e5cfd2b 100644 GIT binary patch literal 34121 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 literal 35597 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 diff --git a/assets/ProjectBanner.afdesign b/assets/ProjectBanner.afdesign index b4200080a6a9b4125eb3169a5dbf47efcd9ff5d9..a73425735276aadb4004254dbff72b6df0202aac 100644 GIT binary patch literal 128991 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⋙P~?f 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

  • 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 diff --git a/assets/ProjectBanner.png b/assets/ProjectBanner.png index 64793008beaab2c8a23fcadd492f4d5f13ea4bd6..fd0c2705a752d249a299d8678e7b53e39e0ce2d7 100644 GIT binary patch literal 16373 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 W;Drivx7}asqfeR`R-ACW`hNhpC9SOh literal 27717 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

    ozc^XIA$1 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%0Any8pJs_|O;Hi> zA{16v=d~ixPzCTycD=_cU&C4doV1Ac1V=$X=4R(%OVEtc^O*c^0g@(7k{N8|(ORU3 z2M08RexRh$aPW6ljm~b9O;xL_uTK(>2?E~YWXC~YrA=}?H{~6dSEaxs+9WAe;_F*q zPZ^f#*7Y_Fk~M>OTq(XgF~JeSuAD6Yuo#pl<>hPZ>v=lGAoiadl~h&D0}Cj1XJ2lu zhTP zR4e_-o*eOx15{YOUkyw!&*2&tf4k@31Ox_B-Qd*J)cpAIpZz`>u9@GKZEtl^HinQI zfXBEgAHy`DQ!QMwrqNtaDZoDq_V?FCI;EhX-QC?WriqtCvC@i>PV4=pqz@$xj8jv$ z7`N`-yC;&-dKyJ`>jpR`eCVg_>}*c$Yy-n8aOx;1G+#LpX9?ByAl9H$p%@;)mKRmZ zHKx#Al#*TfLHK+LS~D+BlJ8uJO1X38k2ot8SdEC8V*7s$n>sAF{@1N(@ZXc9z&2Ib z|2~la@Q1_dH0S5)LBED5PI(0JvFW2Pc&-`Jw>Y>HmEdlim2=#{d7yzt`da e)pfaKwm^uE=Q8~T3o~80qWVJXd4+;i*#7~V@MnVn diff --git a/assets/ProjectIcon.jpg b/assets/ProjectIcon.jpg index 88c81e6c4b4dbf2a5fd6993cde8cf7fdbc8880ec..4fe7bf16de3b3025ecb83ec530ce82d91902abe5 100644 GIT binary patch delta 13702 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 Of!XB$i<0tZ<^KUe&S=^I delta 10240 zcmc(Ebx_+~w{C!zLV@Bg#VIWsG&mI3;ts`vI|L^b*Ay@AQVJ;W zhyVqW;<(WIQ{2#LYNqMY#etkW!kj>m&L8V59$pb19!7d!XEzZw8!roM3oi><3ojdS zMj#LHKZ8^FFq(K|Z9MI5U2Ht)dAWFbxC9vKJ-y^0ULrnrHZK3=q&mV!OISGT04xA4 z00)2zz^}W-9YcrG0MO6?7-$az@Mk=IjD`IK6XVf+4?u(ffc6L-9qS;#}5b$ z^hb{|0ccOKunCEXNk~7CQ|ck67=%IkUXUMTOe~Ddyg&hIUC)QPv=1)P|JCyoEDTKS zM~~6b9?a5&53bRm=#S7aaUVZI``b4<01e~8JK+-|VumylMqb^92`o}FatbCPX&n!* zNoF9wpeCP;mbSILg?nh&{Tu)nxKu0{I1u<5JY!@pbPfWP#fAQTkSP=T?gG*6fZIX%`%{H-q zLORsmW&)@>DaJH?g&=aE#tK_C|3GNu!5?32|DtwW z&}gqNgeL4usyRO|>mR?4k*MywXwdMJfcn>yuT=XwS9?obwMX0uwcZNU2WLN5*kiux zpkDqVzIyq4%pBIX)Yvw4{rigi;@9!X^!Sxi(DSC0-#4ytI_D|>Z)<@5y@>yhYk>aQ zi2qna0RE$kT<<3}fhjmW^R9k5oXdUeL{jpj+D<%bYjMOhzfF$?Yq@4`p3*(zjr$pf zQ;fI>7o^Yr_*sXZc*#MXz%ln#OilLSN~NGkq3m=SJQ$uF0#2oey^h8!aq}yI3DFa; zd0Mi*wPn4g;9`T?P0Wmgf*jN#)osYp00sYb$cyoC;-*b)Y)96?EY-iZLB;H{4 zD@wmzaPvJ!)m#TH*;$1=!MYXZz5xBPMN6HJ-%FQq^=r{KeGDIhPuEC26K}oA{}gy~ z8WLE!j5;Jn*9qSbm1$bH4JjlD6Z~ll#t)tm*r90e0VvisJMj<)Z>VWCu|H9}+%aNK zCEauUGvONvg8A2u#DZZxqphSPuc%_}knZW}yHLE4)#&ma{8ZGlEBhQhfxRM28OvO|zMSrjvbSVy*sNyQ+ zC@PxvM9j|4F61_PZL zJA=c6A>7rY;~7H*RHM#MrjW`fO!VWlvwqHT{?oIN+O0{Pu1`cD&*!yIa~xCo#9#iPq$s+x)AMMwu8yF6S+!?mSe8qt zPUc6f5QE6sCpDG;g6}9AZCQ0p{Bwz(2T)Ur)AHWPNV})FwELm1|EAiC(Oh60Q z+Xd5p8ULuqlP+i>E1fLasGY?I+cgy?uPrF3S=@;~H%?zvSy7Q{j6YhK*_lyVYUT*( zM1FKcw;?Q?_StEBLCqHWLW9|N@Mh|u0Usd~4&DP&@U;kJ+qA`THw!1l4pkR)S_+(PxDgI zKk#3EE~7u@XMb1bk=~b^rL{}4Hq2RNwm%!$eLVMZA-j5w*G0h+;IPrROE?|V026!; z54VZ}ky!mwG)@<}#LI3;m2eK=Uby0Zl#M0LF4nu&rkn651!l-8w`1kfA5B~*K?gLS zBktt!!tz&#&n_d+F~1$Fw==qj!N<4f&@;&UZ1N zvNITTmN`vdiXR5_ympJ%joSC)ytma}T9$Xk zR76(hxqJ0gHzt-_59ivH_;8i`HMH(x*cq2_79x7aW+A&``5p zwHGCWLDFzNr){ZaX@~@q6BXQ+P1Rss4EKN{iDk29ctQPePI0S;^^U6s-kZhXwo9~m zYaUxm|Ec}}L34LY0t$*g!{tTqF(a8E8)$y@doWKS=Cntn0zy86*aclZ+A0BfNv{>T+~>yAsgw#*+d`s>qbGT$w&stlA-U>`8QwN8Y-pLTS;H`Kne8V zByQ5U&PbX!J-CE@c;{GvFLlsJ!>Z$|fqyFFJ%4NTI7V5B*29ui^>=I1k3Lf|lmCpZ zMo%?%4$T=k#slOM*#qx}O046(NCI|wQ3!p7!Y~?1WIAYB>ENJJ+$yTNN0?JHJ-Q(V zNl9(Q5vExgtm+*HRo^xJYdzAgw2s46KA!h5?L*BE+fU;st@7QS@N8K%vsEA3rEu5$ zlms?}WrkT~i>st@xPO}(>cz>yip#G+$ z|7b5nL1aQg!yP;mgG27(l}6wu2);I<`7O>u5wg@;OHF$NeT^?0)~)7gEA8MgEAPkL zXSi0L^^x}Kgjvud6$4^hr`lL!cnu=Rd1gg(v|sADz=T=*&7i*pU`@Z?5L;_3B>xxA zQJ-90i?nrc5oB+O8Th5Smu@V{`m_4v4quw6toC@-on5D``(^Z26 z`f!qznLMqxu@n~Q$%fXz*$^f)CZG?P(AH-M$-#pJ+f{Z_4vQW2CWG@N6gqA2O9Ru@(xJCqKZdVuniD&%Aop^D+8E?+8hdYpnlx^fEd+)OJ%sQwC?FDSzwkLzI1H-ZrQ4Wys1Iw9)2 zxPr-h93xH_|Z2itH!{wI~b26TY6D=Yi;C zBC(6lmj%h!KH4Ql&Lgk~iQA}Uy_TB<@)lFbCd8_d1gRc_^taTaG8TgqNOGwo9;g=j zpM)&S9Y+#(0VTZi{}Ai{TTqM3IF{MU+8`&v>`QQewD1B_sA@h!kC^Sh<@~PJpu{=>XKjp{LSZOPmLCJ_ssNyQREineUI|D0(!N=CtRwpiw zNC}_tcv9AerbZ@Y=uFhtkM{ulXA27F_@5h4l|S*4 z26WBA`A9Lzh$n)?t3+35P#|mWy@}mnRXt#ou3!$r-t|O zc!%VE)l00L$gI?|>uIK-%kpA20ehifPqfHRtcDO9y9>X{Favw&;LrGXwYre7H*-3_+K(J8I1Er16!vv!50ow(agPb9fH&(Aq z5_a(ectNQF4(wv7pmv?(M@~XTIQtts&9y=e%&%qW6?RPWv zx(C#}nLOrJhrQ^yp%qG-DW8@KB}GQGSF_mkow0@oiYkG-lpOv|QETbg zfAKz06zd5EMGVe8Al<-eI&{tNw^1}39L!FiM@gLpvPh+2jz{eiqq{>MVziL0{q}Y4 zR)?g<;TFLmbQja}SKXm*!XTt0N81d_UHkik zNt5ZHU%`!HTz+12qWlz9KJ4g$2Xc9%L1>1U6fqN z51O*T_kd|@8=}pyI#F^NGNL-W%-%Vyc-gPz6zPja{JFVS`IT{Tc_Kx-RxEbLR}Q?1 zr%JidSrq(LQ>STezF!Fu>sB~SC_(+BUejHXY4 zdpCVnS}6n2EG3CGjPKBlmmzgDYDNT~w&tSzsE>2A5mhP46Z_3tc^f;SB)m9}-bz9(zzJw)R*pO8=RPzP*>V^zJ2ab#8f;qT#=mX?CI2OA6YNq>ZmWlP0;nh)}>3n`rR5oMG0l`f=yy=5RX zr3xDASH63jo1I7O24(3Ny$AG#%S$;o^P$qm-w9LHY2L9plK+^C)q*pnKc}23k0XNu zH7#9Vv>|RjE8(9mB#_dmokk_WmKH`pT%GdKz-GmmLyxraYxrV~f za(QAwwf=@mN4`(Q-HoAKt%<2W7xmsSQ#xDYX2~6dj+qm#hv}^puJvxLyMM>E`l2)J z(V9%XgTk`@rvClPbXUQBpclpv;M{3yd)Urgzp&-=-NKT*MBXeYM{ftf+2QjNa|z>0 z@UwSjMvgUP&rDwq#7b!QKH62?G9c|66nJu02u7v+mL#=bHvCC96HGQl_Ui-bIqV1g z1&`iYk*aKdMR2#ie@xB`6Eec4G-kIJGqr*Se)9MdG4VZR!>^sqo>NToobq&`by5Qx zQQ&nRzwH{p4A6+}L3oKE(FAzk(v+(IExf>MwM>5tuYxAd@ee+07+dPg8c?j-NCjQv zSFc{jzWzx0cAjdKtZMm7*)s{>SM=7TLg+(YVxnxlr5$d9ki%St*>rfqIMTFiz=)+Nr^%qAaoVFzR1th zM(Qb@6R7-86SwB9^YeEuC}?HS!Q56DU5*<~pzNf4<=URK3bn?oL7X`tYGC*4IkB3< zW`J27xtiVPP<>%4d|~)&Zkkh@P!d+yC>_W4mcV*hy8pU!XzwZek_G21j0?QIiu>HT zto<#}hmgLCv`}y(I{NcDUcI%yC>jkPK*F41z>)Kn(;o1sLpdYc1v*Mf&-fnVL!48d zYMw3+-UQ1H%BV6fN5$U*JSBRB?M$J?$UsRp&v2^(^~}s$e200RBGPzNgERH%yB}jK zo_erflWjyCdd2Yx3w7Xz84189cPeOO>XO3m%N6A9%Py-{A3oJ?7B}Z>0;}GxowjrP z*u)kJn)tDD%F_=$P+#!miOt%;1|x4yNTWy-{1<2x;VxRzt0#nNRkO?N`9`ZKXr}w- zgLL34DD)0-5Ac}n;C8;GaL?XB>IEm~H@v8L5&>Pjay=Dn%XK%U^UO5!`f~{r^`2aE znRX`sw=eR@7FJgzhJVdDv6#ZWd-JLI#^n0-lzBFka>vAD{Y?3{kXirFJC|l}WQJYN zu&n#80Xy;Aq)E9nOM4*_9eByEhl<@qS`+dS=vPnMoOW$A*VIyb-zLLLiDzY#Qv!N8 zEFl9=1WXb<;&LORw}(dhjpjvn798s0J^NdZnlZ5V9;w~~Y&F*R?1y@Bhl%ETDf`qF zeBS0k*^X`DTnkrv3{25ydqaWUH9Rfm)i@rzLU~P}-y^4eQU^qX{CxcTk2lD zW5Hv5S7C=+Pf}p^T180tXW0>lNyIB7&C<%uuVGK1YT#O7j+`LTCKQ{QDPlFz*fO$5 zeP`s2VR&&3nkZkY*27o(aPUF1zu%57O`jO_DvkRaUw`}jxBVt0Zak_^rgPZsnhhxd?+-s`8km4G zewt`d@FmOef1;*T(XW@r82IIA0HJ*$RL}5TC>Fsr>Li_Q4{8>PpFH{1&5!itlQzz> zAkXvo`C{@yJHe&3IygzyR6Yi9)d}3VOUQUoEn57-DB$GK30(%2RCZmf}}~yS6vhYEuuI zP|e0iUY|5Wc2rH9bEI2mR%{{b>DunG^oyQ}->92`AtM8xKf3G0egv+^)sSQw$2ZgP z#wkrh(gu%Zw{rdKXiDv=E?y`mHdrTbS{Y9!^u(oBtn@RRt9s>S6_$A2328uAP`)pu zI+;f}fX4MMM@G$Ds9T%N@2il7IbQc@K^PCXz&}OYQyW~p{<-DUMRkqFWJKDi$uDhN zVe^x^^kCp>I9o#iI_g= zc#)Bzr(0BYeP0}gzvdazlIoT(;T5zC^!8UBe%XiO5ler7V)c*I#ccTCs1MJV^bNYK z8@sN0Dtk~XyrC=~!P@4It7R*7MK*|)yyxOcxS0W(v9txCa2SQgG9BE^YF34|(DW z+DSV)?g7U4fM%z~4~;kR!oz1am6A#tbE!IE(5{aCTcN9Lvyq2z2Mtw={88ht`{VKt zpY3RzirHN9^&5`UJ-JQaavk_^cBiX(#}!ogn|nC^j0@x$z~W6^MhNw?&WYzp4eWWn zp_x3+ou$+}zzd>kd!O{y{j2q5c4Eb2m#yL?tlbfol6O2~vg|L_5mlbANux`lp}t*q zZ1GE3?2lm}%S01qm=vo!4-Xp=k*y^;1Ts6VSelG&l|?LE`q24e2jr-Om{@uKR_ovX zL#?++;;Ya1|1H*$NCck3_tSib{J}%|vF^s{i}j*k>!^B#^4XZi+Bdev_56W+mQ7Gx zCW^7Ww9jsb3a_EaLIK@YH|%!S4S^kwwPw%p)Wr)=p*#D{fng)l#hf0-FUw5Az_vi~ zALHA2qDF^_g366kM`N&IqzA8SHG%6|UMOB+dx2(YB$u{WG-G_vLgnJ=}<*@@FK21&o3L z?e8wt4g-xPJ^Dh1VP+aZo?q3lKsFK0x<_1Um6^je{^3#x3TC*- zrxWY$C~Xdpg6y`4`WyZf1R837dWeIs3QIO~qWH$5+Nzc$lF7xM{H^%pzbc24Unli# zl*)F&E$znX;8|L5ksB9YAZrw@D;>|nXP&9|M z$W)V{iHWTWvBs!IP}8DwZE6yyz|2QL1`|Zcm`z5Qt@zWHGo}2Bn6@>vftl2kysB?U zZM+6Y<$Vd}LY_$4+I*vrZG%7&k(=9G%Au`}!Ia3SHGO-=!gpDHT&142&*72!u-)P( zCrV)7GrFNWHBxtom6gvrrs`Af2ZccXT8uqg*mD`>!`PY_z8wJ7JFSXq8ndL~{h@BnIBFCv#8>AcmsY^Y*-;X+keT@)nrs*j zN)(&km4oDv_PZ%NBiC3fpFzaCn_!o{yGVUd&Zd6apT<%?=*8d3EcVW}eO>PRFstHhdMMw34f|Ht7DM{;Sj?d{UaUD|bTARBso9 z&yQS&M$`$uqp#KRd5XH$@wv|W?pW2FvZ(kR<6+%p6=Q9atB`>-zY?!N^p1o4jv5pCy3R*BcOjn0{?G_ ze+3N4y0ek$uNtd-JN)omMg?g0eME~f(q$TM@-wYZEdeqla8U3iUv zb^*Gu2zFjrxaDdkwyc3LvsYQ!X|V4Ob4+a`8Jti;ygAkybr9!;YYy`nTkLxPn{VCL zsmhH!_)_6xP(bFP%3^!X|6uq2&ldpww|Ddp7o+-3DA*@%eHQJ4iAo}7^lZXgfB564 zCkg2#aOdgY0%2Akaf}E`v1MnbN?455&vISO91We|n{(97ViDgnGUi_CJIy2{WX{Hf z8sZvc=FY}F?C$ls;r5FADc)*@OqwB$iJ9{0+wggFTX&E6N18dNv$?3C7T~qX<@Ve^ S+Sz|-_|M7yZ(2I;Xa5(@MY6O2 diff --git a/assets/ProjectIcon.png b/assets/ProjectIcon.png index f118711251cc8b458cafc55da44276999f0a7c54..355380b7a8536eb5596a48f02ff05611aac7ac39 100644 GIT binary patch literal 14411 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 delta 9063 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 - Applies skip and take pagination to a query based on page number and page size. - `OrderBy` - Dynamically orders query results by a property name string, with optional descending sort. + +## Soft-delete support with ISoftDeletable + +The `MADE.Data.EFCore.ISoftDeletable` interface adds soft-delete support to your entities. Instead of permanently removing records, entities are marked as deleted and filtered out of queries by default. + +```csharp +public class User : EntityBase, ISoftDeletable +{ + public string Name { get; set; } + + public bool IsDeleted { get; set; } + + public DateTime? DeletedDate { get; set; } +} +``` + +### Applying a global query filter + +Use `ApplySoftDeleteFilter` in your model builder to automatically exclude soft-deleted entities from all queries: + +```csharp +protected override void OnModelCreating(ModelBuilder modelBuilder) +{ + modelBuilder.ApplySoftDeleteFilter(); +} +``` + +To query soft-deleted entities, use `IgnoreQueryFilters()` on a specific query. + +### Automatic soft-delete interception + +Use `InterceptSoftDeletions` in your `SaveChangesAsync` override to automatically convert hard deletes to soft deletes: + +```csharp +public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) +{ + this.InterceptSoftDeletions(); + this.SetEntityDates(); + return await base.SaveChangesAsync(cancellationToken); +} +``` + +### Manual soft-delete and restore + +The `SoftDelete` and `Restore` extension methods allow you to explicitly manage the soft-delete state: + +```csharp +user.SoftDelete(); // Sets IsDeleted = true and DeletedDate +user.Restore(); // Clears IsDeleted and DeletedDate +``` + +## Audit trail support with IAuditableEntity + +The `MADE.Data.EFCore.IAuditableEntity` interface adds user tracking to your entities, recording who created and last updated each record. + +```csharp +public class Order : EntityBase, IAuditableEntity +{ + public string Description { get; set; } + + public string? CreatedBy { get; set; } + + public string? UpdatedBy { get; set; } +} +``` + +### Automatic audit info tracking + +Use `SetEntityAuditInfo` in your `SaveChangesAsync` override to automatically set audit fields: + +```csharp +public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) +{ + this.SetEntityDates(); + this.SetEntityAuditInfo(currentUserId); + return await base.SaveChangesAsync(cancellationToken); +} +``` diff --git a/docs/articles/features/data-validation.md b/docs/articles/features/data-validation.md index 8fdf9601..ea8c7f61 100644 --- a/docs/articles/features/data-validation.md +++ b/docs/articles/features/data-validation.md @@ -247,3 +247,57 @@ The `MADE.Data.Validation.FluentValidation` package provides an easy way to take Using the `MADE.Data.Validation.FluentValidatorCollection` based on a `List` type, you can construct a collection of `AbstractValidator` instances which can be used to validate values. This way, you can bring FluentValidation's out-of-the-box validators or your own custom validators based on the `AbstractValidator` type and get all the benefits of using the existing MADE.NET validation framework. This is great for example with input validator controls that currently support the MADE.NET validation framework! + +## Asynchronous validation with IAsyncValidator + +For validation scenarios that require I/O operations (such as checking uniqueness against a database), the `IAsyncValidator` interface and `AsyncValidatorCollection` provide an asynchronous validation pipeline. + +### Creating an async validator + +Implement the `IAsyncValidator` interface for validators that need to perform asynchronous work: + +```csharp +public class UniqueEmailValidator : IAsyncValidator +{ + private readonly IUserRepository repository; + + public UniqueEmailValidator(IUserRepository repository) + { + this.repository = repository; + } + + public string Key { get; set; } = nameof(UniqueEmailValidator); + + public bool IsInvalid { get; set; } + + public bool IsDirty { get; set; } + + public string FeedbackMessage { get; set; } = "Email address is already in use."; + + public async Task ValidateAsync(object value, CancellationToken cancellationToken = default) + { + var email = value?.ToString(); + this.IsInvalid = !string.IsNullOrWhiteSpace(email) + && await this.repository.EmailExistsAsync(email, cancellationToken); + this.IsDirty = true; + } +} +``` + +### Using the AsyncValidatorCollection + +The `AsyncValidatorCollection` works the same as the `ValidatorCollection` but executes each validator asynchronously: + +```csharp +var validators = new AsyncValidatorCollection +{ + new UniqueEmailValidator(userRepository), +}; + +await validators.ValidateAsync(emailAddress); + +if (validators.IsInvalid) +{ + var messages = validators.FeedbackMessages; +} +``` diff --git a/docs/articles/features/networking.md b/docs/articles/features/networking.md index 45cff269..b2246159 100644 --- a/docs/articles/features/networking.md +++ b/docs/articles/features/networking.md @@ -85,3 +85,39 @@ public void UpdateProfileDetails(Profile profile) `NetworkRequest` objects have a `Guid` identifier also, so if you need to update a pending request with different data or a change in URL, you can do simply by recalling `NetworkManager.AddOrUpdate` passing in a network request with the same ID. The `AddOrUpdate` method has overloads for providing a success callback, as well as an error callback. This allows you to make decisions in your code to handle a successful or failed network request. + +## Uploading files with MultipartFormDataPostNetworkRequest + +The `MultipartFormDataPostNetworkRequest` allows you to upload files and form data using multipart/form-data encoding. It provides a fluent API for building the request content. + +```csharp +public async Task UploadFileAsync(Stream fileStream, string fileName, CancellationToken cancellationToken = default) +{ + var request = new MultipartFormDataPostNetworkRequest(new HttpClient(), "https://api.example.com/upload") + .AddStreamContent("file", fileStream, fileName, "image/png") + .AddStringContent("description", "Profile photo"); + + return await request.ExecuteAsync(cancellationToken); +} +``` + +You can add multiple types of content: + +- `AddStringContent` - Adds a string form field. +- `AddStreamContent` - Adds a file stream with a file name and content type. +- `AddByteArrayContent` - Adds byte array content with a file name and content type. + +## Adding retry support with RetryDelegatingHandler + +The `RetryDelegatingHandler` is a `DelegatingHandler` that automatically retries failed HTTP requests with exponential backoff. It handles transient failures including timeouts, server errors (500, 502, 503, 504), and rate limiting (429). + +```csharp +var handler = new RetryDelegatingHandler(maxRetries: 3, initialDelay: TimeSpan.FromSeconds(1)); +var client = new HttpClient(handler); + +// All requests made with this client will automatically retry on transient failures +var request = new JsonGetNetworkRequest(client, "https://api.example.com/data"); +var result = await request.ExecuteAsync(); +``` + +The handler uses exponential backoff, doubling the delay between each retry attempt. You can customize the maximum number of retries and the initial delay via the constructor parameters. diff --git a/docs/articles/features/testing.md b/docs/articles/features/testing.md index c5dde224..b7f522ab 100644 --- a/docs/articles/features/testing.md +++ b/docs/articles/features/testing.md @@ -38,3 +38,96 @@ public void InvalidTest() ``` You can also perform the same check for scenarios where the collections are **not** equivalent. + +## Asserting null state with ObjectAssertExtensions + +The `ShouldBeNull` and `ShouldNotBeNull` extension methods for any object allow you to assert the null state of a value. + +```csharp +[Test] +public void ShouldBeNullTest() +{ + object? value = null; + value.ShouldBeNull(); +} + +[Test] +public void ShouldNotBeNullTest() +{ + object value = new(); + value.ShouldNotBeNull(); +} +``` + +## Asserting boolean values with BooleanAssertExtensions + +The `ShouldBeTrue` and `ShouldBeFalse` extension methods for `bool` values allow you to assert the expected state of a boolean. + +```csharp +[Test] +public void ShouldBeTrueTest() +{ + bool result = true; + result.ShouldBeTrue(); +} + +[Test] +public void ShouldBeFalseTest() +{ + bool result = false; + result.ShouldBeFalse(); +} +``` + +## Comparing values with ComparableAssertExtensions + +The `ShouldBeGreaterThan`, `ShouldBeGreaterThanOrEqualTo`, `ShouldBeLessThan`, and `ShouldBeLessThanOrEqualTo` extension methods allow you to assert the comparison of `IComparable` values. + +```csharp +[Test] +public void ComparisonTest() +{ + int value = 10; + value.ShouldBeGreaterThan(5); + value.ShouldBeLessThan(20); + value.ShouldBeGreaterThanOrEqualTo(10); + value.ShouldBeLessThanOrEqualTo(10); +} +``` + +## Asserting strings with StringAssertExtensions + +The `ShouldContain`, `ShouldNotContain`, `ShouldStartWith`, and `ShouldEndWith` extension methods allow you to assert the contents of strings. + +```csharp +[Test] +public void StringAssertionTest() +{ + string value = "Hello, World!"; + value.ShouldContain("World"); + value.ShouldNotContain("Goodbye"); + value.ShouldStartWith("Hello"); + value.ShouldEndWith("World!"); +} +``` + +## Asserting exceptions with ExceptionAssertExtensions + +The `ShouldThrow` and `ShouldNotThrow` extension methods allow you to assert that an action throws or does not throw an exception. Async variants `ShouldThrowAsync` and `ShouldNotThrowAsync` are also available. + +```csharp +[Test] +public void ShouldThrowTest() +{ + Action action = () => throw new InvalidOperationException("Oops"); + var exception = action.ShouldThrow(); + // exception.Message is "Oops" +} + +[Test] +public void ShouldNotThrowTest() +{ + Action action = () => { /* no error */ }; + action.ShouldNotThrow(); +} +``` diff --git a/docs/articles/features/threading.md b/docs/articles/features/threading.md index 8d0adcc5..0fd24396 100644 --- a/docs/articles/features/threading.md +++ b/docs/articles/features/threading.md @@ -108,3 +108,54 @@ Convenience extensions that call `Task.WhenAll` and `Task.WhenAny` directly on a var tasks = myItems.Select(item => ProcessAsync(item)); await tasks.WhenAll(); ``` + +## Lazy asynchronous initialization with AsyncLazy + +The `MADE.Threading.AsyncLazy` type provides a way to lazily initialize a value using an asynchronous factory method. The value is computed once on first access and cached for subsequent uses. + +```csharp +private readonly AsyncLazy config = new(async () => +{ + return await LoadConfigurationAsync(); +}); + +public async Task UseConfigAsync() +{ + var configuration = await config; + // Use configuration +} +``` + +You can check whether the value has been created using the `IsValueCreated` property, or use `GetValueAsync()` if you prefer an explicit task return. + +## Rate-limiting actions with Debouncer + +The `MADE.Threading.Debouncer` delays execution of an action until a specified period of inactivity has elapsed. This is useful for scenarios where rapid invocations should be collapsed into a single execution, such as search-as-you-type. + +```csharp +private readonly Debouncer debouncer = new() { Delay = TimeSpan.FromMilliseconds(300) }; + +public void OnSearchTextChanged(string text) +{ + debouncer.Debounce(() => PerformSearch(text)); +} +``` + +Each call to `Debounce` resets the timer. The action only executes after the delay elapses with no further calls. Use `Cancel()` to cancel a pending action, and `Dispose()` to clean up resources. + +An async variant `DebounceAsync` is also available for asynchronous actions. + +## Rate-limiting actions with Throttler + +The `MADE.Threading.Throttler` limits execution of an action to at most once per specified time interval. Unlike the debouncer, the throttler executes the first invocation immediately and suppresses subsequent invocations until the interval elapses. + +```csharp +private readonly Throttler throttler = new() { Interval = TimeSpan.FromMilliseconds(500) }; + +public void OnButtonClicked() +{ + throttler.Throttle(() => SubmitForm()); +} +``` + +An async variant `ThrottleAsync` is also available for asynchronous actions with cancellation support. diff --git a/docs/articles/features/web-mvc.md b/docs/articles/features/web-mvc.md index 0192393d..14ea874d 100644 --- a/docs/articles/features/web-mvc.md +++ b/docs/articles/features/web-mvc.md @@ -23,6 +23,10 @@ return new MADE.Web.Mvc.Responses.JsonResult(myObject, HttpStatusCode.Created); You can also pass custom `JsonSerializerOptions` to control serialization behavior. +## Returning a forbidden ObjectResult + +The `ForbiddenObjectResult` can be used to return a Forbidden (403) response from your API controllers. Like `InternalServerErrorObjectResult`, it supports both an error object and a `ModelStateDictionary`. + ## Controller extensions The `MADE.Web.Mvc.Extensions.ControllerBaseExtensions` class provides helper methods for returning common action results from controllers: @@ -30,3 +34,5 @@ The `MADE.Web.Mvc.Extensions.ControllerBaseExtensions` class provides helper met - `Json(object, HttpStatusCode, JsonSerializerOptions?)` - Returns a `JsonResult` with a custom status code. - `InternalServerError(object)` - Returns an `InternalServerErrorObjectResult` with an error value. - `InternalServerError(ModelStateDictionary)` - Returns an `InternalServerErrorObjectResult` with model state validation errors. +- `Forbidden(object)` - Returns a `ForbiddenObjectResult` with an error value. +- `Forbidden(ModelStateDictionary)` - Returns a `ForbiddenObjectResult` with model state validation errors. diff --git a/docs/articles/intro.md b/docs/articles/intro.md index 679e0d31..c985fbd3 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -90,6 +90,8 @@ It includes features such as: - EntityBaseExtensions, for configuring entity types with EF Core model builders including UTC date property configuration. - QueryableExtensions, for pagination and dynamic ordering of queries. - UtcDateTimeConverter, to help with the storing of entity model dates in a UTC format. +- ISoftDeletable, for adding soft-delete support to entities with automatic query filtering and deletion interception. +- IAuditableEntity, for tracking who created and last updated entities with automatic audit info management. @@ -165,6 +167,8 @@ It includes features such as: - JsonPatchNetworkRequest, for making a HTTP PATCH request with a JSON payload, and a JSON response. - JsonDeleteNetworkRequest, for making a HTTP DELETE request with a JSON response. - StreamGetNetworkRequest, for making a HTTP GET request with a data stream response. +- MultipartFormDataPostNetworkRequest, for making a HTTP POST request with multipart form data content including file uploads. +- RetryDelegatingHandler, a delegating handler for adding automatic retry with exponential backoff to HttpClient instances. @@ -192,7 +196,12 @@ The Testing package is an extension library for assertions in unit testing proje It provides additional assertions such as: -- CollectionAssertExtensions, a collection of extensions for asserting enumerable objects including `ShouldBeEquivalentTo` (comparing two collections to ensure they contain the same items ignoring order), and `ShouldNotBeEquivalentTo` (comparing two collection to ensure they do not contain the same items ignoring order). +- CollectionAssertExtensions, a collection of extensions for asserting enumerable objects including `ShouldBeEquivalentTo` and `ShouldNotBeEquivalentTo`. +- ObjectAssertExtensions, providing `ShouldBeNull` and `ShouldNotBeNull` assertions. +- BooleanAssertExtensions, providing `ShouldBeTrue` and `ShouldBeFalse` assertions. +- ComparableAssertExtensions, providing `ShouldBeGreaterThan`, `ShouldBeLessThan`, and related comparison assertions. +- StringAssertExtensions, providing `ShouldContain`, `ShouldNotContain`, `ShouldStartWith`, and `ShouldEndWith` assertions. +- ExceptionAssertExtensions, providing `ShouldThrow` and `ShouldNotThrow` assertions with async variants. @@ -207,6 +216,9 @@ The Threading package contains a collection of `System.Threading` extensions and It includes features such as: - Timer, a modern take on `System.Threading.Timer` providing properties for configuring the `Interval` and `DueTime`, plus an event handler for `Tick`. It includes simple methods to `Start` and `Stop` the timer running. +- AsyncLazy, a provider for lazy asynchronous initialization of a value. +- Debouncer, for delaying execution until a period of inactivity has elapsed, useful for search-as-you-type scenarios. +- Throttler, for limiting execution to at most once per time interval. @@ -240,7 +252,8 @@ Included in this package is: - InternalServerErrorObjectResult, an `ObjectResult` type that returns an Internal Server Error (500) with the optional `ModelStateDictionary` of validation errors. - JsonResult, a custom `ActionResult` that serializes a value as JSON with a configurable HTTP status code. -- ControllerBaseExtensions, providing `Json` and `InternalServerError` helper methods for controller actions. +- ForbiddenObjectResult, an `ObjectResult` type that returns a Forbidden (403) response. +- ControllerBaseExtensions, providing `Json`, `InternalServerError`, and `Forbidden` helper methods for controller actions. diff --git a/src/Directory.Build.props b/src/Directory.Build.props index da279b35..bc300074 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -32,7 +32,7 @@ - + \ No newline at end of file diff --git a/src/MADE.Data.Converters/Extensions/StringExtensions.cs b/src/MADE.Data.Converters/Extensions/StringExtensions.cs index 92c03ffc..c02ca73d 100644 --- a/src/MADE.Data.Converters/Extensions/StringExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/StringExtensions.cs @@ -118,13 +118,14 @@ public static string FromBase64(this string base64Value, Encoding? encoding = de /// Converts a string value to a . ///

  • /// The value to convert. + /// The cancellation token. /// A representing the string value. - public static async Task ToMemoryStreamAsync(this string value) + public static async Task ToMemoryStreamAsync(this string value, CancellationToken cancellationToken = default) { var stream = new MemoryStream(); var writer = new StreamWriter(stream); - await writer.WriteAsync(value).ConfigureAwait(false); - await writer.FlushAsync().ConfigureAwait(false); + await writer.WriteAsync(value.AsMemory(), cancellationToken).ConfigureAwait(false); + await writer.FlushAsync(cancellationToken).ConfigureAwait(false); stream.Position = 0; return stream; } diff --git a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs index 02fe5239..51443050 100644 --- a/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs +++ b/src/MADE.Data.EFCore/Extensions/DbContextExtensions.cs @@ -116,13 +116,16 @@ public static async Task TrySaveChangesAsync( /// The . /// The action to run. /// An exception for handling the exception thrown, for example, event logging. + /// A to observe while waiting for the task to complete. /// The type of data context. /// True if the action ran successfully; otherwise, false. /// Potentially thrown by the delegate callback. + /// If the is canceled. public static async Task TryAsync( this TContext context, Func? action, - Action? onError = null) + Action? onError = null, + CancellationToken cancellationToken = default) where TContext : DbContext { if (action == null) @@ -130,6 +133,8 @@ public static async Task TryAsync( return false; } + cancellationToken.ThrowIfCancellationRequested(); + try { await action.Invoke(context).ConfigureAwait(false); @@ -142,4 +147,32 @@ public static async Task TryAsync( return false; } + + /// + /// Sets the audit information of entities being tracked in an added or modified state. + /// + /// It is best to call this method in an override of the DbContext.SaveChangesAsync method in your data context. + /// + /// + /// The to update entity audit info for. + /// The identifier of the user performing the operation. + public static void SetEntityAuditInfo(this DbContext context, string? userId) + { + IEnumerable entries = context.ChangeTracker + .Entries() + .Where( + entry => entry.Entity is IAuditableEntity && + entry.State is EntityState.Added or EntityState.Modified); + + foreach (EntityEntry entry in entries) + { + var entity = (IAuditableEntity)entry.Entity; + entity.UpdatedBy = userId; + + if (entry.State == EntityState.Added) + { + entity.CreatedBy = userId; + } + } + } } diff --git a/src/MADE.Data.EFCore/Extensions/SoftDeleteExtensions.cs b/src/MADE.Data.EFCore/Extensions/SoftDeleteExtensions.cs new file mode 100644 index 00000000..6d503966 --- /dev/null +++ b/src/MADE.Data.EFCore/Extensions/SoftDeleteExtensions.cs @@ -0,0 +1,91 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace MADE.Data.EFCore.Extensions; + +/// +/// Defines a collection of extensions for supporting soft deletion of entities. +/// +public static class SoftDeleteExtensions +{ + /// + /// Applies a global query filter to all entities implementing to exclude soft-deleted entities from queries by default. + /// + /// The model builder to apply the filter to. + public static void ApplySoftDeleteFilter(this ModelBuilder builder) + { + foreach (IMutableEntityType entityType in builder.Model.GetEntityTypes()) + { + if (!typeof(ISoftDeletable).IsAssignableFrom(entityType.ClrType)) + { + continue; + } + + ParameterExpression parameter = Expression.Parameter(entityType.ClrType, "e"); + MemberExpression property = Expression.Property(parameter, nameof(ISoftDeletable.IsDeleted)); + UnaryExpression filter = Expression.Not(property); + LambdaExpression lambda = Expression.Lambda(filter, parameter); + + builder.Entity(entityType.ClrType).HasQueryFilter(lambda); + } + } + + /// + /// Soft deletes an entity by setting the flag to true and the to the current UTC time. + /// + /// The type of entity to soft delete. + /// The entity to soft delete. + /// The soft-deleted entity. + /// Thrown if the is . + public static T SoftDelete(this T entity) + where T : ISoftDeletable + { + ArgumentNullException.ThrowIfNull(entity); + + entity.IsDeleted = true; + entity.DeletedDate = DateTime.UtcNow; + return entity; + } + + /// + /// Restores a soft-deleted entity by clearing the flag and the . + /// + /// The type of entity to restore. + /// The entity to restore. + /// The restored entity. + /// Thrown if the is . + public static T Restore(this T entity) + where T : ISoftDeletable + { + ArgumentNullException.ThrowIfNull(entity); + + entity.IsDeleted = false; + entity.DeletedDate = null; + return entity; + } + + /// + /// Intercepts save operations on the to automatically soft delete entities instead of hard deleting them. + /// + /// Call this method in an override of the SaveChangesAsync method before calling the base implementation. + /// + /// + /// The . + public static void InterceptSoftDeletions(this DbContext context) + { + foreach (var entry in context.ChangeTracker.Entries()) + { + if (entry.State != EntityState.Deleted) + { + continue; + } + + entry.State = EntityState.Modified; + entry.Entity.SoftDelete(); + } + } +} diff --git a/src/MADE.Data.EFCore/IAuditableEntity.cs b/src/MADE.Data.EFCore/IAuditableEntity.cs new file mode 100644 index 00000000..6189dc75 --- /dev/null +++ b/src/MADE.Data.EFCore/IAuditableEntity.cs @@ -0,0 +1,20 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Data.EFCore; + +/// +/// Defines an interface for entities that track who created and last updated them. +/// +public interface IAuditableEntity +{ + /// + /// Gets or sets the identifier of the user who created the entity. + /// + string? CreatedBy { get; set; } + + /// + /// Gets or sets the identifier of the user who last updated the entity. + /// + string? UpdatedBy { get; set; } +} diff --git a/src/MADE.Data.EFCore/ISoftDeletable.cs b/src/MADE.Data.EFCore/ISoftDeletable.cs new file mode 100644 index 00000000..e25055f6 --- /dev/null +++ b/src/MADE.Data.EFCore/ISoftDeletable.cs @@ -0,0 +1,20 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Data.EFCore; + +/// +/// Defines an interface for entities that support soft deletion. +/// +public interface ISoftDeletable +{ + /// + /// Gets or sets a value indicating whether the entity has been deleted. + /// + bool IsDeleted { get; set; } + + /// + /// Gets or sets the date the entity was deleted. + /// + DateTime? DeletedDate { get; set; } +} diff --git a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj index fd57677a..5ef43826 100644 --- a/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj +++ b/src/MADE.Data.EFCore/MADE.Data.EFCore.csproj @@ -10,11 +10,11 @@ - + - + diff --git a/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj b/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj index 22f85f2c..37cdbb2f 100644 --- a/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj +++ b/src/MADE.Data.Validation.FluentValidation/MADE.Data.Validation.FluentValidation.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/MADE.Data.Validation/AsyncValidatorCollection.cs b/src/MADE.Data.Validation/AsyncValidatorCollection.cs new file mode 100644 index 00000000..3fdcc596 --- /dev/null +++ b/src/MADE.Data.Validation/AsyncValidatorCollection.cs @@ -0,0 +1,81 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Data.Validation.Extensions; + +namespace MADE.Data.Validation; + +/// +/// Defines a list of objects that can be accessed by index. +/// +public class AsyncValidatorCollection : List +{ + /// Initializes a new instance of the class that is empty and has the default initial capacity. + public AsyncValidatorCollection() + { + } + + /// Initializes a new instance of the class that contains elements copied from the specified collection and has sufficient capacity to accommodate the number of elements copied. + /// The collection whose elements are copied to the new list. + /// collection is null. + public AsyncValidatorCollection(IEnumerable collection) + : base(collection) + { + } + + /// + /// Initializes a new instance of the class that is empty and has the specified initial capacity. + /// + /// The number of elements that the new list can initially store. + /// capacity is less than 0. + public AsyncValidatorCollection(int capacity) + : base(capacity) + { + } + + /// + /// Occurs when the input value is validated against the collection of validators. + /// + public event InputValidatedEventHandler? Validated; + + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + public bool IsInvalid + { + get => this.Any(validator => validator.IsInvalid); + set => this.ForEach(validator => validator.IsInvalid = value); + } + + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + public bool IsDirty + { + get => this.Any(validator => validator.IsDirty); + set => this.ForEach(validator => validator.IsDirty = value); + } + + /// + /// Gets the validator feedback messages for ones which are invalid. + /// + public IEnumerable FeedbackMessages => this.Where(x => x.IsInvalid).Select(x => x.FeedbackMessage).Where(x => !x.IsNullOrWhiteSpace()); + + /// + /// Executes data validation on the provided against the validators provided asynchronously. + /// + /// The value to be validated. + /// The cancellation token. + /// An asynchronous operation. + /// Potentially thrown by the delegate callback. + public async Task ValidateAsync(object value, CancellationToken cancellationToken = default) + { + foreach (var validator in this) + { + cancellationToken.ThrowIfCancellationRequested(); + await validator.ValidateAsync(value, cancellationToken).ConfigureAwait(false); + } + + this.Validated?.Invoke(this, new InputValidatedEventArgs(this.IsInvalid, this.IsDirty)); + } +} diff --git a/src/MADE.Data.Validation/IAsyncValidator.cs b/src/MADE.Data.Validation/IAsyncValidator.cs new file mode 100644 index 00000000..0348dad5 --- /dev/null +++ b/src/MADE.Data.Validation/IAsyncValidator.cs @@ -0,0 +1,38 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Data.Validation; + +/// +/// Defines an interface for a data validator that performs asynchronous validation. +/// +public interface IAsyncValidator +{ + /// + /// Gets or sets the key associated with the validator. + /// + string Key { get; set; } + + /// + /// Gets or sets a value indicating whether the data provided is in an invalid state. + /// + bool IsInvalid { get; set; } + + /// + /// Gets or sets a value indicating whether the data is dirty. + /// + bool IsDirty { get; set; } + + /// + /// Gets or sets the feedback message to display when is true. + /// + string FeedbackMessage { get; set; } + + /// + /// Executes data validation on the provided asynchronously. + /// + /// The value to be validated. + /// The cancellation token. + /// An asynchronous operation. + Task ValidateAsync(object value, CancellationToken cancellationToken = default); +} diff --git a/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs b/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs index 6bd4952c..f440d7fd 100644 --- a/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs +++ b/src/MADE.Networking/Extensions/HttpResponseMessageExtensions.cs @@ -17,11 +17,12 @@ public static class HttpResponseMessageExtensions ///
    /// The type of response expected. /// The task associated with the . + /// The cancellation token. /// A with deserialized content. - public static async Task> DeserializeAsync(this Task responseTask) + public static async Task> DeserializeAsync(this Task responseTask, CancellationToken cancellationToken = default) { HttpResponseMessage response = await responseTask.ConfigureAwait(false); - return await DeserializeAsync(response).ConfigureAwait(false); + return await DeserializeAsync(response, cancellationToken).ConfigureAwait(false); } /// @@ -29,11 +30,12 @@ public static async Task> DeserializeAsync(this Task /// The type of response expected. /// The to deserialize. + /// The cancellation token. /// A with deserialized content. - public static async Task> DeserializeAsync(this HttpResponseMessage response) + public static async Task> DeserializeAsync(this HttpResponseMessage response, CancellationToken cancellationToken = default) { var deserializedResponse = new HttpResponseMessage(response); - await deserializedResponse.DeserializeAsync().ConfigureAwait(false); + await deserializedResponse.DeserializeAsync(cancellationToken).ConfigureAwait(false); return deserializedResponse; } } diff --git a/src/MADE.Networking/Http/Requests/MultipartFormDataPostNetworkRequest.cs b/src/MADE.Networking/Http/Requests/MultipartFormDataPostNetworkRequest.cs new file mode 100644 index 00000000..1092b7fa --- /dev/null +++ b/src/MADE.Networking/Http/Requests/MultipartFormDataPostNetworkRequest.cs @@ -0,0 +1,138 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net.Http; +using System.Text.Json; + +namespace MADE.Networking.Http.Requests; + +/// +/// Defines a network request for a POST call with multipart form data content. +/// +public sealed class MultipartFormDataPostNetworkRequest : NetworkRequest +{ + private readonly HttpClient client; + + /// + /// Initializes a new instance of the class. + /// + /// The for executing the request. + /// The URL for the request. + public MultipartFormDataPostNetworkRequest(HttpClient client, string url) + : this(client, url, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The for executing the request. + /// The URL for the request. + /// The additional headers. + public MultipartFormDataPostNetworkRequest( + HttpClient client, + string url, + Dictionary headers) + : base(url, headers) + { + this.client = client ?? throw new ArgumentNullException(nameof(client)); + this.Content = new MultipartFormDataContent(); + } + + /// + /// Gets the multipart form data content for the request. + /// + public MultipartFormDataContent Content { get; } + + /// + /// Adds a string value to the multipart form data content. + /// + /// The name of the form field. + /// The value of the form field. + /// The current request for chaining. + public MultipartFormDataPostNetworkRequest AddStringContent(string name, string value) + { + this.Content.Add(new StringContent(value), name); + return this; + } + + /// + /// Adds a file stream to the multipart form data content. + /// + /// The name of the form field. + /// The file stream. + /// The file name. + /// The content type of the file. Default is application/octet-stream. + /// The current request for chaining. + public MultipartFormDataPostNetworkRequest AddStreamContent( + string name, + Stream stream, + string fileName, + string contentType = "application/octet-stream") + { + var streamContent = new StreamContent(stream); + streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + this.Content.Add(streamContent, name, fileName); + return this; + } + + /// + /// Adds byte array content to the multipart form data content. + /// + /// The name of the form field. + /// The byte array content. + /// The file name. + /// The content type of the file. Default is application/octet-stream. + /// The current request for chaining. + public MultipartFormDataPostNetworkRequest AddByteArrayContent( + string name, + byte[] bytes, + string fileName, + string contentType = "application/octet-stream") + { + var byteContent = new ByteArrayContent(bytes); + byteContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(contentType); + this.Content.Add(byteContent, name, fileName); + return this; + } + + /// + public override async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + string json = await this.PostAndGetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + /// + public override async Task ExecuteAsync( + Type expectedResponse, + CancellationToken cancellationToken = default) + { + string json = await this.PostAndGetJsonResponseAsync(cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + } + + private async Task PostAndGetJsonResponseAsync(CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(this.Url)) + { + throw new InvalidOperationException("No URL has been specified for executing the network request."); + } + + var uri = new Uri(this.Url); + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = this.Content }; + + if (this.Headers != null) + { + foreach (KeyValuePair header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + } + + HttpResponseMessage response = await this.client.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs index cfa02c6e..fbdc167d 100644 --- a/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs +++ b/src/MADE.Networking/Http/Responses/HttpResponseMessage{T}.cs @@ -90,11 +90,12 @@ public static implicit operator HttpResponseMessage(HttpResponseMessage respo /// /// Deserializes the content of the into the value. /// + /// The cancellation token. /// A representing the result of the asynchronous operation. - public async Task DeserializeAsync() + public async Task DeserializeAsync(CancellationToken cancellationToken = default) { this.DeserializedContent = JsonSerializer.Deserialize( - await this.Content.ReadAsStringAsync().ConfigureAwait(false), + await this.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false), DefaultJsonOptions)!; return this.DeserializedContent; } diff --git a/src/MADE.Networking/Http/RetryDelegatingHandler.cs b/src/MADE.Networking/Http/RetryDelegatingHandler.cs new file mode 100644 index 00000000..a89db955 --- /dev/null +++ b/src/MADE.Networking/Http/RetryDelegatingHandler.cs @@ -0,0 +1,104 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Net.Http; + +namespace MADE.Networking.Http; + +/// +/// Defines a delegating handler that retries failed HTTP requests with exponential backoff. +/// +/// +/// Use this handler when constructing an to automatically retry transient failures. +/// +/// var handler = new RetryDelegatingHandler(maxRetries: 3, initialDelay: TimeSpan.FromSeconds(1)); +/// var client = new HttpClient(handler); +/// +/// +public class RetryDelegatingHandler : DelegatingHandler +{ + private static readonly HashSet TransientStatusCodes = new() + { + HttpStatusCode.RequestTimeout, + HttpStatusCode.TooManyRequests, + HttpStatusCode.InternalServerError, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + }; + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of retry attempts. Default is 3. + /// The initial delay before the first retry. Default is 1 second. + public RetryDelegatingHandler(int maxRetries = 3, TimeSpan? initialDelay = null) + : this(new HttpClientHandler(), maxRetries, initialDelay) + { + } + + /// + /// Initializes a new instance of the class with the specified inner handler. + /// + /// The inner handler which is responsible for processing the HTTP response messages. + /// The maximum number of retry attempts. Default is 3. + /// The initial delay before the first retry. Default is 1 second. + public RetryDelegatingHandler(HttpMessageHandler innerHandler, int maxRetries = 3, TimeSpan? initialDelay = null) + : base(innerHandler) + { + this.MaxRetries = maxRetries; + this.InitialDelay = initialDelay ?? TimeSpan.FromSeconds(1); + } + + /// + /// Gets the maximum number of retry attempts. + /// + public int MaxRetries { get; } + + /// + /// Gets the initial delay before the first retry. Each subsequent retry doubles the delay. + /// + public TimeSpan InitialDelay { get; } + + /// + protected override async Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + + for (int attempt = 0; attempt <= this.MaxRetries; attempt++) + { + try + { + response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + + if (!IsTransientFailure(response) || attempt == this.MaxRetries) + { + return response; + } + + response.Dispose(); + } + catch (HttpRequestException) when (attempt < this.MaxRetries) + { + // Transient network error, will retry + } + catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested && attempt < this.MaxRetries) + { + // Timeout, will retry + } + + TimeSpan delay = TimeSpan.FromMilliseconds(this.InitialDelay.TotalMilliseconds * Math.Pow(2, attempt)); + await Task.Delay(delay, cancellationToken).ConfigureAwait(false); + } + + return response!; + } + + private static bool IsTransientFailure(HttpResponseMessage response) + { + return TransientStatusCodes.Contains(response.StatusCode); + } +} diff --git a/src/MADE.Runtime/Actions/Chain.cs b/src/MADE.Runtime/Actions/Chain.cs index 29561983..40235c64 100644 --- a/src/MADE.Runtime/Actions/Chain.cs +++ b/src/MADE.Runtime/Actions/Chain.cs @@ -77,12 +77,16 @@ public void Invoke(Action func) /// Invokes an asynchronous action with the chain. /// /// The asynchronous action to invoke. + /// The cancellation token. /// An asynchronous operation. /// Potential exceptions thrown if delegate callback throws an exception. - public async Task InvokeAsync(Func func) + /// If the is canceled. + public async Task InvokeAsync(Func func, CancellationToken cancellationToken = default) { foreach (WeakReference instance in this.chain) { + cancellationToken.ThrowIfCancellationRequested(); + if (instance.TryGetTarget(out T? i)) { await func(i).ConfigureAwait(false); diff --git a/src/MADE.Runtime/Actions/IChain.cs b/src/MADE.Runtime/Actions/IChain.cs index 91beb57f..6a1b2319 100644 --- a/src/MADE.Runtime/Actions/IChain.cs +++ b/src/MADE.Runtime/Actions/IChain.cs @@ -38,6 +38,7 @@ public interface IChain /// Invokes an asynchronous action with the chain. /// /// The asynchronous action to invoke. + /// The cancellation token. /// An asynchronous operation. - Task InvokeAsync(Func func); + Task InvokeAsync(Func func, CancellationToken cancellationToken = default); } diff --git a/src/MADE.Testing/AssertFailedException.cs b/src/MADE.Testing/AssertFailedException.cs new file mode 100644 index 00000000..041b3b22 --- /dev/null +++ b/src/MADE.Testing/AssertFailedException.cs @@ -0,0 +1,19 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines an exception that is thrown when an assertion fails. +/// +public class AssertFailedException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the assertion failure. + public AssertFailedException(string message) + : base(message) + { + } +} diff --git a/src/MADE.Testing/BooleanAssertExtensions.cs b/src/MADE.Testing/BooleanAssertExtensions.cs new file mode 100644 index 00000000..6f3aa45b --- /dev/null +++ b/src/MADE.Testing/BooleanAssertExtensions.cs @@ -0,0 +1,36 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines a code assertion helper for boolean-based scenarios. +/// +public static class BooleanAssertExtensions +{ + /// + /// Tests whether the specified value is true and throws an exception if it is false. + /// + /// The value to test. + /// Thrown if the is false. + public static void ShouldBeTrue(this bool value) + { + if (!value) + { + throw new AssertFailedException($"{nameof(ShouldBeTrue)} failed. Expected true but was false."); + } + } + + /// + /// Tests whether the specified value is false and throws an exception if it is true. + /// + /// The value to test. + /// Thrown if the is true. + public static void ShouldBeFalse(this bool value) + { + if (value) + { + throw new AssertFailedException($"{nameof(ShouldBeFalse)} failed. Expected false but was true."); + } + } +} diff --git a/src/MADE.Testing/CollectionAssertExtensions.cs b/src/MADE.Testing/CollectionAssertExtensions.cs index 21ee3c91..a3b69c62 100644 --- a/src/MADE.Testing/CollectionAssertExtensions.cs +++ b/src/MADE.Testing/CollectionAssertExtensions.cs @@ -201,12 +201,4 @@ private static Dictionary GetElementCounts(IEnumerable collection, return dictionary; } - - private class AssertFailedException : Exception - { - public AssertFailedException(string message) - : base(message) - { - } - } } diff --git a/src/MADE.Testing/ComparableAssertExtensions.cs b/src/MADE.Testing/ComparableAssertExtensions.cs new file mode 100644 index 00000000..afb0d7de --- /dev/null +++ b/src/MADE.Testing/ComparableAssertExtensions.cs @@ -0,0 +1,74 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines a code assertion helper for based scenarios. +/// +public static class ComparableAssertExtensions +{ + /// + /// Tests whether the specified value is greater than the given threshold and throws an exception if it is not. + /// + /// The type of value to compare. + /// The value to test. + /// The threshold value that the should be greater than. + /// Thrown if the is not greater than the . + public static void ShouldBeGreaterThan(this T value, T threshold) + where T : IComparable + { + if (value.CompareTo(threshold) <= 0) + { + throw new AssertFailedException($"{nameof(ShouldBeGreaterThan)} failed. Expected '{value}' to be greater than '{threshold}'."); + } + } + + /// + /// Tests whether the specified value is greater than or equal to the given threshold and throws an exception if it is not. + /// + /// The type of value to compare. + /// The value to test. + /// The threshold value that the should be greater than or equal to. + /// Thrown if the is less than the . + public static void ShouldBeGreaterThanOrEqualTo(this T value, T threshold) + where T : IComparable + { + if (value.CompareTo(threshold) < 0) + { + throw new AssertFailedException($"{nameof(ShouldBeGreaterThanOrEqualTo)} failed. Expected '{value}' to be greater than or equal to '{threshold}'."); + } + } + + /// + /// Tests whether the specified value is less than the given threshold and throws an exception if it is not. + /// + /// The type of value to compare. + /// The value to test. + /// The threshold value that the should be less than. + /// Thrown if the is not less than the . + public static void ShouldBeLessThan(this T value, T threshold) + where T : IComparable + { + if (value.CompareTo(threshold) >= 0) + { + throw new AssertFailedException($"{nameof(ShouldBeLessThan)} failed. Expected '{value}' to be less than '{threshold}'."); + } + } + + /// + /// Tests whether the specified value is less than or equal to the given threshold and throws an exception if it is not. + /// + /// The type of value to compare. + /// The value to test. + /// The threshold value that the should be less than or equal to. + /// Thrown if the is greater than the . + public static void ShouldBeLessThanOrEqualTo(this T value, T threshold) + where T : IComparable + { + if (value.CompareTo(threshold) > 0) + { + throw new AssertFailedException($"{nameof(ShouldBeLessThanOrEqualTo)} failed. Expected '{value}' to be less than or equal to '{threshold}'."); + } + } +} diff --git a/src/MADE.Testing/ExceptionAssertExtensions.cs b/src/MADE.Testing/ExceptionAssertExtensions.cs new file mode 100644 index 00000000..fe9ad78a --- /dev/null +++ b/src/MADE.Testing/ExceptionAssertExtensions.cs @@ -0,0 +1,96 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines a code assertion helper for exception-based scenarios. +/// +public static class ExceptionAssertExtensions +{ + /// + /// Tests whether the specified action throws an exception of the given type and throws an assertion exception if it does not. + /// + /// The type of exception expected to be thrown. + /// The action to execute. + /// The exception that was thrown. + /// Thrown if the does not throw an exception of type . + public static TException ShouldThrow(this Action action) + where TException : Exception + { + try + { + action(); + } + catch (TException ex) + { + return ex; + } + catch (Exception ex) + { + throw new AssertFailedException($"{nameof(ShouldThrow)} failed. Expected exception of type '{typeof(TException).Name}' but '{ex.GetType().Name}' was thrown."); + } + + throw new AssertFailedException($"{nameof(ShouldThrow)} failed. Expected exception of type '{typeof(TException).Name}' but no exception was thrown."); + } + + /// + /// Tests whether the specified asynchronous action throws an exception of the given type and throws an assertion exception if it does not. + /// + /// The type of exception expected to be thrown. + /// The asynchronous action to execute. + /// The exception that was thrown. + /// Thrown if the does not throw an exception of type . + public static async Task ShouldThrowAsync(this Func action) + where TException : Exception + { + try + { + await action().ConfigureAwait(false); + } + catch (TException ex) + { + return ex; + } + catch (Exception ex) + { + throw new AssertFailedException($"{nameof(ShouldThrowAsync)} failed. Expected exception of type '{typeof(TException).Name}' but '{ex.GetType().Name}' was thrown."); + } + + throw new AssertFailedException($"{nameof(ShouldThrowAsync)} failed. Expected exception of type '{typeof(TException).Name}' but no exception was thrown."); + } + + /// + /// Tests whether the specified action does not throw any exception and throws an assertion exception if it does. + /// + /// The action to execute. + /// Thrown if the throws an exception. + public static void ShouldNotThrow(this Action action) + { + try + { + action(); + } + catch (Exception ex) + { + throw new AssertFailedException($"{nameof(ShouldNotThrow)} failed. Expected no exception but '{ex.GetType().Name}' was thrown: {ex.Message}"); + } + } + + /// + /// Tests whether the specified asynchronous action does not throw any exception and throws an assertion exception if it does. + /// + /// The asynchronous action to execute. + /// Thrown if the throws an exception. + public static async Task ShouldNotThrowAsync(this Func action) + { + try + { + await action().ConfigureAwait(false); + } + catch (Exception ex) + { + throw new AssertFailedException($"{nameof(ShouldNotThrowAsync)} failed. Expected no exception but '{ex.GetType().Name}' was thrown: {ex.Message}"); + } + } +} diff --git a/src/MADE.Testing/ObjectAssertExtensions.cs b/src/MADE.Testing/ObjectAssertExtensions.cs new file mode 100644 index 00000000..39546a29 --- /dev/null +++ b/src/MADE.Testing/ObjectAssertExtensions.cs @@ -0,0 +1,36 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines a code assertion helper for object-based scenarios. +/// +public static class ObjectAssertExtensions +{ + /// + /// Tests whether the specified value is null and throws an exception if it is not null. + /// + /// The value to test. + /// Thrown if the is not null. + public static void ShouldBeNull(this object? value) + { + if (value is not null) + { + throw new AssertFailedException($"{nameof(ShouldBeNull)} failed. Expected null but was '{value}'."); + } + } + + /// + /// Tests whether the specified value is not null and throws an exception if it is null. + /// + /// The value to test. + /// Thrown if the is null. + public static void ShouldNotBeNull(this object? value) + { + if (value is null) + { + throw new AssertFailedException($"{nameof(ShouldNotBeNull)} failed. Expected a non-null value."); + } + } +} diff --git a/src/MADE.Testing/StringAssertExtensions.cs b/src/MADE.Testing/StringAssertExtensions.cs new file mode 100644 index 00000000..8e594609 --- /dev/null +++ b/src/MADE.Testing/StringAssertExtensions.cs @@ -0,0 +1,70 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Testing; + +/// +/// Defines a code assertion helper for string-based scenarios. +/// +public static class StringAssertExtensions +{ + /// + /// Tests whether the specified string contains the given substring and throws an exception if it does not. + /// + /// The string to test. + /// The substring to search for. + /// The string comparison type to use. Default is . + /// Thrown if the does not contain the . + public static void ShouldContain(this string? value, string substring, StringComparison comparisonType = StringComparison.Ordinal) + { + if (value is null || !value.Contains(substring, comparisonType)) + { + throw new AssertFailedException($"{nameof(ShouldContain)} failed. Expected '{value ?? "null"}' to contain '{substring}'."); + } + } + + /// + /// Tests whether the specified string does not contain the given substring and throws an exception if it does. + /// + /// The string to test. + /// The substring to search for. + /// The string comparison type to use. Default is . + /// Thrown if the contains the . + public static void ShouldNotContain(this string? value, string substring, StringComparison comparisonType = StringComparison.Ordinal) + { + if (value is not null && value.Contains(substring, comparisonType)) + { + throw new AssertFailedException($"{nameof(ShouldNotContain)} failed. Expected '{value}' to not contain '{substring}'."); + } + } + + /// + /// Tests whether the specified string starts with the given prefix and throws an exception if it does not. + /// + /// The string to test. + /// The prefix to search for. + /// The string comparison type to use. Default is . + /// Thrown if the does not start with the . + public static void ShouldStartWith(this string? value, string prefix, StringComparison comparisonType = StringComparison.Ordinal) + { + if (value is null || !value.StartsWith(prefix, comparisonType)) + { + throw new AssertFailedException($"{nameof(ShouldStartWith)} failed. Expected '{value ?? "null"}' to start with '{prefix}'."); + } + } + + /// + /// Tests whether the specified string ends with the given suffix and throws an exception if it does not. + /// + /// The string to test. + /// The suffix to search for. + /// The string comparison type to use. Default is . + /// Thrown if the does not end with the . + public static void ShouldEndWith(this string? value, string suffix, StringComparison comparisonType = StringComparison.Ordinal) + { + if (value is null || !value.EndsWith(suffix, comparisonType)) + { + throw new AssertFailedException($"{nameof(ShouldEndWith)} failed. Expected '{value ?? "null"}' to end with '{suffix}'."); + } + } +} diff --git a/src/MADE.Threading/AsyncLazy{T}.cs b/src/MADE.Threading/AsyncLazy{T}.cs new file mode 100644 index 00000000..4a27c225 --- /dev/null +++ b/src/MADE.Threading/AsyncLazy{T}.cs @@ -0,0 +1,57 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +namespace MADE.Threading; + +/// +/// Defines a provider for lazy asynchronous initialization of a value. +/// +/// The type of object that is being lazily initialized. +public class AsyncLazy +{ + private readonly Lazy> inner; + + /// + /// Initializes a new instance of the class with the specified asynchronous value factory. + /// + /// The asynchronous delegate that is invoked to produce the lazily initialized value when it is needed. + public AsyncLazy(Func> valueFactory) + { + this.inner = new Lazy>(valueFactory); + } + + /// + /// Initializes a new instance of the class with the specified asynchronous value factory and thread safety mode. + /// + /// The asynchronous delegate that is invoked to produce the lazily initialized value when it is needed. + /// A value indicating whether the instance should be usable concurrently by multiple threads. + public AsyncLazy(Func> valueFactory, bool isThreadSafe) + { + this.inner = new Lazy>(valueFactory, isThreadSafe); + } + + /// + /// Gets a value indicating whether a value has been created. + /// + public bool IsValueCreated => this.inner.IsValueCreated; + + /// + /// Gets the lazily initialized value as an awaitable task. + /// + /// An awaitable task that returns the lazily initialized value. + public TaskAwaiter GetAwaiter() + { + return this.inner.Value.GetAwaiter(); + } + + /// + /// Gets the lazily initialized value as a task. + /// + /// A task that returns the lazily initialized value. + public Task GetValueAsync() + { + return this.inner.Value; + } +} diff --git a/src/MADE.Threading/Debouncer.cs b/src/MADE.Threading/Debouncer.cs new file mode 100644 index 00000000..8b4fd85d --- /dev/null +++ b/src/MADE.Threading/Debouncer.cs @@ -0,0 +1,107 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Threading; + +/// +/// Defines a debouncer that delays execution of an action until a specified period of inactivity has elapsed. +/// +/// +/// This is useful for scenarios where rapid invocations should be collapsed into a single execution, such as search-as-you-type or window resize handling. +/// +public sealed class Debouncer : IDisposable +{ + private readonly object lockObj = new(); + + private CancellationTokenSource? cts; + + private bool disposed; + + /// + /// Gets or sets the delay period. Each invocation resets the timer. + /// + public TimeSpan Delay { get; set; } = TimeSpan.FromMilliseconds(300); + + /// + /// Debounces the specified action. If called again before the elapses, the previous pending invocation is cancelled. + /// + /// The action to execute after the delay. + /// Thrown if the is . + /// Thrown if the debouncer has been disposed. + public void Debounce(Action action) + { + ArgumentNullException.ThrowIfNull(action); + ObjectDisposedException.ThrowIf(this.disposed, this); + + lock (this.lockObj) + { + this.cts?.Cancel(); + this.cts?.Dispose(); + this.cts = new CancellationTokenSource(); + + var token = this.cts.Token; + + Task.Delay(this.Delay, token).ContinueWith( + _ => action(), + token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); + } + } + + /// + /// Debounces the specified asynchronous action. If called again before the elapses, the previous pending invocation is cancelled. + /// + /// The asynchronous action to execute after the delay. + /// Thrown if the is . + /// Thrown if the debouncer has been disposed. + public void DebounceAsync(Func action) + { + ArgumentNullException.ThrowIfNull(action); + ObjectDisposedException.ThrowIf(this.disposed, this); + + lock (this.lockObj) + { + this.cts?.Cancel(); + this.cts?.Dispose(); + this.cts = new CancellationTokenSource(); + + var token = this.cts.Token; + + Task.Delay(this.Delay, token).ContinueWith( + async _ => await action().ConfigureAwait(false), + token, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Default); + } + } + + /// + /// Cancels any pending debounced action. + /// + public void Cancel() + { + lock (this.lockObj) + { + this.cts?.Cancel(); + } + } + + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + lock (this.lockObj) + { + this.cts?.Cancel(); + this.cts?.Dispose(); + this.cts = null; + } + + this.disposed = true; + } +} diff --git a/src/MADE.Threading/Throttler.cs b/src/MADE.Threading/Throttler.cs new file mode 100644 index 00000000..7c4858b0 --- /dev/null +++ b/src/MADE.Threading/Throttler.cs @@ -0,0 +1,100 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Threading; + +/// +/// Defines a throttler that limits execution of an action to at most once per specified time interval. +/// +/// +/// This is useful for scenarios where you want to limit the rate of execution, +/// such as rate-limiting API calls or UI updates. +/// Unlike , the throttler executes the first invocation immediately +/// and then suppresses subsequent invocations until the interval elapses. +/// +public sealed class Throttler : IDisposable +{ + private readonly object lockObj = new(); + + private readonly SemaphoreSlim semaphore = new(1, 1); + + private DateTime lastInvocation = DateTime.MinValue; + + private bool disposed; + + /// + /// Gets or sets the minimum interval between executions. + /// + public TimeSpan Interval { get; set; } = TimeSpan.FromMilliseconds(300); + + /// + /// Throttles the specified action. Executes immediately if the has elapsed since the last execution; otherwise, the invocation is suppressed. + /// + /// The action to execute. + /// Thrown if the is . + /// Thrown if the throttler has been disposed. + public void Throttle(Action action) + { + ArgumentNullException.ThrowIfNull(action); + ObjectDisposedException.ThrowIf(this.disposed, this); + + lock (this.lockObj) + { + var now = DateTime.UtcNow; + if (now - this.lastInvocation < this.Interval) + { + return; + } + + this.lastInvocation = now; + } + + action(); + } + + /// + /// Throttles the specified asynchronous action. Executes immediately if the has elapsed since the last execution; otherwise, the invocation is suppressed. + /// + /// The asynchronous action to execute. + /// The cancellation token. + /// An asynchronous operation. + /// Thrown if the is . + /// Thrown if the throttler has been disposed. + /// Thrown if the is cancelled. + public async Task ThrottleAsync(Func action, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(action); + ObjectDisposedException.ThrowIf(this.disposed, this); + + await this.semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + + try + { + var now = DateTime.UtcNow; + if (now - this.lastInvocation < this.Interval) + { + return; + } + + this.lastInvocation = now; + } + finally + { + this.semaphore.Release(); + } + + await action().ConfigureAwait(false); + } + + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.semaphore.Dispose(); + this.disposed = true; + } +} diff --git a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs index e3d8b5f6..1a69ccf7 100644 --- a/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs +++ b/src/MADE.Web.Mvc/Extensions/ControllerBaseExtensions.cs @@ -65,4 +65,33 @@ public static IActionResult InternalServerError(this ControllerBase controller, return new InternalServerErrorObjectResult(modelState); } + + /// + /// Creates a that produces a response. + /// + /// The controller that is performing the response. + /// An error object to be returned to the client. + /// The created for the response. + /// Thrown if the is . + public static IActionResult Forbidden(this ControllerBase controller, object responseContent) + { + ArgumentNullException.ThrowIfNull(controller); + + return new ForbiddenObjectResult(responseContent); + } + + /// + /// Creates a that produces a response. + /// + /// The controller that is performing the response. + /// The containing errors to be returned to the client. + /// The created for the response. + /// Thrown if the or is . + public static IActionResult Forbidden(this ControllerBase controller, ModelStateDictionary modelState) + { + ArgumentNullException.ThrowIfNull(controller); + ArgumentNullException.ThrowIfNull(modelState); + + return new ForbiddenObjectResult(modelState); + } } diff --git a/src/MADE.Web.Mvc/Responses/ForbiddenObjectResult.cs b/src/MADE.Web.Mvc/Responses/ForbiddenObjectResult.cs new file mode 100644 index 00000000..77790121 --- /dev/null +++ b/src/MADE.Web.Mvc/Responses/ForbiddenObjectResult.cs @@ -0,0 +1,39 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace MADE.Web.Mvc.Responses; + +/// +/// Defines an that when executed will produce a Forbidden (403) response. +/// +public class ForbiddenObjectResult : ObjectResult +{ + private const int DefaultStatusCode = StatusCodes.Status403Forbidden; + + /// + /// Initializes a new instance of the class. + /// + /// Contains the errors to be returned to the client. + public ForbiddenObjectResult(object error) + : base(error) + { + this.StatusCode = DefaultStatusCode; + } + + /// + /// Initializes a new instance of the class. + /// + /// The containing the validation errors. + /// Thrown if the is . + public ForbiddenObjectResult(ModelStateDictionary modelState) + : base(new SerializableError(modelState)) + { + ArgumentNullException.ThrowIfNull(modelState); + + this.StatusCode = DefaultStatusCode; + } +} diff --git a/src/MADE.Web/Extensions/HttpResponseExtensions.cs b/src/MADE.Web/Extensions/HttpResponseExtensions.cs index 3cb56a85..079456fa 100644 --- a/src/MADE.Web/Extensions/HttpResponseExtensions.cs +++ b/src/MADE.Web/Extensions/HttpResponseExtensions.cs @@ -23,13 +23,15 @@ public static class HttpResponseExtensions /// The HTTP response to write to. /// The status code of the response. /// The object to serialize as JSON. + /// The cancellation token. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, HttpStatusCode statusCode, - object value) + object value, + CancellationToken cancellationToken = default) { - await WriteJsonAsync(response, (int)statusCode, value, null).ConfigureAwait(false); + await WriteJsonAsync(response, (int)statusCode, value, null, cancellationToken).ConfigureAwait(false); } /// @@ -38,13 +40,15 @@ public static async Task WriteJsonAsync( /// The HTTP response to write to. /// The status code of the response. /// The object to serialize as JSON. + /// The cancellation token. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, int statusCode, - object value) + object value, + CancellationToken cancellationToken = default) { - await WriteJsonAsync(response, statusCode, value, null).ConfigureAwait(false); + await WriteJsonAsync(response, statusCode, value, null, cancellationToken).ConfigureAwait(false); } /// @@ -54,14 +58,16 @@ public static async Task WriteJsonAsync( /// The status code of the response. /// The object to serialize as JSON. /// The JSON serializer options. + /// The cancellation token. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, HttpStatusCode statusCode, object value, - JsonSerializerOptions? serializerOptions) + JsonSerializerOptions? serializerOptions, + CancellationToken cancellationToken = default) { - await WriteJsonAsync(response, (int)statusCode, value, serializerOptions).ConfigureAwait(false); + await WriteJsonAsync(response, (int)statusCode, value, serializerOptions, cancellationToken).ConfigureAwait(false); } /// @@ -71,12 +77,14 @@ public static async Task WriteJsonAsync( /// The status code of the response. /// The object to serialize as JSON. /// The JSON serializer options. + /// The cancellation token. /// An asynchronous operation. public static async Task WriteJsonAsync( this HttpResponse response, int statusCode, object value, - JsonSerializerOptions? serializerOptions) + JsonSerializerOptions? serializerOptions, + CancellationToken cancellationToken = default) { response.ContentType = new MediaTypeHeaderValue("application/json") { Encoding = Encoding.UTF8 }.ToString(); response.StatusCode = statusCode; @@ -85,6 +93,6 @@ public static async Task WriteJsonAsync( string json = JsonSerializer.Serialize(value, options); - await response.WriteAsync(json, Encoding.UTF8).ConfigureAwait(false); + await response.WriteAsync(json, Encoding.UTF8, cancellationToken).ConfigureAwait(false); } } diff --git a/src/MADE.Web/MADE.Web.csproj b/src/MADE.Web/MADE.Web.csproj index b2e0753d..9b6ab40d 100644 --- a/src/MADE.Web/MADE.Web.csproj +++ b/src/MADE.Web/MADE.Web.csproj @@ -14,13 +14,13 @@ - - + + - - + + \ No newline at end of file diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 952a23e7..9a757638 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -16,14 +16,16 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + \ No newline at end of file diff --git a/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj b/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj index aea6f21e..8f070583 100644 --- a/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj +++ b/tests/MADE.Collections.Tests/MADE.Collections.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj b/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj index c81b576b..12662797 100644 --- a/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj +++ b/tests/MADE.Data.Converters.Tests/MADE.Data.Converters.Tests.csproj @@ -3,12 +3,7 @@ net8.0;net10.0 - - - - - - + diff --git a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj index 5a11dcc2..10e8007d 100644 --- a/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj +++ b/tests/MADE.Data.EFCore.Tests/MADE.Data.EFCore.Tests.csproj @@ -4,19 +4,14 @@ net8.0;net10.0 - - - - - - - + + - - + + diff --git a/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj b/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj index 50d5d721..55eec040 100644 --- a/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj +++ b/tests/MADE.Data.Serialization.Tests/MADE.Data.Serialization.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj b/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj index 2f487473..a35ad38f 100644 --- a/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj +++ b/tests/MADE.Data.Validation.FluentValidation.Tests/MADE.Data.Validation.FluentValidation.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj b/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj index d1d1734f..200839be 100644 --- a/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj +++ b/tests/MADE.Data.Validation.Tests/MADE.Data.Validation.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj b/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj index 67cfb976..dbb5dbd4 100644 --- a/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj +++ b/tests/MADE.Diagnostics.Tests/MADE.Diagnostics.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj index b3d1933a..e3739ca0 100644 --- a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj +++ b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - diff --git a/tests/MADE.Web.Tests/MADE.Web.Tests.csproj b/tests/MADE.Web.Tests/MADE.Web.Tests.csproj index f7b36a45..7b61d815 100644 --- a/tests/MADE.Web.Tests/MADE.Web.Tests.csproj +++ b/tests/MADE.Web.Tests/MADE.Web.Tests.csproj @@ -4,11 +4,6 @@ net8.0;net10.0 - - - - - From 1b0aa762cce1876e5f8872728421630f434b4800 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 10:50:05 +0100 Subject: [PATCH 09/12] feat: add value converters and extensions for DateTime, string, and file size; enhance length and math conversions --- .github/workflows/docs.yml | 6 +- docs/articles/features/data-converters.md | 55 +++++++- docs/articles/intro.md | 12 +- .../DateTimeToUnixTimestampValueConverter.cs | 46 +++++++ .../Extensions/FileSizeExtensions.cs | 49 +++++++ .../Extensions/LengthExtensions.cs | 73 +++++++++- .../Extensions/MathExtensions.cs | 14 ++ .../Extensions/StringExtensions.cs | 44 ++++++ .../Extensions/TimeSpanExtensions.cs | 84 +++++++++++ .../StringToEnumValueConverter{TEnum}.cs | 62 +++++++++ ...eTimeToUnixTimestampValueConverterTests.cs | 47 +++++++ .../Tests/LengthExtensionsTests.cs | 130 ++++++++++++++++++ .../Tests/StringToEnumValueConverterTests.cs | 56 ++++++++ .../MADE.Threading.Tests.csproj | 11 ++ .../Tests/AsyncLazyTests.cs | 76 ++++++++++ .../Tests/DebouncerTests.cs | 63 +++++++++ .../Tests/ThrottlerTests.cs | 77 +++++++++++ 17 files changed, 896 insertions(+), 9 deletions(-) create mode 100644 src/MADE.Data.Converters/DateTimeToUnixTimestampValueConverter.cs create mode 100644 src/MADE.Data.Converters/Extensions/FileSizeExtensions.cs create mode 100644 src/MADE.Data.Converters/Extensions/TimeSpanExtensions.cs create mode 100644 src/MADE.Data.Converters/StringToEnumValueConverter{TEnum}.cs create mode 100644 tests/MADE.Data.Converters.Tests/Tests/DateTimeToUnixTimestampValueConverterTests.cs create mode 100644 tests/MADE.Data.Converters.Tests/Tests/LengthExtensionsTests.cs create mode 100644 tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs create mode 100644 tests/MADE.Threading.Tests/MADE.Threading.Tests.csproj create mode 100644 tests/MADE.Threading.Tests/Tests/AsyncLazyTests.cs create mode 100644 tests/MADE.Threading.Tests/Tests/DebouncerTests.cs create mode 100644 tests/MADE.Threading.Tests/Tests/ThrottlerTests.cs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3c306904..816ed6c3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,6 +16,9 @@ on: - .github/workflows/docs.yml workflow_dispatch: +permissions: + contents: write + jobs: generate-docs: @@ -38,8 +41,7 @@ jobs: run: dotnet tool install -g docfx - name: Restore projects - run: dotnet restore - working-directory: src + run: dotnet restore MADE.NET.sln - name: Build DocFX site working-directory: docs diff --git a/docs/articles/features/data-converters.md b/docs/articles/features/data-converters.md index 9d344176..47ad335d 100644 --- a/docs/articles/features/data-converters.md +++ b/docs/articles/features/data-converters.md @@ -58,6 +58,30 @@ These can be used to convert any type to another. Whatever data conversion you t If there is a common value converter you think is missing from MADE.NET, [raise a tracking item on GitHub](https://github.com/MADE-Apps/MADE.NET/issues/new/choose) and we'll get it implemented. +## Converting strings to enums using the StringToEnumValueConverter + +The `MADE.Data.Converters.StringToEnumValueConverter` converts between string values and enum types. It supports case-insensitive matching by default. + +```csharp +var converter = new StringToEnumValueConverter(); + +DayOfWeek day = converter.Convert("Monday"); // DayOfWeek.Monday +string name = converter.ConvertBack(DayOfWeek.Friday); // "Friday" +``` + +Set `IgnoreCase` to `false` if you need exact case matching. The converter throws `InvalidDataConversionException` if the string cannot be parsed as the target enum type. + +## Converting DateTime to Unix timestamps using the DateTimeToUnixTimestampValueConverter + +The `MADE.Data.Converters.DateTimeToUnixTimestampValueConverter` converts between `DateTime` and Unix timestamps (seconds since 1970-01-01 UTC). + +```csharp +var converter = new DateTimeToUnixTimestampValueConverter(); + +long timestamp = converter.Convert(new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc)); +DateTime dateTime = converter.ConvertBack(timestamp); +``` + ## DateTime extensions The `MADE.Data.Converters.Extensions.DateTimeExtensions` class provides a comprehensive set of extensions for working with `DateTime` values: @@ -84,6 +108,18 @@ The `MADE.Data.Converters.Extensions.StringExtensions` class provides extensions - `ToFloat()` / `ToNullableFloat()` - Parses a string to a float. - `ToDouble()` / `ToNullableDouble()` - Parses a string to a double. - `ToBoolean()` - Parses a string to a boolean. +- `ToSlug()` - Converts a string to a URL-friendly slug by removing diacritics, replacing non-alphanumeric characters with hyphens, and lowercasing. + +```csharp +string slug = "Hello World! Cafe\u0301".ToSlug(); // "hello-world-cafe" +``` + +## TimeSpan extensions + +The `MADE.Data.Converters.Extensions.TimeSpanExtensions` class provides extensions for working with `TimeSpan` values: + +- `ToHumanReadableString()` - Converts a TimeSpan to a human-readable string such as "2 hours 30 minutes". +- `TotalWeeks()` - Gets the total number of whole weeks in a TimeSpan. ## Boolean extensions @@ -96,7 +132,10 @@ string result = isActive.ToFormattedString("Active", "Inactive"); // "Active" ## Math extensions -The `MADE.Data.Converters.Extensions.MathExtensions` class provides extensions for common mathematic expressions including `ToRadians` to convert a degrees value to radians. +The `MADE.Data.Converters.Extensions.MathExtensions` class provides extensions for common mathematic expressions: + +- `ToRadians()` - Converts a degrees value to radians. +- `ToDegrees()` - Converts a radians value to degrees. ## Length extensions @@ -104,3 +143,17 @@ The `MADE.Data.Converters.Extensions.LengthExtensions` class provides extensions - `ToMeters()` - Converts a value from miles to meters. - `ToMiles()` - Converts a value from meters to miles. +- `KilometersToMeters()` / `ToKilometers()` - Converts between kilometers and meters. +- `FeetToMeters()` / `ToFeet()` - Converts between feet and meters. +- `InchesToMeters()` / `ToInches()` - Converts between inches and meters. + +## File size extensions + +The `MADE.Data.Converters.Extensions.FileSizeExtensions` class provides extensions for converting byte values to human-readable file size strings: + +- `ToHumanReadableFileSize()` - Converts a byte count to a string such as "1.50 MB" or "256 B". + +```csharp +long bytes = 1_572_864; +string size = bytes.ToHumanReadableFileSize(); // "1.50 MB" +``` diff --git a/docs/articles/intro.md b/docs/articles/intro.md index c985fbd3..f4f14f39 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -66,10 +66,14 @@ It includes features such as: - BooleanToStringValueConverter, a value converter for converting `bool` values to configurable `String` representations, with the capability to convert back. - DateTimeToStringValueConverter, a value converter that takes a `DateTime` string format parameter to convert a `DateTime` value to a `String`, with the capability to convert back. -- DateTimeExtensions, a collection of useful extensions for interacting with `DateTime` values including `ToCurrentAge` (to get an age in years based on a given date from today), `StartOfDay`/`EndOfDay`, `StartOfWeek`/`EndOfWeek`, `StartOfMonth`/`EndOfMonth`, `StartOfYear`/`EndOfYear`, and `ToNearestHour`. -- MathExtensions, a collection of extensions for common mathematic expressions including `ToRadians` (to convert a degrees value to radians). -- StringExtensions, a collection of extensions for manipulating `String` values such as `ToTitleCase`, `ToDefaultCase`, `Truncate`, `ToBase64`, `FromBase64`, `ToInt`, `ToBoolean`, `ToFloat`, and `ToDouble`. -- LengthExtensions, for converting length values such as `ToMeters` and `ToMiles`. +- StringToEnumValueConverter, a generic value converter for converting between `String` and any `Enum` type with case-insensitive matching. +- DateTimeToUnixTimestampValueConverter, a value converter between `DateTime` and Unix timestamps. +- DateTimeExtensions, a collection of useful extensions for interacting with `DateTime` values including `ToCurrentAge`, `StartOfDay`/`EndOfDay`, `StartOfWeek`/`EndOfWeek`, `StartOfMonth`/`EndOfMonth`, `StartOfYear`/`EndOfYear`, and `ToNearestHour`. +- TimeSpanExtensions, providing `ToHumanReadableString` and `TotalWeeks`. +- MathExtensions, providing `ToRadians` and `ToDegrees` for angle conversions. +- StringExtensions, a collection of extensions for manipulating `String` values such as `ToTitleCase`, `ToDefaultCase`, `Truncate`, `ToBase64`, `FromBase64`, `ToSlug`, `ToInt`, `ToBoolean`, `ToFloat`, and `ToDouble`. +- LengthExtensions, for converting between miles, meters, kilometers, feet, and inches. +- FileSizeExtensions, for converting byte counts to human-readable file size strings. - BooleanExtensions, for formatting `bool` values to custom string representations with `ToFormattedString`. diff --git a/src/MADE.Data.Converters/DateTimeToUnixTimestampValueConverter.cs b/src/MADE.Data.Converters/DateTimeToUnixTimestampValueConverter.cs new file mode 100644 index 00000000..4a0f3344 --- /dev/null +++ b/src/MADE.Data.Converters/DateTimeToUnixTimestampValueConverter.cs @@ -0,0 +1,46 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Data.Converters.Constants; + +namespace MADE.Data.Converters; + +/// +/// Defines a value converter from to a Unix timestamp represented as a . +/// +public class DateTimeToUnixTimestampValueConverter : IValueConverter +{ + /// + /// Converts the value to a Unix timestamp in seconds. + /// + /// + /// The value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The Unix timestamp in seconds since the Unix epoch (1970-01-01 00:00:00 UTC). + /// + public long Convert(DateTime value, object? parameter = default) + { + return (long)(value.ToUniversalTime() - DateTimeConstants.UnixEpoch).TotalSeconds; + } + + /// + /// Converts a Unix timestamp in seconds back to a in UTC. + /// + /// + /// The Unix timestamp in seconds to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted in UTC. + /// + public DateTime ConvertBack(long value, object? parameter = default) + { + return DateTimeConstants.UnixEpoch.AddSeconds(value); + } +} diff --git a/src/MADE.Data.Converters/Extensions/FileSizeExtensions.cs b/src/MADE.Data.Converters/Extensions/FileSizeExtensions.cs new file mode 100644 index 00000000..c8a9d17d --- /dev/null +++ b/src/MADE.Data.Converters/Extensions/FileSizeExtensions.cs @@ -0,0 +1,49 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Data.Converters.Extensions; + +/// +/// Defines a collection of extensions for converting byte values to human-readable file size representations. +/// +public static class FileSizeExtensions +{ + private static readonly string[] SizeUnits = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; + + /// + /// Converts a byte count to a human-readable file size string using binary units (1 KB = 1024 bytes). + /// + /// The byte count to convert. + /// The number of decimal places to display. Default is 2. + /// A human-readable file size string such as "1.50 MB" or "256 B". + public static string ToHumanReadableFileSize(this long bytes, int decimalPlaces = 2) + { + if (bytes < 0) + { + return $"-{(-bytes).ToHumanReadableFileSize(decimalPlaces)}"; + } + + if (bytes == 0) + { + return "0 B"; + } + + int unitIndex = (int)Math.Floor(Math.Log(bytes, 1024)); + unitIndex = Math.Min(unitIndex, SizeUnits.Length - 1); + + double size = bytes / Math.Pow(1024, unitIndex); + + return $"{size.ToString($"F{decimalPlaces}")} {SizeUnits[unitIndex]}"; + } + + /// + /// Converts a byte count to a human-readable file size string using binary units (1 KB = 1024 bytes). + /// + /// The byte count to convert. + /// The number of decimal places to display. Default is 2. + /// A human-readable file size string such as "1.50 MB" or "256 B". + public static string ToHumanReadableFileSize(this double bytes, int decimalPlaces = 2) + { + return ((long)bytes).ToHumanReadableFileSize(decimalPlaces); + } +} diff --git a/src/MADE.Data.Converters/Extensions/LengthExtensions.cs b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs index 82b23ac5..139ff0dc 100644 --- a/src/MADE.Data.Converters/Extensions/LengthExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs @@ -8,6 +8,15 @@ namespace MADE.Data.Converters.Extensions; /// public static class LengthExtensions { + private const double MetersPerMile = 1609.344; + private const double MilesPerMeter = 1.0 / MetersPerMile; + private const double MetersPerKilometer = 1000.0; + private const double KilometersPerMeter = 1.0 / MetersPerKilometer; + private const double MetersPerFoot = 0.3048; + private const double FeetPerMeter = 1.0 / MetersPerFoot; + private const double MetersPerInch = 0.0254; + private const double InchesPerMeter = 1.0 / MetersPerInch; + /// /// Converts a distance measured in miles to a distance measured in meters. /// @@ -15,7 +24,7 @@ public static class LengthExtensions /// The meters that represent the miles. public static double ToMeters(this double miles) { - return miles * 1609.344; + return miles * MetersPerMile; } /// @@ -25,6 +34,66 @@ public static double ToMeters(this double miles) /// The miles that represent the meters. public static double ToMiles(this double meters) { - return meters / 1609.344; + return meters * MilesPerMeter; + } + + /// + /// Converts a distance measured in kilometers to a distance measured in meters. + /// + /// The kilometers to convert to meters. + /// The meters that represent the kilometers. + public static double KilometersToMeters(this double kilometers) + { + return kilometers * MetersPerKilometer; + } + + /// + /// Converts a distance measured in meters to a distance measured in kilometers. + /// + /// The meters to convert to kilometers. + /// The kilometers that represent the meters. + public static double ToKilometers(this double meters) + { + return meters * KilometersPerMeter; + } + + /// + /// Converts a distance measured in feet to a distance measured in meters. + /// + /// The feet to convert to meters. + /// The meters that represent the feet. + public static double FeetToMeters(this double feet) + { + return feet * MetersPerFoot; + } + + /// + /// Converts a distance measured in meters to a distance measured in feet. + /// + /// The meters to convert to feet. + /// The feet that represent the meters. + public static double ToFeet(this double meters) + { + return meters * FeetPerMeter; + } + + /// + /// Converts a distance measured in inches to a distance measured in meters. + /// + /// The inches to convert to meters. + /// The meters that represent the inches. + public static double InchesToMeters(this double inches) + { + return inches * MetersPerInch; + } + + /// + /// Converts a distance measured in meters to a distance measured in inches. + /// + /// The meters to convert to inches. + /// The inches that represent the meters. + public static double ToInches(this double meters) + { + return meters * InchesPerMeter; } } diff --git a/src/MADE.Data.Converters/Extensions/MathExtensions.cs b/src/MADE.Data.Converters/Extensions/MathExtensions.cs index a7c72ce2..9e60a8cd 100644 --- a/src/MADE.Data.Converters/Extensions/MathExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/MathExtensions.cs @@ -21,4 +21,18 @@ public static double ToRadians(this double degrees) { return degrees * (System.Math.PI / 180); } + + /// + /// Converts a radians value to a degrees value. + /// + /// + /// The radians value to convert. + /// + /// + /// The converted value as degrees. + /// + public static double ToDegrees(this double radians) + { + return radians * (180 / System.Math.PI); + } } diff --git a/src/MADE.Data.Converters/Extensions/StringExtensions.cs b/src/MADE.Data.Converters/Extensions/StringExtensions.cs index c02ca73d..9a00d896 100644 --- a/src/MADE.Data.Converters/Extensions/StringExtensions.cs +++ b/src/MADE.Data.Converters/Extensions/StringExtensions.cs @@ -2,8 +2,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.IO; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; namespace MADE.Data.Converters.Extensions; @@ -269,4 +271,46 @@ public static double ToDouble(this string value) bool parsed = double.TryParse(value, out double doubleValue); return parsed ? doubleValue : null; } + + /// + /// Converts a string to a URL-friendly slug by removing diacritics, replacing non-alphanumeric characters with hyphens, and lowercasing. + /// + /// The value to convert to a slug. + /// A URL-friendly slug string. + public static string ToSlug(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return string.Empty; + } + + // Normalize to decompose characters (e.g., e with accent -> e + combining accent) + string normalized = value.Normalize(NormalizationForm.FormD); + + // Remove non-spacing marks (diacritics/accents) + var stripped = new StringBuilder(); + foreach (char c in normalized) + { + if (CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark) + { + stripped.Append(c); + } + } + + string result = stripped.ToString().Normalize(NormalizationForm.FormC); + + // Lowercase + result = result.ToLowerInvariant(); + + // Replace non-alphanumeric characters with hyphens + result = Regex.Replace(result, @"[^a-z0-9\s-]", string.Empty); + + // Replace whitespace and multiple hyphens with a single hyphen + result = Regex.Replace(result, @"[\s-]+", "-"); + + // Trim leading and trailing hyphens + result = result.Trim('-'); + + return result; + } } diff --git a/src/MADE.Data.Converters/Extensions/TimeSpanExtensions.cs b/src/MADE.Data.Converters/Extensions/TimeSpanExtensions.cs new file mode 100644 index 00000000..7cfcbefe --- /dev/null +++ b/src/MADE.Data.Converters/Extensions/TimeSpanExtensions.cs @@ -0,0 +1,84 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text; + +namespace MADE.Data.Converters.Extensions; + +/// +/// Defines a collection of extensions for values. +/// +public static class TimeSpanExtensions +{ + /// + /// Converts a to a human-readable string representation. + /// + /// The time span to convert. + /// + /// A human-readable string such as "2 hours 30 minutes" or "1 day 3 hours". + /// Returns "0 seconds" for a zero time span. + /// + public static string ToHumanReadableString(this TimeSpan timeSpan) + { + if (timeSpan == TimeSpan.Zero) + { + return "0 seconds"; + } + + var builder = new StringBuilder(); + bool isNegative = timeSpan < TimeSpan.Zero; + TimeSpan absolute = isNegative ? timeSpan.Negate() : timeSpan; + + if (isNegative) + { + builder.Append('-'); + } + + if (absolute.Days > 0) + { + builder.Append($"{absolute.Days} {(absolute.Days == 1 ? "day" : "days")}"); + } + + if (absolute.Hours > 0) + { + if (builder.Length > (isNegative ? 1 : 0)) + { + builder.Append(' '); + } + + builder.Append($"{absolute.Hours} {(absolute.Hours == 1 ? "hour" : "hours")}"); + } + + if (absolute.Minutes > 0) + { + if (builder.Length > (isNegative ? 1 : 0)) + { + builder.Append(' '); + } + + builder.Append($"{absolute.Minutes} {(absolute.Minutes == 1 ? "minute" : "minutes")}"); + } + + if (absolute.Seconds > 0) + { + if (builder.Length > (isNegative ? 1 : 0)) + { + builder.Append(' '); + } + + builder.Append($"{absolute.Seconds} {(absolute.Seconds == 1 ? "second" : "seconds")}"); + } + + return builder.ToString(); + } + + /// + /// Gets the total number of whole weeks represented by the . + /// + /// The time span to convert. + /// The total number of whole weeks. + public static int TotalWeeks(this TimeSpan timeSpan) + { + return (int)(timeSpan.TotalDays / 7); + } +} diff --git a/src/MADE.Data.Converters/StringToEnumValueConverter{TEnum}.cs b/src/MADE.Data.Converters/StringToEnumValueConverter{TEnum}.cs new file mode 100644 index 00000000..4abe2349 --- /dev/null +++ b/src/MADE.Data.Converters/StringToEnumValueConverter{TEnum}.cs @@ -0,0 +1,62 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Data.Converters.Exceptions; + +namespace MADE.Data.Converters; + +/// +/// Defines a value converter from to an type. +/// +/// The enum type to convert to and from. +public class StringToEnumValueConverter : IValueConverter + where TEnum : struct, Enum +{ + /// + /// Gets or sets a value indicating whether the conversion should ignore case. Default is true. + /// + public bool IgnoreCase { get; set; } = true; + + /// + /// Converts the value to the type. + /// + /// + /// The string value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The converted value. + /// + /// Thrown if the cannot be parsed as a . + public TEnum Convert(string value, object? parameter = default) + { + if (Enum.TryParse(value, this.IgnoreCase, out var result)) + { + return result; + } + + throw new InvalidDataConversionException( + nameof(StringToEnumValueConverter), + value, + $"Cannot convert '{value}' to {typeof(TEnum).Name}."); + } + + /// + /// Converts the value back to a . + /// + /// + /// The enum value to convert. + /// + /// + /// The optional parameter used to help with conversion. + /// + /// + /// The string representation of the value. + /// + public string ConvertBack(TEnum value, object? parameter = default) + { + return value.ToString(); + } +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/DateTimeToUnixTimestampValueConverterTests.cs b/tests/MADE.Data.Converters.Tests/Tests/DateTimeToUnixTimestampValueConverterTests.cs new file mode 100644 index 00000000..873f253a --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/DateTimeToUnixTimestampValueConverterTests.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Constants; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DateTimeToUnixTimestampValueConverterTests +{ + public class WhenConvertingToTimestamp + { + [Test] + public void ShouldReturnZeroForUnixEpoch() + { + var converter = new DateTimeToUnixTimestampValueConverter(); + converter.Convert(DateTimeConstants.UnixEpoch).ShouldBe(0); + } + + [Test] + public void ShouldReturnCorrectTimestamp() + { + var converter = new DateTimeToUnixTimestampValueConverter(); + var date = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); + converter.Convert(date).ShouldBe(1704067200); + } + } + + public class WhenConvertingFromTimestamp + { + [Test] + public void ShouldReturnUnixEpochForZero() + { + var converter = new DateTimeToUnixTimestampValueConverter(); + converter.ConvertBack(0).ShouldBe(DateTimeConstants.UnixEpoch); + } + + [Test] + public void ShouldRoundTrip() + { + var converter = new DateTimeToUnixTimestampValueConverter(); + var date = new DateTime(2024, 6, 15, 12, 30, 0, DateTimeKind.Utc); + converter.ConvertBack(converter.Convert(date)).ShouldBe(date, TimeSpan.FromSeconds(1)); + } + } +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/LengthExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/LengthExtensionsTests.cs new file mode 100644 index 00000000..0e705ef3 --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/LengthExtensionsTests.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class LengthExtensionsTests +{ + public class WhenConvertingMilesToMeters + { + [TestCase(0, 0)] + [TestCase(1, 1609.344)] + [TestCase(5, 8046.72)] + public void ShouldConvertCorrectly(double miles, double expectedMeters) + { + miles.ToMeters().ShouldBe(expectedMeters, 0.001); + } + } + + public class WhenConvertingMetersToMiles + { + [TestCase(0, 0)] + [TestCase(1609.344, 1)] + [TestCase(8046.72, 5)] + public void ShouldConvertCorrectly(double meters, double expectedMiles) + { + meters.ToMiles().ShouldBe(expectedMiles, 0.001); + } + } + + public class WhenConvertingKilometersToMeters + { + [TestCase(0, 0)] + [TestCase(1, 1000)] + [TestCase(2.5, 2500)] + public void ShouldConvertCorrectly(double km, double expectedMeters) + { + km.KilometersToMeters().ShouldBe(expectedMeters, 0.001); + } + } + + public class WhenConvertingMetersToKilometers + { + [TestCase(0, 0)] + [TestCase(1000, 1)] + [TestCase(2500, 2.5)] + public void ShouldConvertCorrectly(double meters, double expectedKm) + { + meters.ToKilometers().ShouldBe(expectedKm, 0.001); + } + } + + public class WhenConvertingFeetToMeters + { + [TestCase(0, 0)] + [TestCase(1, 0.3048)] + [TestCase(100, 30.48)] + public void ShouldConvertCorrectly(double feet, double expectedMeters) + { + feet.FeetToMeters().ShouldBe(expectedMeters, 0.001); + } + } + + public class WhenConvertingMetersToFeet + { + [TestCase(0, 0)] + [TestCase(0.3048, 1)] + [TestCase(30.48, 100)] + public void ShouldConvertCorrectly(double meters, double expectedFeet) + { + meters.ToFeet().ShouldBe(expectedFeet, 0.001); + } + } + + public class WhenConvertingInchesToMeters + { + [TestCase(0, 0)] + [TestCase(1, 0.0254)] + [TestCase(100, 2.54)] + public void ShouldConvertCorrectly(double inches, double expectedMeters) + { + inches.InchesToMeters().ShouldBe(expectedMeters, 0.001); + } + } + + public class WhenConvertingMetersToInches + { + [TestCase(0, 0)] + [TestCase(0.0254, 1)] + [TestCase(2.54, 100)] + public void ShouldConvertCorrectly(double meters, double expectedInches) + { + meters.ToInches().ShouldBe(expectedInches, 0.001); + } + } + + public class WhenRoundTripping + { + [TestCase(3.7)] + [TestCase(100.0)] + public void MilesToMetersShouldRoundTrip(double miles) + { + miles.ToMeters().ToMiles().ShouldBe(miles, 0.0001); + } + + [TestCase(5.0)] + [TestCase(42.195)] + public void KilometersToMetersShouldRoundTrip(double km) + { + km.KilometersToMeters().ToKilometers().ShouldBe(km, 0.0001); + } + + [TestCase(6.0)] + [TestCase(5280.0)] + public void FeetToMetersShouldRoundTrip(double feet) + { + feet.FeetToMeters().ToFeet().ShouldBe(feet, 0.0001); + } + + [TestCase(12.0)] + [TestCase(72.0)] + public void InchesToMetersShouldRoundTrip(double inches) + { + inches.InchesToMeters().ToInches().ShouldBe(inches, 0.0001); + } + } +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs b/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs new file mode 100644 index 00000000..3d434135 --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Exceptions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class StringToEnumValueConverterTests +{ + private enum TestEnum + { + None, + First, + Second, + Third, + } + + public class WhenConvertingStringToEnum + { + [TestCase("First", TestEnum.First)] + [TestCase("second", TestEnum.Second)] + [TestCase("THIRD", TestEnum.Third)] + public void ShouldConvertCaseInsensitiveByDefault(string input, TestEnum expected) + { + var converter = new StringToEnumValueConverter(); + converter.Convert(input).ShouldBe(expected); + } + + [Test] + public void ShouldThrowForInvalidValue() + { + var converter = new StringToEnumValueConverter(); + Should.Throw(() => converter.Convert("Invalid")); + } + + [Test] + public void ShouldRespectCaseSensitiveFlag() + { + var converter = new StringToEnumValueConverter { IgnoreCase = false }; + converter.Convert("First").ShouldBe(TestEnum.First); + Should.Throw(() => converter.Convert("first")); + } + } + + public class WhenConvertingEnumToString + { + [Test] + public void ShouldConvertToString() + { + var converter = new StringToEnumValueConverter(); + converter.ConvertBack(TestEnum.Second).ShouldBe("Second"); + } + } +} diff --git a/tests/MADE.Threading.Tests/MADE.Threading.Tests.csproj b/tests/MADE.Threading.Tests/MADE.Threading.Tests.csproj new file mode 100644 index 00000000..4b8f20bf --- /dev/null +++ b/tests/MADE.Threading.Tests/MADE.Threading.Tests.csproj @@ -0,0 +1,11 @@ + + + + net8.0;net10.0 + + + + + + + diff --git a/tests/MADE.Threading.Tests/Tests/AsyncLazyTests.cs b/tests/MADE.Threading.Tests/Tests/AsyncLazyTests.cs new file mode 100644 index 00000000..ca23b387 --- /dev/null +++ b/tests/MADE.Threading.Tests/Tests/AsyncLazyTests.cs @@ -0,0 +1,76 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Threading.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class AsyncLazyTests +{ + public class WhenGettingValue + { + [Test] + public async Task ShouldReturnValueFromFactory() + { + // Arrange + var lazy = new AsyncLazy(() => Task.FromResult(42)); + + // Act + int result = await lazy; + + // Assert + result.ShouldBe(42); + } + + [Test] + public async Task ShouldOnlyInvokeFactoryOnce() + { + // Arrange + int callCount = 0; + var lazy = new AsyncLazy(() => + { + Interlocked.Increment(ref callCount); + return Task.FromResult(99); + }); + + // Act + int result1 = await lazy; + int result2 = await lazy; + + // Assert + result1.ShouldBe(99); + result2.ShouldBe(99); + callCount.ShouldBe(1); + } + + [Test] + public async Task ShouldReportIsValueCreatedAfterAccess() + { + // Arrange + var lazy = new AsyncLazy(() => Task.FromResult("hello")); + + // Assert - before + lazy.IsValueCreated.ShouldBeFalse(); + + // Act + await lazy; + + // Assert - after + lazy.IsValueCreated.ShouldBeTrue(); + } + + [Test] + public async Task ShouldReturnSameValueViaGetValueAsync() + { + // Arrange + var lazy = new AsyncLazy(() => Task.FromResult(7)); + + // Act + int result = await lazy.GetValueAsync(); + + // Assert + result.ShouldBe(7); + } + } +} diff --git a/tests/MADE.Threading.Tests/Tests/DebouncerTests.cs b/tests/MADE.Threading.Tests/Tests/DebouncerTests.cs new file mode 100644 index 00000000..3bdcaef8 --- /dev/null +++ b/tests/MADE.Threading.Tests/Tests/DebouncerTests.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Threading.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class DebouncerTests +{ + public class WhenDebouncing + { + [Test] + public async Task ShouldExecuteActionAfterDelay() + { + // Arrange + using var debouncer = new Debouncer { Delay = TimeSpan.FromMilliseconds(50) }; + int callCount = 0; + + // Act + debouncer.Debounce(() => Interlocked.Increment(ref callCount)); + await Task.Delay(150); + + // Assert + callCount.ShouldBe(1); + } + + [Test] + public async Task ShouldCollapseRapidInvocationsIntoOne() + { + // Arrange + using var debouncer = new Debouncer { Delay = TimeSpan.FromMilliseconds(100) }; + int callCount = 0; + + // Act - rapid fire + debouncer.Debounce(() => Interlocked.Increment(ref callCount)); + await Task.Delay(20); + debouncer.Debounce(() => Interlocked.Increment(ref callCount)); + await Task.Delay(20); + debouncer.Debounce(() => Interlocked.Increment(ref callCount)); + await Task.Delay(200); + + // Assert - only the last one should have executed + callCount.ShouldBe(1); + } + + [Test] + public async Task ShouldNotExecuteAfterCancel() + { + // Arrange + using var debouncer = new Debouncer { Delay = TimeSpan.FromMilliseconds(100) }; + int callCount = 0; + + // Act + debouncer.Debounce(() => Interlocked.Increment(ref callCount)); + debouncer.Cancel(); + await Task.Delay(200); + + // Assert + callCount.ShouldBe(0); + } + } +} diff --git a/tests/MADE.Threading.Tests/Tests/ThrottlerTests.cs b/tests/MADE.Threading.Tests/Tests/ThrottlerTests.cs new file mode 100644 index 00000000..e8606385 --- /dev/null +++ b/tests/MADE.Threading.Tests/Tests/ThrottlerTests.cs @@ -0,0 +1,77 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Threading.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ThrottlerTests +{ + public class WhenThrottling + { + [Test] + public void ShouldExecuteFirstInvocationImmediately() + { + // Arrange + using var throttler = new Throttler { Interval = TimeSpan.FromMilliseconds(500) }; + int callCount = 0; + + // Act + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + + // Assert + callCount.ShouldBe(1); + } + + [Test] + public void ShouldSuppressRapidInvocations() + { + // Arrange + using var throttler = new Throttler { Interval = TimeSpan.FromMilliseconds(500) }; + int callCount = 0; + + // Act + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + + // Assert + callCount.ShouldBe(1); + } + + [Test] + public async Task ShouldAllowExecutionAfterIntervalElapses() + { + // Arrange + using var throttler = new Throttler { Interval = TimeSpan.FromMilliseconds(50) }; + int callCount = 0; + + // Act + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + await Task.Delay(100); + throttler.Throttle(() => Interlocked.Increment(ref callCount)); + + // Assert + callCount.ShouldBe(2); + } + } + + public class WhenThrottlingAsync + { + [Test] + public async Task ShouldSuppressRapidAsyncInvocations() + { + // Arrange + using var throttler = new Throttler { Interval = TimeSpan.FromMilliseconds(500) }; + int callCount = 0; + + // Act + await throttler.ThrottleAsync(() => { Interlocked.Increment(ref callCount); return Task.CompletedTask; }); + await throttler.ThrottleAsync(() => { Interlocked.Increment(ref callCount); return Task.CompletedTask; }); + + // Assert + callCount.ShouldBe(1); + } + } +} From 4dac51d2a19db8116f7f8f701a865f53b57eee60 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 11:10:16 +0100 Subject: [PATCH 10/12] test: add tests for new features across all packages --- MADE.NET.sln | 21 +++ .../Tests/FileSizeExtensionsTests.cs | 49 +++++++ .../Tests/MathExtensionsTests.cs | 24 ++++ .../Tests/StringExtensionsTests.cs | 17 +++ .../Tests/StringToEnumValueConverterTests.cs | 2 +- .../Tests/TimeSpanExtensionsTests.cs | 56 ++++++++ .../Tests/AuditableEntityTests.cs | 80 +++++++++++ .../Tests/SoftDeleteExtensionsTests.cs | 72 ++++++++++ .../Tests/AsyncValidatorCollectionTests.cs | 121 +++++++++++++++++ .../Tests/RetryDelegatingHandlerTests.cs | 125 ++++++++++++++++++ .../MADE.Testing.Tests.csproj | 11 ++ .../Tests/BooleanAssertExtensionsTests.cs | 40 ++++++ .../Tests/ComparableAssertExtensionsTests.cs | 94 +++++++++++++ .../Tests/ExceptionAssertExtensionsTests.cs | 80 +++++++++++ .../Tests/ObjectAssertExtensionsTests.cs | 44 ++++++ .../Tests/StringAssertExtensionsTests.cs | 77 +++++++++++ .../MADE.Web.Mvc.Tests.csproj | 11 ++ .../Tests/ForbiddenObjectResultTests.cs | 26 ++++ 18 files changed, 949 insertions(+), 1 deletion(-) create mode 100644 tests/MADE.Data.Converters.Tests/Tests/FileSizeExtensionsTests.cs create mode 100644 tests/MADE.Data.Converters.Tests/Tests/TimeSpanExtensionsTests.cs create mode 100644 tests/MADE.Data.EFCore.Tests/Tests/AuditableEntityTests.cs create mode 100644 tests/MADE.Data.EFCore.Tests/Tests/SoftDeleteExtensionsTests.cs create mode 100644 tests/MADE.Data.Validation.Tests/Tests/AsyncValidatorCollectionTests.cs create mode 100644 tests/MADE.Networking.Tests/Tests/RetryDelegatingHandlerTests.cs create mode 100644 tests/MADE.Testing.Tests/MADE.Testing.Tests.csproj create mode 100644 tests/MADE.Testing.Tests/Tests/BooleanAssertExtensionsTests.cs create mode 100644 tests/MADE.Testing.Tests/Tests/ComparableAssertExtensionsTests.cs create mode 100644 tests/MADE.Testing.Tests/Tests/ExceptionAssertExtensionsTests.cs create mode 100644 tests/MADE.Testing.Tests/Tests/ObjectAssertExtensionsTests.cs create mode 100644 tests/MADE.Testing.Tests/Tests/StringAssertExtensionsTests.cs create mode 100644 tests/MADE.Web.Mvc.Tests/MADE.Web.Mvc.Tests.csproj create mode 100644 tests/MADE.Web.Mvc.Tests/Tests/ForbiddenObjectResultTests.cs diff --git a/MADE.NET.sln b/MADE.NET.sln index cfd07db7..cedbba29 100644 --- a/MADE.NET.sln +++ b/MADE.NET.sln @@ -53,6 +53,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.Data.Serialization.Tes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.Data.EFCore.Tests", "tests\MADE.Data.EFCore.Tests\MADE.Data.EFCore.Tests.csproj", "{7ECFE1EE-A42A-495E-BBB1-A34C95F70413}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Threading.Tests", "tests\MADE.Threading.Tests\MADE.Threading.Tests.csproj", "{865FBD49-C64B-4B36-AEFC-FD960DDC4CF8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Testing.Tests", "tests\MADE.Testing.Tests\MADE.Testing.Tests.csproj", "{40B5F4EB-45DD-410A-B0FB-2384C863FC33}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Web.Mvc.Tests", "tests\MADE.Web.Mvc.Tests\MADE.Web.Mvc.Tests.csproj", "{F994F941-474A-4FDD-A9CB-280EB0D78407}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +157,18 @@ Global {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Debug|Any CPU.Build.0 = Debug|Any CPU {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|Any CPU.ActiveCfg = Release|Any CPU {7ECFE1EE-A42A-495E-BBB1-A34C95F70413}.Release|Any CPU.Build.0 = Release|Any CPU + {865FBD49-C64B-4B36-AEFC-FD960DDC4CF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {865FBD49-C64B-4B36-AEFC-FD960DDC4CF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {865FBD49-C64B-4B36-AEFC-FD960DDC4CF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {865FBD49-C64B-4B36-AEFC-FD960DDC4CF8}.Release|Any CPU.Build.0 = Release|Any CPU + {40B5F4EB-45DD-410A-B0FB-2384C863FC33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {40B5F4EB-45DD-410A-B0FB-2384C863FC33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {40B5F4EB-45DD-410A-B0FB-2384C863FC33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {40B5F4EB-45DD-410A-B0FB-2384C863FC33}.Release|Any CPU.Build.0 = Release|Any CPU + {F994F941-474A-4FDD-A9CB-280EB0D78407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F994F941-474A-4FDD-A9CB-280EB0D78407}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F994F941-474A-4FDD-A9CB-280EB0D78407}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F994F941-474A-4FDD-A9CB-280EB0D78407}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -179,6 +197,9 @@ Global {0ACCC377-5FA5-47D9-B6EF-7936F1038B90} = {01380FB8-F8A7-4416-AABA-5407574B7723} {7D789D04-A010-4F11-91AD-B1B94A23BAE0} = {69149D0F-BB09-411B-88F0-A1E845058D70} {7ECFE1EE-A42A-495E-BBB1-A34C95F70413} = {69149D0F-BB09-411B-88F0-A1E845058D70} + {865FBD49-C64B-4B36-AEFC-FD960DDC4CF8} = {69149D0F-BB09-411B-88F0-A1E845058D70} + {40B5F4EB-45DD-410A-B0FB-2384C863FC33} = {69149D0F-BB09-411B-88F0-A1E845058D70} + {F994F941-474A-4FDD-A9CB-280EB0D78407} = {69149D0F-BB09-411B-88F0-A1E845058D70} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3921AD86-E6C0-4436-8880-2D9EDFAD6151} diff --git a/tests/MADE.Data.Converters.Tests/Tests/FileSizeExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/FileSizeExtensionsTests.cs new file mode 100644 index 00000000..559fdf1d --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/FileSizeExtensionsTests.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class FileSizeExtensionsTests +{ + public class WhenConvertingToHumanReadableFileSize + { + [Test] + public void ShouldReturnZeroBytesForZero() + { + 0L.ToHumanReadableFileSize().ShouldBe("0 B"); + } + + [TestCase(512L, "512.00 B")] + [TestCase(1024L, "1.00 KB")] + [TestCase(1_048_576L, "1.00 MB")] + [TestCase(1_073_741_824L, "1.00 GB")] + [TestCase(1_572_864L, "1.50 MB")] + public void ShouldConvertToCorrectUnit(long bytes, string expected) + { + bytes.ToHumanReadableFileSize().ShouldBe(expected); + } + + [Test] + public void ShouldRespectDecimalPlaces() + { + 1_572_864L.ToHumanReadableFileSize(0).ShouldBe("2 MB"); + 1_572_864L.ToHumanReadableFileSize(1).ShouldBe("1.5 MB"); + } + + [Test] + public void ShouldHandleNegativeValues() + { + (-1024L).ToHumanReadableFileSize().ShouldBe("-1.00 KB"); + } + + [Test] + public void ShouldWorkWithDoubleOverload() + { + 1048576.0.ToHumanReadableFileSize().ShouldBe("1.00 MB"); + } + } +} diff --git a/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs index 3ed289a0..baab0ffe 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/MathExtensionsTests.cs @@ -28,4 +28,28 @@ public void ShouldConvertToRadians(double degrees, double expected) actual.ShouldBe(expected); } } + + public class WhenConvertingToDegrees + { + private static readonly object[] ToDegreesTestCases = + { + new object[] { 0, 0 }, new object[] { Math.PI / 2, 90 }, new object[] { Math.PI, 180 }, + new object[] { Math.PI * 2, 360 }, + }; + + [TestCaseSource(nameof(ToDegreesTestCases))] + public void ShouldConvertToDegrees(double radians, double expected) + { + double actual = radians.ToDegrees(); + actual.ShouldBe(expected, 0.0001); + } + + [TestCase(45.0)] + [TestCase(90.0)] + [TestCase(270.0)] + public void ShouldRoundTripWithToRadians(double degrees) + { + degrees.ToRadians().ToDegrees().ShouldBe(degrees, 0.0001); + } + } } diff --git a/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs index b10d5778..178af078 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/StringExtensionsTests.cs @@ -185,4 +185,21 @@ public void ShouldConvert(string value, double? expected) actual.ShouldBe(expected); } } + + public class WhenConvertingToSlug + { + [TestCase("Hello World", "hello-world")] + [TestCase("Hello, World!", "hello-world")] + [TestCase(" Multiple Spaces ", "multiple-spaces")] + [TestCase("Caf\u00e9 Latt\u00e9", "cafe-latte")] + [TestCase("C# is GREAT!", "c-is-great")] + [TestCase("already-a-slug", "already-a-slug")] + [TestCase("", "")] + [TestCase(" ", "")] + public void ShouldConvert(string value, string expected) + { + string actual = value.ToSlug(); + actual.ShouldBe(expected); + } + } } diff --git a/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs b/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs index 3d434135..ead10dc8 100644 --- a/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs +++ b/tests/MADE.Data.Converters.Tests/Tests/StringToEnumValueConverterTests.cs @@ -9,7 +9,7 @@ namespace MADE.Data.Converters.Tests.Tests; [TestFixture] public class StringToEnumValueConverterTests { - private enum TestEnum + public enum TestEnum { None, First, diff --git a/tests/MADE.Data.Converters.Tests/Tests/TimeSpanExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/TimeSpanExtensionsTests.cs new file mode 100644 index 00000000..98b460b5 --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/TimeSpanExtensionsTests.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.Converters.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Converters.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class TimeSpanExtensionsTests +{ + public class WhenConvertingToHumanReadableString + { + [Test] + public void ShouldReturnZeroSecondsForZero() + { + TimeSpan.Zero.ToHumanReadableString().ShouldBe("0 seconds"); + } + + [Test] + public void ShouldHandleSingularUnits() + { + new TimeSpan(1, 1, 1, 1).ToHumanReadableString().ShouldBe("1 day 1 hour 1 minute 1 second"); + } + + [Test] + public void ShouldHandlePluralUnits() + { + new TimeSpan(2, 3, 30, 45).ToHumanReadableString().ShouldBe("2 days 3 hours 30 minutes 45 seconds"); + } + + [Test] + public void ShouldOmitZeroComponents() + { + new TimeSpan(0, 2, 0, 0).ToHumanReadableString().ShouldBe("2 hours"); + } + + [Test] + public void ShouldHandleNegativeTimeSpans() + { + new TimeSpan(-1, -2, 0, 0).ToHumanReadableString().ShouldStartWith("-"); + } + } + + public class WhenGettingTotalWeeks + { + [TestCase(0, 0)] + [TestCase(7, 1)] + [TestCase(14, 2)] + [TestCase(10, 1)] + public void ShouldReturnWholeWeeks(int days, int expectedWeeks) + { + TimeSpan.FromDays(days).TotalWeeks().ShouldBe(expectedWeeks); + } + } +} diff --git a/tests/MADE.Data.EFCore.Tests/Tests/AuditableEntityTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/AuditableEntityTests.cs new file mode 100644 index 00000000..8cc0c521 --- /dev/null +++ b/tests/MADE.Data.EFCore.Tests/Tests/AuditableEntityTests.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.EFCore.Extensions; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.EFCore.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class AuditableEntityTests +{ + private class AuditableTestEntity : EntityBase, IAuditableEntity + { + public string Name { get; set; } = string.Empty; + + public string? CreatedBy { get; set; } + + public string? UpdatedBy { get; set; } + } + + private class TestDbContext : DbContext + { + public TestDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Entities { get; set; } + } + + private static TestDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + + return new TestDbContext(options); + } + + public class WhenSettingAuditInfo + { + [Test] + public async Task ShouldSetCreatedByOnAdd() + { + // Arrange + using var context = CreateContext(); + var entity = new AuditableTestEntity { Name = "Test" }; + context.Entities.Add(entity); + + // Act + context.SetEntityAuditInfo("user-123"); + await context.SaveChangesAsync(); + + // Assert + entity.CreatedBy.ShouldBe("user-123"); + entity.UpdatedBy.ShouldBe("user-123"); + } + + [Test] + public async Task ShouldSetUpdatedByOnModify() + { + // Arrange + using var context = CreateContext(); + var entity = new AuditableTestEntity { Name = "Test", CreatedBy = "user-1" }; + context.Entities.Add(entity); + await context.SaveChangesAsync(); + + // Act + entity.Name = "Updated"; + context.Entry(entity).State = EntityState.Modified; + context.SetEntityAuditInfo("user-2"); + await context.SaveChangesAsync(); + + // Assert + entity.CreatedBy.ShouldBe("user-1"); + entity.UpdatedBy.ShouldBe("user-2"); + } + } +} diff --git a/tests/MADE.Data.EFCore.Tests/Tests/SoftDeleteExtensionsTests.cs b/tests/MADE.Data.EFCore.Tests/Tests/SoftDeleteExtensionsTests.cs new file mode 100644 index 00000000..0de1af41 --- /dev/null +++ b/tests/MADE.Data.EFCore.Tests/Tests/SoftDeleteExtensionsTests.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Data.EFCore.Extensions; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.EFCore.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class SoftDeleteExtensionsTests +{ + private class SoftDeletableEntity : EntityBase, ISoftDeletable + { + public string Name { get; set; } = string.Empty; + + public bool IsDeleted { get; set; } + + public DateTime? DeletedDate { get; set; } + } + + public class WhenSoftDeleting + { + [Test] + public void ShouldSetIsDeletedAndDeletedDate() + { + // Arrange + var entity = new SoftDeletableEntity { Name = "Test" }; + + // Act + entity.SoftDelete(); + + // Assert + entity.IsDeleted.ShouldBeTrue(); + entity.DeletedDate.ShouldNotBeNull(); + } + } + + public class WhenRestoring + { + [Test] + public void ShouldClearIsDeletedAndDeletedDate() + { + // Arrange + var entity = new SoftDeletableEntity { IsDeleted = true, DeletedDate = DateTime.UtcNow }; + + // Act + entity.Restore(); + + // Assert + entity.IsDeleted.ShouldBeFalse(); + entity.DeletedDate.ShouldBeNull(); + } + } + + public class WhenRoundTripping + { + [Test] + public void ShouldReturnToOriginalState() + { + // Arrange + var entity = new SoftDeletableEntity { Name = "Test" }; + + // Act + entity.SoftDelete(); + entity.Restore(); + + // Assert + entity.IsDeleted.ShouldBeFalse(); + entity.DeletedDate.ShouldBeNull(); + } + } +} diff --git a/tests/MADE.Data.Validation.Tests/Tests/AsyncValidatorCollectionTests.cs b/tests/MADE.Data.Validation.Tests/Tests/AsyncValidatorCollectionTests.cs new file mode 100644 index 00000000..50aabf1e --- /dev/null +++ b/tests/MADE.Data.Validation.Tests/Tests/AsyncValidatorCollectionTests.cs @@ -0,0 +1,121 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Data.Validation.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class AsyncValidatorCollectionTests +{ + private class AlwaysValidAsyncValidator : IAsyncValidator + { + public string Key { get; set; } = "AlwaysValid"; + + public bool IsInvalid { get; set; } + + public bool IsDirty { get; set; } + + public string FeedbackMessage { get; set; } = string.Empty; + + public Task ValidateAsync(object value, CancellationToken cancellationToken = default) + { + this.IsInvalid = false; + this.IsDirty = true; + return Task.CompletedTask; + } + } + + private class AlwaysInvalidAsyncValidator : IAsyncValidator + { + public string Key { get; set; } = "AlwaysInvalid"; + + public bool IsInvalid { get; set; } + + public bool IsDirty { get; set; } + + public string FeedbackMessage { get; set; } = "Value is invalid."; + + public Task ValidateAsync(object value, CancellationToken cancellationToken = default) + { + this.IsInvalid = true; + this.IsDirty = true; + return Task.CompletedTask; + } + } + + public class WhenValidatingAsync + { + [Test] + public async Task ShouldReportValidWhenAllValidatorsPass() + { + // Arrange + var collection = new AsyncValidatorCollection { new AlwaysValidAsyncValidator() }; + + // Act + await collection.ValidateAsync("test"); + + // Assert + collection.IsInvalid.ShouldBeFalse(); + collection.IsDirty.ShouldBeTrue(); + } + + [Test] + public async Task ShouldReportInvalidWhenAnyValidatorFails() + { + // Arrange + var collection = new AsyncValidatorCollection + { + new AlwaysValidAsyncValidator(), + new AlwaysInvalidAsyncValidator(), + }; + + // Act + await collection.ValidateAsync("test"); + + // Assert + collection.IsInvalid.ShouldBeTrue(); + } + + [Test] + public async Task ShouldPopulateFeedbackMessages() + { + // Arrange + var collection = new AsyncValidatorCollection { new AlwaysInvalidAsyncValidator() }; + + // Act + await collection.ValidateAsync("test"); + + // Assert + collection.FeedbackMessages.ShouldContain("Value is invalid."); + } + + [Test] + public async Task ShouldFireValidatedEvent() + { + // Arrange + var collection = new AsyncValidatorCollection { new AlwaysValidAsyncValidator() }; + bool eventFired = false; + collection.Validated += (_, _) => eventFired = true; + + // Act + await collection.ValidateAsync("test"); + + // Assert + eventFired.ShouldBeTrue(); + } + + [Test] + public void ShouldSupportCancellation() + { + // Arrange + var collection = new AsyncValidatorCollection { new AlwaysValidAsyncValidator() }; + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + // Act & Assert + Should.ThrowAsync( + () => collection.ValidateAsync("test", cts.Token)); + } + } +} diff --git a/tests/MADE.Networking.Tests/Tests/RetryDelegatingHandlerTests.cs b/tests/MADE.Networking.Tests/Tests/RetryDelegatingHandlerTests.cs new file mode 100644 index 00000000..b5cd2573 --- /dev/null +++ b/tests/MADE.Networking.Tests/Tests/RetryDelegatingHandlerTests.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using MADE.Networking.Http; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class RetryDelegatingHandlerTests +{ + private class FakeHandler : HttpMessageHandler + { + private readonly Queue responses = new(); + + public int CallCount { get; private set; } + + public void EnqueueResponse(HttpStatusCode statusCode) + { + this.responses.Enqueue(new HttpResponseMessage(statusCode)); + } + + protected override Task SendAsync( + HttpRequestMessage request, + CancellationToken cancellationToken) + { + this.CallCount++; + return Task.FromResult(this.responses.Dequeue()); + } + } + + public class WhenRequestSucceeds + { + [Test] + public async Task ShouldNotRetry() + { + // Arrange + var fakeHandler = new FakeHandler(); + fakeHandler.EnqueueResponse(HttpStatusCode.OK); + + using var retryHandler = new RetryDelegatingHandler(fakeHandler, maxRetries: 3); + using var client = new HttpClient(retryHandler); + + // Act + var response = await client.GetAsync("http://localhost/test"); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.OK); + fakeHandler.CallCount.ShouldBe(1); + } + } + + public class WhenRequestFailsWithTransientError + { + [Test] + public async Task ShouldRetryAndEventuallySucceed() + { + // Arrange + var fakeHandler = new FakeHandler(); + fakeHandler.EnqueueResponse(HttpStatusCode.ServiceUnavailable); + fakeHandler.EnqueueResponse(HttpStatusCode.ServiceUnavailable); + fakeHandler.EnqueueResponse(HttpStatusCode.OK); + + using var retryHandler = new RetryDelegatingHandler( + fakeHandler, + maxRetries: 3, + initialDelay: TimeSpan.FromMilliseconds(10)); + using var client = new HttpClient(retryHandler); + + // Act + var response = await client.GetAsync("http://localhost/test"); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.OK); + fakeHandler.CallCount.ShouldBe(3); + } + + [Test] + public async Task ShouldReturnLastResponseWhenRetriesExhausted() + { + // Arrange + var fakeHandler = new FakeHandler(); + fakeHandler.EnqueueResponse(HttpStatusCode.ServiceUnavailable); + fakeHandler.EnqueueResponse(HttpStatusCode.ServiceUnavailable); + + using var retryHandler = new RetryDelegatingHandler( + fakeHandler, + maxRetries: 1, + initialDelay: TimeSpan.FromMilliseconds(10)); + using var client = new HttpClient(retryHandler); + + // Act + var response = await client.GetAsync("http://localhost/test"); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable); + fakeHandler.CallCount.ShouldBe(2); + } + } + + public class WhenRequestFailsWithNonTransientError + { + [Test] + public async Task ShouldNotRetry() + { + // Arrange + var fakeHandler = new FakeHandler(); + fakeHandler.EnqueueResponse(HttpStatusCode.BadRequest); + + using var retryHandler = new RetryDelegatingHandler( + fakeHandler, + maxRetries: 3, + initialDelay: TimeSpan.FromMilliseconds(10)); + using var client = new HttpClient(retryHandler); + + // Act + var response = await client.GetAsync("http://localhost/test"); + + // Assert + response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); + fakeHandler.CallCount.ShouldBe(1); + } + } +} diff --git a/tests/MADE.Testing.Tests/MADE.Testing.Tests.csproj b/tests/MADE.Testing.Tests/MADE.Testing.Tests.csproj new file mode 100644 index 00000000..cf63712b --- /dev/null +++ b/tests/MADE.Testing.Tests/MADE.Testing.Tests.csproj @@ -0,0 +1,11 @@ + + + + net8.0;net10.0 + + + + + + + diff --git a/tests/MADE.Testing.Tests/Tests/BooleanAssertExtensionsTests.cs b/tests/MADE.Testing.Tests/Tests/BooleanAssertExtensionsTests.cs new file mode 100644 index 00000000..d8c923dc --- /dev/null +++ b/tests/MADE.Testing.Tests/Tests/BooleanAssertExtensionsTests.cs @@ -0,0 +1,40 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Testing.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class BooleanAssertExtensionsTests +{ + public class WhenAssertingShouldBeTrue + { + [Test] + public void ShouldPassForTrue() + { + Should.NotThrow(() => true.ShouldBeTrue()); + } + + [Test] + public void ShouldFailForFalse() + { + Should.Throw(() => false.ShouldBeTrue()); + } + } + + public class WhenAssertingShouldBeFalse + { + [Test] + public void ShouldPassForFalse() + { + Should.NotThrow(() => false.ShouldBeFalse()); + } + + [Test] + public void ShouldFailForTrue() + { + Should.Throw(() => true.ShouldBeFalse()); + } + } +} diff --git a/tests/MADE.Testing.Tests/Tests/ComparableAssertExtensionsTests.cs b/tests/MADE.Testing.Tests/Tests/ComparableAssertExtensionsTests.cs new file mode 100644 index 00000000..413578ca --- /dev/null +++ b/tests/MADE.Testing.Tests/Tests/ComparableAssertExtensionsTests.cs @@ -0,0 +1,94 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Testing.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ComparableAssertExtensionsTests +{ + public class WhenAssertingShouldBeGreaterThan + { + [Test] + public void ShouldPassWhenGreater() + { + Should.NotThrow(() => 10.ShouldBeGreaterThan(5)); + } + + [Test] + public void ShouldFailWhenEqual() + { + Should.Throw(() => 5.ShouldBeGreaterThan(5)); + } + + [Test] + public void ShouldFailWhenLess() + { + Should.Throw(() => 3.ShouldBeGreaterThan(5)); + } + } + + public class WhenAssertingShouldBeLessThan + { + [Test] + public void ShouldPassWhenLess() + { + Should.NotThrow(() => 3.ShouldBeLessThan(5)); + } + + [Test] + public void ShouldFailWhenEqual() + { + Should.Throw(() => 5.ShouldBeLessThan(5)); + } + + [Test] + public void ShouldFailWhenGreater() + { + Should.Throw(() => 10.ShouldBeLessThan(5)); + } + } + + public class WhenAssertingShouldBeGreaterThanOrEqualTo + { + [Test] + public void ShouldPassWhenGreater() + { + Should.NotThrow(() => 10.ShouldBeGreaterThanOrEqualTo(5)); + } + + [Test] + public void ShouldPassWhenEqual() + { + Should.NotThrow(() => 5.ShouldBeGreaterThanOrEqualTo(5)); + } + + [Test] + public void ShouldFailWhenLess() + { + Should.Throw(() => 3.ShouldBeGreaterThanOrEqualTo(5)); + } + } + + public class WhenAssertingShouldBeLessThanOrEqualTo + { + [Test] + public void ShouldPassWhenLess() + { + Should.NotThrow(() => 3.ShouldBeLessThanOrEqualTo(5)); + } + + [Test] + public void ShouldPassWhenEqual() + { + Should.NotThrow(() => 5.ShouldBeLessThanOrEqualTo(5)); + } + + [Test] + public void ShouldFailWhenGreater() + { + Should.Throw(() => 10.ShouldBeLessThanOrEqualTo(5)); + } + } +} diff --git a/tests/MADE.Testing.Tests/Tests/ExceptionAssertExtensionsTests.cs b/tests/MADE.Testing.Tests/Tests/ExceptionAssertExtensionsTests.cs new file mode 100644 index 00000000..7287feea --- /dev/null +++ b/tests/MADE.Testing.Tests/Tests/ExceptionAssertExtensionsTests.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Testing.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ExceptionAssertExtensionsTests +{ + public class WhenAssertingShouldThrow + { + [Test] + public void ShouldPassWhenExpectedExceptionIsThrown() + { + Action action = () => throw new InvalidOperationException("test"); + var ex = action.ShouldThrow(); + ex.Message.ShouldBe("test"); + } + + [Test] + public void ShouldFailWhenNoExceptionIsThrown() + { + Action action = () => { }; + Should.Throw(() => action.ShouldThrow()); + } + + [Test] + public void ShouldFailWhenDifferentExceptionIsThrown() + { + Action action = () => throw new ArgumentException("wrong"); + Should.Throw(() => action.ShouldThrow()); + } + } + + public class WhenAssertingShouldThrowAsync + { + [Test] + public async Task ShouldPassWhenExpectedExceptionIsThrown() + { + Func action = () => throw new InvalidOperationException("async test"); + var ex = await action.ShouldThrowAsync(); + ex.Message.ShouldBe("async test"); + } + } + + public class WhenAssertingShouldNotThrow + { + [Test] + public void ShouldPassWhenNoExceptionIsThrown() + { + Action action = () => { }; + Should.NotThrow(() => action.ShouldNotThrow()); + } + + [Test] + public void ShouldFailWhenExceptionIsThrown() + { + Action action = () => throw new InvalidOperationException(); + Should.Throw(() => action.ShouldNotThrow()); + } + } + + public class WhenAssertingShouldNotThrowAsync + { + [Test] + public async Task ShouldPassWhenNoExceptionIsThrown() + { + Func action = () => Task.CompletedTask; + await Should.NotThrowAsync(() => action.ShouldNotThrowAsync()); + } + + [Test] + public async Task ShouldFailWhenExceptionIsThrown() + { + Func action = () => throw new InvalidOperationException(); + await Should.ThrowAsync(() => action.ShouldNotThrowAsync()); + } + } +} diff --git a/tests/MADE.Testing.Tests/Tests/ObjectAssertExtensionsTests.cs b/tests/MADE.Testing.Tests/Tests/ObjectAssertExtensionsTests.cs new file mode 100644 index 00000000..8fc55923 --- /dev/null +++ b/tests/MADE.Testing.Tests/Tests/ObjectAssertExtensionsTests.cs @@ -0,0 +1,44 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Testing.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ObjectAssertExtensionsTests +{ + public class WhenAssertingShouldBeNull + { + [Test] + public void ShouldPassForNullValue() + { + object? value = null; + Should.NotThrow(() => value.ShouldBeNull()); + } + + [Test] + public void ShouldFailForNonNullValue() + { + object value = new(); + Should.Throw(() => value.ShouldBeNull()); + } + } + + public class WhenAssertingShouldNotBeNull + { + [Test] + public void ShouldPassForNonNullValue() + { + object value = new(); + Should.NotThrow(() => value.ShouldNotBeNull()); + } + + [Test] + public void ShouldFailForNullValue() + { + object? value = null; + Should.Throw(() => value.ShouldNotBeNull()); + } + } +} diff --git a/tests/MADE.Testing.Tests/Tests/StringAssertExtensionsTests.cs b/tests/MADE.Testing.Tests/Tests/StringAssertExtensionsTests.cs new file mode 100644 index 00000000..a28e9af6 --- /dev/null +++ b/tests/MADE.Testing.Tests/Tests/StringAssertExtensionsTests.cs @@ -0,0 +1,77 @@ +using System.Diagnostics.CodeAnalysis; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Testing.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class StringAssertExtensionsTests +{ + public class WhenAssertingShouldContain + { + [Test] + public void ShouldPassWhenContains() + { + Should.NotThrow(() => "Hello, World!".ShouldContain("World")); + } + + [Test] + public void ShouldFailWhenNotContains() + { + Should.Throw(() => "Hello".ShouldContain("World")); + } + + [Test] + public void ShouldFailForNull() + { + string? value = null; + Should.Throw(() => value.ShouldContain("test")); + } + } + + public class WhenAssertingShouldNotContain + { + [Test] + public void ShouldPassWhenNotContains() + { + Should.NotThrow(() => "Hello".ShouldNotContain("World")); + } + + [Test] + public void ShouldFailWhenContains() + { + Should.Throw(() => "Hello, World!".ShouldNotContain("World")); + } + } + + public class WhenAssertingShouldStartWith + { + [Test] + public void ShouldPassWhenStartsWith() + { + Should.NotThrow(() => "Hello, World!".ShouldStartWith("Hello")); + } + + [Test] + public void ShouldFailWhenDoesNotStartWith() + { + Should.Throw(() => "Hello".ShouldStartWith("World")); + } + } + + public class WhenAssertingShouldEndWith + { + [Test] + public void ShouldPassWhenEndsWith() + { + Should.NotThrow(() => "Hello, World!".ShouldEndWith("World!")); + } + + [Test] + public void ShouldFailWhenDoesNotEndWith() + { + Should.Throw(() => "Hello".ShouldEndWith("World")); + } + } +} diff --git a/tests/MADE.Web.Mvc.Tests/MADE.Web.Mvc.Tests.csproj b/tests/MADE.Web.Mvc.Tests/MADE.Web.Mvc.Tests.csproj new file mode 100644 index 00000000..abcb817d --- /dev/null +++ b/tests/MADE.Web.Mvc.Tests/MADE.Web.Mvc.Tests.csproj @@ -0,0 +1,11 @@ + + + + net8.0;net10.0 + + + + + + + diff --git a/tests/MADE.Web.Mvc.Tests/Tests/ForbiddenObjectResultTests.cs b/tests/MADE.Web.Mvc.Tests/Tests/ForbiddenObjectResultTests.cs new file mode 100644 index 00000000..4f5f7f32 --- /dev/null +++ b/tests/MADE.Web.Mvc.Tests/Tests/ForbiddenObjectResultTests.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using MADE.Web.Mvc.Responses; +using Microsoft.AspNetCore.Http; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Web.Mvc.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class ForbiddenObjectResultTests +{ + public class WhenCreatingWithError + { + [Test] + public void ShouldReturnForbiddenStatusCode() + { + // Arrange & Act + var result = new ForbiddenObjectResult("Access denied"); + + // Assert + result.StatusCode.ShouldBe(StatusCodes.Status403Forbidden); + result.Value.ShouldBe("Access denied"); + } + } +} From 78f9d1cdfda4e5711ee51c92dd55a27721de8cd0 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 11:24:27 +0100 Subject: [PATCH 11/12] fix(tests): use mock handler for error tests, keep real service for success The httpbin.org error tests were flaky in CI because the external service can return transient errors (502) instead of the expected status code (405). Success tests still hit httpbin.org to prove real HTTP plumbing works. Error tests now use a MockHttpMessageHandler for deterministic assertions. --- .../MockHttpMessageHandler.cs | 44 +++++++++++++++++++ .../Tests/JsonDeleteNetworkRequestTests.cs | 13 ++---- .../Tests/JsonGetNetworkRequestTests.cs | 13 ++---- .../Tests/JsonPatchNetworkRequestTests.cs | 16 +++---- .../Tests/JsonPostNetworkRequestTests.cs | 18 +++----- .../Tests/JsonPutNetworkRequestTests.cs | 18 +++----- 6 files changed, 72 insertions(+), 50 deletions(-) create mode 100644 tests/MADE.Networking.Tests/MockHttpMessageHandler.cs diff --git a/tests/MADE.Networking.Tests/MockHttpMessageHandler.cs b/tests/MADE.Networking.Tests/MockHttpMessageHandler.cs new file mode 100644 index 00000000..ed9564a2 --- /dev/null +++ b/tests/MADE.Networking.Tests/MockHttpMessageHandler.cs @@ -0,0 +1,44 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Net; +using System.Text; + +namespace MADE.Networking.Tests; + +/// +/// A mock that returns pre-configured responses for testing. +/// +internal class MockHttpMessageHandler : HttpMessageHandler +{ + private readonly Func> handler; + + public MockHttpMessageHandler(HttpStatusCode statusCode, string content = "{}", string contentType = "application/json") + : this(_ => new HttpResponseMessage(statusCode) + { + Content = new StringContent(content, Encoding.UTF8, contentType), + }) + { + } + + public MockHttpMessageHandler(Func handler) + { + this.handler = (request, _) => Task.FromResult(handler(request)); + } + + public MockHttpMessageHandler(Func> handler) + { + this.handler = handler; + } + + public int CallCount { get; private set; } + + public HttpRequestMessage? LastRequest { get; private set; } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.CallCount++; + this.LastRequest = request; + return this.handler(request, cancellationToken); + } +} diff --git a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs index aaec7e7f..449a9649 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonDeleteNetworkRequestTests.cs @@ -1,9 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MADE.Networking.Http.Requests.Json; using System.Text.Json.Nodes; +using MADE.Networking.Http.Requests.Json; using NUnit.Framework; using Shouldly; @@ -35,14 +33,11 @@ public async Task ShouldReturnSuccessFromDeleteEndpointWithResponse() } [Test] - public async Task ShouldReturnErrorFromGetEndpoint() + public async Task ShouldThrowWhenMethodNotAllowed() { // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/get?{query}={queryValue}"; - var request = new JsonDeleteNetworkRequest(new HttpClient(), requestUrl); + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.MethodNotAllowed); + var request = new JsonDeleteNetworkRequest(new HttpClient(mockHandler), "http://localhost/get"); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); diff --git a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs index ba9ad2bc..8abb0a9d 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonGetNetworkRequestTests.cs @@ -1,9 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MADE.Networking.Http.Requests.Json; using System.Text.Json.Nodes; +using MADE.Networking.Http.Requests.Json; using NUnit.Framework; using Shouldly; @@ -35,14 +33,11 @@ public async Task ShouldReturnSuccessFromGetEndpointWithResponse() } [Test] - public async Task ShouldReturnErrorFromDeleteEndpoint() + public async Task ShouldThrowWhenMethodNotAllowed() { // Arrange - const string query = "test"; - const bool queryValue = true; - - var requestUrl = $"https://httpbin.org/delete?{query}={queryValue}"; - var request = new JsonGetNetworkRequest(new HttpClient(), requestUrl); + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.MethodNotAllowed); + var request = new JsonGetNetworkRequest(new HttpClient(mockHandler), "http://localhost/delete"); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); diff --git a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs index 2127d63f..f2f06ab1 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPatchNetworkRequestTests.cs @@ -1,10 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MADE.Networking.Http.Requests.Json; using System.Text.Json; using System.Text.Json.Nodes; +using MADE.Networking.Http.Requests.Json; using NUnit.Framework; using Shouldly; @@ -43,16 +41,14 @@ public async Task ShouldReturnSuccessFromPatchEndpointWithResponse() } [Test] - public async Task ShouldReturnErrorFromGetEndpoint() + public async Task ShouldThrowWhenMethodNotAllowed() { // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; - - const string requestUrl = "https://httpbin.org/get"; + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.MethodNotAllowed); var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + new HttpClient(mockHandler), + "http://localhost/get", + JsonSerializer.Serialize(new RequestData { Key = "test" })); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); diff --git a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs index 720b8f2f..69af5c76 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPostNetworkRequestTests.cs @@ -1,10 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MADE.Networking.Http.Requests.Json; using System.Text.Json; using System.Text.Json.Nodes; +using MADE.Networking.Http.Requests.Json; using NUnit.Framework; using Shouldly; @@ -43,16 +41,14 @@ public async Task ShouldReturnSuccessFromPostEndpointWithResponse() } [Test] - public async Task ShouldReturnErrorFromGetEndpoint() + public async Task ShouldThrowWhenMethodNotAllowed() { // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; - - const string requestUrl = "https://httpbin.org/get"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.MethodNotAllowed); + var request = new JsonPostNetworkRequest( + new HttpClient(mockHandler), + "http://localhost/get", + JsonSerializer.Serialize(new RequestData { Key = "test" })); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); diff --git a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs index eece3488..be260964 100644 --- a/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs +++ b/tests/MADE.Networking.Tests/Tests/JsonPutNetworkRequestTests.cs @@ -1,10 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using MADE.Networking.Http.Requests.Json; using System.Text.Json; using System.Text.Json.Nodes; +using MADE.Networking.Http.Requests.Json; using NUnit.Framework; using Shouldly; @@ -43,16 +41,14 @@ public async Task ShouldReturnSuccessFromPutEndpointWithResponse() } [Test] - public async Task ShouldReturnErrorFromGetEndpoint() + public async Task ShouldThrowWhenMethodNotAllowed() { // Arrange - var requestData = new RequestData { Key = "test", Enabled = true }; - - const string requestUrl = "https://httpbin.org/get"; - var request = new JsonPatchNetworkRequest( - new HttpClient(), - requestUrl, - JsonSerializer.Serialize(requestData)); + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.MethodNotAllowed); + var request = new JsonPutNetworkRequest( + new HttpClient(mockHandler), + "http://localhost/get", + JsonSerializer.Serialize(new RequestData { Key = "test" })); // Act var exception = await request.ExecuteAsync().ShouldThrowAsync(); From 6625423da7bed7d4a5be91b2234906f46e591849 Mon Sep 17 00:00:00 2001 From: James Croft Date: Fri, 15 May 2026 11:35:47 +0100 Subject: [PATCH 12/12] feat(networking): add INetworkRequestFactory for DI-friendly request creation Wraps IHttpClientFactory to eliminate manual HttpClient management. Supports named clients via WithClient() and one-line DI registration with services.AddNetworkRequestFactory(). Documentation updated to use the factory as the primary recommended approach. --- docs/articles/features/networking.md | 110 ++++++++--- docs/articles/intro.md | 1 + .../Extensions/ServiceCollectionExtensions.cs | 42 ++++ .../Http/INetworkRequestFactory.cs | 80 ++++++++ .../Http/NetworkRequestFactory.cs | 88 +++++++++ src/MADE.Networking/MADE.Networking.csproj | 8 + .../MADE.Networking.Tests.csproj | 8 + .../Tests/NetworkRequestFactoryTests.cs | 185 ++++++++++++++++++ 8 files changed, 490 insertions(+), 32 deletions(-) create mode 100644 src/MADE.Networking/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/MADE.Networking/Http/INetworkRequestFactory.cs create mode 100644 src/MADE.Networking/Http/NetworkRequestFactory.cs create mode 100644 tests/MADE.Networking.Tests/Tests/NetworkRequestFactoryTests.cs diff --git a/docs/articles/features/networking.md b/docs/articles/features/networking.md index b2246159..1e8e82d5 100644 --- a/docs/articles/features/networking.md +++ b/docs/articles/features/networking.md @@ -7,6 +7,66 @@ title: Using the Networking package The Networking package contains a collection of helpers for applications that use `HttpClient` for making network requests to APIs. +> **Important:** You should not create a new `HttpClient` for each request. Use the built-in `INetworkRequestFactory` or [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) via dependency injection to manage `HttpClient` lifetimes. + +## Getting started with INetworkRequestFactory + +The `INetworkRequestFactory` is the recommended way to create network requests. It wraps `IHttpClientFactory` and provides a clean API for creating requests without manual `HttpClient` management. + +### Registration + +```csharp +// In your Startup or Program.cs +services.AddNetworkRequestFactory(); +``` + +You can also register a named client with pre-configured settings: + +```csharp +services.AddNetworkRequestFactory("MyApi", client => +{ + client.BaseAddress = new Uri("https://api.example.com/"); +}); +``` + +### Usage + +Inject `INetworkRequestFactory` into your services and create requests directly: + +```csharp +public class ProfileService +{ + private readonly INetworkRequestFactory requestFactory; + + public ProfileService(INetworkRequestFactory requestFactory) + { + this.requestFactory = requestFactory; + } + + public async Task GetProfileAsync(CancellationToken cancellationToken = default) + { + var request = this.requestFactory.Get("https://api.example.com/profile"); + return await request.ExecuteAsync(cancellationToken); + } + + public async Task UpdateProfileAsync(Profile profile, CancellationToken cancellationToken = default) + { + var request = this.requestFactory.Put( + "https://api.example.com/profile", + JsonSerializer.Serialize(profile)); + await request.ExecuteAsync(cancellationToken); + } +} +``` + +### Using named clients + +If you have registered named `HttpClient` configurations, use `WithClient` to select one: + +```csharp +var request = this.requestFactory.WithClient("MyApi").Get("/profile"); +``` + ## Making simple network requests using NetworkRequest instances The Network package comes with a variety of `NetworkRequest` types that can be used to perform network requests without a lot of additional overhead. @@ -20,35 +80,14 @@ The current available in-box `NetworkRequest` types are: - JsonDeleteNetworkRequest, for making a HTTP DELETE request with a JSON response. - StreamGetNetworkRequest, for making a HTTP GET request with a data stream response. -Each one needs a `HttpClient` instance, a URL to call, and any additional headers that may be required for the request. - -The example below shows how you can use a `JsonGetNetworkRequest` to make a request to an API endpoint and retrieve data with a specified type. - -```csharp -public async Task GetMyProfileAsync(CancellationToken cancellationToken = default) -{ - JsonGetNetworkRequest request = new JsonGetNetworkRequest( - new HttpClient(), - "https://jamescroft.co.uk/api/profile", - this.GetRequestHeaders()); - - return await request.ExecuteAsync(cancellationToken); -} -``` +## Using NetworkRequest types directly -The `JsonPostNetworkRequest`, `JsonPutNetworkRequest`, and `JsonPatchNetworkRequest` types all have an additional parameter which require a JSON string. +If you prefer to create `NetworkRequest` instances directly (e.g., outside of a DI container), each type accepts an `HttpClient` instance, a URL, and optional headers. ```csharp -public async Task UpdateMyProfileAsync(Profile profile, CancellationToken cancellationToken = default) -{ - JsonPutNetworkRequest request = new JsonPutNetworkRequest( - new HttpClient(), - "https://jamescroft.co.uk/api/profile", - JsonConvert.SerializeObject(profile), - this.GetRequestHeaders()); - - await request.ExecuteAsync(cancellationToken); -} +var client = this.httpClientFactory.CreateClient(); +var request = new JsonGetNetworkRequest(client, "https://api.example.com/profile"); +var profile = await request.ExecuteAsync(cancellationToken); ``` ## Queuing your network requests using NetworkRequestManager @@ -64,12 +103,11 @@ This can be achieved by registering your `NetworkRequest` instances with an inst ```csharp private INetworkRequestManager NetworkManager { get; } +private INetworkRequestFactory RequestFactory { get; } + public void GetMyProfileAsync() { - JsonGetNetworkRequest request = new JsonGetNetworkRequest( - new HttpClient(), - "https://jamescroft.co.uk/api/profile", - this.GetRequestHeaders()); + var request = this.RequestFactory.Get("https://api.example.com/profile"); NetworkManager.AddOrUpdate( request, @@ -93,7 +131,7 @@ The `MultipartFormDataPostNetworkRequest` allows you to upload files and form da ```csharp public async Task UploadFileAsync(Stream fileStream, string fileName, CancellationToken cancellationToken = default) { - var request = new MultipartFormDataPostNetworkRequest(new HttpClient(), "https://api.example.com/upload") + var request = this.requestFactory.PostMultipart("https://api.example.com/upload") .AddStreamContent("file", fileStream, fileName, "image/png") .AddStringContent("description", "Profile photo"); @@ -111,11 +149,19 @@ You can add multiple types of content: The `RetryDelegatingHandler` is a `DelegatingHandler` that automatically retries failed HTTP requests with exponential backoff. It handles transient failures including timeouts, server errors (500, 502, 503, 504), and rate limiting (429). +Register it with `IHttpClientFactory` for use across your application: + +```csharp +services.AddHttpClient("ResilientApi") + .AddHttpMessageHandler(() => new RetryDelegatingHandler(maxRetries: 3, initialDelay: TimeSpan.FromSeconds(1))); +``` + +Or use it directly when constructing an `HttpClient`: + ```csharp var handler = new RetryDelegatingHandler(maxRetries: 3, initialDelay: TimeSpan.FromSeconds(1)); var client = new HttpClient(handler); -// All requests made with this client will automatically retry on transient failures var request = new JsonGetNetworkRequest(client, "https://api.example.com/data"); var result = await request.ExecuteAsync(); ``` diff --git a/docs/articles/intro.md b/docs/articles/intro.md index f4f14f39..8886c575 100644 --- a/docs/articles/intro.md +++ b/docs/articles/intro.md @@ -164,6 +164,7 @@ The Networking package contains a collection of helpers for applications that us It includes features such as: +- INetworkRequestFactory, a DI-friendly factory for creating network requests without manual HttpClient management. Register with `services.AddNetworkRequestFactory()`. - NetworkRequestManager, for managing a queue of HTTP network requests with success and error callbacks. - JsonGetNetworkRequest, for making a HTTP GET request with a JSON response, deserializing to a specified type. - JsonPostNetworkRequest, for making a HTTP POST request with a JSON payload, and a JSON response. diff --git a/src/MADE.Networking/Extensions/ServiceCollectionExtensions.cs b/src/MADE.Networking/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..3518536f --- /dev/null +++ b/src/MADE.Networking/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,42 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Networking.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace MADE.Networking.Extensions; + +/// +/// Defines a collection of extensions for registering MADE.NET Networking services with dependency injection. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds the and its dependencies to the specified . + /// + /// The to add services to. + /// The for chaining. + public static IServiceCollection AddNetworkRequestFactory(this IServiceCollection services) + { + services.AddHttpClient(); + services.TryAddSingleton(); + return services; + } + + /// + /// Adds the and configures a named with the specified action. + /// + /// The to add services to. + /// The name of the to configure. + /// An action to configure the . + /// The for further configuration. + public static IHttpClientBuilder AddNetworkRequestFactory( + this IServiceCollection services, + string clientName, + Action configureClient) + { + services.TryAddSingleton(); + return services.AddHttpClient(clientName, configureClient); + } +} diff --git a/src/MADE.Networking/Http/INetworkRequestFactory.cs b/src/MADE.Networking/Http/INetworkRequestFactory.cs new file mode 100644 index 00000000..d37669ea --- /dev/null +++ b/src/MADE.Networking/Http/INetworkRequestFactory.cs @@ -0,0 +1,80 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Networking.Http.Requests; +using MADE.Networking.Http.Requests.Json; +using MADE.Networking.Http.Requests.Streams; + +namespace MADE.Networking.Http; + +/// +/// Defines an interface for creating instances using a managed . +/// +public interface INetworkRequestFactory +{ + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// Optional additional headers for the request. + /// A configured . + JsonGetNetworkRequest Get(string url, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// The JSON data to post. + /// Optional additional headers for the request. + /// A configured . + JsonPostNetworkRequest Post(string url, string? jsonData = null, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// The JSON data to put. + /// Optional additional headers for the request. + /// A configured . + JsonPutNetworkRequest Put(string url, string? jsonData = null, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// The JSON data to patch. + /// Optional additional headers for the request. + /// A configured . + JsonPatchNetworkRequest Patch(string url, string? jsonData = null, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// Optional additional headers for the request. + /// A configured . + JsonDeleteNetworkRequest Delete(string url, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// Optional additional headers for the request. + /// A configured . + StreamGetNetworkRequest GetStream(string url, Dictionary? headers = null); + + /// + /// Creates a for the specified URL. + /// + /// The URL for the request. + /// Optional additional headers for the request. + /// A configured . + MultipartFormDataPostNetworkRequest PostMultipart(string url, Dictionary? headers = null); + + /// + /// Creates a new that uses the specified named . + /// + /// The name of the to use. + /// A new configured with the named client. + INetworkRequestFactory WithClient(string clientName); +} diff --git a/src/MADE.Networking/Http/NetworkRequestFactory.cs b/src/MADE.Networking/Http/NetworkRequestFactory.cs new file mode 100644 index 00000000..0aca7ef2 --- /dev/null +++ b/src/MADE.Networking/Http/NetworkRequestFactory.cs @@ -0,0 +1,88 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using MADE.Networking.Http.Requests; +using MADE.Networking.Http.Requests.Json; +using MADE.Networking.Http.Requests.Streams; + +namespace MADE.Networking.Http; + +/// +/// Defines a factory for creating instances using a managed from . +/// +public class NetworkRequestFactory : INetworkRequestFactory +{ + private readonly IHttpClientFactory httpClientFactory; + private readonly string? clientName; + + /// + /// Initializes a new instance of the class. + /// + /// The used to create instances. + public NetworkRequestFactory(IHttpClientFactory httpClientFactory) + : this(httpClientFactory, null) + { + } + + private NetworkRequestFactory(IHttpClientFactory httpClientFactory, string? clientName) + { + this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); + this.clientName = clientName; + } + + /// + public JsonGetNetworkRequest Get(string url, Dictionary? headers = null) + { + return new JsonGetNetworkRequest(this.CreateClient(), url, headers!); + } + + /// + public JsonPostNetworkRequest Post(string url, string? jsonData = null, Dictionary? headers = null) + { + return new JsonPostNetworkRequest(this.CreateClient(), url, jsonData!, headers!); + } + + /// + public JsonPutNetworkRequest Put(string url, string? jsonData = null, Dictionary? headers = null) + { + return new JsonPutNetworkRequest(this.CreateClient(), url, jsonData!, headers!); + } + + /// + public JsonPatchNetworkRequest Patch(string url, string? jsonData = null, Dictionary? headers = null) + { + return new JsonPatchNetworkRequest(this.CreateClient(), url, jsonData!, headers!); + } + + /// + public JsonDeleteNetworkRequest Delete(string url, Dictionary? headers = null) + { + return new JsonDeleteNetworkRequest(this.CreateClient(), url, headers!); + } + + /// + public StreamGetNetworkRequest GetStream(string url, Dictionary? headers = null) + { + return new StreamGetNetworkRequest(this.CreateClient(), url, headers!); + } + + /// + public MultipartFormDataPostNetworkRequest PostMultipart(string url, Dictionary? headers = null) + { + return new MultipartFormDataPostNetworkRequest(this.CreateClient(), url, headers!); + } + + /// + public INetworkRequestFactory WithClient(string clientName) + { + ArgumentNullException.ThrowIfNull(clientName); + return new NetworkRequestFactory(this.httpClientFactory, clientName); + } + + private HttpClient CreateClient() + { + return this.clientName is not null + ? this.httpClientFactory.CreateClient(this.clientName) + : this.httpClientFactory.CreateClient(); + } +} diff --git a/src/MADE.Networking/MADE.Networking.csproj b/src/MADE.Networking/MADE.Networking.csproj index 733f99e4..8f50f425 100644 --- a/src/MADE.Networking/MADE.Networking.csproj +++ b/src/MADE.Networking/MADE.Networking.csproj @@ -16,4 +16,12 @@ + + + + + + + + diff --git a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj index e3739ca0..d3a4c853 100644 --- a/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj +++ b/tests/MADE.Networking.Tests/MADE.Networking.Tests.csproj @@ -9,4 +9,12 @@ + + + + + + + + \ No newline at end of file diff --git a/tests/MADE.Networking.Tests/Tests/NetworkRequestFactoryTests.cs b/tests/MADE.Networking.Tests/Tests/NetworkRequestFactoryTests.cs new file mode 100644 index 00000000..68a2016b --- /dev/null +++ b/tests/MADE.Networking.Tests/Tests/NetworkRequestFactoryTests.cs @@ -0,0 +1,185 @@ +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Text.Json; +using MADE.Networking.Extensions; +using MADE.Networking.Http; +using MADE.Networking.Http.Requests.Json; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Shouldly; + +namespace MADE.Networking.Tests.Tests; + +[ExcludeFromCodeCoverage] +[TestFixture] +public class NetworkRequestFactoryTests +{ + public class WhenCreatingRequests + { + [Test] + public async Task ShouldCreateGetRequest() + { + // Arrange + var factory = CreateFactory(); + + // Act + var request = factory.Get("https://httpbin.org/get?key=value"); + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldContain("key=value"); + } + + [Test] + public async Task ShouldCreatePostRequest() + { + // Arrange + var factory = CreateFactory(); + var data = JsonSerializer.Serialize(new { key = "value" }); + + // Act + var request = factory.Post("https://httpbin.org/post", data); + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + response.Data.ShouldNotBeNull(); + } + + [Test] + public async Task ShouldCreateDeleteRequest() + { + // Arrange + var factory = CreateFactory(); + + // Act + var request = factory.Delete("https://httpbin.org/delete"); + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + } + + [Test] + public void ShouldCreateMultipartRequest() + { + // Arrange + var factory = CreateFactory(); + + // Act + var request = factory.PostMultipart("https://httpbin.org/post"); + + // Assert + request.ShouldNotBeNull(); + request.Content.ShouldNotBeNull(); + } + } + + public class WhenUsingMockHandler + { + [Test] + public async Task ShouldThrowForErrorStatusCode() + { + // Arrange + var factory = CreateFactoryWithMock(new MockHttpMessageHandler(HttpStatusCode.NotFound)); + + // Act & Assert + var request = factory.Get("http://localhost/missing"); + await request.ExecuteAsync().ShouldThrowAsync(); + } + } + + public class WhenUsingNamedClient + { + [Test] + public async Task ShouldUseNamedClient() + { + // Arrange + var expected = new JsonGetResponse { Url = "http://localhost/test" }; + var mockHandler = new MockHttpMessageHandler(HttpStatusCode.OK, JsonSerializer.Serialize(expected)); + + var services = new ServiceCollection(); + services.AddNetworkRequestFactory(); + services.AddHttpClient("test") + .ConfigurePrimaryHttpMessageHandler(() => mockHandler); + + var provider = services.BuildServiceProvider(); + var factory = provider.GetRequiredService(); + + // Act + var request = factory.WithClient("test").Get("http://localhost/test"); + var response = await request.ExecuteAsync(); + + // Assert + response.ShouldNotBeNull(); + response.Url.ShouldBe("http://localhost/test"); + mockHandler.CallCount.ShouldBe(1); + } + } + + public class WhenRegisteringWithDI + { + [Test] + public void ShouldResolveFactory() + { + // Arrange + var services = new ServiceCollection(); + services.AddNetworkRequestFactory(); + var provider = services.BuildServiceProvider(); + + // Act + var factory = provider.GetService(); + + // Assert + factory.ShouldNotBeNull(); + } + + [Test] + public void ShouldResolveWithNamedClientRegistration() + { + // Arrange + var services = new ServiceCollection(); + services.AddNetworkRequestFactory("MyApi", client => + { + client.BaseAddress = new Uri("https://httpbin.org"); + }); + + var provider = services.BuildServiceProvider(); + + // Act + var factory = provider.GetService(); + + // Assert + factory.ShouldNotBeNull(); + } + } + + private static INetworkRequestFactory CreateFactory() + { + var services = new ServiceCollection(); + services.AddNetworkRequestFactory(); + return services.BuildServiceProvider().GetRequiredService(); + } + + private static INetworkRequestFactory CreateFactoryWithMock(MockHttpMessageHandler handler) + { + var services = new ServiceCollection(); + services.AddNetworkRequestFactory(); + services.AddHttpClient(string.Empty) + .ConfigurePrimaryHttpMessageHandler(() => handler); + return services.BuildServiceProvider().GetRequiredService(); + } + + public class JsonGetResponse + { + public string Url { get; set; } = string.Empty; + } + + public class JsonPostResponse + { + public string Data { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; + } +}