Using C# Source Generators to Automatically Generate HttpClient Implementations
This article explains how C# source generators introduced in .NET 5/6 can analyze compile‑time metadata and automatically generate HttpClient implementation classes, register them in the DI container, and handle multi‑project assembly analysis for streamlined API consumption.
Source generators, introduced with .NET 5 and widely used in .NET 6, allow compile‑time analysis of existing code to create new code that is added to the compilation. By leveraging source generators, developers can offload repetitive template‑based tasks and focus on creative work while retaining native performance.
The article demonstrates generating HttpClient implementations from HTTP API interfaces using custom attributes such as HttpClientAttribute , HttpGetAttribute , and others. These attributes provide the necessary metadata (client name, HTTP method, route template) for the generator.
///
/// Identity a Interface which will be implemented by SourceGenerators
///
[AttributeUsage(AttributeTargets.Interface)]
public class HttpClientAttribute : Attribute
{
///
/// HttpClient name
///
public string Name { get; }
///
/// Create a new HttpClientAttribute
///
public HttpClientAttribute() { }
///
/// Create a new HttpClientAttribute with given name
///
public HttpClientAttribute(string name) { Name = name; }
}Method‑level attributes like HttpGetAttribute inherit from an abstract HttpMethodAttribute that stores the route template.
public class HttpGetAttribute : HttpMethodAttribute
{
public HttpGetAttribute(string template) : base(template) { }
}
[AttributeUsage(AttributeTargets.Method)]
public abstract class HttpMethodAttribute : Attribute
{
private string Template { get; }
protected HttpMethodAttribute(string template) { Template = template; }
}To create the generator, a new project (e.g., HttpClient.SourceGenerator ) is set up with Microsoft.CodeAnalysis.Analyzers and Microsoft.CodeAnalysis.CSharp packages, targeting netstandard2.0 . The generator implements ISourceGenerator and registers a syntax receiver that collects interfaces marked with HttpClientAttribute .
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}The syntax receiver examines InterfaceDeclarationSyntax nodes, checks for the custom attribute via the semantic model, and stores matching symbols.
class HttpClientSyntax : ISyntaxContextReceiver
{
public List
TypeSymbols { get; set; } = new();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is InterfaceDeclarationSyntax ids && ids.AttributeLists.Count > 0)
{
var typeSymbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, ids) as INamedTypeSymbol;
if (typeSymbol!.GetAttributes().Any(x => x.AttributeClass!.ToDisplayString() == "SourceGeneratorPower.HttpClient.HttpClientAttribute"))
TypeSymbols.Add(typeSymbol);
}
}
}For multi‑project solutions, the generator also scans referenced assemblies using context.Compilation.SourceModule.ReferencedAssemblySymbols , filtering only project references (public key empty) and traversing their symbols with a custom SymbolVisitor to locate additional interfaces.
class HttpClientVisitor : SymbolVisitor
{
private readonly HashSet
_httpClientTypeSymbols = new(SymbolEqualityComparer.Default);
public override void VisitAssembly(IAssemblySymbol symbol) => symbol.GlobalNamespace.Accept(this);
public override void VisitNamespace(INamespaceSymbol symbol)
{
foreach (var member in symbol.GetMembers()) member.Accept(this);
}
public override void VisitNamedType(INamedTypeSymbol symbol)
{
if (symbol.DeclaredAccessibility != Accessibility.Public) return;
if (symbol.GetAttributes().Any(x => x.AttributeClass!.ToDisplayString() == "SourceGeneratorPower.HttpClient.HttpClientAttribute"))
_httpClientTypeSymbols.Add(symbol);
foreach (var nested in symbol.GetMembers()) nested.Accept(this);
}
public ImmutableArray
GetHttpClientTypes() => _httpClientTypeSymbols.ToImmutableArray();
}During Execute , the generator merges symbols from the syntax receiver and the assembly visitor, then generates extension methods that register the generated implementations with the DI container using services.AddScoped . The generated source is added via context.AddSource .
var extensionSource = new StringBuilder($@"
using SourceGeneratorPower.HttpClient;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{{
public static class ScanInjectOptions
{{
public static void AddGeneratedHttpClient(this IServiceCollection services)
{{
// generated registrations
}}
}}
}}");Usage involves defining an interface with the custom attributes, adding the generated client registration in the host project, configuring the underlying HttpClient , and injecting the interface into controllers.
[HttpClient("JsonServer")]
public interface IJsonServerApi
{{
[HttpGet("/todos/{id}")]
Task
Get(int id, CancellationToken cancellationToken = default);
// other CRUD methods omitted for brevity
}}
builder.Services.AddGeneratedHttpClient();
builder.Services.AddHttpClient("JsonServer", options => options.BaseAddress = new Uri("https://jsonplaceholder.typicode.com"));The source code and NuGet packages are available on GitHub and NuGet, allowing developers to adopt the generator quickly.
In conclusion, source generators provide a powerful, human‑readable way to eliminate repetitive coding while delivering performance comparable to hand‑written code; readers are encouraged to experiment with the provided examples and contribute suggestions or new features via GitHub.
YunZhu Net Technology Team
Technical practice sharing from the YunZhu Net Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.