diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/WebApplication/Properties/launchSettings.json b/WebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..ac1b200 --- /dev/null +++ b/WebApplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55292", + "sslPort": 44383 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApplication": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/WebApplication/Properties/launchSettings.json b/WebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..ac1b200 --- /dev/null +++ b/WebApplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55292", + "sslPort": 44383 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApplication": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebApplication/Startup.cs b/WebApplication/Startup.cs new file mode 100644 index 0000000..7d1489a --- /dev/null +++ b/WebApplication/Startup.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace WebApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" }); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/WebApplication/Properties/launchSettings.json b/WebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..ac1b200 --- /dev/null +++ b/WebApplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55292", + "sslPort": 44383 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApplication": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebApplication/Startup.cs b/WebApplication/Startup.cs new file mode 100644 index 0000000..7d1489a --- /dev/null +++ b/WebApplication/Startup.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace WebApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" }); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/WebApplication/WebApplication.csproj b/WebApplication/WebApplication.csproj new file mode 100644 index 0000000..39eb5ab --- /dev/null +++ b/WebApplication/WebApplication.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/WebApplication/Properties/launchSettings.json b/WebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..ac1b200 --- /dev/null +++ b/WebApplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55292", + "sslPort": 44383 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApplication": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebApplication/Startup.cs b/WebApplication/Startup.cs new file mode 100644 index 0000000..7d1489a --- /dev/null +++ b/WebApplication/Startup.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace WebApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" }); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/WebApplication/WebApplication.csproj b/WebApplication/WebApplication.csproj new file mode 100644 index 0000000..39eb5ab --- /dev/null +++ b/WebApplication/WebApplication.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/WebApplication/appsettings.Development.json b/WebApplication/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/WebApplication/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea5ebf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,205 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +x64/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# AWS +*.aws-sam/ + +# 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 + +*_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 +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# 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 addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# 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 do note that will include unencrypted +## passwords +#*.pubxml + +# NuGet Packages Directory +#packages/* +## TODO: If the tool you use requires repositories.config +## uncomment the next line +#!packages/repositories.config + +# Enable "build/" folder in the NuGet Packages folder since +# NuGet packages use it for MSBuild targets. +# This line needs to be after the ignore of the build folder +# (and the packages folder if the line above has been uncommented) +#!packages/build/ + +!src/packages/**/*.dll +!src/packages/**/*.pdb + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# 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/ + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml +src/packages/EventStore.Client.3.3.1/lib/net40/EventStore.ClientAPI.xml + +# JetBrains Rider +.idea/ +*.sln.iml + +# Vagrant VM files +.vagrant +vagrant/dbv/data/meta/revision + +# Visual Studio 2015 cache/options directory +.vs/ \ No newline at end of file diff --git a/ConsoleApp1/ConsoleApp1.csproj b/ConsoleApp1/ConsoleApp1.csproj new file mode 100644 index 0000000..76e2276 --- /dev/null +++ b/ConsoleApp1/ConsoleApp1.csproj @@ -0,0 +1,13 @@ + + + + Exe + net5.0 + + + + + + + + diff --git a/ConsoleApp1/ProblemDetails/ProblemDetails.cs b/ConsoleApp1/ProblemDetails/ProblemDetails.cs new file mode 100644 index 0000000..c5ecf3a --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetails.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + [JsonConverter(typeof (ProblemDetailsJsonConverter))] + public class ProblemDetails + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + [JsonExtensionData] + public IDictionary Extensions { get; } = new Dictionary(StringComparer.Ordinal); + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs new file mode 100644 index 0000000..d9688fc --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsException.cs @@ -0,0 +1,18 @@ +using System; +using System.Net.Http; + +namespace ConsoleApp1 +{ + public sealed class ProblemDetailsException : Exception + { + public ProblemDetails Details { get; } + public HttpResponseMessage Response { get; } + public override string Message => Details.Title; + + public ProblemDetailsException(ProblemDetails details, HttpResponseMessage response) : base(null) + { + Response = response; + Details = details; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs new file mode 100644 index 0000000..17790a5 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsHttpClientHandler.cs @@ -0,0 +1,27 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + public class ProblemDetailsHttpMessageHandler : DelegatingHandler + { + public ProblemDetailsHttpMessageHandler() : base(new HttpClientHandler()) { } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken ct) + { + var response = await base.SendAsync(request, ct); + + var mediaType = response.Content.Headers.ContentType?.MediaType; + if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase)) + { + var problemDetails = await response.Content.ReadFromJsonAsync(null, ct) ?? new ProblemDetails(); + throw new ProblemDetailsException(problemDetails, response); + } + + return response; + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs new file mode 100644 index 0000000..a22dfa4 --- /dev/null +++ b/ConsoleApp1/ProblemDetails/ProblemDetailsJsonConverter.cs @@ -0,0 +1,137 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace ConsoleApp1 +{ + internal sealed class ProblemDetailsJsonConverter : JsonConverter + { + private static readonly JsonEncodedText Type = JsonEncodedText.Encode("type"); + private static readonly JsonEncodedText Title = JsonEncodedText.Encode("title"); + private static readonly JsonEncodedText Status = JsonEncodedText.Encode("status"); + private static readonly JsonEncodedText Detail = JsonEncodedText.Encode("detail"); + private static readonly JsonEncodedText Instance = JsonEncodedText.Encode("instance"); + + public override ProblemDetails Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var problemDetails = new ProblemDetails(); + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + while (reader.Read() && reader.TokenType != JsonTokenType.EndObject) + { + ReadValue(ref reader, problemDetails, options); + } + + if (reader.TokenType != JsonTokenType.EndObject) + { + throw new JsonException("Unexpected end when reading JSON."); + } + + return problemDetails; + } + + public override void Write(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + WriteProblemDetails(writer, value, options); + writer.WriteEndObject(); + } + + private static void ReadValue(ref Utf8JsonReader reader, ProblemDetails value, JsonSerializerOptions options) + { + string str; + if (TryReadStringProperty(ref reader, Type, out str)) + { + value.Type = str; + } + else if (TryReadStringProperty(ref reader, Title, out str)) + { + value.Title = str; + } + else if (TryReadStringProperty(ref reader, Detail, out str)) + { + value.Detail = str; + } + else if (TryReadStringProperty(ref reader, Instance, out str)) + { + value.Instance = str; + } + else if (reader.ValueTextEquals(Status.EncodedUtf8Bytes)) + { + reader.Read(); + if (reader.TokenType == JsonTokenType.Null) + { + return; + } + + value.Status = reader.GetInt32(); + } + else + { + var key = reader.GetString(); + reader.Read(); + value.Extensions[key] = JsonSerializer.Deserialize(ref reader, typeof (object), options); + } + } + + private static bool TryReadStringProperty(ref Utf8JsonReader reader, JsonEncodedText propertyName, out string value) + { + if (!reader.ValueTextEquals(propertyName.EncodedUtf8Bytes)) + { + value = null; + return false; + } + reader.Read(); + value = reader.GetString(); + return true; + } + + private static void WriteProblemDetails(Utf8JsonWriter writer, ProblemDetails value, JsonSerializerOptions options) + { + if (value.Type != null) + { + writer.WriteString(Type, value.Type); + } + + if (value.Title != null) + { + writer.WriteString(Title, value.Title); + } + + var status1 = value.Status; + if (status1.HasValue) + { + var utf8JsonWriter = writer; + var status2 = Status; + status1 = value.Status; + var num = status1.Value; + utf8JsonWriter.WriteNumber(status2, num); + } + + if (value.Detail != null) + { + writer.WriteString(Detail, value.Detail); + } + + if (value.Instance != null) + { + writer.WriteString(Instance, value.Instance); + } + + foreach (var extension in value.Extensions) + { + writer.WritePropertyName(extension.Key); + var writer1 = writer; + var obj = extension.Value; + var inputType = extension.Value?.GetType(); + if ((object) inputType == null) + inputType = typeof (object); + var options1 = options; + JsonSerializer.Serialize(writer1, obj, inputType, options1); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/Program.cs b/ConsoleApp1/Program.cs new file mode 100644 index 0000000..0eec63c --- /dev/null +++ b/ConsoleApp1/Program.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; + +namespace ConsoleApp1 +{ + class Program + { + static async Task Main(string[] args) + { + var httpClient = new HttpClient(new ProblemDetailsHttpMessageHandler()); + try + { + var result = await httpClient.PostAsJsonAsync("https://localhost:5001/Demo", new {}); + } + catch (ProblemDetailsException problemDetailsException) + { + Console.WriteLine($"Title: {problemDetailsException.Details.Title}"); + Console.WriteLine($"Detail: {problemDetailsException.Details.Detail}"); + Console.WriteLine($"Instance: {problemDetailsException.Details.Instance}"); + Console.WriteLine($"Type: {problemDetailsException.Details.Type}"); + Console.WriteLine($"Status: {problemDetailsException.Details.Status}"); + + if (problemDetailsException.Details.Type == "https://example.net/validation-error") + { + Console.WriteLine($"Validation Errors:"); + var validationErrors = problemDetailsException.Details.ToValidationProblemDetails(); + foreach (var invalidParam in validationErrors.InvalidParams) + { + Console.WriteLine($"{invalidParam.Name} - {invalidParam.Reason}"); + } + } + + Console.ReadKey(); + } + } + } +} \ No newline at end of file diff --git a/ConsoleApp1/ValidationProblemDetails.cs b/ConsoleApp1/ValidationProblemDetails.cs new file mode 100644 index 0000000..176f1de --- /dev/null +++ b/ConsoleApp1/ValidationProblemDetails.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace ConsoleApp1 +{ + public static class ProblemDetailsExtension + { + public static ValidationProblemDetails ToValidationProblemDetails(this ProblemDetails problemDetails) + { + var obj = new ValidationProblemDetails + { + Detail = problemDetails.Detail, + Instance = problemDetails.Instance, + Status = problemDetails.Status, + Title = problemDetails.Title, + Type = problemDetails.Type, + InvalidParams = JsonConvert.DeserializeObject>(problemDetails.Extensions["invalidParams"].ToString() ?? string.Empty) + }; + return obj; + } + } + + public class ValidationProblemDetails : ProblemDetails + { + public List InvalidParams { get; set; } + } + + public class ValidationProblemDetailsParam + { + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication.sln b/WebApplication.sln new file mode 100644 index 0000000..04827a3 --- /dev/null +++ b/WebApplication.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication", "WebApplication\WebApplication.csproj", "{9125605D-34F3-4052-A7FB-560AA5B3011F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp1", "ConsoleApp1\ConsoleApp1.csproj", "{79308C59-4532-45A5-B5C8-9A9AE275EBDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9125605D-34F3-4052-A7FB-560AA5B3011F}.Release|Any CPU.Build.0 = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79308C59-4532-45A5-B5C8-9A9AE275EBDE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/WebApplication/Controllers/DemoController.cs b/WebApplication/Controllers/DemoController.cs new file mode 100644 index 0000000..c0084d4 --- /dev/null +++ b/WebApplication/Controllers/DemoController.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace WebApplication.Controllers +{ + [ApiController] + [Route("[controller]")] + public class DemoController : ControllerBase + { + [HttpPost] + public ActionResult Post() + { + var problemDetails = new ProblemDetails + { + Detail = "The request parameters failed to validate.", + Instance = null, + Status = 400, + Title = "Validation Error", + Type = "https://example.net/validation-error", + }; + + problemDetails.Extensions.Add("invalidParams", new List() + { + new("name", "Cannot be blank."), + new("age", "Must be great or equals to 18.") + }); + + return new ObjectResult(problemDetails) + { + StatusCode = 400 + }; + } + } + + public class ValidationProblemDetailsParam + { + public ValidationProblemDetailsParam(string name, string reason) + { + Name = name; + Reason = reason; + } + + public string Name { get; set; } + public string Reason { get; set; } + } +} \ No newline at end of file diff --git a/WebApplication/Program.cs b/WebApplication/Program.cs new file mode 100644 index 0000000..4090891 --- /dev/null +++ b/WebApplication/Program.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace WebApplication +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); + } +} \ No newline at end of file diff --git a/WebApplication/Properties/launchSettings.json b/WebApplication/Properties/launchSettings.json new file mode 100644 index 0000000..ac1b200 --- /dev/null +++ b/WebApplication/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55292", + "sslPort": 44383 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "WebApplication": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/WebApplication/Startup.cs b/WebApplication/Startup.cs new file mode 100644 index 0000000..7d1489a --- /dev/null +++ b/WebApplication/Startup.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; + +namespace WebApplication +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebApplication", Version = "v1" }); }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebApplication v1")); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/WebApplication/WebApplication.csproj b/WebApplication/WebApplication.csproj new file mode 100644 index 0000000..39eb5ab --- /dev/null +++ b/WebApplication/WebApplication.csproj @@ -0,0 +1,11 @@ + + + + net5.0 + + + + + + + diff --git a/WebApplication/appsettings.Development.json b/WebApplication/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/WebApplication/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/WebApplication/appsettings.json b/WebApplication/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/WebApplication/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +}