Skip to content

Commit 7d57c17

Browse files
committed
wip
1 parent d60a217 commit 7d57c17

9 files changed

Lines changed: 213 additions & 35 deletions

File tree

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2525
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2626
<Product>Managed Code - Communication</Product>
27-
<Version>9.0.0</Version>
28-
<PackageVersion>9.0.0</PackageVersion>
27+
<Version>9.0.1</Version>
28+
<PackageVersion>9.0.1</PackageVersion>
2929

3030
</PropertyGroup>
3131
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

ManagedCode.Communication.Extensions/CommunicationMiddleware.cs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,17 @@
88

99
namespace ManagedCode.Communication.Extensions;
1010

11-
public class CommunicationMiddleware
11+
public class CommunicationMiddleware(ILogger<CommunicationMiddleware> logger, RequestDelegate next, IOptions<CommunicationOptions> options)
1212
{
13-
private readonly ILogger<CommunicationMiddleware> _logger;
14-
private readonly RequestDelegate _next;
15-
private readonly IOptions<CommunicationOptions> _options;
16-
17-
public CommunicationMiddleware(ILogger<CommunicationMiddleware> logger, RequestDelegate next,
18-
IOptions<CommunicationOptions> options)
19-
{
20-
_logger = logger;
21-
_next = next;
22-
_options = options;
23-
}
24-
2513
public async Task Invoke(HttpContext httpContext)
2614
{
2715
try
2816
{
29-
await _next(httpContext);
17+
await next(httpContext);
3018
}
3119
catch (Exception ex)
3220
{
33-
_logger.LogError(ex, httpContext.Request.Method + "::" + httpContext.Request.Path);
21+
logger.LogError(ex, httpContext.Request.Method + "::" + httpContext.Request.Path);
3422

3523
if (httpContext.Response.HasStarted)
3624
throw;
@@ -43,7 +31,7 @@ public async Task Invoke(HttpContext httpContext)
4331
httpContext.Response.ContentType = "application/json; charset=utf-8";
4432
httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
4533

46-
if (_options.Value.ShowErrorDetails)
34+
if (options.Value.ShowErrorDetails)
4735
await httpContext.Response.WriteAsJsonAsync(Result.Fail(HttpStatusCode.InternalServerError,
4836
ex.Message));
4937
else
Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,66 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using Microsoft.AspNetCore.Http;
5+
using Microsoft.AspNetCore.WebUtilities;
26
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Hosting;
38

49
namespace ManagedCode.Communication.Extensions.Extensions;
510

11+
12+
public static class HostApplicationBuilderExtensions
13+
{
14+
public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder)
15+
{
16+
builder.Services.AddCommunication(options => options.ShowErrorDetails = builder.Environment.IsDevelopment());
17+
return builder;
18+
}
19+
20+
public static IHostApplicationBuilder AddCommunication(this IHostApplicationBuilder builder, Action<CommunicationOptions> config)
21+
{
22+
builder.Services.AddCommunication(config);
23+
return builder;
24+
}
25+
}
26+
627
public static class ServiceCollectionExtensions
728
{
8-
public static IServiceCollection AddCommunication(this IServiceCollection services,
9-
Action<CommunicationOptions> options)
29+
30+
public static IServiceCollection AddCommunication(this IServiceCollection services, Action<CommunicationOptions> options)
31+
{
32+
services.AddOptions<CommunicationOptions>()
33+
.Configure(options);
34+
35+
return services;
36+
}
37+
38+
39+
40+
public static IServiceCollection AddDefaultProblemDetails(this IServiceCollection services)
1041
{
11-
services.AddOptions<CommunicationOptions>().Configure(options);
42+
services.AddProblemDetails(options =>
43+
{
44+
options.CustomizeProblemDetails = context =>
45+
{
46+
var statusCode = context.ProblemDetails.Status.GetValueOrDefault(StatusCodes.Status500InternalServerError);
47+
48+
context.ProblemDetails.Type ??= $"https://httpstatuses.io/{statusCode}";
49+
context.ProblemDetails.Title ??= ReasonPhrases.GetReasonPhrase(statusCode);
50+
context.ProblemDetails.Instance ??= context.HttpContext.Request.Path;
51+
context.ProblemDetails.Extensions.TryAdd("traceId", Activity.Current?.Id ?? context.HttpContext.TraceIdentifier);
52+
};
53+
});
54+
55+
return services;
56+
}
57+
58+
public static IServiceCollection AddCommunicationExceptionHandler(this IServiceCollection services)
59+
{
60+
// Ensures that the ProblemDetails service is registered.
61+
services.AddProblemDetails();
62+
63+
services.AddExceptionHandler<CommunicationExceptionHandler>();
1264
return services;
1365
}
1466
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Net;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using ManagedCode.Communication.Extensions.Extensions;
7+
using Microsoft.AspNetCore.Diagnostics;
8+
using Microsoft.AspNetCore.Hosting;
9+
using Microsoft.AspNetCore.Http;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.Extensions.Hosting;
12+
using Microsoft.Extensions.Logging;
13+
using Microsoft.Extensions.Options;
14+
15+
namespace ManagedCode.Communication.Extensions;
16+
17+
internal class CommunicationExceptionHandler(IProblemDetailsService problemDetailsService, IWebHostEnvironment webHostEnvironment) : IExceptionHandler
18+
{
19+
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
20+
{
21+
var problemDetails = new ProblemDetails
22+
{
23+
Status = httpContext.Response.StatusCode,
24+
Title = exception.GetType().FullName,
25+
Detail = exception.Message,
26+
Instance = httpContext.Request.Path,
27+
Extensions =
28+
{
29+
["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier
30+
}
31+
};
32+
33+
if (exception.InnerException is not null)
34+
{
35+
problemDetails.Extensions["innerException"] = new
36+
{
37+
Title = exception.InnerException.GetType().FullName,
38+
Detail = exception.InnerException.Message
39+
};
40+
}
41+
42+
if (webHostEnvironment.IsDevelopment())
43+
{
44+
problemDetails.Extensions["stackTrace"] = exception.StackTrace;
45+
}
46+
47+
await problemDetailsService.WriteAsync(new()
48+
{
49+
HttpContext = httpContext,
50+
AdditionalMetadata = httpContext.Features.Get<IExceptionHandlerFeature>()?.Endpoint?.Metadata,
51+
ProblemDetails = problemDetails,
52+
Exception = exception
53+
});
54+
55+
return true;
56+
}
57+
}

ManagedCode.Communication/CollectionResultT/CollectionResult.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,18 @@ public void AddError(Error error)
4444
}
4545
}
4646

47-
public void ThrowIfFail()
47+
[MemberNotNullWhen(false, nameof(Collection))]
48+
public bool ThrowIfFail()
4849
{
50+
if(IsSuccess)
51+
return false;
52+
4953
if (Errors?.Any() is not true)
5054
{
5155
if(IsFailed)
5256
throw new Exception(nameof(IsFailed));
5357

54-
return;
58+
return false;
5559
}
5660

5761
var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)));
@@ -62,14 +66,15 @@ public void ThrowIfFail()
6266
throw new AggregateException(exceptions);
6367
}
6468

65-
public void ThrowIfFailWithStackPreserved()
69+
[MemberNotNullWhen(false, nameof(Collection))]
70+
public bool ThrowIfFailWithStackPreserved()
6671
{
6772
if (Errors?.Any() is not true)
6873
{
6974
if (IsFailed)
7075
throw new Exception(nameof(IsFailed));
7176

72-
return;
77+
return false;
7378
}
7479

7580
var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))));

ManagedCode.Communication/Error.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,77 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Runtime.ExceptionServices;
34
using System.Text.Json.Serialization;
45

56
namespace ManagedCode.Communication;
67

78

9+
/// <summary>
10+
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>.
11+
/// </summary>
12+
public class Problem
13+
{
14+
/// <summary>
15+
/// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when
16+
/// dereferenced, it provide human-readable documentation for the problem type
17+
/// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be
18+
/// "about:blank".
19+
/// </summary>
20+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
21+
[JsonPropertyOrder(-5)]
22+
[JsonPropertyName("type")]
23+
public string? Type { get; set; }
24+
25+
/// <summary>
26+
/// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence
27+
/// of the problem, except for purposes of localization(e.g., using proactive content negotiation;
28+
/// see[RFC7231], Section 3.4).
29+
/// </summary>
30+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
31+
[JsonPropertyOrder(-4)]
32+
[JsonPropertyName("title")]
33+
public string? Title { get; set; }
34+
35+
/// <summary>
36+
/// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.
37+
/// </summary>
38+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
39+
[JsonPropertyOrder(-3)]
40+
[JsonPropertyName("status")]
41+
public int? Status { get; set; }
42+
43+
/// <summary>
44+
/// A human-readable explanation specific to this occurrence of the problem.
45+
/// </summary>
46+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
47+
[JsonPropertyOrder(-2)]
48+
[JsonPropertyName("detail")]
49+
public string? Detail { get; set; }
50+
51+
/// <summary>
52+
/// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.
53+
/// </summary>
54+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
55+
[JsonPropertyOrder(-1)]
56+
[JsonPropertyName("instance")]
57+
public string? Instance { get; set; }
58+
59+
/// <summary>
60+
/// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members.
61+
/// <para>
62+
/// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as
63+
/// other members of a problem type.
64+
/// </para>
65+
/// </summary>
66+
/// <remarks>
67+
/// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters.
68+
/// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters.
69+
/// </remarks>
70+
[JsonExtensionData]
71+
public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal);
72+
}
73+
74+
875

976
public struct Error
1077
{

ManagedCode.Communication/IResultError.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ public interface IResultError
2222
/// <summary>
2323
/// Throws an exception if the result indicates a failure.
2424
/// </summary>
25-
void ThrowIfFail();
25+
bool ThrowIfFail();
2626

2727
/// <summary>
2828
/// Throws an exception with stack trace preserved if the result indicates a failure.
2929
/// </summary>
30-
void ThrowIfFailWithStackPreserved();
30+
bool ThrowIfFailWithStackPreserved();
3131

3232
/// <summary>
3333
/// Gets the error code as a specific enumeration type.

ManagedCode.Communication/Result/Result.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.Diagnostics.CodeAnalysis;
45
using System.Linq;
56
using System.Runtime.ExceptionServices;
67
using System.Text.Json.Serialization;
@@ -68,14 +69,17 @@ public void AddError(Error error)
6869
/// <summary>
6970
/// Throws an exception if the result indicates a failure.
7071
/// </summary>
71-
public void ThrowIfFail()
72+
public bool ThrowIfFail()
7273
{
74+
if(IsSuccess)
75+
return false;
76+
7377
if (Errors?.Any() is not true)
7478
{
7579
if(IsFailed)
7680
throw new Exception(nameof(IsFailed));
7781

78-
return;
82+
return false;
7983
}
8084

8185
var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)));
@@ -88,14 +92,14 @@ public void ThrowIfFail()
8892
/// <summary>
8993
/// Throws an exception with stack trace preserved if the result indicates a failure.
9094
/// </summary>
91-
public void ThrowIfFailWithStackPreserved()
95+
public bool ThrowIfFailWithStackPreserved()
9296
{
9397
if (Errors?.Any() is not true)
9498
{
9599
if (IsFailed)
96100
throw new Exception(nameof(IsFailed));
97101

98-
return;
102+
return false;
99103
}
100104

101105
var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))));

ManagedCode.Communication/ResultT/Result.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,18 @@ public void AddError(Error error)
5555
/// <summary>
5656
/// Throws an exception if the result is a failure.
5757
/// </summary>
58-
public void ThrowIfFail()
58+
[MemberNotNullWhen(false, nameof(Value))]
59+
public bool ThrowIfFail()
5960
{
61+
if(IsSuccess)
62+
return false;
63+
6064
if (Errors?.Any() is not true)
6165
{
6266
if(IsFailed)
6367
throw new Exception(nameof(IsFailed));
6468

65-
return;
69+
return false;
6670
}
6771

6872
var exceptions = Errors.Select(s => s.Exception() ?? new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message)));
@@ -76,14 +80,15 @@ public void ThrowIfFail()
7680
/// <summary>
7781
/// Throws an exception with stack trace preserved if the result indicates a failure.
7882
/// </summary>
79-
public void ThrowIfFailWithStackPreserved()
83+
[MemberNotNullWhen(false, nameof(Value))]
84+
public bool ThrowIfFailWithStackPreserved()
8085
{
8186
if (Errors?.Any() is not true)
8287
{
8388
if (IsFailed)
8489
throw new Exception(nameof(IsFailed));
8590

86-
return;
91+
return false;
8792
}
8893

8994
var exceptions = Errors.Select(s => s.ExceptionInfo() ?? ExceptionDispatchInfo.Capture(new Exception(StringExtension.JoinFilter(';', s.ErrorCode, s.Message))));

0 commit comments

Comments
 (0)