From 68e12c84a91117e3a17f7d8415fdcc0c7d4714e0 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Thu, 7 May 2020 23:58:54 -0400 Subject: [PATCH 01/42] Add gitignore --- .gitignore | 265 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 265 insertions(+) create mode 100644 .gitignore 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/ From 974a5dd4187cc4a01cf95a650ad5b89d4d7306a6 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Thu, 7 May 2020 23:59:09 -0400 Subject: [PATCH 02/42] Move OperationResults --- Dependencies.Build.props | 30 ++ Directory.Build.props | 15 + src/Directory.Build.props | 33 ++ .../ModelBinderErrorActionFilter.cs | 42 ++ ...rEvolve.OperationResults.AspNetCore.csproj | 19 + .../MediatR/ForEvolveMediatRExtensions.cs | 59 +++ .../MediatR/ValidationBehavior.cs | 80 ++++ .../OperationResultStartupExtensions.cs | 63 +++ .../DefaultOperationResultStandardizer.cs | 82 ++++ ...faultOperationResultStandardizerOptions.cs | 19 + .../DefaultPropertyNameFormatter.cs | 21 + .../DefaultPropertyValueFormatter.cs | 16 + .../IOperationResultStandardizer.cs | 15 + .../Standardizer/IPropertyNameFormatter.cs | 15 + .../Standardizer/IPropertyValueFormatter.cs | 15 + ...OperationResultStandardizerActionFilter.cs | 47 ++ .../ExceptionMessage.cs | 40 ++ .../ForEvolve.OperationResults.csproj | 9 + src/ForEvolve.OperationResults/IMessage.cs | 59 +++ .../IOperationResult.cs | 47 ++ src/ForEvolve.OperationResults/Message.cs | 258 +++++++++++ .../MessageCollection.cs | 251 +++++++++++ .../OperationMessageLevel.cs | 24 ++ .../OperationResult.cs | 241 +++++++++++ src/ForEvolve.OperationResults/README.md | 7 + .../TypeMismatchException.cs | 27 ++ test/Directory.Build.props | 20 + ...e.OperationResults.AspNetCore.Tests.csproj | 14 + .../MediatR/ForEvolveMediatRExtensionsTest.cs | 18 + .../MediatR/ValidationBehaviorTest.cs | 18 + .../OperationResultSerializationTest.cs | 51 +++ .../DefaultOperationResultStandardizerTest.cs | 201 +++++++++ .../DefaultPropertyNameFormatterTest.cs | 39 ++ .../DefaultPropertyValueFormatterTest.cs | 30 ++ .../OperationResultStartupExtensionsTest.cs | 103 +++++ .../ExceptionMessageTest.cs | 103 +++++ .../ForEvolve.OperationResults.Tests.csproj | 15 + .../MessageCollectionTest.cs | 400 ++++++++++++++++++ .../MessageTest.cs | 399 +++++++++++++++++ .../OperationResultExtensionsTest.cs | 132 ++++++ .../OperationResultTest.cs | 213 ++++++++++ .../OperationResultsMessageExtensionsTest.cs | 120 ++++++ .../TypeMismatchExceptionTest.cs | 70 +++ test/TestServer.Build.props | 9 + 44 files changed, 3489 insertions(+) create mode 100644 Dependencies.Build.props create mode 100644 Directory.Build.props create mode 100644 src/Directory.Build.props create mode 100644 src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj create mode 100644 src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizer.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultOperationResultStandardizerOptions.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/IOperationResultStandardizer.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyNameFormatter.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/IPropertyValueFormatter.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerActionFilter.cs create mode 100644 src/ForEvolve.OperationResults/ExceptionMessage.cs create mode 100644 src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj create mode 100644 src/ForEvolve.OperationResults/IMessage.cs create mode 100644 src/ForEvolve.OperationResults/IOperationResult.cs create mode 100644 src/ForEvolve.OperationResults/Message.cs create mode 100644 src/ForEvolve.OperationResults/MessageCollection.cs create mode 100644 src/ForEvolve.OperationResults/OperationMessageLevel.cs create mode 100644 src/ForEvolve.OperationResults/OperationResult.cs create mode 100644 src/ForEvolve.OperationResults/README.md create mode 100644 src/ForEvolve.OperationResults/TypeMismatchException.cs create mode 100644 test/Directory.Build.props create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ForEvolveMediatRExtensionsTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/MediatR/ValidationBehaviorTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultOperationResultStandardizerTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyNameFormatterTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/DefaultPropertyValueFormatterTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Standardizer/OperationResultStartupExtensionsTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/ExceptionMessageTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj create mode 100644 test/ForEvolve.OperationResults.Tests/MessageCollectionTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/MessageTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/OperationResultTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs create mode 100644 test/ForEvolve.OperationResults.Tests/TypeMismatchExceptionTest.cs create mode 100644 test/TestServer.Build.props diff --git a/Dependencies.Build.props b/Dependencies.Build.props new file mode 100644 index 0000000..cc0be3f --- /dev/null +++ b/Dependencies.Build.props @@ -0,0 +1,30 @@ + + + + [2.0.0,3.0) + 1.0.0 + + + + net5.0 + net5.0 + + + + + 5.0.0 + 5.0.0 + 5.0.0-preview.3.20181.2 + SYSTEM_TEXT_JSON + + + + + 1.0.6 + 2.4.1 + 4.14.1 + 16.6.1 + 5.0.0-preview.3.20215.14 + + + \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..1699a4c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + + + ForEvolve Framework + https://github.com/ForEvolve/ForEvolve-Framework + git + latest + + + + + + + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..d4e57cb --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,33 @@ + + + + + $(ForEvolveTargetFrameworks) + True + False + Carl-Hugo Marcotte + ForEvolve + https://github.com/ForEvolve/ForEvolve-Framework + https://github.com/ForEvolve/ForEvolve-Framework/blob/master/LICENSE + 1.0.0.0 + 1.0.0.0 + 1.0.0 + True + Carl-Hugo Marcotte + True + True + Full + true + true + snupkg + + + + + + + + \ No newline at end of file diff --git a/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs b/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs new file mode 100644 index 0000000..e11e08a --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs @@ -0,0 +1,42 @@ +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 +{ + 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/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj new file mode 100644 index 0000000..41011cb --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -0,0 +1,19 @@ + + + + $(ForEvolveTargetFrameworks) + This package contains an operation result helpers for Asp.Net Core MVC and MediatR; for example: automatic validation. + forevolve,aspnetcore,mvc,filters,asp.net,core,aspnet,asp,operation,result,results,message,exception + + + + + + + + + + + + + diff --git a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs new file mode 100644 index 0000000..109189b --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FluentValidation; +using MediatR; +using Scrutor; + +namespace ForEvolve.OperationResults.MediatR +{ + /// + /// Contains extension methods to help wire up MediatR. + /// + public static class ForEvolveMediatRExtensions + { + /// + /// Registers all + /// that uses + /// as MediatR . + /// + /// Automatic command and query validation should work as long there is a for the handler. + /// + /// The selector used to scan for classes. + /// The selector. + public static IImplementationTypeSelector AddValidationBehaviors(this IImplementationTypeSelector selector) + { + return selector.AddClasses(classes => classes.AssignableTo(typeof(ValidationBehavior<,>))) + .As(typeof(IPipelineBehavior<,>)) + .WithTransientLifetime(); + } + + /// + /// Scans and registers all . + /// + /// The selector used to scan for classes. + /// The selector. + public static IImplementationTypeSelector AddValidators(this IImplementationTypeSelector selector) + { + return selector.AddClasses(classes => classes.AssignableTo(typeof(IValidator<>))) + .AsImplementedInterfaces() + .WithScopedLifetime(); + } + + /// + /// Automatic validators discovery and automatic validation of commands/queries into the MediatR pipeline + /// + /// Shortcut that calls and . + /// + /// The selector used to scan for classes. + /// The selector. + public static IImplementationTypeSelector AddValidatorsAndBehaviors(this IImplementationTypeSelector selector) + { + return selector + .AddValidators() + .AddValidationBehaviors(); + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs new file mode 100644 index 0000000..b3d5fb8 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs @@ -0,0 +1,80 @@ +using FluentValidation; +using FluentValidation.Results; +using MediatR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults.MediatR +{ + /// + /// This behavior runs all validators and returns the error as an + /// if validation fails. + /// + /// + /// + public class ValidationBehavior : IPipelineBehavior + where TRequest : IRequest + where TResponse : IOperationResult + { + private readonly IEnumerable> _validators; + + public ValidationBehavior(IEnumerable> validators) + { + _validators = validators ?? throw new ArgumentNullException(nameof(validators)); + } + + /// + public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) + { + // Validate + var failures = _validators + .Select(v => v.Validate(request)) + .SelectMany(r => r.Errors); + var validationResult = new ValidationResult(failures); + if (!validationResult.IsValid) + { + // Map errors to output + var messages = validationResult.Errors + .Select(validationFailure => Map(validationFailure)) + .ToArray(); + + // Try to return the result + var castedResult = OperationResult + .Failure(messages) + .ConvertTo(); + if (castedResult != null) + { + return castedResult; + } + } + return await next(); + } + + private static IMessage Map(ValidationFailure validationFailure) + { + var severity = Map(validationFailure.Severity); + return new Message(severity, new + { + validationFailure.ErrorCode, + validationFailure.ErrorMessage + }); + } + + private static OperationMessageLevel Map(Severity severity) + { + switch (severity) + { + case Severity.Warning: + return OperationMessageLevel.Warning; + case Severity.Info: + return OperationMessageLevel.Information; + default: + return OperationMessageLevel.Error; + } + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs new file mode 100644 index 0000000..9f378f8 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs @@ -0,0 +1,63 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.AspNetCore; +using ForEvolve.OperationResults.Standardizer; +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 OperationResultStartupExtensions + { + /// + /// 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 AddForEvolveOperationResultFilters(this IServiceCollection services) + { + services.AddSingleton(); + services.Configure(options => + { + options.Filters.AddService(); + }); + return services; + } + + /// + /// Adds the default ForEvolve operation result standardizer filters. + /// + /// The services. + /// IServiceCollection. + 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.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..3925e46 --- /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..01339b1 --- /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/ExceptionMessage.cs b/src/ForEvolve.OperationResults/ExceptionMessage.cs new file mode 100644 index 0000000..eb7931f --- /dev/null +++ b/src/ForEvolve.OperationResults/ExceptionMessage.cs @@ -0,0 +1,40 @@ +using System; +using System.Text.Json.Serialization; + +namespace ForEvolve.OperationResults +{ + /// + /// 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/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj new file mode 100644 index 0000000..99b1183 --- /dev/null +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -0,0 +1,9 @@ + + + + $(ForEvolveTargetFrameworks) + This package contains an operation result generic implementation that should fits most needs. + forevolve,aspnetcore,asp.net,core,aspnet,asp,operation,result,results,message,exception + + + diff --git a/src/ForEvolve.OperationResults/IMessage.cs b/src/ForEvolve.OperationResults/IMessage.cs new file mode 100644 index 0000000..b582dd0 --- /dev/null +++ b/src/ForEvolve.OperationResults/IMessage.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +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. + 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..7e83e21 --- /dev/null +++ b/src/ForEvolve.OperationResults/Message.cs @@ -0,0 +1,258 @@ +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) + { + if (details == null) { throw new ArgumentNullException(nameof(details)); } + Type = details.GetType(); + IsAnonymous = Type.Name.Contains("AnonymousType"); + OriginalObject = details; + LoadDetails(details, ignoreNull); + } + + 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; } + } + + /// + /// Represents an operation result message build around [RFC3986] . + /// Implements the + /// + /// + public class ProblemDetailsMessage : Message + { + /// + /// Gets the problem details. + /// + /// The problem details. + 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 ?? 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); + } + } + } + + + /// + /// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. + /// + public class ProblemDetails + { + /// + /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + /// dereferenced, it provide human-readable documentation for the problem type + /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + /// "about:blank". + /// + public string Type { get; set; } + + /// + /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence + /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + /// see[RFC7231], Section 3.4). + /// + public string Title { get; set; } + + /// + /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + /// + public int? Status { get; set; } + + /// + /// A human-readable explanation specific to this occurrence of the problem. + /// + public string Detail { get; set; } + + /// + /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. + /// + public string Instance { get; set; } + + /// + /// Gets the for extension members. + /// + /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + /// other members of a problem type. + /// + /// + /// + /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + /// + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} diff --git a/src/ForEvolve.OperationResults/MessageCollection.cs b/src/ForEvolve.OperationResults/MessageCollection.cs new file mode 100644 index 0000000..ae3f055 --- /dev/null +++ b/src/ForEvolve.OperationResults/MessageCollection.cs @@ -0,0 +1,251 @@ +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()); + } + + /// + /// 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/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..a89abd6 --- /dev/null +++ b/src/ForEvolve.OperationResults/OperationResult.cs @@ -0,0 +1,241 @@ +using System; + +namespace ForEvolve.OperationResults +{ + /// + /// Represents an operation result containing optional messages, generated by the operation. + /// Implements the + /// + /// + public class OperationResult : IOperationResult + { + /// +#if SYSTEM_TEXT_JSON + [System.Text.Json.Serialization.JsonIgnore] +#endif + public bool Succeeded => !Messages.HasError(); + + /// +#if SYSTEM_TEXT_JSON + [System.Text.Json.Serialization.JsonIgnore] +#endif + 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; + } + + public static IOperationResult Failure(Exception exception) + { + var result = new OperationResult(); + result.Messages.Add(new ExceptionMessage(exception)); + return result; + } + + public static IOperationResult Failure(ProblemDetails problemDetails) + { + return Failure(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult Failure(ProblemDetails problemDetails, OperationMessageLevel severity) + { + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + 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; + } + + public static IOperationResult Failure(Exception exception) + { + var result = new OperationResult(); + result.Messages.Add(new ExceptionMessage(exception)); + return result; + } + + public static IOperationResult Failure(ProblemDetails problemDetails) + { + return Failure(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult Failure(ProblemDetails problemDetails, OperationMessageLevel severity) + { + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + 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; + } + } + + /// + /// 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..3e7b618 --- /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..9fb3dab --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj @@ -0,0 +1,14 @@ + + + ForEvolve.OperationResults + + + + $(ForEvolveTestTargetFramework) + + + + + + + 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/OperationResultSerializationTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs new file mode 100644 index 0000000..988fdb0 --- /dev/null +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +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 OperationResult.Failure(ex); + } + } + } + + 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/ExceptionMessageTest.cs b/test/ForEvolve.OperationResults.Tests/ExceptionMessageTest.cs new file mode 100644 index 0000000..3aeb31d --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/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 +{ + 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.Tests/ForEvolve.OperationResults.Tests.csproj b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj new file mode 100644 index 0000000..6f26ddc --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(ForEvolveTestTargetFramework) + 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..4538fe0 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +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(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(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 + { + + } + } +} diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs new file mode 100644 index 0000000..191bfb8 --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs @@ -0,0 +1,213 @@ +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[])) + ); + } + + [Fact] + public void Should_throw_a_ArgumentNullException_when_exception_is_null() + { + Assert.Throws( + "exception", + () => OperationResult.Failure(default(Exception)) + ); + } + + [Fact] + public void Should_throw_a_ArgumentNullException_when_problemDetails_is_null() + { + Assert.Throws( + "problemDetails", + () => OperationResult.Failure(default(ProblemDetails)) + ); + Assert.Throws( + "problemDetails", + () => OperationResult.Failure(default(ProblemDetails), OperationMessageLevel.Error) + ); + } + + 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..503587d --- /dev/null +++ b/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs @@ -0,0 +1,120 @@ +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) + ); + } + } + + public class GetExceptionsOfType : OperationResultsMessageExtensionsTest + { + [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.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/test/TestServer.Build.props b/test/TestServer.Build.props new file mode 100644 index 0000000..02d63a2 --- /dev/null +++ b/test/TestServer.Build.props @@ -0,0 +1,9 @@ + + + + + false + false + + + \ No newline at end of file From 800ffa1e74de96304c8f9573cce9c80eb6e3b100 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Fri, 8 May 2020 00:03:47 -0400 Subject: [PATCH 03/42] Add solution --- ForEvolve.OperationResults.sln | 89 ++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 ForEvolve.OperationResults.sln diff --git a/ForEvolve.OperationResults.sln b/ForEvolve.OperationResults.sln new file mode 100644 index 0000000..c34d420 --- /dev/null +++ b/ForEvolve.OperationResults.sln @@ -0,0 +1,89 @@ + +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 +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} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {18F52EC3-580A-4474-8BBE-C7DBF2F0855A} + EndGlobalSection +EndGlobal From f78f84526808713f56526b4e49c8f3a8fc227d89 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Sun, 29 Nov 2020 01:43:23 -0500 Subject: [PATCH 04/42] Fix serialization of Type --- Dependencies.Build.props | 2 +- .../MediatR/ValidationBehavior.cs | 16 ++-- .../ExceptionMessage.cs | 3 +- src/ForEvolve.OperationResults/IMessage.cs | 2 + src/ForEvolve.OperationResults/Message.cs | 89 ++++++++++--------- .../OperationResult.cs | 13 ++- test/Directory.Build.props | 6 +- .../OperationResultSerializationTest.cs | 1 + .../OperationResultTest.cs | 3 +- 9 files changed, 66 insertions(+), 69 deletions(-) diff --git a/Dependencies.Build.props b/Dependencies.Build.props index cc0be3f..06edb40 100644 --- a/Dependencies.Build.props +++ b/Dependencies.Build.props @@ -24,7 +24,7 @@ 2.4.1 4.14.1 16.6.1 - 5.0.0-preview.3.20215.14 + 5.0.0 \ No newline at end of file diff --git a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs index b3d5fb8..ec8a75d 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs @@ -64,17 +64,11 @@ private static IMessage Map(ValidationFailure validationFailure) }); } - private static OperationMessageLevel Map(Severity severity) + private static OperationMessageLevel Map(Severity severity) => severity switch { - switch (severity) - { - case Severity.Warning: - return OperationMessageLevel.Warning; - case Severity.Info: - return OperationMessageLevel.Information; - default: - return OperationMessageLevel.Error; - } - } + Severity.Warning => OperationMessageLevel.Warning, + Severity.Info => OperationMessageLevel.Information, + _ => OperationMessageLevel.Error, + }; } } diff --git a/src/ForEvolve.OperationResults/ExceptionMessage.cs b/src/ForEvolve.OperationResults/ExceptionMessage.cs index eb7931f..973be6e 100644 --- a/src/ForEvolve.OperationResults/ExceptionMessage.cs +++ b/src/ForEvolve.OperationResults/ExceptionMessage.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; using System.Text.Json.Serialization; namespace ForEvolve.OperationResults diff --git a/src/ForEvolve.OperationResults/IMessage.cs b/src/ForEvolve.OperationResults/IMessage.cs index b582dd0..2c86370 100644 --- a/src/ForEvolve.OperationResults/IMessage.cs +++ b/src/ForEvolve.OperationResults/IMessage.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace ForEvolve.OperationResults { @@ -24,6 +25,7 @@ public interface IMessage /// Gets the message type. /// /// The type of message. + [JsonIgnore] Type Type { get; } /// diff --git a/src/ForEvolve.OperationResults/Message.cs b/src/ForEvolve.OperationResults/Message.cs index 7e83e21..d849d9c 100644 --- a/src/ForEvolve.OperationResults/Message.cs +++ b/src/ForEvolve.OperationResults/Message.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Text.Json.Serialization; @@ -207,52 +208,52 @@ protected void LoadProblemDetails(ProblemDetails problemDetails) } - /// - /// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. - /// - public class ProblemDetails - { - /// - /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when - /// dereferenced, it provide human-readable documentation for the problem type - /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be - /// "about:blank". - /// - public string Type { get; set; } + ///// + ///// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. + ///// + //public class ProblemDetails + //{ + // /// + // /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when + // /// dereferenced, it provide human-readable documentation for the problem type + // /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be + // /// "about:blank". + // /// + // public string Type { get; set; } - /// - /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence - /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - /// see[RFC7231], Section 3.4). - /// - public string Title { get; set; } + // /// + // /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence + // /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; + // /// see[RFC7231], Section 3.4). + // /// + // public string Title { get; set; } - /// - /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. - /// - public int? Status { get; set; } + // /// + // /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. + // /// + // public int? Status { get; set; } - /// - /// A human-readable explanation specific to this occurrence of the problem. - /// - public string Detail { get; set; } + // /// + // /// A human-readable explanation specific to this occurrence of the problem. + // /// + // public string Detail { get; set; } - /// - /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. - /// - public string Instance { get; set; } + // /// + // /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. + // /// + // public string Instance { get; set; } - /// - /// Gets the for extension members. - /// - /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as - /// other members of a problem type. - /// - /// - /// - /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. - /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. - /// - public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); - } + // /// + // /// Gets the for extension members. + // /// + // /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as + // /// other members of a problem type. + // /// + // /// + // /// + // /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. + // /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. + // /// + // public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + //} } diff --git a/src/ForEvolve.OperationResults/OperationResult.cs b/src/ForEvolve.OperationResults/OperationResult.cs index a89abd6..b6be515 100644 --- a/src/ForEvolve.OperationResults/OperationResult.cs +++ b/src/ForEvolve.OperationResults/OperationResult.cs @@ -1,4 +1,6 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; +using System.Text.Json.Serialization; namespace ForEvolve.OperationResults { @@ -10,15 +12,10 @@ namespace ForEvolve.OperationResults public class OperationResult : IOperationResult { /// -#if SYSTEM_TEXT_JSON - [System.Text.Json.Serialization.JsonIgnore] -#endif + [JsonIgnore] public bool Succeeded => !Messages.HasError(); /// -#if SYSTEM_TEXT_JSON - [System.Text.Json.Serialization.JsonIgnore] -#endif public MessageCollection Messages { get; } = new MessageCollection(); /// @@ -214,7 +211,7 @@ public static TOperationResult OnFailure(this TOperationResult } /// - /// DELETE ME + /// TODO: DELETE ME /// class MyClass { diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 3e7b618..6263ab9 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -3,9 +3,9 @@ false - + - + @@ -16,5 +16,5 @@ - + \ No newline at end of file diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs index 988fdb0..41314fc 100644 --- a/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Xunit; diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs index 191bfb8..9ff6280 100644 --- a/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs +++ b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.AspNetCore.Mvc; +using System; using System.Collections.Generic; using System.Linq; using System.Text; From 8a6cdcea102c6799bb2f11993615d1cd364cf488 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Sun, 29 Nov 2020 23:04:56 -0500 Subject: [PATCH 05/42] Simplify props and csproj files --- Dependencies.Build.props | 30 ----------------- Directory.Build.props | 10 ------ ForEvolve.OperationResults.sln | 17 ++++++++++ src/Directory.Build.props | 33 +++++++++---------- ...rEvolve.OperationResults.AspNetCore.csproj | 6 +++- .../ForEvolve.OperationResults.csproj | 5 ++- test/Directory.Build.props | 12 +++---- ...e.OperationResults.AspNetCore.Tests.csproj | 2 +- .../ForEvolve.OperationResults.Tests.csproj | 2 +- test/TestServer.Build.props | 9 ----- 10 files changed, 50 insertions(+), 76 deletions(-) delete mode 100644 Dependencies.Build.props delete mode 100644 test/TestServer.Build.props diff --git a/Dependencies.Build.props b/Dependencies.Build.props deleted file mode 100644 index 06edb40..0000000 --- a/Dependencies.Build.props +++ /dev/null @@ -1,30 +0,0 @@ - - - - [2.0.0,3.0) - 1.0.0 - - - - net5.0 - net5.0 - - - - - 5.0.0 - 5.0.0 - 5.0.0-preview.3.20181.2 - SYSTEM_TEXT_JSON - - - - - 1.0.6 - 2.4.1 - 4.14.1 - 16.6.1 - 5.0.0 - - - \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 1699a4c..fcb2be8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,15 +1,5 @@ - - - ForEvolve Framework - https://github.com/ForEvolve/ForEvolve-Framework - git latest - - - - - \ No newline at end of file diff --git a/ForEvolve.OperationResults.sln b/ForEvolve.OperationResults.sln index c34d420..9fd31c4 100644 --- a/ForEvolve.OperationResults.sln +++ b/ForEvolve.OperationResults.sln @@ -15,6 +15,21 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ForEvolve.OperationResults. 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +97,8 @@ Global {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} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d4e57cb..0343121 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,33 +1,32 @@ - - $(ForEvolveTargetFrameworks) True False Carl-Hugo Marcotte ForEvolve - https://github.com/ForEvolve/ForEvolve-Framework - https://github.com/ForEvolve/ForEvolve-Framework/blob/master/LICENSE - 1.0.0.0 - 1.0.0.0 - 1.0.0 - True + https://github.com/ForEvolve/StateR + MIT Carl-Hugo Marcotte - True - True - Full true true + True snupkg - - - + + + preview + + + latest + + + + 3.3.37 + all + + \ No newline at end of file diff --git a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj index 41011cb..8c100c2 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -1,10 +1,14 @@  - $(ForEvolveTargetFrameworks) + net5.0 This package contains an operation result helpers for Asp.Net Core MVC and MediatR; for example: automatic validation. forevolve,aspnetcore,mvc,filters,asp.net,core,aspnet,asp,operation,result,results,message,exception + + + + diff --git a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj index 99b1183..3eee867 100644 --- a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -1,9 +1,12 @@  - $(ForEvolveTargetFrameworks) + net5.0 This package contains an operation result generic implementation that should fits most needs. forevolve,aspnetcore,asp.net,core,aspnet,asp,operation,result,results,message,exception + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 6263ab9..0a3cbf3 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -6,15 +6,15 @@ - - - - - + + + + + - + \ 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 index 9fb3dab..ff69d84 100644 --- a/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj @@ -4,7 +4,7 @@ - $(ForEvolveTestTargetFramework) + net5.0 diff --git a/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj index 6f26ddc..807444e 100644 --- a/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj +++ b/test/ForEvolve.OperationResults.Tests/ForEvolve.OperationResults.Tests.csproj @@ -1,7 +1,7 @@  - $(ForEvolveTestTargetFramework) + net5.0 ForEvolve.OperationResults diff --git a/test/TestServer.Build.props b/test/TestServer.Build.props deleted file mode 100644 index 02d63a2..0000000 --- a/test/TestServer.Build.props +++ /dev/null @@ -1,9 +0,0 @@ - - - - - false - false - - - \ No newline at end of file From af932051e90072a9b9797ccc89df5476917ec9ea Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Sun, 29 Nov 2020 23:39:37 -0500 Subject: [PATCH 06/42] Move dependency on ProblemDetails to AspNetCore --- .../ModelBinderErrorActionFilter.cs | 3 +- .../Mvc/ExceptionExtensions.cs | 26 ++++ .../Mvc}/ExceptionMessage.cs | 2 +- .../Mvc/MessageCollectionExtensions.cs | 26 ++++ .../Mvc/ProblemDetailsExtensions.cs | 37 ++++++ .../Mvc/ProblemDetailsMessage.cs | 78 +++++++++++ .../ForEvolve.OperationResults.csproj | 3 - src/ForEvolve.OperationResults/Message.cs | 122 +----------------- .../MessageCollection.cs | 14 -- .../OperationResult.cs | 53 +------- ...e.OperationResults.AspNetCore.Tests.csproj | 2 +- .../Mvc/ExceptionExtensionsTest.cs | 20 +++ .../Mvc}/ExceptionMessageTest.cs | 2 +- .../Mvc/MessageCollectionExtensionsTest.cs | 39 ++++++ .../Mvc/ProblemDetailsExtensionsTest.cs | 29 +++++ .../OperationResultSerializationTest.cs | 2 +- .../OperationResultExtensionsTest.cs | 19 ++- .../OperationResultTest.cs | 23 ---- .../OperationResultsMessageExtensionsTest.cs | 28 ---- 19 files changed, 284 insertions(+), 244 deletions(-) create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs rename src/{ForEvolve.OperationResults => ForEvolve.OperationResults.AspNetCore/Mvc}/ExceptionMessage.cs (96%) create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionExtensionsTest.cs rename test/{ForEvolve.OperationResults.Tests => ForEvolve.OperationResults.AspNetCore.Tests/Mvc}/ExceptionMessageTest.cs (98%) create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/MessageCollectionExtensionsTest.cs create mode 100644 test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ProblemDetailsExtensionsTest.cs diff --git a/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs b/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs index e11e08a..f78e3d1 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using ForEvolve.OperationResults.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; using System.Collections.Generic; diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs new file mode 100644 index 0000000..220a677 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs @@ -0,0 +1,26 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.AspNetCore.Mvc; +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/ExceptionMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs similarity index 96% rename from src/ForEvolve.OperationResults/ExceptionMessage.cs rename to src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs index 973be6e..07cd9d6 100644 --- a/src/ForEvolve.OperationResults/ExceptionMessage.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs @@ -2,7 +2,7 @@ using System; using System.Text.Json.Serialization; -namespace ForEvolve.OperationResults +namespace ForEvolve.OperationResults.AspNetCore.Mvc { /// /// Represents a wrapper message around an . diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs new file mode 100644 index 0000000..3d11802 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs @@ -0,0 +1,26 @@ +using ForEvolve.OperationResults.AspNetCore.Mvc; +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/ProblemDetailsExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs new file mode 100644 index 0000000..6747e91 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs @@ -0,0 +1,37 @@ +using ForEvolve.OperationResults; +using ForEvolve.OperationResults.AspNetCore.Mvc; +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) + { + return ToOperationResult(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult ToOperationResult(this ProblemDetails problemDetails, OperationMessageLevel severity) + { + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + return result; + } + + public static IOperationResult ToOperationResult(ProblemDetails problemDetails) + { + return ToOperationResult(problemDetails, OperationMessageLevel.Error); + } + + public static IOperationResult ToOperationResult(ProblemDetails problemDetails, OperationMessageLevel severity) + { + var result = new OperationResult(); + result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); + return result; + } + } +} diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs new file mode 100644 index 0000000..24f00c7 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ForEvolve.OperationResults.AspNetCore.Mvc +{ + /// + /// Represents an operation result message build around [RFC3986] . + /// Implements the + /// + /// + public class ProblemDetailsMessage : Message + { + /// + /// Gets the problem details. + /// + /// The problem details. + 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 ?? 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/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj index 3eee867..3840518 100644 --- a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -6,7 +6,4 @@ forevolve,aspnetcore,asp.net,core,aspnet,asp,operation,result,results,message,exception - - - diff --git a/src/ForEvolve.OperationResults/Message.cs b/src/ForEvolve.OperationResults/Message.cs index d849d9c..d7813a5 100644 --- a/src/ForEvolve.OperationResults/Message.cs +++ b/src/ForEvolve.OperationResults/Message.cs @@ -1,5 +1,4 @@ -using Microsoft.AspNetCore.Mvc; -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Text.Json.Serialization; @@ -137,123 +136,4 @@ private bool CanReturnTheOriginalObject(Type type) [JsonIgnore] public virtual object OriginalObject { get; } } - - /// - /// Represents an operation result message build around [RFC3986] . - /// Implements the - /// - /// - public class ProblemDetailsMessage : Message - { - /// - /// Gets the problem details. - /// - /// The problem details. - 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 ?? 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); - } - } - } - - - ///// - ///// A machine-readable format for specifying errors in HTTP API responses based on https://tools.ietf.org/html/rfc7807. - ///// - //public class ProblemDetails - //{ - // /// - // /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when - // /// dereferenced, it provide human-readable documentation for the problem type - // /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be - // /// "about:blank". - // /// - // public string Type { get; set; } - - // /// - // /// A short, human-readable summary of the problem type.It SHOULD NOT change from occurrence to occurrence - // /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; - // /// see[RFC7231], Section 3.4). - // /// - // public string Title { get; set; } - - // /// - // /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. - // /// - // public int? Status { get; set; } - - // /// - // /// A human-readable explanation specific to this occurrence of the problem. - // /// - // public string Detail { get; set; } - - // /// - // /// A URI reference that identifies the specific occurrence of the problem.It may or may not yield further information if dereferenced. - // /// - // public string Instance { get; set; } - - // /// - // /// Gets the for extension members. - // /// - // /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as - // /// other members of a problem type. - // /// - // /// - // /// - // /// The round-tripping behavior for is determined by the implementation of the Input \ Output formatters. - // /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. - // /// - // public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); - //} } diff --git a/src/ForEvolve.OperationResults/MessageCollection.cs b/src/ForEvolve.OperationResults/MessageCollection.cs index ae3f055..a99997a 100644 --- a/src/ForEvolve.OperationResults/MessageCollection.cs +++ b/src/ForEvolve.OperationResults/MessageCollection.cs @@ -233,19 +233,5 @@ public static IEnumerable HavingDetailsOfTypeAs x.Is()) .Select(x => x.As()); } - - /// - /// 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/OperationResult.cs b/src/ForEvolve.OperationResults/OperationResult.cs index b6be515..bc38961 100644 --- a/src/ForEvolve.OperationResults/OperationResult.cs +++ b/src/ForEvolve.OperationResults/OperationResult.cs @@ -1,14 +1,13 @@ -using Microsoft.AspNetCore.Mvc; -using System; +using System; using System.Text.Json.Serialization; namespace ForEvolve.OperationResults { /// /// Represents an operation result containing optional messages, generated by the operation. - /// Implements the + /// Implements the /// - /// + /// public class OperationResult : IOperationResult { /// @@ -39,25 +38,6 @@ public static IOperationResult Failure(params IMessage[] messages) return result; } - public static IOperationResult Failure(Exception exception) - { - var result = new OperationResult(); - result.Messages.Add(new ExceptionMessage(exception)); - return result; - } - - public static IOperationResult Failure(ProblemDetails problemDetails) - { - return Failure(problemDetails, OperationMessageLevel.Error); - } - - public static IOperationResult Failure(ProblemDetails problemDetails, OperationMessageLevel severity) - { - var result = new OperationResult(); - result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); - return result; - } - #endregion #region OperationResult Factory Methods @@ -79,36 +59,17 @@ public static IOperationResult Failure(params IMessage[] message return result; } - public static IOperationResult Failure(Exception exception) - { - var result = new OperationResult(); - result.Messages.Add(new ExceptionMessage(exception)); - return result; - } - - public static IOperationResult Failure(ProblemDetails problemDetails) - { - return Failure(problemDetails, OperationMessageLevel.Error); - } - - public static IOperationResult Failure(ProblemDetails problemDetails, OperationMessageLevel severity) - { - var result = new OperationResult(); - result.Messages.Add(new ProblemDetailsMessage(problemDetails, severity)); - return result; - } - #endregion } /// /// Represents an operation result containing optional messages, generated by the operation, and an optional resulting object. - /// Implements the - /// Implements the + /// Implements the + /// Implements the /// /// The type of the t value. - /// - /// + /// + /// public class OperationResult : OperationResult, IOperationResult { /// diff --git a/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj index ff69d84..e399832 100644 --- a/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/ForEvolve.OperationResults.AspNetCore.Tests.csproj @@ -1,6 +1,6 @@  - ForEvolve.OperationResults + ForEvolve.OperationResults.AspNetCore 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.Tests/ExceptionMessageTest.cs b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs similarity index 98% rename from test/ForEvolve.OperationResults.Tests/ExceptionMessageTest.cs rename to test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs index 3aeb31d..a1fc988 100644 --- a/test/ForEvolve.OperationResults.Tests/ExceptionMessageTest.cs +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/Mvc/ExceptionMessageTest.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Xunit; -namespace ForEvolve.OperationResults +namespace ForEvolve.OperationResults.AspNetCore.Mvc { public class ExceptionMessageTest { 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 index 41314fc..864d405 100644 --- a/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs +++ b/test/ForEvolve.OperationResults.AspNetCore.Tests/OperationResultSerializationTest.cs @@ -21,7 +21,7 @@ protected override IOperationResult MakeOperationResult() } catch (Exception ex) { - return OperationResult.Failure(ex); + return ex.ToOperationResult(); } } } diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs index 4538fe0..7c95aae 100644 --- a/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs +++ b/test/ForEvolve.OperationResults.Tests/OperationResultExtensionsTest.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Xunit; +using Xunit.Sdk; namespace ForEvolve.OperationResults { @@ -30,7 +31,7 @@ public void Should_convert_FailureResult_to_FailureValueResult() { // Arrange var exception = new Exception("Some error"); - var failure = OperationResult.Failure(exception); + var failure = OperationResult.Failure(new TestExMessage(exception)); // Act var result = failure.ConvertTo, object>(); @@ -39,7 +40,7 @@ public void Should_convert_FailureResult_to_FailureValueResult() Assert.NotNull(result); Assert.Collection(result.Messages, m => { - var exceptionMessage = Assert.IsType(m); + var exceptionMessage = Assert.IsType(m); Assert.Same(exception, exceptionMessage.Exception); } ); @@ -66,7 +67,7 @@ public void Should_convert_FailureValueResult_to_FailureResult() { // Arrange var exception = new Exception("Some error"); - var failure = OperationResult.Failure(exception); + var failure = OperationResult.Failure(new TestExMessage(exception)); // Act var result = failure.ConvertTo(); @@ -76,7 +77,7 @@ public void Should_convert_FailureValueResult_to_FailureResult() Assert.IsType(result); Assert.Collection(result.Messages, m => { - var exceptionMessage = Assert.IsType(m); + var exceptionMessage = Assert.IsType(m); Assert.Same(exception, exceptionMessage.Exception); } ); @@ -124,9 +125,19 @@ public void Should_convert_NonGenericResult_to_GenericResult() 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 index 9ff6280..b4f201a 100644 --- a/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs +++ b/test/ForEvolve.OperationResults.Tests/OperationResultTest.cs @@ -127,28 +127,6 @@ public void Should_throw_a_ArgumentNullException_when_messages_is_null() ); } - [Fact] - public void Should_throw_a_ArgumentNullException_when_exception_is_null() - { - Assert.Throws( - "exception", - () => OperationResult.Failure(default(Exception)) - ); - } - - [Fact] - public void Should_throw_a_ArgumentNullException_when_problemDetails_is_null() - { - Assert.Throws( - "problemDetails", - () => OperationResult.Failure(default(ProblemDetails)) - ); - Assert.Throws( - "problemDetails", - () => OperationResult.Failure(default(ProblemDetails), OperationMessageLevel.Error) - ); - } - public static TheoryData FailureData = new TheoryData { new IMessage[] { new Message(OperationMessageLevel.Error) }, @@ -168,7 +146,6 @@ public void Should_return_a_not_successful_OperationResult(IMessage[] messages) Assert.False(result.Succeeded); Assert.Equal(messages, result.Messages); } - } } diff --git a/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs b/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs index 503587d..20d7306 100644 --- a/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs +++ b/test/ForEvolve.OperationResults.Tests/OperationResultsMessageExtensionsTest.cs @@ -88,33 +88,5 @@ public void Should_return_all_messages_details_as_their_Details_type() ); } } - - public class GetExceptionsOfType : OperationResultsMessageExtensionsTest - { - [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) - ); - } - - } } } From 8e2e79e91755b8497f49acf1e6f2c0525bf4c3a1 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Sun, 29 Nov 2020 23:54:12 -0500 Subject: [PATCH 07/42] Remove invalid namespace --- .../Standardizer/DefaultPropertyNameFormatter.cs | 4 ++-- .../Standardizer/DefaultPropertyValueFormatter.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs index 3925e46..c717138 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyNameFormatter.cs @@ -3,9 +3,9 @@ namespace ForEvolve.OperationResults.Standardizer { /// /// Represents the default property name formatter, used by . - /// Implements the + /// Implements the /// - /// + /// public class DefaultPropertyNameFormatter : IPropertyNameFormatter { /// diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs index 01339b1..f66a686 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/DefaultPropertyValueFormatter.cs @@ -2,9 +2,9 @@ { /// /// Represents the default property value formatter, used by . - /// Implements the + /// Implements the /// - /// + /// public class DefaultPropertyValueFormatter : IPropertyValueFormatter { /// From 5e93425fe3b26547587562f5b2eaec5dc34a78f4 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 00:18:43 -0500 Subject: [PATCH 08/42] Remove MediatR dependency --- ...rEvolve.OperationResults.AspNetCore.csproj | 6 -- .../MediatR/ForEvolveMediatRExtensions.cs | 59 --------------- .../MediatR/ValidationBehavior.cs | 74 ------------------- 3 files changed, 139 deletions(-) delete mode 100644 src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs delete mode 100644 src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs diff --git a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj index 8c100c2..e564fb0 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -10,12 +10,6 @@ - - - - - - diff --git a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs deleted file mode 100644 index 109189b..0000000 --- a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ForEvolveMediatRExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentValidation; -using MediatR; -using Scrutor; - -namespace ForEvolve.OperationResults.MediatR -{ - /// - /// Contains extension methods to help wire up MediatR. - /// - public static class ForEvolveMediatRExtensions - { - /// - /// Registers all - /// that uses - /// as MediatR . - /// - /// Automatic command and query validation should work as long there is a for the handler. - /// - /// The selector used to scan for classes. - /// The selector. - public static IImplementationTypeSelector AddValidationBehaviors(this IImplementationTypeSelector selector) - { - return selector.AddClasses(classes => classes.AssignableTo(typeof(ValidationBehavior<,>))) - .As(typeof(IPipelineBehavior<,>)) - .WithTransientLifetime(); - } - - /// - /// Scans and registers all . - /// - /// The selector used to scan for classes. - /// The selector. - public static IImplementationTypeSelector AddValidators(this IImplementationTypeSelector selector) - { - return selector.AddClasses(classes => classes.AssignableTo(typeof(IValidator<>))) - .AsImplementedInterfaces() - .WithScopedLifetime(); - } - - /// - /// Automatic validators discovery and automatic validation of commands/queries into the MediatR pipeline - /// - /// Shortcut that calls and . - /// - /// The selector used to scan for classes. - /// The selector. - public static IImplementationTypeSelector AddValidatorsAndBehaviors(this IImplementationTypeSelector selector) - { - return selector - .AddValidators() - .AddValidationBehaviors(); - } - } -} diff --git a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs b/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs deleted file mode 100644 index ec8a75d..0000000 --- a/src/ForEvolve.OperationResults.AspNetCore/MediatR/ValidationBehavior.cs +++ /dev/null @@ -1,74 +0,0 @@ -using FluentValidation; -using FluentValidation.Results; -using MediatR; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace ForEvolve.OperationResults.MediatR -{ - /// - /// This behavior runs all validators and returns the error as an - /// if validation fails. - /// - /// - /// - public class ValidationBehavior : IPipelineBehavior - where TRequest : IRequest - where TResponse : IOperationResult - { - private readonly IEnumerable> _validators; - - public ValidationBehavior(IEnumerable> validators) - { - _validators = validators ?? throw new ArgumentNullException(nameof(validators)); - } - - /// - public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) - { - // Validate - var failures = _validators - .Select(v => v.Validate(request)) - .SelectMany(r => r.Errors); - var validationResult = new ValidationResult(failures); - if (!validationResult.IsValid) - { - // Map errors to output - var messages = validationResult.Errors - .Select(validationFailure => Map(validationFailure)) - .ToArray(); - - // Try to return the result - var castedResult = OperationResult - .Failure(messages) - .ConvertTo(); - if (castedResult != null) - { - return castedResult; - } - } - return await next(); - } - - private static IMessage Map(ValidationFailure validationFailure) - { - var severity = Map(validationFailure.Severity); - return new Message(severity, new - { - validationFailure.ErrorCode, - validationFailure.ErrorMessage - }); - } - - private static OperationMessageLevel Map(Severity severity) => severity switch - { - Severity.Warning => OperationMessageLevel.Warning, - Severity.Info => OperationMessageLevel.Information, - _ => OperationMessageLevel.Error, - }; - } -} From 1dc00e7c2ccb4353d6efb2ffbe66cb675bf482da Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 00:19:02 -0500 Subject: [PATCH 09/42] Split startup extension in two --- .../OperationResultStartupExtensions.cs | 39 ++---------------- ...tionResultStandardizerStartupExtensions.cs | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 36 deletions(-) create mode 100644 src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs diff --git a/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs index 9f378f8..ccfbe2c 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs @@ -1,6 +1,4 @@ -using ForEvolve.OperationResults; -using ForEvolve.OperationResults.AspNetCore; -using ForEvolve.OperationResults.Standardizer; +using ForEvolve.OperationResults.AspNetCore; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; @@ -10,7 +8,7 @@ namespace Microsoft.Extensions.DependencyInjection { - public static class OperationResultStartupExtensions + public static class OperationResultAspNetCoreStartupExtensions { /// /// Adds operation results Asp.Net Core filters. @@ -19,7 +17,7 @@ public static class OperationResultStartupExtensions /// /// The services. /// IServiceCollection. - public static IServiceCollection AddForEvolveOperationResultFilters(this IServiceCollection services) + public static IServiceCollection AddForEvolveOperationResultModelBinderErrorActionFilter(this IServiceCollection services) { services.AddSingleton(); services.Configure(options => @@ -28,36 +26,5 @@ public static IServiceCollection AddForEvolveOperationResultFilters(this IServic }); return services; } - - /// - /// Adds the default ForEvolve operation result standardizer filters. - /// - /// The services. - /// IServiceCollection. - 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.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs new file mode 100644 index 0000000..a7e6f12 --- /dev/null +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs @@ -0,0 +1,40 @@ +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. + 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; + } + } +} From 0547668716d8838cbe3e1c31402f7d6340ac08e6 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 00:22:55 -0500 Subject: [PATCH 10/42] Add a --- .../Standardizer/OperationResultStandardizerStartupExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs index a7e6f12..bd3e045 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Standardizer/OperationResultStandardizerStartupExtensions.cs @@ -11,6 +11,7 @@ public static class OperationResultStandardizerStartupExtensions /// /// The services. /// IServiceCollection. + /// This subsystem should to be revised. public static IServiceCollection AddForEvolveOperationResultStandardizer(this IServiceCollection services) { services From d3e8fdfeb5c6395c7f6e1098265fce1d4cd8572b Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 00:27:15 -0500 Subject: [PATCH 11/42] Update namespaces Classes are in more intuitive namespaces. --- .../{Mvc => }/ExceptionExtensions.cs | 2 +- .../{Mvc => }/ExceptionMessage.cs | 2 +- .../{Mvc => }/MessageCollectionExtensions.cs | 2 +- .../ModelBinderErrorActionFilter.cs | 4 ++-- .../{ => Mvc}/OperationResultStartupExtensions.cs | 2 +- .../{Mvc => }/ProblemDetailsExtensions.cs | 2 +- .../{Mvc => }/ProblemDetailsMessage.cs | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename src/ForEvolve.OperationResults.AspNetCore/{Mvc => }/ExceptionExtensions.cs (93%) rename src/ForEvolve.OperationResults.AspNetCore/{Mvc => }/ExceptionMessage.cs (96%) rename src/ForEvolve.OperationResults.AspNetCore/{Mvc => }/MessageCollectionExtensions.cs (94%) rename src/ForEvolve.OperationResults.AspNetCore/{ErrorToOperationResultConverter => Mvc}/ModelBinderErrorActionFilter.cs (93%) rename src/ForEvolve.OperationResults.AspNetCore/{ => Mvc}/OperationResultStartupExtensions.cs (95%) rename src/ForEvolve.OperationResults.AspNetCore/{Mvc => }/ProblemDetailsExtensions.cs (96%) rename src/ForEvolve.OperationResults.AspNetCore/{Mvc => }/ProblemDetailsMessage.cs (98%) diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs similarity index 93% rename from src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs rename to src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs index 220a677..29b1e7c 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ExceptionExtensions.cs @@ -1,5 +1,5 @@ using ForEvolve.OperationResults; -using ForEvolve.OperationResults.AspNetCore.Mvc; +using ForEvolve.OperationResults.AspNetCore; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs similarity index 96% rename from src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs rename to src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs index 07cd9d6..8923dda 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ExceptionMessage.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ExceptionMessage.cs @@ -2,7 +2,7 @@ using System; using System.Text.Json.Serialization; -namespace ForEvolve.OperationResults.AspNetCore.Mvc +namespace ForEvolve.OperationResults.AspNetCore { /// /// Represents a wrapper message around an . diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs similarity index 94% rename from src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs rename to src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs index 3d11802..1649c5c 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Mvc/MessageCollectionExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/MessageCollectionExtensions.cs @@ -1,4 +1,4 @@ -using ForEvolve.OperationResults.AspNetCore.Mvc; +using ForEvolve.OperationResults.AspNetCore; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs similarity index 93% rename from src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs rename to src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs index f78e3d1..4fdb645 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ErrorToOperationResultConverter/ModelBinderErrorActionFilter.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/ModelBinderErrorActionFilter.cs @@ -1,4 +1,4 @@ -using ForEvolve.OperationResults.AspNetCore.Mvc; +using ForEvolve.OperationResults.AspNetCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System; @@ -7,7 +7,7 @@ using System.Text; using System.Threading.Tasks; -namespace ForEvolve.OperationResults.AspNetCore +namespace ForEvolve.OperationResults.AspNetCore.Mvc { public class ModelBinderErrorActionFilter : IAsyncActionFilter { diff --git a/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs similarity index 95% rename from src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs rename to src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs index ccfbe2c..063e69f 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/OperationResultStartupExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/Mvc/OperationResultStartupExtensions.cs @@ -1,4 +1,4 @@ -using ForEvolve.OperationResults.AspNetCore; +using ForEvolve.OperationResults.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs similarity index 96% rename from src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs rename to src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs index 6747e91..cc03ed0 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs @@ -1,5 +1,5 @@ using ForEvolve.OperationResults; -using ForEvolve.OperationResults.AspNetCore.Mvc; +using ForEvolve.OperationResults.AspNetCore; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs similarity index 98% rename from src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs rename to src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs index 24f00c7..7935db1 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/Mvc/ProblemDetailsMessage.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs @@ -5,7 +5,7 @@ using System.Text; using System.Threading.Tasks; -namespace ForEvolve.OperationResults.AspNetCore.Mvc +namespace ForEvolve.OperationResults.AspNetCore { /// /// Represents an operation result message build around [RFC3986] . From d7d84b42ed0c5d698e4399ee98b0c956c743f09d Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 22:50:15 -0500 Subject: [PATCH 12/42] Add editorconfig --- .editorconfig | 237 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 .editorconfig 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 From a44b5dff0dbad7710e7a66a311d381c201891cc8 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 22:50:27 -0500 Subject: [PATCH 13/42] add prettierrc --- .prettierrc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .prettierrc 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 + } + } + ] +} From 6e37542ee67cb618fe836ab3b68a3d8a4a3ffee9 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 22:50:43 -0500 Subject: [PATCH 14/42] Update PackageProjectUrl --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0343121..45adfe3 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,7 +5,7 @@ False Carl-Hugo Marcotte ForEvolve - https://github.com/ForEvolve/StateR + https://github.com/ForEvolve/ForEvolve.OperationResults MIT Carl-Hugo Marcotte true From 30be7718311595ff3686efd0aaacc46518371b0f Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 22:51:04 -0500 Subject: [PATCH 15/42] Add Build, Test, and Deploy GitHub Action --- .github/workflows/master.yml | 74 ++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 .github/workflows/master.yml diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 0000000..9f41662 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,74 @@ +name: Build, Test, and Deploy + +on: + push: + branches: + - master + - main + paths-ignore: + - 'samples/**' + + pull_request: + branches: + - master + - main + +env: + DOTNET_2_VERSION: '2.1.x' + DOTNET_3_VERSION: '3.1.x' + DOTNET_5_VERSION: '5.0.x' + BUILD_CONFIGURATION: Release + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + +jobs: + build-and-test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dotnet: ['5.0.x'] + + steps: + - uses: actions/checkout@v1 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Build + run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} + + - name: Unit Test + 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 + + - 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 https://f.feedz.io/forevolve/operationresults/nuget/index.json + if: github.event_name == 'pull_request' + + - 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' From 5772af72b425b47e94699ba89d29278a756e7da7 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 22:52:52 -0500 Subject: [PATCH 16/42] Add version.json --- version.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 version.json 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 + } + } +} From 3261dc028d28f04b7c2077051ec9ab5e0f608b38 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Mon, 30 Nov 2020 23:16:48 -0500 Subject: [PATCH 17/42] Update packages description --- ForEvolve.OperationResults.sln | 5 +++++ .../ForEvolve.OperationResults.AspNetCore.csproj | 6 +++--- .../ForEvolve.OperationResults.csproj | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ForEvolve.OperationResults.sln b/ForEvolve.OperationResults.sln index 9fd31c4..92d002e 100644 --- a/ForEvolve.OperationResults.sln +++ b/ForEvolve.OperationResults.sln @@ -30,6 +30,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C4D44813-C 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 diff --git a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj index e564fb0..aec3aed 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -2,10 +2,10 @@ net5.0 - This package contains an operation result helpers for Asp.Net Core MVC and MediatR; for example: automatic validation. - forevolve,aspnetcore,mvc,filters,asp.net,core,aspnet,asp,operation,result,results,message,exception + 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/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj index 3840518..3f19db3 100644 --- a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -2,8 +2,8 @@ net5.0 - This package contains an operation result generic implementation that should fits most needs. - forevolve,aspnetcore,asp.net,core,aspnet,asp,operation,result,results,message,exception + 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 From 84dfaa2520296b31a8369bede67221c12ba01c7b Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 19:31:17 -0500 Subject: [PATCH 18/42] Add repository_dispatch support --- .github/workflows/master.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9f41662..96eba1a 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -13,15 +13,20 @@ on: - master - main + repository_dispatch: + types: + - deploy env: DOTNET_2_VERSION: '2.1.x' DOTNET_3_VERSION: '3.1.x' DOTNET_5_VERSION: '5.0.x' BUILD_CONFIGURATION: Release ACTIONS_ALLOW_UNSECURE_COMMANDS: true + FEEDZ_URI: https://f.feedz.io/forevolve/operationresults/nuget/index.json jobs: build-and-test: + if: github.event_name != 'repository_dispatch' runs-on: ubuntu-latest strategy: fail-fast: false @@ -52,6 +57,8 @@ jobs: steps: - uses: actions/checkout@v1 + with: + ref: ${{ github.event.client_payload.ref }} - name: Setup .NET Core uses: actions/setup-dotnet@v1 @@ -66,9 +73,9 @@ jobs: run: dotnet pack --configuration ${{ env.BUILD_CONFIGURATION }} - name: Push to feedz.io - run: dotnet nuget push **/*.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s https://f.feedz.io/forevolve/operationresults/nuget/index.json - if: github.event_name == 'pull_request' + 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) - 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' + if: github.event_name == 'push' || (github.event_name == 'repository_dispatch' && github.event.client_payload.nuget == true) From 8482f3f7f09a9c7e71507f70d2e5bc72b48c540d Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 19:53:54 -0500 Subject: [PATCH 19/42] Add workflow_dispatch support --- .github/workflows/master.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 96eba1a..bbe3838 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -16,6 +16,9 @@ on: repository_dispatch: types: - deploy + + workflow_dispatch: + env: DOTNET_2_VERSION: '2.1.x' DOTNET_3_VERSION: '3.1.x' @@ -26,7 +29,7 @@ env: jobs: build-and-test: - if: github.event_name != 'repository_dispatch' + if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest strategy: fail-fast: false @@ -74,8 +77,8 @@ jobs: - 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) + if: github.event_name == 'pull_request' || ((github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch') && github.event.client_payload.feedz == 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) + if: github.event_name == 'push' || ((github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch') && github.event.client_payload.nuget == true) From bf0093e22faa9f4c25371f66d64859d055addeda Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 20:05:20 -0500 Subject: [PATCH 20/42] Add workflow_dispatch inputs --- .github/workflows/master.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index bbe3838..5e40f8f 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -18,6 +18,15 @@ on: - 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' @@ -77,8 +86,8 @@ jobs: - 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_name == 'workflow_dispatch') && github.event.client_payload.feedz == true) + 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_name == 'workflow_dispatch') && github.event.client_payload.nuget == true) + 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') From 6c0e6b96d4ed9b22b4ae012a452206fb4cdf82d6 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:25:54 -0500 Subject: [PATCH 21/42] build-and-test should run even during dispatch --- .github/workflows/master.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 5e40f8f..9c06578 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -38,7 +38,6 @@ env: jobs: build-and-test: - if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' runs-on: ubuntu-latest strategy: fail-fast: false @@ -47,16 +46,20 @@ jobs: steps: - uses: actions/checkout@v1 + if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' - name: Setup .NET Core + if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Build + if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} - name: Unit Test + if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} deploy: From 42aa36a0803e8fef86ef7e78d86d1636c20de494 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:29:07 -0500 Subject: [PATCH 22/42] Try encapsulate logic in env --- .github/workflows/master.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9c06578..62bc4f8 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -43,8 +43,11 @@ jobs: fail-fast: false matrix: dotnet: ['5.0.x'] + env: + IS_DISPATCH: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' steps: + - run: echo ${{ IS_DISPATCH }} - uses: actions/checkout@v1 if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' From 19cf6a4536db6f80df3b57bf1bc532d37ea58b5a Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:29:59 -0500 Subject: [PATCH 23/42] Try encapsulate logic in env --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 62bc4f8..25c6bfb 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -47,7 +47,7 @@ jobs: IS_DISPATCH: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' steps: - - run: echo ${{ IS_DISPATCH }} + - run: echo ${{ env.IS_DISPATCH }} - uses: actions/checkout@v1 if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' From 9af8979d02430be22922766a214d560cefd7a04c Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:32:29 -0500 Subject: [PATCH 24/42] Try encapsulate logic in env --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 25c6bfb..95971e9 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -44,7 +44,7 @@ jobs: matrix: dotnet: ['5.0.x'] env: - IS_DISPATCH: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' + IS_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} steps: - run: echo ${{ env.IS_DISPATCH }} From d026f8d9bfb25ff6ce56895fe5ab78949bab8a43 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:35:31 -0500 Subject: [PATCH 25/42] Use IS_DISPATCH --- .github/workflows/master.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 95971e9..9d93d91 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -44,25 +44,24 @@ jobs: matrix: dotnet: ['5.0.x'] env: - IS_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} + IS_DISPATCH: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' }} steps: - - run: echo ${{ env.IS_DISPATCH }} - uses: actions/checkout@v1 - if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' + if: !env.IS_DISPATCH - name: Setup .NET Core - if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' + if: !env.IS_DISPATCH uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Build - if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' + if: !env.IS_DISPATCH run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} - name: Unit Test - if: github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' + if: !env.IS_DISPATCH run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} deploy: From a9fd617e6e915662fd8fb7dbf5dd0fa669eb3237 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:36:07 -0500 Subject: [PATCH 26/42] Use IS_DISPATCH --- .github/workflows/master.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 9d93d91..6dac32d 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,20 +48,20 @@ jobs: steps: - uses: actions/checkout@v1 - if: !env.IS_DISPATCH + if: ! env.IS_DISPATCH - name: Setup .NET Core - if: !env.IS_DISPATCH + if: ! env.IS_DISPATCH uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Build - if: !env.IS_DISPATCH + if: ! env.IS_DISPATCH run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} - name: Unit Test - if: !env.IS_DISPATCH + if: ! env.IS_DISPATCH run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} deploy: From 0f6bb283aa7c2d6403143f47926785b83d69dce1 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:39:01 -0500 Subject: [PATCH 27/42] Use IS_DISPATCH --- .github/workflows/master.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 6dac32d..7cb7d5b 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,20 +48,20 @@ jobs: steps: - uses: actions/checkout@v1 - if: ! env.IS_DISPATCH + if: env.IS_DISPATCH == false - name: Setup .NET Core - if: ! env.IS_DISPATCH + if: env.IS_DISPATCH == false uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Build - if: ! env.IS_DISPATCH + if: env.IS_DISPATCH == false run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} - name: Unit Test - if: ! env.IS_DISPATCH + if: env.IS_DISPATCH == false run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} deploy: From c0fd7fbc5d3f03220391b0fd3b644930b2ae0f69 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:47:26 -0500 Subject: [PATCH 28/42] echo vars --- .github/workflows/master.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7cb7d5b..0780ded 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -33,8 +33,10 @@ env: DOTNET_3_VERSION: '3.1.x' DOTNET_5_VERSION: '5.0.x' BUILD_CONFIGURATION: Release - ACTIONS_ALLOW_UNSECURE_COMMANDS: true FEEDZ_URI: https://f.feedz.io/forevolve/operationresults/nuget/index.json + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + IS_REPO_DISPATCH: ${{ github.event_name == 'repository_dispatch' }} + IS_WORKFLOW_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} jobs: build-and-test: @@ -44,9 +46,11 @@ jobs: matrix: dotnet: ['5.0.x'] env: - IS_DISPATCH: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' }} + IS_DISPATCH: ${{ env.IS_REPO_DISPATCH || env.IS_WORKFLOW_DISPATCH }} steps: + - run: echo -e "IS_REPO_DISPATCH:" ${{ IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ IS_DISPATCH }} + - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From d17c0d7f78c191ae6910b46e683a3e74fcaa4073 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:48:29 -0500 Subject: [PATCH 29/42] try fix env --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 0780ded..574f2e2 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -46,7 +46,7 @@ jobs: matrix: dotnet: ['5.0.x'] env: - IS_DISPATCH: ${{ env.IS_REPO_DISPATCH || env.IS_WORKFLOW_DISPATCH }} + IS_DISPATCH: ${{ IS_REPO_DISPATCH || IS_WORKFLOW_DISPATCH }} steps: - run: echo -e "IS_REPO_DISPATCH:" ${{ IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ IS_DISPATCH }} From 33752c9fa85d7a94e8df120fb9e75d9e02e95eae Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:49:28 -0500 Subject: [PATCH 30/42] try fix env --- .github/workflows/master.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 574f2e2..0f71918 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -37,6 +37,7 @@ env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true IS_REPO_DISPATCH: ${{ github.event_name == 'repository_dispatch' }} IS_WORKFLOW_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} + IS_DISPATCH: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' }} jobs: build-and-test: @@ -45,8 +46,6 @@ jobs: fail-fast: false matrix: dotnet: ['5.0.x'] - env: - IS_DISPATCH: ${{ IS_REPO_DISPATCH || IS_WORKFLOW_DISPATCH }} steps: - run: echo -e "IS_REPO_DISPATCH:" ${{ IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ IS_DISPATCH }} From d735128111410e38b19f564373ccf512b0b0919a Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:50:08 -0500 Subject: [PATCH 31/42] Fix echo --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 0f71918..11c2183 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,7 +48,7 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ IS_DISPATCH }} + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From 46581d02ab43429fc641a0ca66bcb9d4f3a21b04 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:56:37 -0500 Subject: [PATCH 32/42] Use IS_*_DISPATCH variables --- .github/workflows/master.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 11c2183..e5b973c 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,8 +48,6 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} - - uses: actions/checkout@v1 if: env.IS_DISPATCH == false @@ -94,8 +92,8 @@ jobs: - 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') + if: github.event_name == 'pull_request' || (env.IS_REPO_DISPATCH && github.event.client_payload.feedz == true) || (env.IS_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') + if: github.event_name == 'push' || (env.IS_REPO_DISPATCH && github.event.client_payload.nuget == true) || (env.IS_WORKFLOW_DISPATCH && github.event.inputs.deployToNuget == 'true') From c91f7187311b4294a5b6b7731c91b61a7c4292eb Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Tue, 1 Dec 2020 23:57:54 -0500 Subject: [PATCH 33/42] Add echo back --- .github/workflows/master.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index e5b973c..e8f0998 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,6 +48,8 @@ jobs: dotnet: ['5.0.x'] steps: + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} + - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From 0441ac61ccece526c3bfb98dce3a2b5e2b7f8a3a Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:00:23 -0500 Subject: [PATCH 34/42] echo condition --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index e8f0998..7d734d5 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,7 +48,7 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == false }} - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From 4f956f6252399d97910fa8fd891c16dfbe4a231e Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:01:33 -0500 Subject: [PATCH 35/42] Replace false by 0 --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 7d734d5..a0cfd9b 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,7 +48,7 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == false }} + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == 0 }} - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From 5a8275ebd3cf0d56d03c980fa9aea9b18dc252f5 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:02:49 -0500 Subject: [PATCH 36/42] Replace 0 by "false" --- .github/workflows/master.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index a0cfd9b..b11241e 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -48,7 +48,7 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == 0 }} + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == "false" }} - uses: actions/checkout@v1 if: env.IS_DISPATCH == false From 0c00a045ce1720b527ae4feca59662c7a694a6d2 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:04:51 -0500 Subject: [PATCH 37/42] Invert condition to IS_NOT_DISPATCH --- .github/workflows/master.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index b11241e..0f49cef 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -37,7 +37,7 @@ env: ACTIONS_ALLOW_UNSECURE_COMMANDS: true IS_REPO_DISPATCH: ${{ github.event_name == 'repository_dispatch' }} IS_WORKFLOW_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} - IS_DISPATCH: ${{ github.event_name == 'repository_dispatch' || github.event_name == 'workflow_dispatch' }} + IS_NOT_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} jobs: build-and-test: @@ -48,23 +48,23 @@ jobs: dotnet: ['5.0.x'] steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_DISPATCH:" ${{ env.IS_DISPATCH }} "\nIF:" ${{ env.IS_DISPATCH == "false" }} + - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_NOT_DISPATCH:" ${{ env.IS_NOT_DISPATCH }} - uses: actions/checkout@v1 - if: env.IS_DISPATCH == false + if: env.IS_NOT_DISPATCH - name: Setup .NET Core - if: env.IS_DISPATCH == false + if: env.IS_NOT_DISPATCH uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - name: Build - if: env.IS_DISPATCH == false + if: env.IS_NOT_DISPATCH run: dotnet build --configuration ${{ env.BUILD_CONFIGURATION }} - name: Unit Test - if: env.IS_DISPATCH == false + if: env.IS_NOT_DISPATCH run: dotnet test --configuration ${{ env.BUILD_CONFIGURATION }} deploy: From 45b241aeaae0e2c1c68d01fe4d23829f92e8a121 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:12:03 -0500 Subject: [PATCH 38/42] Clean environment variables and usage --- .github/workflows/master.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 0f49cef..f5ab36f 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -35,9 +35,6 @@ env: BUILD_CONFIGURATION: Release FEEDZ_URI: https://f.feedz.io/forevolve/operationresults/nuget/index.json ACTIONS_ALLOW_UNSECURE_COMMANDS: true - IS_REPO_DISPATCH: ${{ github.event_name == 'repository_dispatch' }} - IS_WORKFLOW_DISPATCH: ${{ github.event_name == 'workflow_dispatch' }} - IS_NOT_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} jobs: build-and-test: @@ -46,10 +43,10 @@ jobs: fail-fast: false matrix: dotnet: ['5.0.x'] + env: + IS_NOT_DISPATCH: ${{ github.event_name != 'repository_dispatch' && github.event_name != 'workflow_dispatch' }} steps: - - run: echo -e "IS_REPO_DISPATCH:" ${{ env.IS_REPO_DISPATCH }} "\nIS_WORKFLOW_DISPATCH:" ${{ env.IS_WORKFLOW_DISPATCH }} "\nIS_NOT_DISPATCH:" ${{ env.IS_NOT_DISPATCH }} - - uses: actions/checkout@v1 if: env.IS_NOT_DISPATCH @@ -74,6 +71,9 @@ jobs: fail-fast: true matrix: dotnet: ['5.0.x'] + env: + PUSH_TO_FEEDZ: 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') + PUSH_TO_NUGET: 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') steps: - uses: actions/checkout@v1 @@ -94,8 +94,8 @@ jobs: - 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' || (env.IS_REPO_DISPATCH && github.event.client_payload.feedz == true) || (env.IS_WORKFLOW_DISPATCH && github.event.inputs.deployToFeedz == 'true') + if: env.PUSH_TO_FEEDZ - 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' || (env.IS_REPO_DISPATCH && github.event.client_payload.nuget == true) || (env.IS_WORKFLOW_DISPATCH && github.event.inputs.deployToNuget == 'true') + if: env.PUSH_TO_NUGET From 90b145e9ce9cfc35f1860d1174a91d380e1d8a88 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 00:17:19 -0500 Subject: [PATCH 39/42] Move conditions back to steps --- .github/workflows/master.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index f5ab36f..7aaeaf1 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -71,9 +71,6 @@ jobs: fail-fast: true matrix: dotnet: ['5.0.x'] - env: - PUSH_TO_FEEDZ: 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') - PUSH_TO_NUGET: 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') steps: - uses: actions/checkout@v1 @@ -94,8 +91,8 @@ jobs: - name: Push to feedz.io run: dotnet nuget push **/*.nupkg -k ${{ secrets.FEEDZ_API_KEY }} -s ${{ env.FEEDZ_URI }} - if: env.PUSH_TO_FEEDZ + 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: env.PUSH_TO_NUGET + 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') From cfc7992ed721089ce9f2f55cb2b40259740f8ec2 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Wed, 2 Dec 2020 23:28:30 -0500 Subject: [PATCH 40/42] Set OriginalObject from ProblemDetailsMessage --- .../ProblemDetailsMessage.cs | 6 ++++-- src/ForEvolve.OperationResults/Message.cs | 13 ++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs index 7935db1..5f3c3ba 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsMessage.cs @@ -3,13 +3,14 @@ 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] . - /// Implements the + /// Inherits from /// /// public class ProblemDetailsMessage : Message @@ -18,6 +19,7 @@ public class ProblemDetailsMessage : Message /// Gets the problem details. /// /// The problem details. + [JsonIgnore] public ProblemDetails ProblemDetails { get; } /// @@ -27,7 +29,7 @@ public class ProblemDetailsMessage : Message /// The severity. /// problemDetails public ProblemDetailsMessage(ProblemDetails problemDetails, OperationMessageLevel severity) - : base(severity) + : base(severity, problemDetails) { ProblemDetails = problemDetails ?? throw new ArgumentNullException(nameof(problemDetails)); LoadProblemDetails(problemDetails); diff --git a/src/ForEvolve.OperationResults/Message.cs b/src/ForEvolve.OperationResults/Message.cs index d7813a5..54cf975 100644 --- a/src/ForEvolve.OperationResults/Message.cs +++ b/src/ForEvolve.OperationResults/Message.cs @@ -42,13 +42,24 @@ public Message(OperationMessageLevel severity, IDictionary detai /// 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; - LoadDetails(details, ignoreNull); } protected virtual void LoadDetails(object details, bool ignoreNull) From 1aca379b8ace92e58fd40eda4dfb341ddae5baed Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Thu, 3 Dec 2020 23:40:09 -0500 Subject: [PATCH 41/42] Fix a failing test --- .../ProblemDetailsExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs index cc03ed0..cac14c0 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs +++ b/src/ForEvolve.OperationResults.AspNetCore/ProblemDetailsExtensions.cs @@ -12,11 +12,13 @@ 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; @@ -24,11 +26,13 @@ public static IOperationResult ToOperationResult(this ProblemDetails problemDeta 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; From e40e7827062b20f86fbae36df268c70714809236 Mon Sep 17 00:00:00 2001 From: Carl-Hugo Marcotte Date: Thu, 3 Dec 2020 23:40:36 -0500 Subject: [PATCH 42/42] Add support for .NET Core 3.1 (Azure Functions) --- .../ForEvolve.OperationResults.AspNetCore.csproj | 2 +- .../ForEvolve.OperationResults.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj index aec3aed..50da2dc 100644 --- a/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj +++ b/src/ForEvolve.OperationResults.AspNetCore/ForEvolve.OperationResults.AspNetCore.csproj @@ -1,7 +1,7 @@  - net5.0 + 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/ForEvolve.OperationResults.csproj b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj index 3f19db3..8786eee 100644 --- a/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj +++ b/src/ForEvolve.OperationResults/ForEvolve.OperationResults.csproj @@ -1,7 +1,7 @@  - net5.0 + 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