Skip to content

Commit

Permalink
Merge pull request #174 from christianhelle/ioc-registration
Browse files Browse the repository at this point in the history
Add support for generating IServiceCollection extension methods for registering Refit clients
  • Loading branch information
christianhelle authored Oct 6, 2023
2 parents 8ff9c76 + 8461a77 commit c0607de
Show file tree
Hide file tree
Showing 30 changed files with 791 additions and 53 deletions.
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,17 @@ The following is an example `.refitter` file
"includePathMatches": [ // Optional. Only include Paths that match the provided regular expression
"^/pet/.*",
"^/store/.*"
]
],
"dependencyInjectionSettings": {
"baseUrl": "https://petstore3.swagger.io/api/v3", // Optional. Leave this blank to set the base address manually
"httpMessageHandlers": [ // Optional
"AuthorizationMessageHandler",
"TelemetryMessageHandler"
],
"usePolly": true, // Optional. Set this to true, to configure Polly with a retry policy that uses a jittered backoff. Default=false
"pollyMaxRetryCount": 3, // Optional. Default=6
"firstBackoffRetryInSeconds": 0.5 // Optional. Default=1.0
}
}
```

Expand All @@ -174,6 +184,12 @@ The following is an example `.refitter` file
- `generateDeprecatedOperations` - a boolean indicating whether deprecated operations should be generated or skipped. Default is `true`
- `operationNameTemplate` - Generate operation names using pattern. This must contain the string {operationName}. An example usage of this could be `{operationName}Async` to suffix all method names with Async
- `optionalParameters` - Generate non-required parameters as nullable optional parameters
- `dependencyInjectionSettings` - Setting this will generated extension methods to `IServiceCollection` for configuring Refit clients
- `baseUrl` - Used as the HttpClient base address. Leave this blank to manually set the base URL
- `httpMessageHandlers` - A collection of `HttpMessageHandler` that is added to the HttpClient pipeline
- `usePolly` - Set this to true to configure the HttpClient to use Polly using a retry policy with a jittered backoff
- `pollyMaxRetryCount` - This is the max retry count used in the Polly retry policy. Default is 6
- `firstBackoffRetryInSeconds` - This is the duration of the initial retry backoff. Default is 1 second


# Using the generated code
Expand Down
95 changes: 95 additions & 0 deletions src/Refitter.Core/DependencyInjectionGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Text;

namespace Refitter.Core;

public static class DependencyInjectionGenerator
{
public static string Generate(
RefitGeneratorSettings settings,
string[] interfaceNames)
{
var iocSettings = settings.DependencyInjectionSettings;
if (iocSettings is null || !interfaceNames.Any())
return string.Empty;

var code = new StringBuilder();

var methodDeclaration = string.IsNullOrEmpty(iocSettings.BaseUrl)
? "public static IServiceCollection ConfigureRefitClients(this IServiceCollection services, Uri baseUrl)"
: "public static IServiceCollection ConfigureRefitClients(this IServiceCollection services)";

var configureRefitClient = string.IsNullOrEmpty(iocSettings.BaseUrl)
? ".ConfigureHttpClient(c => c.BaseAddress = baseUrl)"
: $".ConfigureHttpClient(c => c.BaseAddress = new Uri(\"{iocSettings.BaseUrl}\"))";

var usings = iocSettings.UsePolly
? """
using System;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Contrib.WaitAndRetry;
using Polly.Extensions.Http;
"""
: """
using System;
using Microsoft.Extensions.DependencyInjection;
""";

code.AppendLine();
code.AppendLine();
code.AppendLine(
$$""""
namespace {{settings.Namespace}}
{
{{usings}}
public static partial class IServiceCollectionExtensions
{
{{methodDeclaration}}
{
"""");
foreach (var interfaceName in interfaceNames)
{
code.Append(
$$"""
services
.AddRefitClient<{{interfaceName}}>()
{{configureRefitClient}}
""");

foreach (string httpMessageHandler in iocSettings.HttpMessageHandlers)
{
code.AppendLine();
code.Append($" .AddHttpMessageHandler<{httpMessageHandler}>()");
}

if (iocSettings.UsePolly)
{
code.AppendLine();
code.Append(
$$"""
.AddPolicyHandler(
HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(
Backoff.DecorrelatedJitterBackoffV2(
TimeSpan.FromSeconds({{iocSettings.FirstBackoffRetryInSeconds}}),
{{iocSettings.PollyMaxRetryCount}}))
""");
}

code.Append(");");
code.AppendLine();
code.AppendLine();
}

code.Remove(code.Length - 2, 2);
code.AppendLine();
code.AppendLine(" return services;");
code.AppendLine(" }");
code.AppendLine(" }");
code.AppendLine("}");
code.AppendLine();
return code.ToString();
}
}
2 changes: 1 addition & 1 deletion src/Refitter.Core/IRefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace Refitter.Core;

internal interface IRefitInterfaceGenerator
{
string GenerateCode();
RefitGeneratedCode GenerateCode();
}
14 changes: 14 additions & 0 deletions src/Refitter.Core/RefitGeneratedCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Refitter.Core;

public record RefitGeneratedCode(
string SourceCode,
params string[] InterfaceNames)
{
public string SourceCode { get; } = SourceCode;
public string[] InterfaceNames { get; } = InterfaceNames;

public override string ToString()
{
return SourceCode;
}
}
27 changes: 16 additions & 11 deletions src/Refitter.Core/RefitGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,24 +96,28 @@ public string Generate()
_ => new RefitInterfaceGenerator(settings, document, generator),
};

var client = GenerateClient(interfaceGenerator);
var generatedCode = GenerateClient(interfaceGenerator);
return new StringBuilder()
.AppendLine(client)
.AppendLine(generatedCode.SourceCode)
.AppendLine()
.AppendLine(settings.GenerateContracts ? contracts : string.Empty)
.ToString();
.AppendLine(DependencyInjectionGenerator.Generate(settings, generatedCode.InterfaceNames))
.ToString()
.TrimEnd();
}

/// <summary>
/// Generates the client code based on the specified interface generator.
/// </summary>
/// <param name="interfaceGenerator">The interface generator used to generate the client code.</param>
/// <returns>The generated client code as a string.</returns>
private string GenerateClient(IRefitInterfaceGenerator interfaceGenerator)
private RefitGeneratedCode GenerateClient(IRefitInterfaceGenerator interfaceGenerator)
{
var code = new StringBuilder();
GenerateAutoGeneratedHeader(code);
code.AppendLine(RefitInterfaceImports.GenerateNamespaceImports(settings))

code.AppendLine()
.AppendLine(RefitInterfaceImports.GenerateNamespaceImports(settings))
.AppendLine();

if (settings.AdditionalNamespaces.Any())
Expand All @@ -131,14 +135,15 @@ private string GenerateClient(IRefitInterfaceGenerator interfaceGenerator)
code.AppendLine("#nullable enable");
}

var refitInterfaces = interfaceGenerator.GenerateCode();
code.AppendLine($$"""
namespace {{settings.Namespace}}
{
{{interfaceGenerator.GenerateCode()}}
}
""");
namespace {{settings.Namespace}}
{
{{refitInterfaces}}
}
""");

return code.ToString();
return new RefitGeneratedCode(code.ToString(), refitInterfaces.InterfaceNames);
}

/// <summary>
Expand Down
50 changes: 50 additions & 0 deletions src/Refitter.Core/RefitGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ public class RefitGeneratorSettings
[JsonPropertyName("outputFolder")]
[JsonProperty("outputFolder")]
public string OutputFolder { get; set; } = "./Generated";

/// <summary>
/// Gets or sets the settings describing how to register generated interface to the .NET Core DI container
/// </summary>
[JsonPropertyName("dependencyInjectionSettings")]
[JsonProperty("dependencyInjectionSettings")]
public DependencyInjectionSettings? DependencyInjectionSettings { get; set; }
}

public enum MultipleInterfaces
Expand Down Expand Up @@ -198,3 +205,46 @@ public enum TypeAccessibility
/// </summary>
Internal
}

/// <summary>
/// Dependency Injection settings describing how the Refit client should be configured.
/// This can be used to configure the HttpClient pipeline with additional handlers
/// </summary>
public class DependencyInjectionSettings
{
/// <summary>
/// Base Address for the HttpClient
/// </summary>
[JsonPropertyName("baseUrl")]
[JsonProperty("baseUrl")]
public string? BaseUrl { get; set; }

/// <summary>
/// A collection of HttpMessageHandlers to be added to the HttpClient pipeline.
/// This can be for telemetry logging, authorization, etc.
/// </summary>
[JsonPropertyName("httpMessageHandlers")]
[JsonProperty("httpMessageHandlers")]
public string[] HttpMessageHandlers { get; set; } = Array.Empty<string>();

/// <summary>
/// Set this to true to use Polly for transient fault handling.
/// </summary>
[JsonPropertyName("usePolly")]
[JsonProperty("usePolly")]
public bool UsePolly { get; set; }

/// <summary>
/// Default max retry count for Polly. Default is 6.
/// </summary>
[JsonPropertyName("pollyMaxRetryCount")]
[JsonProperty("pollyMaxRetryCount")]
public int PollyMaxRetryCount { get; set; } = 6;

/// <summary>
/// The median delay to target before the first retry in seconds. Default is 1 second
/// </summary>
[JsonPropertyName("firstBackoffRetryInSeconds")]
[JsonProperty("firstBackoffRetryInSeconds")]
public double FirstBackoffRetryInSeconds { get; set; } = 1.0;
}
19 changes: 11 additions & 8 deletions src/Refitter.Core/RefitInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ internal RefitInterfaceGenerator(
generator.BaseSettings.OperationNameGenerator = new OperationNameGenerator(document);
}

public virtual string GenerateCode()
public virtual RefitGeneratedCode GenerateCode()
{
return $$"""
{{GenerateInterfaceDeclaration()}}
{{Separator}}{
{{GenerateInterfaceBody()}}
{{Separator}}}
""";
return new RefitGeneratedCode(
$$"""
{{GenerateInterfaceDeclaration(out var interfaceName)}}
{{Separator}}{
{{GenerateInterfaceBody()}}
{{Separator}}}
""",
interfaceName);
}

private string GenerateInterfaceBody()
Expand Down Expand Up @@ -168,12 +170,13 @@ protected void GenerateObsoleteAttribute(OpenApiOperation operation, StringBuild
}
}

private string GenerateInterfaceDeclaration()
private string GenerateInterfaceDeclaration(out string interfaceName)
{
var title = settings.Naming.UseOpenApiTitle
? IdentifierUtils.Sanitize(document.Info?.Title ?? "ApiClient")
: settings.Naming.InterfaceName;

interfaceName = $"I{title.CapitalizeFirstCharacter()}";
var modifier = settings.TypeAccessibility.ToString().ToLowerInvariant();
return $"""
{Separator}{GetGeneratedCodeAttribute()}
Expand Down
11 changes: 8 additions & 3 deletions src/Refitter.Core/RefitMultipleInterfaceByTagGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal RefitMultipleInterfaceByTagGenerator(
{
}

public override string GenerateCode()
public override RefitGeneratedCode GenerateCode()
{
var ungroupedTitle = settings.Naming.UseOpenApiTitle
? IdentifierUtils.Sanitize(document.Info?.Title ?? "ApiClient")
Expand All @@ -28,6 +28,8 @@ public override string GenerateCode()
.GroupBy(x => GetGroupName(x.Operation.Value, ungroupedTitle), (k, v) => new { Key = k, Combined = v });

Dictionary<string, StringBuilder> interfacesByGroup = new();
var interfaceNames = new List<string>();

foreach (var kv in byGroup)
{
foreach (var op in kv.Combined)
Expand All @@ -53,8 +55,11 @@ public override string GenerateCode()
{
interfacesByGroup[kv.Key] = sb = new StringBuilder();
GenerateInterfaceXmlDocComments(operation, sb);

var interfaceName = GetInterfaceName(kv.Key);
interfaceNames.Add(interfaceName);
sb.AppendLine($$"""
{{GenerateInterfaceDeclaration(GetInterfaceName(kv.Key))}}
{{GenerateInterfaceDeclaration(interfaceName)}}
{{Separator}}{
""");
}
Expand Down Expand Up @@ -88,7 +93,7 @@ public override string GenerateCode()
code.AppendLine();
}

return code.ToString();
return new RefitGeneratedCode(code.ToString(), interfaceNames.ToArray());
}

private string GetGroupName(OpenApiOperation operation, string ungroupedTitle)
Expand Down
11 changes: 8 additions & 3 deletions src/Refitter.Core/RefitMultipleInterfaceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ internal RefitMultipleInterfaceGenerator(
{
}

public override string GenerateCode()
public override RefitGeneratedCode GenerateCode()
{
var code = new StringBuilder();
var interfaceNames = new List<string>();

foreach (var kv in document.Paths)
{
foreach (var operations in kv.Value)
Expand All @@ -40,8 +42,11 @@ public override string GenerateCode()
var verb = operations.Key.CapitalizeFirstCharacter();

GenerateInterfaceXmlDocComments(operation, code);

var interfaceName = GetInterfaceName(kv, verb, operation);
interfaceNames.Add(interfaceName);
code.AppendLine($$"""
{{GenerateInterfaceDeclaration(GetInterfaceName(kv, verb, operation))}}
{{GenerateInterfaceDeclaration(interfaceName)}}
{{Separator}}{
""");

Expand All @@ -61,7 +66,7 @@ public override string GenerateCode()
}
}

return code.ToString();
return new RefitGeneratedCode(code.ToString(), interfaceNames.ToArray());
}

private string GetInterfaceName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// This code was generated by Refitter.
// </auto-generated>


using Refit;
using System.Collections.Generic;
using System.Text.Json.Serialization;
Expand Down Expand Up @@ -539,4 +540,4 @@ public FileParameter(System.IO.Stream data, string fileName, string contentType)
#pragma warning restore 8073
#pragma warning restore 3016
#pragma warning restore 8603
#pragma warning restore 8604
#pragma warning restore 8604
Loading

0 comments on commit c0607de

Please sign in to comment.