Skip to content

LykkeBusinessPlatform/Lykke.HttpClientGenerator

 
 

Repository files navigation

Lykke.HttpClientGenerator

Helps to generate client proxy for an http api using the Refit library. It adds cache and retries using Polly to the generated proxies.

Nuget: https://www.nuget.org/packages/Lykke.HttpClientGenerator/

Quickstart guide

  1. Create a nuget package containing interface of your service with the data types used in it.
  2. In a consumer application add reference to the interface package and to this one.
  3. To create an instance of the client proxy use this code:
var generator = HttpClientGenerator.BuildForUrl(serviceUrl).Create();
var client = generator.Generate<IApi>();

This example creates a client with a linear retry strategy with default params (6 times with 5 sec pauses) and caching with parameters specified in method attributes. To add caching to a method of an interface:

[ClientCaching(Minutes = 1, Seconds = 30)]
[Get("/api/get-foo"]
Task<string> GetFoo();

String TimeSpan form is also supported: [ClientCaching("00:01:30")]

Applying this attribute to a non-get method does nothing by default.

Usually you should create a single instance of each proxy in your application and reuse it everywhere. It is completely thread-safe. Creating multiple instances can cause some problems.

Customizations

HttpClientGenerator.BuildForUrl() returns a HttpClientGeneratorBuilder object with fluent customisation api.

By default it adds:

  • an autogenerated User-Agent header (from application metadata);
  • retries with a linear policy: 6 times with a delay of 5 seconds;
  • the caching ability (to enable it add the ClientCachingAttribute to the method and specify the time).

Api-key header is not added by default. To add it - use the HttpClientGeneratorBuilder.WithApiKey() method.

Calls wrappers

Calls wrappers (or handlers) are executed around the api interface methods:

public interface ICallsWrapper
{
    Task<object> HandleMethodCall(MethodInfo targetMethod, object[] args, Func<Task<object>> innerHandler);
}

Example wrapper implementation:

public async Task<object> HandleMethodCall(MethodInfo targetMethod, object[] args, Func<Task<object>> innerHandler)
{
    Console.WriteLine($"{method.Name} before");
    var result = await inner();
    Console.WriteLine($"{method.Name} after");
    return result;
};

If the function do not call inner() - it effectively restricts the invocation of actual wrapped method. One can add try-catch here, execute inner code in a separate thread, add logging, caching or retries, etc. The wrappers are executed in the order they are added. Calling inner() in the first handler means calling the second one. Calling inner() in the last one - means calling the actial method.

To add your custom wrapper use the HttpClientGeneratorBuilder.WithAdditionalCallsWrapper() method. This is intended for advanced usecases.

HttpClientGenerator has multiple wrappers, one of them is added by default: CachingCallsWrapper.

ProblemDetailsExceptionHandlerCallsWrapper

This wrapper is intended to deal with the ProblemDetails exceptions (RFC 7807) and map API error codes into domain errors. In order to have it worked, one should add a mapper delegate while initializing the wrapper:

object Mapper(string apiErrorCode)
{
    switch (apiErrorCode)
    {
        case "FooError":
            return "DomainFooError";
        case "BarError":
            return "DomainBarError";
        default:
            return null;
    }
}

var generator = HttpClientGenerator.BuildForUrl(serviceUrl)
    .WithAdditionalCallsWrapper(new ProblemDetailsExceptionHandlerCallsWrapper(Mapper))
    .Create();

The wrapper will catch the ValidationApiException, enriches and rethrows it.

Here is an example of client code:

try
{
    await client.GetFoo();
}
catch (ValidationApiException ex)
{
    var errorCode = ex.GetDomainErrorCode();
    
    switch (errorCode)
    {
        case "DomainFooError":
            return BadRequest(new { Message = "Foo error" });   
        case "DomainBarError":
            return BadRequest(new { Message = "Bar error" });
    }
}

The one can use typed errors instead of strings:

enum DomainErrorCodes
{
    FooError,
    BarError
}

object TypedMapper(string apiErrorCode)
{
    switch (apiErrorCode)
    {
        case "FooError":
            return DomainErrorCodes.FooError;
        case "BarError":
            return DomainErrorCodes.BarError;
        default:
            return null;
    }
}

Here is an example of client code:

try
{
    await client.GetFoo();
}
catch (ValidationApiException ex)
{
    var errorCode = ex.GetDomainError<DomainErrorCodes>();
    
    switch (errorCode)
    {
        case DomainErrorCodes.FooError:
            return BadRequest(new { Message = "Foo error" });   
        case DomainErrorCodes.BarError:
            return BadRequest(new { Message = "Bar error" });
    }
}

Caching customization

Constructor of CachingCallsWrapper accepts a ICachingStrategy object. There is one default strategy: AttributeBasedCachingStrategy. Implement the ICachingStrategy interface and pass it to the HttpClientGeneratorBuilder.WithCachingStrategy() method to specify your own behavior.

To disable caching call the HttpClientGeneratorBuilder.WithoutCaching() method.

Http DelegatingHandlers

Other, more performant, way of adding custom logic to requests is to implement the DelegatingHandler abstract class.

DelegatingHandlers do not wrap the Refit logic of transforming a method call to an http request, instead they wrap the actual http request logic.

You can add several DelegatingHandlers. They are executed in the order they are added. The last one will call an HttpClientHandler which actually makes the request.

Example:

var generator = HttpClientGenerator.BuildForUrl(serviceUrl)
.WithAdditionalDelegatingHandler(new MySpecialHandler())
.Create();
public class MySpecialHandler : DelegatingHandler
{    
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        Console.WriteLine("before");
        await base.SendAsync(request, cancellationToken);
        Console.WriteLine("after");
    }
}

There are some default handlers in the library:

  • ApiKeyHeaderHttpClientHandler (added if HttpClientGeneratorBuilder.WithApiKey() is called)
  • UserAgentHeaderHttpClientHandler (added by default)
  • RetryingHttpClientHandler (added by default)

Retries customization

The RetryingHttpClientHandler constructor accepts an IRetryStrategy:

public interface IRetryStrategy
{
    TimeSpan GetRetrySleepDuration(int retryAttempt, string url);
    int RetryAttemptsCount { get; }
}

There are 2 default configurable implementations:

  • LinearRetryStrategy
  • ExponentialRetryStrategy

You can customize the strategy used via the HttpClientGeneratorBuilder.WithRetriesStrategy() method.

To disable retries call the HttpClientGeneratorBuilder.WithoutRetries() method.

Implementing a more sophisticated retries logic may require creating a separate DelegatingHandler implementation.

About

Helps to generate client proxy for an api using the Refit library. It adds cache and retries using Polly to the generated proxies.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • C# 100.0%