Skip to content

Commit 85fc510

Browse files
committed
error handling fix
1 parent 3e302a1 commit 85fc510

14 files changed

Lines changed: 408 additions & 20 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ always check all test are passed.
1818
- Do not add redundant `result.Problem is not null` checks after `result.IsFailed`; rely on result nullability contract/attributes and only use null-forgiving where needed.
1919
- Keep documentation aligned with the current major version (for this repository now: version 10); do not add cross-major migration sections unless explicitly requested.
2020
- When behavior changes in Result/Problem flows, include a clear README update with concrete usage examples.
21+
- When a framework adapter catches an exception and converts it into a failed `Result`, it must log the original exception object before returning the failure so distributed traces keep the real stack trace.
2122

2223
# Repository Guidelines
2324

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2727
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2828
<Product>Managed Code - Communication</Product>
29-
<Version>10.0.2</Version>
30-
<PackageVersion>10.0.2</PackageVersion>
29+
<Version>10.0.4</Version>
30+
<PackageVersion>10.0.4</PackageVersion>
3131

3232
</PropertyGroup>
3333
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

ManagedCode.Communication.AspNetCore/WebApi/Filters/ExceptionFilterWithProblemDetails.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using ManagedCode.Communication.AspNetCore.Constants;
3+
using ManagedCode.Communication.Logging;
34
using Microsoft.AspNetCore.Mvc;
45
using Microsoft.AspNetCore.Mvc.Filters;
56
using Microsoft.Extensions.Logging;
@@ -18,6 +19,11 @@ public abstract class ExceptionFilterWithProblemDetails(ILogger logger) : IExcep
1819
public virtual void OnException(ExceptionContext context)
1920
{
2021
var exception = context.Exception;
22+
var actionName = context.ActionDescriptor.DisplayName ?? "Unknown";
23+
var controllerName = context.ActionDescriptor.RouteValues["controller"] ?? "Unknown";
24+
25+
LoggerCenter.LogControllerException(Logger, exception, controllerName, actionName);
26+
2127
var statusCode = GetStatusCodeForException(exception);
2228

2329
// Option 1: Create ProblemDetails and convert to Problem
@@ -55,4 +61,4 @@ public virtual void OnException(ExceptionContext context)
5561

5662
context.ExceptionHandled = true;
5763
}
58-
}
64+
}

ManagedCode.Communication.Extensions/ManagedCode.Communication.Extensions.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<ItemGroup>
1717
<FrameworkReference Include="Microsoft.AspNetCore.App" />
1818
<ProjectReference Include="..\ManagedCode.Communication.AspNetCore\ManagedCode.Communication.AspNetCore.csproj" />
19-
<PackageReference Include="Polly" Version="8.6.6" />
19+
<PackageReference Include="Polly" Version="8.7.0" />
2020
</ItemGroup>
2121

2222
</Project>

ManagedCode.Communication.Orleans/Filters/CommunicationGrainCallResultFactory.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,20 @@ internal static class CommunicationGrainCallResultFactory
1717
?? throw new MissingMethodException(nameof(CommunicationGrainCallResultFactory), nameof(CreateFailureGeneric));
1818

1919
public static bool TrySetFailure(IGrainCallContext context, Exception exception)
20+
{
21+
return TrySetFailure(context, exception, out _);
22+
}
23+
24+
public static bool TrySetFailure(IGrainCallContext context, Exception exception, out HttpStatusCode statusCode)
2025
{
2126
var resultType = GetCommunicationResultType(context.InterfaceMethod.ReturnType);
2227
if (resultType is null)
2328
{
29+
statusCode = HttpStatusCode.InternalServerError;
2430
return false;
2531
}
2632

27-
var statusCode = OrleansHttpStatusCodeHelper.GetStatusCodeForException(exception);
33+
statusCode = OrleansHttpStatusCodeHelper.GetStatusCodeForException(exception);
2834
context.Result = CreateFailure(resultType, exception, statusCode);
2935
return true;
3036
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
2+
using System.Net;
23
using System.Threading.Tasks;
4+
using ManagedCode.Communication.Logging;
5+
using Microsoft.Extensions.Logging;
36
using Orleans;
47

58
namespace ManagedCode.Communication.Filters;
69

7-
public class CommunicationIncomingGrainCallFilter : IIncomingGrainCallFilter
10+
public class CommunicationIncomingGrainCallFilter(ILogger<CommunicationIncomingGrainCallFilter> logger) : IIncomingGrainCallFilter
811
{
912
public async Task Invoke(IIncomingGrainCallContext context)
1013
{
@@ -14,12 +17,26 @@ public async Task Invoke(IIncomingGrainCallContext context)
1417
}
1518
catch (Exception exception)
1619
{
17-
if (CommunicationGrainCallResultFactory.TrySetFailure(context, exception))
20+
if (CommunicationGrainCallResultFactory.TrySetFailure(context, exception, out var statusCode))
1821
{
22+
LogExceptionConverted(context, exception, (int)statusCode);
1923
return;
2024
}
2125

26+
LogExceptionConverted(context, exception, (int)HttpStatusCode.InternalServerError);
27+
2228
throw;
2329
}
2430
}
31+
32+
private void LogExceptionConverted(IIncomingGrainCallContext context, Exception exception, int statusCode)
33+
{
34+
LoggerCenter.LogOrleansGrainCallExceptionConverted(
35+
logger,
36+
exception,
37+
context.InterfaceName,
38+
context.MethodName,
39+
context.TargetId.ToString(),
40+
statusCode);
41+
}
2542
}
Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
using System;
2+
using System.Net;
23
using System.Threading.Tasks;
4+
using ManagedCode.Communication.Logging;
5+
using Microsoft.Extensions.Logging;
36
using Orleans;
47

58
namespace ManagedCode.Communication.Filters;
69

7-
public class CommunicationOutgoingGrainCallFilter : IOutgoingGrainCallFilter
10+
public class CommunicationOutgoingGrainCallFilter(ILogger<CommunicationOutgoingGrainCallFilter> logger) : IOutgoingGrainCallFilter
811
{
912
public async Task Invoke(IOutgoingGrainCallContext context)
1013
{
@@ -14,12 +17,26 @@ public async Task Invoke(IOutgoingGrainCallContext context)
1417
}
1518
catch (Exception exception)
1619
{
17-
if (CommunicationGrainCallResultFactory.TrySetFailure(context, exception))
20+
if (CommunicationGrainCallResultFactory.TrySetFailure(context, exception, out var statusCode))
1821
{
22+
LogExceptionConverted(context, exception, (int)statusCode);
1923
return;
2024
}
2125

26+
LogExceptionConverted(context, exception, (int)HttpStatusCode.InternalServerError);
27+
2228
throw;
2329
}
2430
}
31+
32+
private void LogExceptionConverted(IOutgoingGrainCallContext context, Exception exception, int statusCode)
33+
{
34+
LoggerCenter.LogOrleansGrainCallExceptionConverted(
35+
logger,
36+
exception,
37+
context.InterfaceName,
38+
context.MethodName,
39+
context.TargetId.ToString(),
40+
statusCode);
41+
}
2542
}

ManagedCode.Communication.Orleans/ManagedCode.Communication.Orleans.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
</PropertyGroup>
1616

1717
<ItemGroup>
18-
<PackageReference Include="Microsoft.Orleans.Sdk" Version="10.1.0" />
19-
<PackageReference Include="Microsoft.Orleans.Runtime" Version="10.1.0" />
20-
<PackageReference Include="Microsoft.Orleans.Serialization.Abstractions" Version="10.1.0" />
18+
<PackageReference Include="Microsoft.Orleans.Sdk" Version="10.2.0" />
19+
<PackageReference Include="Microsoft.Orleans.Runtime" Version="10.2.0" />
20+
<PackageReference Include="Microsoft.Orleans.Serialization.Abstractions" Version="10.2.0" />
2121
</ItemGroup>
2222

2323
<ItemGroup>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using ManagedCode.Communication.AspNetCore.Filters;
5+
using Microsoft.AspNetCore.Http;
6+
using Microsoft.AspNetCore.Mvc;
7+
using Microsoft.AspNetCore.Mvc.Abstractions;
8+
using Microsoft.AspNetCore.Mvc.Filters;
9+
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.Extensions.Logging;
11+
using Shouldly;
12+
using Xunit;
13+
14+
namespace ManagedCode.Communication.Tests.AspNetCore.Filters;
15+
16+
public sealed class ExceptionFilterWithProblemDetailsTests
17+
{
18+
[Fact]
19+
public void OnException_ShouldLogOriginalExceptionBeforeHandling()
20+
{
21+
var logger = new CapturingLogger();
22+
var filter = new TestExceptionFilter(logger);
23+
var exception = new InvalidOperationException("problem-details failure");
24+
var context = CreateExceptionContext(exception);
25+
26+
filter.OnException(context);
27+
28+
context.ExceptionHandled.ShouldBeTrue();
29+
context.Result.ShouldBeOfType<ObjectResult>();
30+
31+
var entry = logger.Entries.Single();
32+
entry.Level.ShouldBe(LogLevel.Error);
33+
entry.EventId.Id.ShouldBe(5001);
34+
entry.Exception.ShouldBeSameAs(exception);
35+
entry.Message.ShouldContain("TestController");
36+
entry.Message.ShouldContain("TestAction");
37+
}
38+
39+
private static ExceptionContext CreateExceptionContext(Exception exception)
40+
{
41+
var actionDescriptor = new ActionDescriptor
42+
{
43+
DisplayName = "TestAction",
44+
RouteValues = { ["controller"] = "TestController" }
45+
};
46+
var actionContext = new ActionContext(
47+
new DefaultHttpContext(),
48+
new RouteData(),
49+
actionDescriptor);
50+
51+
return new ExceptionContext(actionContext, [])
52+
{
53+
Exception = exception
54+
};
55+
}
56+
57+
private sealed class TestExceptionFilter(ILogger logger) : ExceptionFilterWithProblemDetails(logger);
58+
59+
private sealed class CapturingLogger : ILogger
60+
{
61+
public List<CapturedLogEntry> Entries { get; } = [];
62+
63+
public IDisposable BeginScope<TState>(TState state)
64+
where TState : notnull
65+
{
66+
return NullScope.Instance;
67+
}
68+
69+
public bool IsEnabled(LogLevel logLevel)
70+
{
71+
return true;
72+
}
73+
74+
public void Log<TState>(
75+
LogLevel logLevel,
76+
EventId eventId,
77+
TState state,
78+
Exception? exception,
79+
Func<TState, Exception?, string> formatter)
80+
{
81+
Entries.Add(new CapturedLogEntry(logLevel, eventId, exception, formatter(state, exception)));
82+
}
83+
}
84+
85+
private sealed class NullScope : IDisposable
86+
{
87+
public static readonly NullScope Instance = new();
88+
89+
public void Dispose()
90+
{
91+
}
92+
}
93+
94+
private sealed record CapturedLogEntry(
95+
LogLevel Level,
96+
EventId EventId,
97+
Exception? Exception,
98+
string Message);
99+
}

ManagedCode.Communication.Tests/ManagedCode.Communication.Tests.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
</None>
2323
</ItemGroup>
2424
<ItemGroup>
25-
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="10.1.0" />
25+
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="10.2.0" />
2626
<PackageReference Include="Shouldly" Version="4.3.0" />
2727
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
28-
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
29-
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.8" />
30-
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.8" />
28+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.9" />
29+
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.9" />
30+
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.9" />
3131
<PackageReference Include="xunit" Version="2.9.3"/>
3232
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
3333
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

0 commit comments

Comments
 (0)