Last active
March 22, 2026 01:12
-
-
Save AnthonyGiretti/931e17e60f281fae9ff88e05e2ed6f2b to your computer and use it in GitHub Desktop.
C# 14 Interceptors - The HttpClient Interceptor Generator
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Collections.Immutable; | |
| using System.Text; | |
| using Microsoft.CodeAnalysis; | |
| using Microsoft.CodeAnalysis.CSharp; | |
| using Microsoft.CodeAnalysis.CSharp.Syntax; | |
| namespace CorrelationInterceptors.Generator; | |
| [Generator] | |
| public sealed class HttpClientInterceptorGenerator : IIncrementalGenerator | |
| { | |
| public void Initialize(IncrementalGeneratorInitializationContext context) | |
| { | |
| var targets = context.SyntaxProvider | |
| .CreateSyntaxProvider( | |
| predicate: static (node, _) => IsSendAsyncInvocation(node), | |
| transform: static (ctx, ct) => GetTargetOrNull(ctx, ct)) | |
| .Where(static t => t is not null) | |
| .Select(static (t, _) => t!) | |
| .Collect(); | |
| // Generate implementation: https://gist.github.com/AnthonyGiretti/e9413cce4b72452c4006e90b97427fcd | |
| context.RegisterSourceOutput(targets, Generate); | |
| } | |
| private static bool IsSendAsyncInvocation(SyntaxNode node) | |
| => node is InvocationExpressionSyntax | |
| { | |
| Expression: MemberAccessExpressionSyntax { Name.Identifier.Text: "SendAsync" } | |
| }; | |
| private static InterceptorTarget? GetTargetOrNull( | |
| GeneratorSyntaxContext ctx, | |
| System.Threading.CancellationToken ct) | |
| { | |
| var invocation = (InvocationExpressionSyntax)ctx.Node; | |
| // Skip generated files, prevents the generator from intercepting | |
| // the SendAsync calls it already emitted in HttpClientInterceptors.g.cs | |
| var filePath = invocation.SyntaxTree.FilePath; | |
| if (filePath.EndsWith(".g.cs", System.StringComparison.OrdinalIgnoreCase) | |
| || filePath.EndsWith(".generated.cs", System.StringComparison.OrdinalIgnoreCase)) | |
| return null; | |
| if (ctx.SemanticModel.GetSymbolInfo(invocation, ct).Symbol | |
| is not IMethodSymbol method) | |
| return null; | |
| if (method.ContainingType.ToDisplayString() != "System.Net.Http.HttpClient" | |
| || method.Name != "SendAsync") | |
| return null; | |
| // GetInterceptableLocation is an extension method on SemanticModel | |
| // from Microsoft.CodeAnalysis.CSharp, no cast to CSharpSemanticModel needed | |
| var location = ctx.SemanticModel.GetInterceptableLocation(invocation, ct); | |
| if (location is null) return null; | |
| var secondParamType = method.Parameters.Length >= 2 | |
| ? method.Parameters[1].Type.ToDisplayString() | |
| : string.Empty; | |
| return new InterceptorTarget(location, method.Parameters.Length, secondParamType); | |
| } | |
| } | |
| internal sealed class InterceptorTarget | |
| { | |
| public InterceptableLocation Location { get; } | |
| public int ParameterCount { get; } | |
| public string SecondParamType { get; } | |
| public InterceptorTarget( | |
| InterceptableLocation location, int parameterCount, string secondParamType) | |
| { | |
| Location = location; | |
| ParameterCount = parameterCount; | |
| SecondParamType = secondParamType; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment