Skip to content

Instantly share code, notes, and snippets.

@AnthonyGiretti
Last active March 22, 2026 01:12
Show Gist options
  • Select an option

  • Save AnthonyGiretti/931e17e60f281fae9ff88e05e2ed6f2b to your computer and use it in GitHub Desktop.

Select an option

Save AnthonyGiretti/931e17e60f281fae9ff88e05e2ed6f2b to your computer and use it in GitHub Desktop.
C# 14 Interceptors - The HttpClient Interceptor Generator
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