diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d339575 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,237 @@ +# +# Severity: suggestion, warning, error +# +#top-most EditorConfig file +root = true + +[*] + +#Formatting - indentation +#use soft tabs (spaces) for indentation +indent_style = space + +[*.cs] + +#Formatting - indentation + +#size of soft tabs (spaces) +indent_size = 4 +#remove any whitespace characters preceding newline characters +trim_trailing_whitespace = true + +#Formatting - indentation options + +#indent switch case contents. +csharp_indent_case_contents = true +#indent switch labels +csharp_indent_switch_labels = true + +#Formatting - new line options + +#place catch statements on a new line +csharp_new_line_before_catch = true +#place else statements on a new line +csharp_new_line_before_else = true +#require finally statements to be on a new line after the closing brace +csharp_new_line_before_finally = true +#require members of object initializers to be on separate lines +csharp_new_line_before_members_in_object_initializers = true +#require members of anonymous types to be on separate lines +csharp_new_line_before_members_in_anonymous_types = true +#require elements of query expression clauses to be on separate lines +csharp_new_line_between_query_expression_clauses = true +#require braces to be on a new line for all expressions ("Allman" style) +csharp_new_line_before_open_brace = all + +#Formatting - organize using options + +#do not place System.* using directives before other using directives +dotnet_sort_system_directives_first = false + +#Formatting - spacing options + +#require a space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true +#leave statements and member declarations on the same line +csharp_preserve_single_line_statements = true + +#Style - code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = false:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:error + +#Style - expression-level preferences + +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion +#prefer collections to be initialized using collection initializers when possible +dotnet_style_collection_initializer = true:suggestion +#prefer tuple names to ItemX properties +dotnet_style_explicit_tuple_names = true:error +#prefer inferred tuple element names +dotnet_style_prefer_inferred_tuple_names = true:warning +#prefer inferred anonymous type member names +dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning +#prefer autoproperties over properties with private backing fields +dotnet_style_prefer_auto_properties = true:warning +#prefer assignments with a ternary conditional over an if-else statement +dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion +#prefer return statements to use a ternary conditional over an if-else statement +dotnet_style_prefer_conditional_expression_over_return = false +#prefer using a null check with pattern-matching over object.ReferenceEquals +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:error + +#Style - implicit and explicit types + +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:warning +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:warning +#prefer var is used to declare variables over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:warning + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:error + +#Style - qualification options + +#prefer fields not to be prefaced with This +dotnet_style_qualification_for_field = false:error +##prefer methods not to be prefaced with This +dotnet_style_qualification_for_method = false:error +##prefer properties not to be prefaced with This +dotnet_style_qualification_for_property = false:error +##prefer events not to be prefaced with This +dotnet_style_qualification_for_event = false:error + +#Style - modifier preferences + +#prefer accessibility modifiers to be specified +dotnet_style_require_accessibility_modifiers = always:error +#when this rule is set to a list of modifiers, prefer the specified ordering +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +#Style - parentheses preferences + +#prefer parentheses to clarify arithmetic operator(*, /, %, +, -, <<, >>, &, ^, |) precedence +dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion +#prefer parentheses to clarify relational operator (>, <, <=, >=, is, as, ==, !=) precedence +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion +#prefer parentheses to clarify other binary operator (&&, ||, ??) precedence +dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion +#prefer parentheses to clarify operator precedence +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion + +#Naming rules + +#async methods are PascalCase and end with Async +dotnet_naming_rule.async_methods_should_end_in_async.severity = error +dotnet_naming_rule.async_methods_should_end_in_async.symbols = async_methods +dotnet_naming_rule.async_methods_should_end_in_async.style = async_methods_style + +dotnet_naming_symbols.async_methods.applicable_kinds = method +dotnet_naming_symbols.async_methods.applicable_accessibilities = * +dotnet_naming_symbols.async_methods.required_modifiers = async + +dotnet_naming_style.async_methods_style.capitalization = pascal_case +dotnet_naming_style.async_methods_style.required_suffix = Async + + +#private fields are camelCase and start with _ +dotnet_naming_rule.private_fields_should_be_camel_case.severity = error +dotnet_naming_rule.private_fields_should_be_camel_case.symbols = private_fields +dotnet_naming_rule.private_fields_should_be_camel_case.style = private_field_style + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.private_field_style.required_prefix = _ +dotnet_naming_style.private_field_style.capitalization = camel_case + + +#methods are PascalCase +dotnet_naming_rule.methods_should_be_pascal_case.severity = error +dotnet_naming_rule.methods_should_be_pascal_case.symbols = methods +dotnet_naming_rule.methods_should_be_pascal_case.style = methods_style + +dotnet_naming_symbols.methods.applicable_kinds = method +dotnet_naming_symbols.methods.applicable_accessibilities = * + +dotnet_naming_style.methods_style.capitalization = pascal_case + + +#classes are PacalCase +dotnet_naming_rule.classes_should_be_pascal_case.severity = error +dotnet_naming_rule.classes_should_be_pascal_case.symbols = classes +dotnet_naming_rule.classes_should_be_pascal_case.style = classes_style + +dotnet_naming_symbols.classes.applicable_kinds = class +dotnet_naming_symbols.classes.applicable_accessibilities = * + +dotnet_naming_style.classes_style.capitalization = pascal_case + + +#parameters are camelCase +dotnet_naming_rule.parameters_should_be_camel_case.severity = error +dotnet_naming_rule.parameters_should_be_camel_case.symbols = parameters +dotnet_naming_rule.parameters_should_be_camel_case.style = parameters_style + +dotnet_naming_symbols.parameters.applicable_kinds = parameter + +dotnet_naming_style.parameters_style.capitalization = camel_case + + +#interfaces are PacalCase and start with I +dotnet_naming_rule.interfaces_should_be_pascal_case.severity = error +dotnet_naming_rule.interfaces_should_be_pascal_case.symbols = interfaces +dotnet_naming_rule.interfaces_should_be_pascal_case.style = interfaces_style + +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_symbols.interfaces.applicable_accessibilities = * + +dotnet_naming_style.interfaces_style.capitalization = pascal_case +dotnet_naming_style.interfaces_style.required_prefix = I + + +# private fields and Async methods naming are optional for tests. +# this allows to declare `private sut` and name async test without the `Async` suffix. +[**/*.{Tests,IntegrationTests,FunctionalTests}/**.cs] +dotnet_naming_rule.private_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.async_methods_should_end_in_async.severity = suggestion diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 0000000..7aaeaf1 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,98 @@ +name: Build, Test, and Deploy + +on: + push: + branches: + - master + - main + paths-ignore: + - 'samples/**' + + pull_request: + branches: + - master + - main + + repository_dispatch: + types: + - deploy + + workflow_dispatch: + inputs: + deployToFeedz: + description: 'Set to `true` to deploy to Feedz.io' + required: false + default: '' + deployToNuget: + description: 'Set to `true` to deploy to NuGet.org' + required: false + default: '' + +env: + DOTNET_2_VERSION: '2.1.x' + DOTNET_3_VERSION: '3.1.x' + DOTNET_5_VERSION: '5.0.x' + BUILD_CONFIGURATION: Release + FEEDZ_URI: https://f.feedz.io/forevolve/operationresults/nuget/index.json + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dotnet: ['5.0.x'] + env: + IS_NOT_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} + + steps: + - uses: actions/checkout@v1 + if: env.IS_NOT_DISPATCH + + - name: Setup .NET Core + if: env.IS_NOT_DISPATCH + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Build + if: env.IS_NOT_DISPATCH + run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} + + - name: Unit Test + if: env.IS_NOT_DISPATCH + run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} + + deploy: + runs-on: ubuntu-latest + needs: build-and-test + strategy: + fail-fast: true + matrix: + dotnet: ['5.0.x'] + + steps: + - uses: actions/checkout@v1 + with: + ref: ${{ github.event.client_payload.ref }} + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + + - uses: dotnet/nbgv@master + with: + setAllVars: true + + - name: Pack + run: dotnet pack --configuration ${{ env.BUILD_CONFIGURATION }} + + - name: Push to feedz.io + run: dotnet nuget push **/*.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s ${{ env.FEEDZ_URI }} + if: github.event_name == 'pull_request' || (github.event_name == 'repository_dispatch' && github.event.client_payload.feedz == true) || (github.event_name == 'workflow_dispatch' && github.event.inputs.deployToFeedz == 'true') + + - name: Push to NuGet.org + run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json + if: github.event_name == 'push' || (github.event_name == 'repository_dispatch' && github.event.client_payload.nuget == true) || (github.event_name == 'workflow_dispatch' && github.event.inputs.deployToNuget == 'true') diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62e1b92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,265 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# Code coverage +coverage/ +.nyc_output/ + +# Local build scripts +local-build.ps1 +local-signed.ps1 +local-test.ps1 +local-test-output/ + +# VS Code +.vscode/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..7eb69ac --- /dev/null +++ b/.prettierrc @@ -0,0 +1,14 @@ +{ + "$schema": "http://json.schemastore.org/prettierrc", + "tabWidth": 4, + "printWidth": 140, + "singleQuote": true, + "overrides": [ + { + "files": ["*.yaml", "*.yml"], + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..fcb2be8 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + latest + + \ No newline at end of file diff --git a/ForEvolve.OperationResults.sln b/ForEvolve.OperationResults.sln new file mode 100644 index 0000000..92d002e --- /dev/null +++ b/ForEvolve.OperationResults.sln @@ -0,0 +1,111 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30021.99 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5E6BF7CA-D8BA-40DC-A57F-DC42AE9BAF6E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C2A89744-3ADE-4349-B861-243D394CE14E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.OperationResults", "src\ForEvolve.OperationResults\ForEvolve.OperationResults.csproj", "{4263DE5B-45F9-43A2-9916-0CDC7910DD4C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.OperationResults.AspNetCore", "src\ForEvolve.OperationResults.AspNetCore\ForEvolve.OperationResults.AspNetCore.csproj", "{0DEA78FB-065C-4B01-9B66-57E70A3B647F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.OperationResults.Tests", "test\ForEvolve.OperationResults.Tests\ForEvolve.OperationResults.Tests.csproj", "{2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.OperationResults.AspNetCore.Tests", "test\ForEvolve.OperationResults.AspNetCore.Tests\ForEvolve.OperationResults.AspNetCore.Tests.csproj", "{89FD91BC-13E4-4A7B-9F37-7AB075C2700A}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{CAE60B3D-061E-4B12-BFD3-E966AA7B10E8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{206A433D-0EEB-4BB5-9C9A-7636AB172D4F}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C4D44813-C5C9-45B7-B918-7B078BEBCE9C}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{243DF7F2-15EF-45E9-81E9-C223F04E2015}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|x64.Build.0 = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Debug|x86.Build.0 = Debug|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|Any CPU.Build.0 = Release|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|x64.ActiveCfg = Release|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|x64.Build.0 = Release|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|x86.ActiveCfg = Release|Any CPU + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C}.Release|x86.Build.0 = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|x64.ActiveCfg = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|x64.Build.0 = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|x86.ActiveCfg = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Debug|x86.Build.0 = Debug|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|Any CPU.Build.0 = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|x64.ActiveCfg = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|x64.Build.0 = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|x86.ActiveCfg = Release|Any CPU + {0DEA78FB-065C-4B01-9B66-57E70A3B647F}.Release|x86.Build.0 = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|x64.Build.0 = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Debug|x86.Build.0 = Debug|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|Any CPU.Build.0 = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|x64.ActiveCfg = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|x64.Build.0 = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|x86.ActiveCfg = Release|Any CPU + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C}.Release|x86.Build.0 = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|x64.ActiveCfg = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|x64.Build.0 = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|x86.ActiveCfg = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Debug|x86.Build.0 = Debug|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|Any CPU.Build.0 = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|x64.ActiveCfg = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|x64.Build.0 = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|x86.ActiveCfg = Release|Any CPU + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {4263DE5B-45F9-43A2-9916-0CDC7910DD4C} = {5E6BF7CA-D8BA-40DC-A57F-DC42AE9BAF6E} + {0DEA78FB-065C-4B01-9B66-57E70A3B647F} = {5E6BF7CA-D8BA-40DC-A57F-DC42AE9BAF6E} + {2F188C34-E6F5-4C8F-851A-AF09D8AE2E5C} = {C2A89744-3ADE-4349-B861-243D394CE14E} + {89FD91BC-13E4-4A7B-9F37-7AB075C2700A} = {C2A89744-3ADE-4349-B861-243D394CE14E} + {206A433D-0EEB-4BB5-9C9A-7636AB172D4F} = {CAE60B3D-061E-4B12-BFD3-E966AA7B10E8} + {C4D44813-C5C9-45B7-B918-7B078BEBCE9C} = {CAE60B3D-061E-4B12-BFD3-E966AA7B10E8} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {18F52EC3-580A-4474-8BBE-C7DBF2F0855A} + EndGlobalSection +EndGlobal diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..45adfe3 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,32 @@ + + + + True + False + Carl-Hugo Marcotte + ForEvolve + https://github.com/ForEvolve/ForEvolve.OperationResults + MIT + Carl-Hugo Marcotte + true + true + True + snupkg + + + + preview + + + latest + + + + + + 3.3.37 + all + + + + \ No newline at end of file diff --git a/src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs new file mode 100644 index 0000000..29b1e7c --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs @@ -0,0 +1,26 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.AspNetCore; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System +{ + public static class ExceptionExtensions + { + public static IOperationResult ToOperationResult(this Exception exception) + { + var result = new OperationResult(); + result.Messages.Add(new ExceptionMessage(exception)); + return result; + } + + public static IOperationResult ToOperationResult(this Exception exception) + { + var result = new OperationResult(); + result.Messages.Add(new ExceptionMessage(exception)); + return result; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs new file mode 100644 index 0000000..8923dda --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Text.Json.Serialization; + +namespace ForEvolve.OperationResults.AspNetCore +{ + /// + /// Represents a wrapper message around an . + /// + public class ExceptionMessage : ProblemDetailsMessage + { + /// + /// Get the exception represented by this message. + /// + [JsonIgnore] + public Exception Exception { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The that represents the message. + public ExceptionMessage(Exception exception) + : base(OperationMessageLevel.Error) + { + Exception = exception ?? throw new ArgumentNullException(nameof(exception)); + LoadProblemDetails(new ProblemDetails + { + Title = exception.GetType().Name, + Detail = exception.Message + }); + } + + /// + [JsonIgnore] + public override Type Type => Exception.GetType(); + + /// + [JsonIgnore] + public override object OriginalObject => Exception; + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj new file mode 100644 index 0000000..50da2dc --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -0,0 +1,17 @@ + + + + net5.0;netcoreapp3.1 + Extensions of ForEvolve.OperationResults ASP.NET Core. Adds support for ProblemDetails and includes a ModelBinderErrorActionFilter. + forevolve,aspnetcore,mvc,filters,asp.net,core,aspnet,asp,operation,result,results,message,exception,net5 + + + + + + + + + + + diff --git a/src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs new file mode 100644 index 0000000..1649c5c --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs @@ -0,0 +1,26 @@ +using ForEvolve.OperationResults.AspNetCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults +{ + public static class MessageCollectionExtensions + { + /// + /// Filters exception messages and returns their that are of the specified type. + /// + /// The type of to search for. + /// + /// The filtered messages . + public static IEnumerable GetExceptionsOfType(this MessageCollection messages) + where TException : Exception + { + return messages + .GetAll() + .HavingDetailsOfTypeAs(); + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs new file mode 100644 index 0000000..4fdb645 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs @@ -0,0 +1,43 @@ +using ForEvolve.OperationResults.AspNetCore; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + public class ModelBinderErrorActionFilter : IAsyncActionFilter + { + public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + if (!context.ModelState.IsValid) + { + var errorMessages = context.ModelState.Values + .SelectMany(x => + { + return x.Errors; + }) + .Where(x => x.Exception == default) + .Select(x => new Message(OperationMessageLevel.Error, new + { + ErrorCode = "ModelBindingError", + x.ErrorMessage + })); + var exceptionMessages = context.ModelState.Values + .SelectMany(x => x.Errors) + .Where(x => x.Exception != default) + .Select(x => new ExceptionMessage(x.Exception)); + + var messages = errorMessages.Concat(exceptionMessages); + var failure = new OperationResult(); + failure.Messages.AddRange(messages); + context.Result = new BadRequestObjectResult(failure); + return Task.CompletedTask; + } + return next(); + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs new file mode 100644 index 0000000..063e69f --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs @@ -0,0 +1,30 @@ +using ForEvolve.OperationResults.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OperationResultAspNetCoreStartupExtensions + { + /// + /// Adds operation results Asp.Net Core filters. + /// This includes an interceptor that returns a non-successful operation result + /// when the ModelBinder is not able to create the model; see for more info. + /// + /// The services. + /// IServiceCollection. + public static IServiceCollection AddForEvolveOperationResultModelBinderErrorActionFilter(this IServiceCollection services) + { + services.AddSingleton(); + services.Configure(options => + { + options.Filters.AddService(); + }); + return services; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..cac14c0 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs @@ -0,0 +1,41 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.AspNetCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Mvc +{ + public static class ProblemDetailsExtensions + { + public static IOperationResult ToOperationResult(this ProblemDetails problemDetails) + { + if (problemDetails == null) { throw new ArgumentNullException(nameof(problemDetails)); } + return ToOperationResult(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult ToOperationResult(this ProblemDetails problemDetails, OperationMessageLevel severity) + { + if (problemDetails == null) { throw new ArgumentNullException(nameof(problemDetails)); } + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + return result; + } + + public static IOperationResult ToOperationResult(ProblemDetails problemDetails) + { + if (problemDetails == null) { throw new ArgumentNullException(nameof(problemDetails)); } + return ToOperationResult(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult ToOperationResult(ProblemDetails problemDetails, OperationMessageLevel severity) + { + if (problemDetails == null) { throw new ArgumentNullException(nameof(problemDetails)); } + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + return result; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs new file mode 100644 index 0000000..5f3c3ba --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults.AspNetCore +{ + /// + /// Represents an operation result message build around [RFC3986] . + /// Inherits from + /// + /// + public class ProblemDetailsMessage : Message + { + /// + /// Gets the problem details. + /// + /// The problem details. + [JsonIgnore] + public ProblemDetails ProblemDetails { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The problem details. + /// The severity. + /// problemDetails + public ProblemDetailsMessage(ProblemDetails problemDetails, OperationMessageLevel severity) + : base(severity, problemDetails) + { + ProblemDetails = problemDetails ?? throw new ArgumentNullException(nameof(problemDetails)); + LoadProblemDetails(problemDetails); + } + + /// + /// Initializes a new instance of the class. + /// Sub-classes must manually call the method. + /// + /// The severity. + protected ProblemDetailsMessage(OperationMessageLevel severity) + : base(severity) + { + } + + /// + /// Loads the specified problem details into the dictionary. + /// + /// The problem details to load. + protected void LoadProblemDetails(ProblemDetails problemDetails) + { + if (problemDetails.Type != null) + { + Details.Add(nameof(problemDetails.Type).ToLowerInvariant(), problemDetails.Type); + } + if (problemDetails.Title != null) + { + Details.Add(nameof(problemDetails.Title).ToLowerInvariant(), problemDetails.Title); + } + if (problemDetails.Status != null) + { + Details.Add(nameof(problemDetails.Status).ToLowerInvariant(), problemDetails.Status); + } + if (problemDetails.Detail != null) + { + Details.Add(nameof(problemDetails.Detail).ToLowerInvariant(), problemDetails.Detail); + } + if (problemDetails.Instance != null) + { + Details.Add(nameof(problemDetails.Instance).ToLowerInvariant(), problemDetails.Instance); + } + foreach (var item in problemDetails.Extensions) + { + Details.Add(item); + } + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizer.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizer.cs new file mode 100644 index 0000000..d9aeb22 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizer.cs @@ -0,0 +1,82 @@ +using Microsoft.CSharp.RuntimeBinder; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents the default standardizer. + /// Implements the + /// + /// + public class DefaultOperationResultStandardizer : IOperationResultStandardizer + { + private readonly IPropertyNameFormatter _propertyNameFormatter; + private readonly IPropertyValueFormatter _propertyValueFormatter; + private readonly DefaultOperationResultStandardizerOptions _options; + private readonly ILogger _logger; + + public DefaultOperationResultStandardizer( + IPropertyNameFormatter propertyNameFormatter, + IPropertyValueFormatter propertyValueFormatter, + IOptionsMonitor options, + ILogger logger) + { + _propertyNameFormatter = propertyNameFormatter ?? throw new ArgumentNullException(nameof(propertyNameFormatter)); + _propertyValueFormatter = propertyValueFormatter ?? throw new ArgumentNullException(nameof(propertyValueFormatter)); + _options = options.CurrentValue ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public object Standardize(IOperationResult operationResult) + { + if (operationResult == null) { throw new ArgumentNullException(nameof(operationResult)); } + + var dictionary = new Dictionary(); + if (!string.IsNullOrEmpty(_options.OperationName)) + { + dictionary.Add(_options.OperationName, new + { + operationResult.Messages, + operationResult.Succeeded + }); + } + try + { + AddValueToDictionary(operationResult, dictionary); + } + catch (RuntimeBinderException ex) + { + _logger.LogError(ex, ex.Message); + } + return dictionary; + } + + private void AddValueToDictionary(IOperationResult operationResult, Dictionary dictionary) + { + var value = FindValueProperty(operationResult); + if (value != null) + { + foreach (var property in value.GetType().GetProperties()) + { + var formattedName = _propertyNameFormatter.Format(property.Name); + var formattedValue = _propertyValueFormatter.Format(property.GetValue(value)); + dictionary.Add(formattedName, formattedValue); + } + } + } + + private static object FindValueProperty(IOperationResult operationResult) + { + const string valuePropertyName = nameof(IOperationResult.Value); + var valueProperty = operationResult.GetType() + .GetProperties() + .SingleOrDefault(x => x.Name == valuePropertyName); + var value = valueProperty?.GetValue(operationResult); + return value; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizerOptions.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizerOptions.cs new file mode 100644 index 0000000..fe66562 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizerOptions.cs @@ -0,0 +1,19 @@ +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents the options. + /// + public class DefaultOperationResultStandardizerOptions + { + /// + /// The default member name of the operation values. + /// + public const string DefaultOperationName = "_operation"; + + /// + /// Gets or sets the member name of the operation values. + /// + /// The member name of the operation values. + public string OperationName { get; set; } = DefaultOperationName; + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs new file mode 100644 index 0000000..c717138 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs @@ -0,0 +1,21 @@ +using System; +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents the default property name formatter, used by . + /// Implements the + /// + /// + public class DefaultPropertyNameFormatter : IPropertyNameFormatter + { + /// + public string Format(string name) + { + if (string.IsNullOrEmpty(name)) { throw new ArgumentNullException(nameof(name)); } + + var firstChar = name.Substring(0, 1).ToLowerInvariant(); + var rest = name.Substring(1); + return firstChar + rest; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs new file mode 100644 index 0000000..f66a686 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs @@ -0,0 +1,16 @@ +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents the default property value formatter, used by . + /// Implements the + /// + /// + public class DefaultPropertyValueFormatter : IPropertyValueFormatter + { + /// + public object Format(object @object) + { + return @object; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IOperationResultStandardizer.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IOperationResultStandardizer.cs new file mode 100644 index 0000000..7a971e6 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IOperationResultStandardizer.cs @@ -0,0 +1,15 @@ +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents an standardizer. + /// + public interface IOperationResultStandardizer + { + /// + /// Standardizes the specified operation result into a serializable object. + /// + /// The operation result. + /// System.Object. + object Standardize(IOperationResult operationResult); + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyNameFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyNameFormatter.cs new file mode 100644 index 0000000..7bb192e --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyNameFormatter.cs @@ -0,0 +1,15 @@ +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents a property name formatter, used by . + /// + public interface IPropertyNameFormatter + { + /// + /// Formats the specified name. + /// + /// The name. + /// System.String. + string Format(string name); + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyValueFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyValueFormatter.cs new file mode 100644 index 0000000..8df866d --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyValueFormatter.cs @@ -0,0 +1,15 @@ +namespace ForEvolve.OperationResults.Standardizer +{ + /// + /// Represents a property value formatter, used by . + /// + public interface IPropertyValueFormatter + { + /// + /// Formats the specified object. + /// + /// The object. + /// System.Object. + object Format(object @object); + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerActionFilter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerActionFilter.cs new file mode 100644 index 0000000..50812e6 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerActionFilter.cs @@ -0,0 +1,47 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.Standardizer; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents an action filter that convert object to a + /// more standard result. + /// Implements the + /// + /// + public class OperationResultStandardizerActionFilter : IAsyncActionFilter + where TObjectResult : ObjectResult + { + private readonly IOperationResultStandardizer _operationResultStandardizer; + + public OperationResultStandardizerActionFilter(IOperationResultStandardizer operationResultStandardizer) + { + _operationResultStandardizer = operationResultStandardizer ?? throw new ArgumentNullException(nameof(operationResultStandardizer)); + } + + /// + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + var actionExecutedContext = await next.Invoke(); + OnActionExecuted(actionExecutedContext); + } + + private void OnActionExecuted(ActionExecutedContext actionExecutedContext) + { + if (actionExecutedContext.Result is TObjectResult objectResult) + { + if (objectResult.Value is IOperationResult operationResult) + { + objectResult.Value = _operationResultStandardizer.Standardize(operationResult); + } + } + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs new file mode 100644 index 0000000..bd3e045 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs @@ -0,0 +1,41 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.Standardizer; +using Microsoft.AspNetCore.Mvc; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class OperationResultStandardizerStartupExtensions + { + /// + /// Adds the default ForEvolve operation result standardizer filters. + /// + /// The services. + /// IServiceCollection. + /// This subsystem should to be revised. + public static IServiceCollection AddForEvolveOperationResultStandardizer(this IServiceCollection services) + { + services + .AddLogging() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddOptions() + ; + services + .Configure(options => + { + options.Filters.Add>(); + options.Filters.Add>(); + options.Filters.Add>(); + + options.Filters.Add>(); + + options.Filters.Add>(); + options.Filters.Add>(); + options.Filters.Add>(); + options.Filters.Add>(); + }); + return services; + } + } +} diff --git a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj new file mode 100644 index 0000000..8786eee --- /dev/null +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -0,0 +1,9 @@ + + + + net5.0;netcoreapp3.1 + Generic implementation of the operation result pattern that should fits most needs. + forevolve,aspnetcore,asp.net,core,aspnet,asp,operation,result,results,message,exception,net5 + + + diff --git a/src/ForEvolve.OperationResults/IMessage.cs b/src/ForEvolve.OperationResults/IMessage.cs new file mode 100644 index 0000000..2c86370 --- /dev/null +++ b/src/ForEvolve.OperationResults/IMessage.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents a generic operation result message. + /// + public interface IMessage + { + /// + /// Gets the severity associated with the message. + /// + /// The severity. + OperationMessageLevel Severity { get; } + + /// + /// Gets the message details. + /// + /// The details. + IDictionary Details { get; } + + /// + /// Gets the message type. + /// + /// The type of message. + [JsonIgnore] + Type Type { get; } + + /// + /// Validate if the value match the . + /// + /// The type to validate. + /// true is the matches ; otherwise false. + bool Is(); + + /// + /// Validate if the value match the specified type. + /// + /// The type to validate. + /// true is the matches the specified type; otherwise false. + bool Is(Type type); + + /// + /// Convert the to the specified type, assuming they are compatible. + /// + /// The type of the expected object to return. + /// The converted object. + /// + TType As(); + + /// + /// Convert the to the specified type, assuming they are compatible. + /// + /// The type of the expected object to return. + /// The converted object. + /// + object As(Type type); + } +} diff --git a/src/ForEvolve.OperationResults/IOperationResult.cs b/src/ForEvolve.OperationResults/IOperationResult.cs new file mode 100644 index 0000000..a3a23c1 --- /dev/null +++ b/src/ForEvolve.OperationResults/IOperationResult.cs @@ -0,0 +1,47 @@ +namespace ForEvolve.OperationResults +{ + /// + /// Represents an operation result containing optional messages, generated by the operation. + /// + public interface IOperationResult + { + /// + /// Gets a value indicating whether the operation has succeeded. + /// + /// true if the operation has succeeded; otherwise, false. + bool Succeeded { get; } + + /// + /// Gets the messages associated with the operation result. + /// + /// The operation result messages. + MessageCollection Messages { get; } + + /// + /// Determines whether the operation generated any messages. + /// + /// true if the operation generated messages; otherwise, false. + bool HasMessages(); + } + + /// + /// Represents an operation result containing optional messages, generated by the operation, and an optional resulting object. + /// Implements the + /// + /// The type of the t value. + /// + public interface IOperationResult : IOperationResult + { + /// + /// Gets the value attached by the operation. + /// + /// The operation result's value. + TValue Value { get; } + + /// + /// Determines whether the operation attached a value. + /// + /// true if the operation attached a value; otherwise, false. + bool HasValue(); + } +} diff --git a/src/ForEvolve.OperationResults/Message.cs b/src/ForEvolve.OperationResults/Message.cs new file mode 100644 index 0000000..54cf975 --- /dev/null +++ b/src/ForEvolve.OperationResults/Message.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text.Json.Serialization; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents a generic operation result message. + /// Implements the + /// + /// + public class Message : IMessage + { + /// + /// Initializes a new instance of the class. + /// + /// The severity. + /// The message type. + public Message(OperationMessageLevel severity) + : this(severity, new Dictionary()) { } + + /// + /// Initializes a new instance of the class. + /// + /// The severity. + /// The details. + /// The message type. + /// details + public Message(OperationMessageLevel severity, IDictionary details, Type type = null) + { + Severity = severity; + Details = details ?? throw new ArgumentNullException(nameof(details)); + Type = type; + } + + /// + /// Initializes a new instance of the class. + /// + /// The message severity. + /// The message details that will be loaded in the . + /// if set to true null properties will be ignored (not added in the ). + /// details + public Message(OperationMessageLevel severity, object details, bool ignoreNull = true) + : this(severity, details) + { + LoadDetails(details, ignoreNull); + } + + /// + /// Initializes a new instance of the class. + /// + /// The message severity. + /// The message details that will be loaded in the . + /// details + protected Message(OperationMessageLevel severity, object details) + : this(severity) + { + if (details == null) { throw new ArgumentNullException(nameof(details)); } + Type = details.GetType(); + IsAnonymous = Type.Name.Contains("AnonymousType"); + OriginalObject = details; + } + + protected virtual void LoadDetails(object details, bool ignoreNull) + { + var properties = TypeDescriptor.GetProperties(details); + foreach (PropertyDescriptor property in properties) + { + var value = property.GetValue(details); + if (!ignoreNull || value != null) + { + Details.Add(property.Name, value); + } + } + } + + /// + public virtual bool Is() + { + return typeof(TType) == Type; + } + + /// + public virtual bool Is(Type type) + { + return type == Type; + } + + /// + public virtual TType As() + { + if (!Is()) + { + throw new TypeMismatchException(this, typeof(TType)); + } + return (TType)As(typeof(TType)); + } + + /// + public virtual object As(Type type) + { + if (!Is(type)) + { + throw new TypeMismatchException(this, type); + } + if (CanReturnTheOriginalObject(type)) + { + return OriginalObject; + } + var result = Activator­.CreateInstance(type); + var properties = TypeDescriptor.GetProperties(result); + foreach (PropertyDescriptor property in properties) + { + if (Details.ContainsKey(property.Name)) + { + property.SetValue(result, Details[property.Name]); + } + } + return result; + } + + private bool CanReturnTheOriginalObject(Type type) + { + return OriginalObject != null && type.IsAssignableFrom(OriginalObject.GetType()); + } + + /// + public virtual OperationMessageLevel Severity { get; } + + /// + public virtual IDictionary Details { get; } + + /// + [JsonIgnore] + public virtual Type Type { get; } + + /// + /// Gets if the was an anonymous type. + /// + [JsonIgnore] + public virtual bool IsAnonymous { get; } + + /// + /// Gets the original object that was used to load the Details, if any. + /// + [JsonIgnore] + public virtual object OriginalObject { get; } + } +} diff --git a/src/ForEvolve.OperationResults/MessageCollection.cs b/src/ForEvolve.OperationResults/MessageCollection.cs new file mode 100644 index 0000000..a99997a --- /dev/null +++ b/src/ForEvolve.OperationResults/MessageCollection.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents a collection of . + /// Implements the where T is a . + /// + /// + public class MessageCollection : IList + { + private readonly List _items = new List(); + + /// + public IMessage this[int index] { get => _items[index]; set => _items[index] = value; } + + /// + public int Count => _items.Count; + + /// + public bool IsReadOnly => ((IList)_items).IsReadOnly; + + /// + public void Add(IMessage item) + { + _items.Add(item); + } + + /// + /// Adds the elements of the specified collection to the end of the current . + /// + /// + /// The collection whose elements should be added to the end of the . + /// The collection itself cannot be null, but it can contain elements that are null. + /// + /// collection is null. + public void AddRange(IEnumerable collection) + { + _items.AddRange(collection); + } + + /// + public void Clear() + { + _items.Clear(); + } + + /// + public bool Contains(IMessage item) + { + return _items.Contains(item); + } + + /// + public void CopyTo(IMessage[] array, int arrayIndex) + { + _items.CopyTo(array, arrayIndex); + } + + /// + public IEnumerator GetEnumerator() + { + return ((IList)_items).GetEnumerator(); + } + + /// + public int IndexOf(IMessage item) + { + return _items.IndexOf(item); + } + + /// + public void Insert(int index, IMessage item) + { + _items.Insert(index, item); + } + + /// + public bool Remove(IMessage item) + { + return _items.Remove(item); + } + + /// + public void RemoveAt(int index) + { + _items.RemoveAt(index); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IList)_items).GetEnumerator(); + } + + /// + /// Determines whether this instance contains error messages. + /// + /// true if this instance contains error messages; otherwise, false. + public virtual bool HasError() + { + return HasLevel(OperationMessageLevel.Error); + } + + /// + /// Determines whether this instance contains warning messages. + /// + /// true if this instance contains warning messages; otherwise, false. + public virtual bool HasWarning() + { + return HasLevel(OperationMessageLevel.Warning); + } + + /// + /// Determines whether this instance contains information messages. + /// + /// true if this instance contains information messages; otherwise, false. + public virtual bool HasInformation() + { + return HasLevel(OperationMessageLevel.Information); + } + + // + // TODO: those next methods could maybe become extensions instead? + // + /// + /// Determines whether this instance contains a message of type . + /// + /// The type of message to look for. + /// true if this instance contains a message of the specified type; otherwise, false. + public bool Contains() + where TMessage : IMessage + { + return _items.Any(x => x is TMessage); + } + + /// + /// Gets the single message of type . + /// + /// The type of message to look for. + /// The single message. + public TMessage GetSingle() + where TMessage : IMessage + { + return (TMessage)_items.Single(x => x is TMessage); + } + + /// + /// Gets the first message of type . + /// + /// The type of message to look for. + /// The first message. + public TMessage GetFirst() + where TMessage : IMessage + { + return (TMessage)_items.First(x => x is TMessage); + } + + /// + /// Gets the last message of type . + /// + /// The type of message to look for. + /// The last message. + public TMessage GetLast() + where TMessage : IMessage + { + return (TMessage)_items.Last(x => x is TMessage); + } + + /// + /// Gets all messages of type . + /// + /// The type of message to look for. + /// The all messages. + public IEnumerable GetAll() + where TMessage : IMessage + { + return _items.Where(x => x is TMessage).Select(x => (TMessage)x); + } + + private bool HasLevel(OperationMessageLevel level) + { + return _items.Any(x => x.Severity == level); + } + } + + /// + /// Extensions to help handles OperationResults messages. + /// + public static class OperationResultsMessageExtensions + { + /// + /// Filters the messages and returns only those that are of the specified type. + /// + /// + /// + /// + /// The messages that are of the specified type. + public static IEnumerable HavingDetailsOfType(this IEnumerable messages) + where TMessage : IMessage + { + return messages.Where(x => x.Is()); + } + + /// + /// Determines whether this instance contains a message having its of type . + /// + /// The message type that is inputted and outputted back. + /// The type of to search for. + /// + /// true if the messages instance contains at least a message having its of the specified type; otherwise, false. + public static bool ContainsDetails(this IEnumerable messages) + where TMessage : IMessage + { + return messages.Any(x => x.Is()); + } + + /// + /// Filters the messages and returns their that are of the specified type. + /// + /// The type of to search for. + /// + /// The filtered messages . + public static IEnumerable HavingDetailsOfTypeAs(this IEnumerable messages) + { + return messages + .Where(x => x.Is()) + .Select(x => x.As()); + } + } +} diff --git a/src/ForEvolve.OperationResults/OperationMessageLevel.cs b/src/ForEvolve.OperationResults/OperationMessageLevel.cs new file mode 100644 index 0000000..d6dfdec --- /dev/null +++ b/src/ForEvolve.OperationResults/OperationMessageLevel.cs @@ -0,0 +1,24 @@ +namespace ForEvolve.OperationResults +{ + /// + /// Represents the severity level. + /// + public enum OperationMessageLevel + { + /// + /// Messages that has no impact in the application flow. + /// + Information = 0, + + /// + /// Messages that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the application execution to stop. + /// + Warning = 1, + + /// + /// Messages that highlight when the current flow of execution is stopped due to a failure. + /// These should indicate a failure in the current activity, not an application-wide failure. + /// + Error = 2, + } +} diff --git a/src/ForEvolve.OperationResults/OperationResult.cs b/src/ForEvolve.OperationResults/OperationResult.cs new file mode 100644 index 0000000..bc38961 --- /dev/null +++ b/src/ForEvolve.OperationResults/OperationResult.cs @@ -0,0 +1,199 @@ +using System; +using System.Text.Json.Serialization; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents an operation result containing optional messages, generated by the operation. + /// Implements the + /// + /// + public class OperationResult : IOperationResult + { + /// + [JsonIgnore] + public bool Succeeded => !Messages.HasError(); + + /// + public MessageCollection Messages { get; } = new MessageCollection(); + + /// + public bool HasMessages() + { + return Messages.Count > 0; + } + + #region OperationResult Factory Methods + + public static IOperationResult Success() + { + return new OperationResult(); + } + + public static IOperationResult Failure(params IMessage[] messages) + { + if (messages == null || messages.Length == 0) { throw new ArgumentNullException(nameof(messages)); } + var result = new OperationResult(); + result.Messages.AddRange(messages); + return result; + } + + #endregion + + #region OperationResult Factory Methods + + public static IOperationResult Success() + { + return new OperationResult(); + } + + public static IOperationResult Success(TValue value) + { + return new OperationResult { Value = value }; + } + + public static IOperationResult Failure(params IMessage[] messages) + { + var result = new OperationResult(); + result.Messages.AddRange(messages); + return result; + } + + #endregion + } + + /// + /// Represents an operation result containing optional messages, generated by the operation, and an optional resulting object. + /// Implements the + /// Implements the + /// + /// The type of the t value. + /// + /// + public class OperationResult : OperationResult, IOperationResult + { + /// + public TValue Value { get; set; } + + /// + public bool HasValue() + { + return Value != null; + } + } + + public static class OperationResultExtensions + { + #region Conversion operators + + public static TOperationResult ConvertTo( + this IOperationResult operationResult) + where TOperationResult : IOperationResult + { + TOperationResult result; + var type = typeof(TOperationResult); + var genericOperationResultType = typeof(OperationResult<>); + if (type.IsGenericType && type.Name.Equals(genericOperationResultType.Name)) + { + var genericArgs = type.GetGenericArguments(); + var finalType = genericOperationResultType.MakeGenericType(genericArgs); + result = (TOperationResult)Activator.CreateInstance(finalType); + } + else + { + var targetType = typeof(TOperationResult); + if (targetType.IsGenericType) + { + var genericImplementationType = typeof(OperationResult<>); + var genericArgs = targetType.GetGenericArguments(); + var finalType = genericImplementationType.MakeGenericType(genericArgs); + result = (TOperationResult)Activator.CreateInstance(finalType); + } + else + { + var nonGenericResult = new OperationResult(); + result = (TOperationResult)(IOperationResult)nonGenericResult; + } + } + result.Messages.AddRange(operationResult.Messages); + return result; + } + + public static IOperationResult ConvertTo( + this IOperationResult operationResult) + where TOperationResult : IOperationResult + { + var genericResult = new OperationResult(); + genericResult.Messages.AddRange(operationResult.Messages); + return genericResult; + } + + #endregion + + public static TOperationResult On(this TOperationResult operationResult, + Action success = null, + Action failure = null + ) + where TOperationResult : IOperationResult + { + var result = operationResult; + if (success != null) + { + result = result.OnSuccess(success); + } + if (failure != null) + { + result = result.OnFailure(failure); + } + return result; + } + + public static TOperationResult OnSuccess(this TOperationResult operationResult, Action successDelegate) + where TOperationResult : IOperationResult + { + if (operationResult == null) { throw new ArgumentNullException(nameof(operationResult)); } + if (operationResult.Succeeded) + { + successDelegate(operationResult); + } + return operationResult; + } + + public static TOperationResult OnFailure(this TOperationResult operationResult, Action failureDelegate) + where TOperationResult : IOperationResult + { + if (operationResult == null) { throw new ArgumentNullException(nameof(operationResult)); } + if (!operationResult.Succeeded) + { + failureDelegate(operationResult); + } + return operationResult; + } + } + + /// + /// TODO: DELETE ME + /// + class MyClass + { + public IOperationResult Operation() + { + return OperationResult.Success(); + } + + public void Consumer() + { + Operation() + .OnSuccess(r => Console.WriteLine("Success")) + .OnFailure(r => Console.WriteLine("Failure!")); + } + + public void Consumer2() + { + Operation().On( + success: r => Console.WriteLine("Success"), + failure: r => Console.WriteLine("Failure!") + ); + } + } +} diff --git a/src/ForEvolve.OperationResults/README.md b/src/ForEvolve.OperationResults/README.md new file mode 100644 index 0000000..a27cbeb --- /dev/null +++ b/src/ForEvolve.OperationResults/README.md @@ -0,0 +1,7 @@ +# ForEvolve.OperationResults + +TODO... + +### How to use it + +TODO diff --git a/src/ForEvolve.OperationResults/TypeMismatchException.cs b/src/ForEvolve.OperationResults/TypeMismatchException.cs new file mode 100644 index 0000000..7906777 --- /dev/null +++ b/src/ForEvolve.OperationResults/TypeMismatchException.cs @@ -0,0 +1,27 @@ +using System; + +namespace ForEvolve.OperationResults +{ + /// + /// The exception that is thrown when the is not loadable as the specified type. + /// + public class TypeMismatchException : TypeLoadException + { + public TypeMismatchException(IMessage sourceMessage, Type type) + : base($"Type mismatch; cannot convert '{sourceMessage?.Type?.Name ?? "null"}' to '{type?.Name ?? "null"}'.") + { + SourceMessage = sourceMessage ?? throw new ArgumentNullException(nameof(sourceMessage)); + Type = type ?? throw new ArgumentNullException(nameof(type)); + } + + /// + /// Gets the source message that generated the exception. + /// + public IMessage SourceMessage { get; } + + /// + /// Gets the type that the message was supposed to be converted into. + /// + public Type Type { get; } + } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 0000000..0a3cbf3 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,20 @@ + + + + false + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj new file mode 100644 index 0000000..e399832 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj @@ -0,0 +1,14 @@ + + + ForEvolve.OperationResults.AspNetCore + + + + net5.0 + + + + + + + diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ForEvolveMediatRExtensionsTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ForEvolveMediatRExtensionsTest.cs new file mode 100644 index 0000000..816aa6e --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ForEvolveMediatRExtensionsTest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.MediatR +{ + public class ForEvolveMediatRExtensionsTest + { + [Fact(Skip = "Should be tested")] + public void Should_be_tested() + { + throw new NotImplementedException(); + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ValidationBehaviorTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ValidationBehaviorTest.cs new file mode 100644 index 0000000..3b1a499 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ValidationBehaviorTest.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.MediatR +{ + public class ValidationBehaviorTest + { + [Fact(Skip = "Should be tested")] + public void Should_be_tested() + { + throw new NotImplementedException(); + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionExtensionsTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionExtensionsTest.cs new file mode 100644 index 0000000..3cc3935 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionExtensionsTest.cs @@ -0,0 +1,20 @@ +using System; +using Xunit; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + public class ExceptionExtensionsTest + { + public class ToOperationResult : ProblemDetailsExtensionsTest + { + [Fact] + public void Should_throw_a_ArgumentNullException_when_exception_is_null() + { + Assert.Throws( + "exception", + () => ExceptionExtensions.ToOperationResult(default) + ); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs new file mode 100644 index 0000000..a1fc988 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + public class ExceptionMessageTest + { + protected virtual Exception ExpectedException { get; } = new ArgumentNullException(); + private readonly ExceptionMessage sut; + + public ExceptionMessageTest() + { + sut = new ExceptionMessage(ExpectedException); + } + + public class Ctor : ExceptionMessageTest + { + [Fact] + public void Should_guard_against_null_exception() + { + var nullException = default(Exception); + Assert.Throws( + "exception", + () => new ExceptionMessage(nullException)); + } + + [Fact] + public void Should_set_Severity_to_Error() + { + Assert.Equal(OperationMessageLevel.Error, sut.Severity); + } + + [Fact(Skip = "Should be implemented as part of issue #49.")] + public void Should_load_details_including_innerExceptions() + { + // Arrange + + + // Act + + + // Assert + throw new NotImplementedException(); + } + } + + public class Is_TType : ExceptionMessageTest + { + [Fact] + public void Should_return_true_when_TType_is_the_Exception_type() + { + // Act + var result = sut.Is(); + + // Assert + Assert.True(result); + } + } + + public class Is_Type : ExceptionMessageTest + { + [Fact] + public void Should_return_true_when_Type_is_the_Exception_type() + { + // Act + var result = sut.Is(typeof(ArgumentNullException)); + + // Assert + Assert.True(result); + } + } + + public class As_TType : ExceptionMessageTest + { + [Fact] + public void Should_return_the_Exception() + { + // Act + var result = sut.As(); + + // Assert + Assert.Same(ExpectedException, result); + } + } + + public class As_Type : ExceptionMessageTest + { + [Fact] + public void Should_return_the_Exception() + { + // Act + var result = sut.As(typeof(ArgumentNullException)); + + // Assert + Assert.Same(ExpectedException, result); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/MessageCollectionExtensionsTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/MessageCollectionExtensionsTest.cs new file mode 100644 index 0000000..7b457f5 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/MessageCollectionExtensionsTest.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + public class MessageCollectionExtensionsTest + { + public class GetExceptionsOfType : MessageCollectionExtensionsTest + { + [Fact] + public void Should_return_all_ExceptionMessage_Exception() + { + // Arrange + var sut = new MessageCollection(); + var exception1 = new Exception(); + var exception2 = new ArgumentNullException(); + var exception3 = new ArgumentException(); + var exception4 = new ArgumentNullException(); + sut.Add(new ExceptionMessage(exception1)); + sut.Add(new ExceptionMessage(exception2)); + sut.Add(new ExceptionMessage(exception3)); + sut.Add(new ExceptionMessage(exception4)); + + // Act + var result = sut.GetExceptionsOfType(); + + // Assert + Assert.Collection(result, + ex => Assert.Same(exception2, ex), + ex => Assert.Same(exception4, ex) + ); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ProblemDetailsExtensionsTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ProblemDetailsExtensionsTest.cs new file mode 100644 index 0000000..067f6a7 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ProblemDetailsExtensionsTest.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + public class ProblemDetailsExtensionsTest + { + public class ToOperationResult : ProblemDetailsExtensionsTest + { + [Fact] + public void Should_throw_a_ArgumentNullException_when_problemDetails_is_null() + { + Assert.Throws( + "problemDetails", + () => ProblemDetailsExtensions.ToOperationResult(default) + ); + Assert.Throws( + "problemDetails", + () => ProblemDetailsExtensions.ToOperationResult(default, OperationMessageLevel.Error) + ); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs new file mode 100644 index 0000000..864d405 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public abstract class OperationResultSerializationTest + { + public class Failure : OperationResultSerializationTest + { + protected override IOperationResult MakeOperationResult() + { + try + { + throw new Exception(); + } + catch (Exception ex) + { + return ex.ToOperationResult(); + } + } + } + + public class Success : OperationResultSerializationTest + { + protected override IOperationResult MakeOperationResult() + { + return OperationResult.Success(); + } + } + + [Fact] + public void Should_serialize_using_SystemTextJson() + { + // Arrange + var operationResult = MakeOperationResult(); + + // Act + var json = JsonSerializer.Serialize(operationResult); + + // Assert + Assert.NotNull(json); + } + + protected abstract IOperationResult MakeOperationResult(); + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultOperationResultStandardizerTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultOperationResultStandardizerTest.cs new file mode 100644 index 0000000..716f50b --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultOperationResultStandardizerTest.cs @@ -0,0 +1,201 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace ForEvolve.OperationResults.Standardizer +{ + public class DefaultOperationResultStandardizerTest + { + private readonly DefaultOperationResultStandardizer sut; + + private readonly Mock _propertyNameFormatterMock; + private readonly Mock _propertyValueFormatterMock; + private readonly DefaultOperationResultStandardizerOptions _options; + private readonly Mock> _optionsMock; + private readonly ITestOutputHelper _output; + + public DefaultOperationResultStandardizerTest(ITestOutputHelper output) + { + _output = output ?? throw new ArgumentNullException(nameof(output)); + + _propertyNameFormatterMock = new Mock(); + _propertyNameFormatterMock + .Setup(x => x.Format(It.IsAny())) + .Returns((string input) => input); + _propertyValueFormatterMock = new Mock(); + _propertyValueFormatterMock + .Setup(x => x.Format(It.IsAny())) + .Returns((object input) => input); + _options = new DefaultOperationResultStandardizerOptions(); + _optionsMock = new Mock>(); + _optionsMock.Setup(x => x.CurrentValue).Returns(_options); + + var logger = new ServiceCollection() + .AddLogging(builder => + { + builder.AddDebug(); + }) + .BuildServiceProvider() + .GetService() + .CreateLogger(); + + sut = new DefaultOperationResultStandardizer( + _propertyNameFormatterMock.Object, + _propertyValueFormatterMock.Object, + _optionsMock.Object, + logger + ); + } + + public class Standardize : DefaultOperationResultStandardizerTest + { + public Standardize(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Should_guard_against_null() + { + // Arrange + var operationResult = default(IOperationResult); + + // Act & Assert + Assert.Throws("operationResult", + () => sut.Standardize(operationResult)); + } + + public class Given_an_IOperationResult : DefaultOperationResultStandardizerTest + { + public Given_an_IOperationResult(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Should_return_a_Dictionary_containing_only_the_OperationName_key() + { + // Arrange + var operationResult = OperationResult.Success(); + _options.OperationName = "op"; + + // Act + var result = sut.Standardize(operationResult); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Collection(dictionary, + keyValue => + { + Assert.Equal("op", keyValue.Key); + Assert.NotNull(keyValue.Value); + + var value = keyValue.Value; + value.Should().OwnProperty("Succeeded").That().Is().EqualTo(true); + value.Should().OwnProperty("Messages").That().Is().Empty(); + } + ); + } + } + + public class Given_an_IOperationResult_with_Value : DefaultOperationResultStandardizerTest + { + public Given_an_IOperationResult_with_Value(ITestOutputHelper output) : base(output) { } + + public class And_Given_an_anonymous_object : Given_an_IOperationResult_with_Value + { + public And_Given_an_anonymous_object(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Should_return_a_Dictionary_containing_the_OperationName_key_and_the_Value_properties() + { + // Arrange + var expectedValue = new { SomeProp = "asdf", SomeOtherProp = true }; + var operationResult = OperationResult.Success(expectedValue); + _options.OperationName = "op"; + + // Act + var result = sut.Standardize(operationResult); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Collection(dictionary, + keyValue => + { + Assert.Equal("op", keyValue.Key); + Assert.NotNull(keyValue.Value); + + var value = keyValue.Value; + value.Should().OwnProperty("Succeeded").That().Is().EqualTo(true); + value.Should().OwnProperty("Messages").That().Is().Empty(); + }, + keyValue => + { + Assert.Equal("SomeProp", keyValue.Key); + keyValue.Value.Should().Be().EqualTo("asdf"); + }, + keyValue => + { + Assert.Equal("SomeOtherProp", keyValue.Key); + keyValue.Value.Should().Be().EqualTo(true); + } + ); + } + } + public class And_Given_an_typed_object : Given_an_IOperationResult_with_Value + { + public And_Given_an_typed_object(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Should_return_a_Dictionary_containing_the_OperationName_key_and_the_Value_properties() + { + // Arrange + var expectedValue = new MyInternalTestObject + { + Name = "Old Man", + Age = 192 + }; + var operationResult = OperationResult.Success(expectedValue); + _options.OperationName = "op"; + + // Act + var result = sut.Standardize(operationResult); + + // Assert + var dictionary = Assert.IsType>(result); + Assert.Collection(dictionary, + keyValue => + { + Assert.Equal("op", keyValue.Key); + Assert.NotNull(keyValue.Value); + + var value = keyValue.Value; + value.Should().OwnProperty("Succeeded").That().Is().EqualTo(true); + value.Should().OwnProperty("Messages").That().Is().Empty(); + }, + keyValue => + { + Assert.Equal("Name", keyValue.Key); + keyValue.Value.Should().Be().EqualTo("Old Man"); + }, + keyValue => + { + Assert.Equal("Age", keyValue.Key); + keyValue.Value.Should().Be().EqualTo(192); + } + ); + } + + private class MyInternalTestObject + { + public string Name { get; set; } + public int Age { get; set; } + } + } + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyNameFormatterTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyNameFormatterTest.cs new file mode 100644 index 0000000..fedcdd2 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyNameFormatterTest.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.Standardizer +{ + public class DefaultPropertyNameFormatterTest + { + private readonly DefaultPropertyNameFormatter sut = new DefaultPropertyNameFormatter(); + + public class Format : DefaultPropertyNameFormatterTest + { + [Theory] + [InlineData("_someString", "_someString")] + [InlineData("someString", "someString")] + [InlineData("SomeString", "someString")] + public void Should_convert_the_first_character_to_lowercase(string input, string expected) + { + // Act + var actual = sut.Format(input); + + // Assert + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void Should_throw_ArgumentNullException_when_input_is_null_or_empty(string input) + { + // Act & Assert + Assert.Throws("name", () => sut.Format(input)); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyValueFormatterTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyValueFormatterTest.cs new file mode 100644 index 0000000..aebe72f --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyValueFormatterTest.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.Standardizer +{ + public class DefaultPropertyValueFormatterTest + { + private readonly DefaultPropertyValueFormatter sut = new DefaultPropertyValueFormatter(); + public class Format : DefaultPropertyValueFormatterTest + { + [Fact] + public void Should_return_the_input() + { + // Arrange + var input = new { Whatever = "" }; + + // Act + var result = sut.Format(input); + + // Assert + Assert.Same(input, result); + } + + } + } +} diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/OperationResultStartupExtensionsTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/OperationResultStartupExtensionsTest.cs new file mode 100644 index 0000000..07c9312 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/OperationResultStartupExtensionsTest.cs @@ -0,0 +1,103 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults.Standardizer +{ + [Collection(OperationResultStartupExtensionsServerCollection.Name)] + public class OperationResultStartupExtensionsTest + { + private readonly OperationResultStartupExtensionsServerFixture _server; + public OperationResultStartupExtensionsTest(OperationResultStartupExtensionsServerFixture server) + { + _server = server ?? throw new ArgumentNullException(nameof(server)); + } + + [Fact] + public async Task Should_standardize_OkObjectResult() + { + // Arrange + var expectedBody = "{\"" + DefaultOperationResultStandardizerOptions.DefaultOperationName + "\":"; + expectedBody += "{\"messages\":[],\"succeeded\":true},"; + expectedBody += "\"someProp\":\"Oh Yeah!\",\"someOtherProp\":true}"; + + var expectedBody2 = "{\"" + DefaultOperationResultStandardizerOptions.DefaultOperationName + "\":"; + expectedBody2 += "{\"succeeded\":true,\"messages\":[]},"; + expectedBody2 += "\"someProp\":\"Oh Yeah!\",\"someOtherProp\":true}"; + + /* + * Can be inverted... see how to fix this to make the test result + * consistent between test runs. + Expected: {"_operation":{"messages":[],"succeeded":true},"someProp"··· + Actual: {"_operation":{"succeeded":true,"messages":[]},"someProp"··· + */ + + // Act + var result = await _server.Client.GetAsync("/OperationResultStartupExtensionsTestController/OkObjectResult"); + + // Assert + result.EnsureSuccessStatusCode(); + var body = await result.Content.ReadAsStringAsync(); + //Assert.Equal(expectedBody, body); + // Hack + var equality1 = body == expectedBody; + var equality2 = body == expectedBody2; + Assert.True(equality1 || equality2, "Invalid body."); + } + + } + + public class OperationResultStartupExtensionsServerFixture + { + public TestServer Server { get; } + public HttpClient Client { get; } + + public OperationResultStartupExtensionsServerFixture() + { + //Action configureServices + //Action configureApp + var hostBuilder = WebHost.CreateDefaultBuilder() + .ConfigureServices(services => + { + services.AddForEvolveOperationResultStandardizer(); + services.AddControllers(); + }) + .Configure(app => + { + app.UseRouting(); + app.UseEndpoints(c => c.MapControllers()); + }) + ; + Server = new TestServer(hostBuilder); + Client = Server.CreateClient(); + } + } + + [CollectionDefinition(Name)] + public class OperationResultStartupExtensionsServerCollection : ICollectionFixture + { + public const string Name = "OperationResultStartupExtensions Server"; + } + + [Route("OperationResultStartupExtensionsTestController")] + public class OperationResultStartupExtensionsTestController : ControllerBase + { + [HttpGet("OkObjectResult")] + public IActionResult OkObjectResult() + { + var result = OperationResult.Success(new { SomeProp = "Oh Yeah!", SomeOtherProp = true }); + return Ok(result); + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj new file mode 100644 index 0000000..807444e --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj @@ -0,0 +1,15 @@ + + + + net5.0 + ForEvolve.OperationResults + + + + + + + + + + diff --git a/test/ForEvolve.OperationResults.Tests/MessageCollectionTest.cs b/test/ForEvolve.OperationResults.Tests/MessageCollectionTest.cs new file mode 100644 index 0000000..4829ec7 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/MessageCollectionTest.cs @@ -0,0 +1,400 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public class MessageCollectionTest + { + private readonly MessageCollection sut = new MessageCollection(); + + public class HasError : MessageCollectionTest + { + public static TheoryData> trueMessages = new TheoryData> + { + new List{ + new Message(OperationMessageLevel.Error) + }, + new List{ + new Message(OperationMessageLevel.Error), + new Message(OperationMessageLevel.Error), + new Message(OperationMessageLevel.Error) + }, + new List{ + new Message(OperationMessageLevel.Information), + new Message(OperationMessageLevel.Warning), + new Message(OperationMessageLevel.Error) + }, + }; + + [Theory] + [MemberData(nameof(trueMessages))] + public void Should_return_true_when_at_least_a_message_is_an_error(List messages) + { + // Arrange + messages.ForEach(message => sut.Add(message)); + + // Act + var result = sut.HasError(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_no_message_is_an_error() + { + // Arrange + sut.Add(new Message(OperationMessageLevel.Information)); + sut.Add(new Message(OperationMessageLevel.Warning)); + + // Act + var result = sut.HasError(); + + // Assert + Assert.False(result); + } + + [Fact] + public void Should_return_false_when_the_collection_is_empty() + { + // Act + var result = sut.HasError(); + + // Assert + Assert.False(result); + } + } + + public class HasWarning : MessageCollectionTest + { + public static TheoryData> trueMessages = new TheoryData> + { + new List{ + new Message(OperationMessageLevel.Warning) + }, + new List{ + new Message(OperationMessageLevel.Warning), + new Message(OperationMessageLevel.Warning), + new Message(OperationMessageLevel.Warning) + }, + new List{ + new Message(OperationMessageLevel.Information), + new Message(OperationMessageLevel.Warning), + new Message(OperationMessageLevel.Error) + }, + }; + + [Theory] + [MemberData(nameof(trueMessages))] + public void Should_return_true_when_at_least_a_message_is_a_warning(List messages) + { + // Arrange + messages.ForEach(message => sut.Add(message)); + + // Act + var result = sut.HasWarning(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_no_message_is_a_warning() + { + // Arrange + sut.Add(new Message(OperationMessageLevel.Information)); + sut.Add(new Message(OperationMessageLevel.Error)); + + // Act + var result = sut.HasWarning(); + + // Assert + Assert.False(result); + } + + [Fact] + public void Should_return_false_when_the_collection_is_empty() + { + // Act + var result = sut.HasWarning(); + + // Assert + Assert.False(result); + } + } + + public class HasInformation : MessageCollectionTest + { + public static TheoryData> trueMessages = new TheoryData> + { + new List{ + new Message(OperationMessageLevel.Information) + }, + new List{ + new Message(OperationMessageLevel.Information), + new Message(OperationMessageLevel.Information), + new Message(OperationMessageLevel.Information) + }, + new List{ + new Message(OperationMessageLevel.Information), + new Message(OperationMessageLevel.Warning), + new Message(OperationMessageLevel.Error) + }, + }; + + [Theory] + [MemberData(nameof(trueMessages))] + public void Should_return_true_when_at_least_a_message_is_a_warning(List messages) + { + // Arrange + messages.ForEach(message => sut.Add(message)); + + // Act + var result = sut.HasInformation(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_no_message_is_a_warning() + { + // Arrange + sut.Add(new Message(OperationMessageLevel.Warning)); + sut.Add(new Message(OperationMessageLevel.Error)); + + // Act + var result = sut.HasInformation(); + + // Assert + Assert.False(result); + } + + [Fact] + public void Should_return_false_when_the_collection_is_empty() + { + // Act + var result = sut.HasInformation(); + + // Assert + Assert.False(result); + } + } + + public class Contains : MessageCollectionTest + { + [Fact] + public void Should_return_true_when_the_collection_contains_a_message_of_the_specified_type() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage2()); + sut.Add(new MyMessage3()); + + // Act + var result = sut.Contains(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_the_collection_is_empty() + { + // Act + var result = sut.Contains(); + + // Assert + Assert.False(result); + } + + [Fact] + public void Should_return_false_when_the_collection_does_not_contain_a_message_of_the_specified_type() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage3()); + + // Act + var result = sut.Contains(); + + // Assert + Assert.False(result); + } + } + + public class GetSingle : MessageCollectionTest + { + [Fact] + public void Should_return_the_message_of_the_specified_type() + { + // Arrange + var expectedMessage = new MyMessage2(); + sut.Add(new MyMessage1()); + sut.Add(expectedMessage); + sut.Add(new MyMessage3()); + + // Act + var result = sut.GetSingle(); + + // Assert + Assert.NotNull(result); + Assert.Same(expectedMessage, result); + } + + [Fact] + public void Should_throw_an_InvalidOperationException_when_no_message_is_found() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage3()); + + // Act & Assert + Assert.Throws(() => sut.GetSingle()); + } + + [Fact] + public void Should_throw_an_InvalidOperationException_when_more_than_one_message_is_found() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage2()); + sut.Add(new MyMessage2()); + sut.Add(new MyMessage3()); + + // Act & Assert + Assert.Throws(() => sut.GetSingle()); + } + } + + public class GetFirst : MessageCollectionTest + { + [Fact] + public void Should_return_the_first_message_of_the_specified_type() + { + // Arrange + var expectedMessage = new MyMessage2(); + sut.Add(new MyMessage1()); + sut.Add(expectedMessage); + sut.Add(new MyMessage2()); + sut.Add(new MyMessage3()); + + // Act + var result = sut.GetFirst(); + + // Assert + Assert.NotNull(result); + Assert.Same(expectedMessage, result); + } + + [Fact] + public void Should_throw_an_InvalidOperationException_when_no_message_is_found() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage3()); + + // Act & Assert + Assert.Throws(() => sut.GetFirst()); + } + } + + public class GetLast : MessageCollectionTest + { + [Fact] + public void Should_return_the_last_message_of_the_specified_type() + { + // Arrange + var expectedMessage = new MyMessage2(); + sut.Add(new MyMessage1()); + sut.Add(new MyMessage2()); + sut.Add(expectedMessage); + sut.Add(new MyMessage3()); + + // Act + var result = sut.GetLast(); + + // Assert + Assert.NotNull(result); + Assert.Same(expectedMessage, result); + } + + [Fact] + public void Should_throw_an_InvalidOperationException_when_no_message_is_found() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage3()); + + // Act & Assert + Assert.Throws(() => sut.GetLast()); + } + } + + public class GetAll : MessageCollectionTest + { + [Fact] + public void Should_return_all_messages_of_the_specified_type() + { + // Arrange + var expectedMessage1 = new MyMessage2(); + var expectedMessage2 = new MyMessage2(); + sut.Add(new MyMessage1()); + sut.Add(expectedMessage1); + sut.Add(expectedMessage2); + sut.Add(new MyMessage3()); + + // Act + var result = sut.GetAll(); + + // Assert + Assert.Collection(result, + x => Assert.Same(expectedMessage1, x), + x => Assert.Same(expectedMessage2, x) + ); + } + + [Fact] + public void Should_return_an_empty_enumerable_when_no_message_of_the_specified_type_exists() + { + // Arrange + sut.Add(new MyMessage1()); + sut.Add(new MyMessage3()); + + // Act + var result = sut.GetAll(); + + // Assert + Assert.Empty(result); + } + } + + private class MyMessage1 : Message + { + public MyMessage1() + : base(OperationMessageLevel.Error) + { + } + } + + private class MyMessage2 : Message + { + public MyMessage2() + : base(OperationMessageLevel.Information) + { + } + } + + private class MyMessage3 : Message + { + public MyMessage3() + : base(OperationMessageLevel.Warning) + { + } + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/MessageTest.cs b/test/ForEvolve.OperationResults.Tests/MessageTest.cs new file mode 100644 index 0000000..48afe44 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/MessageTest.cs @@ -0,0 +1,399 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public class MessageTest + { + // Arrange + private readonly OperationMessageLevel _severity = OperationMessageLevel.Information; + + public class Ctor1 : MessageTest + { + [Fact] + public void Should_set_the_level() + { + // Act + var obj = new Message(_severity); + + // Assert + Assert.Equal(_severity, obj.Severity); + } + + [Fact] + public void Should_create_a_default_details_dictionary() + { + // Act + var obj = new Message(_severity); + + // Assert + Assert.NotNull(obj.Details); + } + + } + + public class Ctor2 : MessageTest + { + // Arrange + private readonly IDictionary _details = new Dictionary(); + + [Fact] + public void Should_set_the_level() + { + // Act + var obj = new Message(_severity, _details); + + // Assert + Assert.Equal(_severity, obj.Severity); + } + + [Fact] + public void Should_set_the_details() + { + // Act + var obj = new Message(_severity, _details); + + // Assert + Assert.Equal(_details, obj.Details); + } + + [Fact] + public void Should_throw_an_ArgumentNullException_when_details_is_null() + { + Assert.Throws("details", () => new Message(_severity, null)); + } + } + + public class Ctor3 : MessageTest + { + public abstract class Ctor3TestCases : Ctor3 + { + protected abstract bool IgnoreNull { get; } + + [Fact] + public void Should_set_the_level() + { + // Act + var obj = new Message(_severity, new { }, IgnoreNull); + + // Assert + Assert.Equal(_severity, obj.Severity); + } + + [Fact] + public void Should_load_anonymous_object_into_details() + { + // Arrange + var details = new { SomeProp = "Some value", SomeCheck = true }; + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.Collection(obj.Details, + p => AssertDetailsKeyValue(p, "SomeProp", "Some value"), + p => AssertDetailsKeyValue(p, "SomeCheck", true) + ); + } + + [Fact] + public void Should_load_typed_object_into_details() + { + // Arrange + var details = new SomeClass + { + SomeProp = "Some value", + SomeCheck = true + }; + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.Collection(obj.Details, + p => AssertDetailsKeyValue(p, "SomeProp", "Some value"), + p => AssertDetailsKeyValue(p, "SomeCheck", true) + ); + } + + [Fact] + public void Should_throw_an_ArgumentNullException_when_details_is_null() + { + Assert.Throws("details", () => new Message(_severity, null, IgnoreNull)); + } + + [Fact] + public void Should_set_the_Type_when_the_details_is_typed() + { + // Arrange + var details = new SomeClass(); + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.Equal(typeof(SomeClass), obj.Type); + } + + [Fact] + public void Should_set_the_Type_when_the_details_is_anonymous() + { + // Arrange + var details = new { SomeProp = true }; + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.Equal(details.GetType(), obj.Type); + } + + [Fact] + public void Should_set_the_IsAnonymous_to_false_when_the_details_is_typed() + { + // Arrange + var details = new SomeClass(); + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.False(obj.IsAnonymous); + } + + [Fact] + public void Should_set_the_IsAnonymous_to_true_when_the_details_is_an_anonymous_type() + { + // Arrange + var details = new { SomeProp = true }; + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.True(obj.IsAnonymous); + } + + [Fact] + public void Should_set_the_OriginalObject() + { + // Arrange + var details = new SomeClass(); + + // Act + var obj = new Message(_severity, details, IgnoreNull); + + // Assert + Assert.Same(obj.OriginalObject, details); + } + + private void AssertDetailsKeyValue(KeyValuePair pair, string expectedKey, object expectedValue) + { + Assert.Equal(expectedKey, pair.Key); + Assert.Equal(expectedValue, pair.Value); + } + } + + public class When_ignoreNull_is_true : Ctor3TestCases + { + protected override bool IgnoreNull => true; + } + + public class When_ignoreNull_is_false : Ctor3TestCases + { + protected override bool IgnoreNull => false; + } + } + + public class Is_TType : MessageTest + { + [Fact(Skip = "This would need a good design to implement.")] + public void Should_return_true_when_the_types_are_compatible() + { + // Arrange + + + // Act + + + // Assert + throw new NotImplementedException(); + } + + [Fact] + public void Should_return_true_when_the_types_are_the_same() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.Is(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_the_types_are_not_the_same() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.Is(); + + // Assert + Assert.False(result); + } + } + + public class Is_Type : MessageTest + { + [Fact(Skip = "This would need a good design to implement.")] + public void Should_return_true_when_the_types_are_compatible() + { + // Arrange + + + // Act + + + // Assert + throw new NotImplementedException(); + } + + [Fact] + public void Should_return_true_when_the_types_are_the_same() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.Is(typeof(SomeClass)); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_the_types_are_not_the_same() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.Is(typeof(SomeOtherClass)); + + // Assert + Assert.False(result); + } + } + + public class As_TType : MessageTest + { + [Fact] + public void Should_convert_Details_back_to_the_specified_type() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.As(); + + // Assert + Assert.Equal(details.SomeCheck, result.SomeCheck); + Assert.Equal(details.SomeProp, result.SomeProp); + } + + [Fact] + public void Should_throw_a_TypeMismatchException_when_types_are_incompatible() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act & Assert + Assert.Throws(() => sut.As()); + } + + [Fact] + public void Should_return_the_OriginalObject_when_one_exists() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.As(); + + // Assert + Assert.Same(sut.OriginalObject, result); + } + } + + public class As_Type : MessageTest + { + [Fact] + public void Should_convert_Details_back_to_the_specified_type() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.As(typeof(SomeClass)); + + // Assert + var typedResult = Assert.IsType(result); + Assert.Equal(details.SomeCheck, typedResult.SomeCheck); + Assert.Equal(details.SomeProp, typedResult.SomeProp); + } + + [Fact] + public void Should_throw_a_TypeMismatchException_when_types_are_incompatible() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act & Assert + Assert.Throws(() => sut.As(typeof(SomeOtherClass))); + } + + [Fact] + public void Should_return_the_OriginalObject_when_one_exists() + { + // Arrange + var details = new SomeClass { SomeCheck = true, SomeProp = "Value!" }; + var sut = new Message(_severity, details); + + // Act + var result = sut.As(typeof(SomeClass)); + + // Assert + Assert.Same(sut.OriginalObject, result); + } + } + + private class SomeClass + { + public string SomeProp { get; set; } + public bool SomeCheck { get; set; } + } + + private class SomeOtherClass + { + public int SomeProp { get; set; } + public bool SomeOtherProps { get; set; } + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs new file mode 100644 index 0000000..7c95aae --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Sdk; + +namespace ForEvolve.OperationResults +{ + public class OperationResultExtensionsTest + { + public class ConvertTo + { + [Fact] + public void Should_convert_SuccessResult_to_SuccessValueResult() + { + // Arrange + var success = OperationResult.Success(); + + // Act + var result = success.ConvertTo, object>(); + + // Assert + Assert.NotNull(result); + Assert.False(result.HasValue()); + } + + [Fact] + public void Should_convert_FailureResult_to_FailureValueResult() + { + // Arrange + var exception = new Exception("Some error"); + var failure = OperationResult.Failure(new TestExMessage(exception)); + + // Act + var result = failure.ConvertTo, object>(); + + // Assert + Assert.NotNull(result); + Assert.Collection(result.Messages, + m => { + var exceptionMessage = Assert.IsType(m); + Assert.Same(exception, exceptionMessage.Exception); + } + ); + Assert.False(result.HasValue()); + } + + [Fact] + public void Should_convert_SuccessValueResult_to_SucessResult() + { + // Arrange + var value = new { Name = "Some test value" }; + var success = OperationResult.Success(value); + + // Act + var result = success.ConvertTo(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Should_convert_FailureValueResult_to_FailureResult() + { + // Arrange + var exception = new Exception("Some error"); + var failure = OperationResult.Failure(new TestExMessage(exception)); + + // Act + var result = failure.ConvertTo(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Collection(result.Messages, + m => { + var exceptionMessage = Assert.IsType(m); + Assert.Same(exception, exceptionMessage.Exception); + } + ); + } + + [Fact] + public void Should_convert_GenericResult_to_GenericResult() + { + // Arrange + IOperationResult resultToConvert = OperationResult.Success(new ConvertTestClass()); + + // Act + var result = resultToConvert.ConvertTo>(); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + } + + [Fact] + public void Should_convert_GenericResult_to_NonGenericResult() + { + // Arrange + IOperationResult resultToConvert = OperationResult.Success(new ConvertTestClass()); + + // Act + var result = resultToConvert.ConvertTo(); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + } + + [Fact] + public void Should_convert_NonGenericResult_to_GenericResult() + { + // Arrange + var resultToConvert = OperationResult.Success(); + + // Act + var result = resultToConvert.ConvertTo>(); + + // Assert + Assert.NotNull(result); + Assert.IsType>(result); + } + } + + private class ConvertTestClass + { + + } + + private class TestExMessage : Message + { + public Exception Exception { get; } + + public TestExMessage(Exception exception) + : base(OperationMessageLevel.Error) + => Exception = exception; + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs new file mode 100644 index 0000000..b4f201a --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs @@ -0,0 +1,191 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public class OperationResultTest + { + private readonly OperationResult sut = new OperationResult(); + + public class Messages : OperationResultTest + { + [Fact] + public void Should_be_initialized() + { + // Assert + Assert.NotNull(sut.Messages); + } + } + + public class HasMessages : OperationResultTest + { + [Fact] + public void Should_return_false_when_Messages_is_empty() + { + // Act + var result = sut.HasMessages(); + + // Assert + Assert.False(result); + } + + [Fact] + public void Should_return_true_when_Messages_contains_at_least_one_message() + { + // Arrange + sut.Messages.Add(new Message(OperationMessageLevel.Error)); + + // Act + var result = sut.HasMessages(); + + // Assert + Assert.True(result); + } + } + + public class Succeeded : OperationResultTest + { + public static readonly TheoryData> TrueMessages = new TheoryData> + { + new List(), + new List{ new Message(OperationMessageLevel.Information) }, + new List{ new Message(OperationMessageLevel.Warning) }, + new List{ new Message(OperationMessageLevel.Information), new Message(OperationMessageLevel.Warning) }, + }; + public static readonly TheoryData> FalseMessages = new TheoryData> + { + new List{ new Message(OperationMessageLevel.Error) }, + new List{ new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Information) }, + new List{ new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Warning) }, + new List{ new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Information), new Message(OperationMessageLevel.Warning) }, + }; + + [Theory] + [MemberData(nameof(TrueMessages))] + public void Should_be_true_when_Messages_contains_no_error(List messages) + { + // Arrange + messages.ForEach(message => sut.Messages.Add(message)); + + // Act + var result = sut.Succeeded; + + // Assert + Assert.True(result); + } + + [Theory] + [MemberData(nameof(FalseMessages))] + public void Should_be_false_when_Messages_contains_at_least_an_error(List messages) + { + // Arrange + messages.ForEach(message => sut.Messages.Add(message)); + + // Act + var result = sut.Succeeded; + + // Assert + Assert.False(result); + } + } + + public class Success : OperationResultTest + { + [Fact] + public void Should_return_a_successful_OperationResult() + { + // Act + var result = OperationResult.Success(); + + // Assert + Assert.True(result.Succeeded); + } + } + + public class Failure : OperationResultTest + { + [Fact] + public void Should_throw_a_ArgumentNullException_when_no_messages_are_supplied() + { + Assert.Throws( + "messages", + () => OperationResult.Failure() + ); + } + + [Fact] + public void Should_throw_a_ArgumentNullException_when_messages_is_null() + { + Assert.Throws( + "messages", + () => OperationResult.Failure(default(IMessage[])) + ); + } + + public static TheoryData FailureData = new TheoryData + { + new IMessage[] { new Message(OperationMessageLevel.Error) }, + new IMessage[] { new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Information) }, + new IMessage[] { new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Warning) }, + new IMessage[] { new Message(OperationMessageLevel.Error), new Message(OperationMessageLevel.Warning), new Message(OperationMessageLevel.Information) }, + }; + + [Theory] + [MemberData(nameof(FailureData))] + public void Should_return_a_not_successful_OperationResult(IMessage[] messages) + { + // Act + var result = OperationResult.Failure(messages); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(messages, result.Messages); + } + } + } + + public class OperationResult_TValue + { + private readonly OperationResult sut = new OperationResult(); + + public class HasValue : OperationResult_TValue + { + [Fact] + public void Should_return_true_when_value_is_not_null() + { + // Arrange + sut.Value = new SomeValue { Prop = 123 }; + + // Act + var result = sut.HasValue(); + + // Assert + Assert.True(result); + } + + [Fact] + public void Should_return_false_when_value_is_null() + { + // Arrange + sut.Value = null; + + // Act + var result = sut.HasValue(); + + // Assert + Assert.False(result); + } + + } + + private class SomeValue + { + public int Prop { get; set; } + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs new file mode 100644 index 0000000..20d7306 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs @@ -0,0 +1,92 @@ +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public class OperationResultsMessageExtensionsTest + { + public class HavingDetailsOfType : OperationResultsMessageExtensionsTest + { + [Fact] + public void Should_return_all_messages_that_are_of_the_specified_type() + { + // Arrange + var sut = new MessageCollection(); + var messageMock1 = new Mock(); + var messageMock2 = new Mock(); + var messageMock3 = new Mock(); + messageMock1.Setup(x => x.Is()).Returns(true); + messageMock2.Setup(x => x.Is()).Returns(false); + messageMock3.Setup(x => x.Is()).Returns(true); + sut.AddRange(new[] { messageMock1.Object, messageMock2.Object, messageMock3.Object }); + + // Act + var result = sut.HavingDetailsOfType(); + + // Assert + Assert.Collection(result, + message => Assert.Same(messageMock1.Object, message), + message => Assert.Same(messageMock3.Object, message) + ); + } + } + + public class ContainsDetails : OperationResultsMessageExtensionsTest + { + [Fact] + public void Should_return_true_when_a_message_Is_of_the_specified_type() + { + // Arrange + var sut = new MessageCollection(); + var messageMock1 = new Mock(); + var messageMock2 = new Mock(); + var messageMock3 = new Mock(); + messageMock1.Setup(x => x.Is()).Returns(true); + messageMock2.Setup(x => x.Is()).Returns(false); + messageMock3.Setup(x => x.Is()).Returns(true); + sut.AddRange(new[] { messageMock1.Object, messageMock2.Object, messageMock3.Object }); + + // Act + var result = sut.ContainsDetails(); + + // Assert + Assert.True(result); + } + } + + public class HavingDetailsOfTypeAs : OperationResultsMessageExtensionsTest + { + [Fact] + public void Should_return_all_messages_details_as_their_Details_type() + { + // Arrange + var sut = new MessageCollection(); + var exception1 = new ArgumentNullException(); + var exception2 = new ArgumentNullException(); + var messageMock1 = new Mock(); + var messageMock2 = new Mock(); + var messageMock3 = new Mock(); + messageMock1.Setup(x => x.Is()).Returns(true); + messageMock2.Setup(x => x.Is()).Returns(false); + messageMock3.Setup(x => x.Is()).Returns(true); + messageMock1.Setup(x => x.As()).Returns(exception1); + messageMock3.Setup(x => x.As()).Returns(exception2); + sut.AddRange(new[] { messageMock1.Object, messageMock2.Object, messageMock3.Object }); + + // Act + var result = sut.HavingDetailsOfTypeAs(); + + // Assert + Assert.Collection(result, + ex => Assert.Same(exception1, ex), + ex => Assert.Same(exception2, ex) + ); + } + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/TypeMismatchExceptionTest.cs b/test/ForEvolve.OperationResults.Tests/TypeMismatchExceptionTest.cs new file mode 100644 index 0000000..f8fb371 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/TypeMismatchExceptionTest.cs @@ -0,0 +1,70 @@ +using Moq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace ForEvolve.OperationResults +{ + public class TypeMismatchExceptionTest + { + public class Ctor + { + private readonly Mock _messageMock; + private readonly Type _type; + public Ctor() + { + _messageMock = new Mock(); + _type = typeof(object); + } + + [Fact] + public void Should_guard_against_null_sourceMessage() + { + // Arrange + IMessage nullMessage = null; + + // Act & Assert + Assert.Throws( + "sourceMessage", + () => new TypeMismatchException(nullMessage, _type) + ); + } + + [Fact] + public void Should_guard_against_null_type() + { + // Arrange + Type nullType = null; + + // Act & Assert + Assert.Throws( + "type", + () => new TypeMismatchException(_messageMock.Object, nullType) + ); + } + + [Fact] + public void Should_set_SourceMessage() + { + // Act + var sut = new TypeMismatchException(_messageMock.Object, _type); + + // Assert + Assert.Same(_messageMock.Object, sut.SourceMessage); + } + + [Fact] + public void Should_set_Type() + { + // Act + var sut = new TypeMismatchException(_messageMock.Object, _type); + + // Assert + Assert.Same(_type, sut.Type); + } + } + } +} diff --git a/version.json b/version.json new file mode 100644 index 0000000..fe60411 --- /dev/null +++ b/version.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "3.0", + "publicReleaseRefSpec": ["^refs/heads/master-disabled$"], + "cloudBuild": { + "buildNumber": { + "enabled": true + } + } +}