4

I am trying to find a way to use a HttpClient using IHttpClientFactory.CreateClient([name]) where [name] is an HttpClient that was configured using Polly in the Startup class using PolicyRegistry. I want that policy to apply for each of the urls that i make calls using that client.

So if I create a client called "myclient" and throughout my application I will make calls to N different urls , using that named client, I want to have the policy applied for each url individually , not as a whole.

Startup

PolicyRegistry registry = new PolicyRegistry();
AsyncPolicyWrap type1Policy = //somePolicy;
registry.Add("type1",type1Policy);
services.AddHttpClient("someClient").AddPolicyHandlerFromRegistry("type1");

Some Consumer Service

public class SomeService
{
    private IHttpClientFactory factory;
    public SomeService(IHttpClientFactory factory)
    {
        this.factory=factory;
    }
    public void SomeCall(string url)
    {
        var client=factory.GetClient("someClient");
        client.SendAsync(...);
    }
}

In the above case I want the policy set up in the startup class to be taken individually for each of the urls that I am calling.

Is that possible ?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Bercovici Adrian
  • 8,794
  • 17
  • 73
  • 152

1 Answers1

2

In order to achieve that you need to register your HttpClient and your PolicyRegistry separately:

services.AddHttpClient("someClient");
services.AddPolicyRegistry(registry);

Then you can ask the DI to provide that registry for you:

public class SomeService
{
    private readonly IHttpClientFactory factory;
    private readonly IReadOnlyPolicyRegistry<string> policyRegistry;

    public SomeService(IHttpClientFactory factory, IReadOnlyPolicyRegistry<string> policyRegistry)
    {
        this.factory = factory;
        this.policyRegistry = policyRegistry;
    }
    ...
}

Finally inside your SomeCall you can retrieve the policy:

public async Task SomeCall(string url)
{
     var client = factory.GetClient("someClient");
     var policy = policyRegistry.Get<IAsyncPolicy>("type1");

     await policy.ExecuteAsync(async ct => await client.SendAsync(..., ct), CancellationToken.None);
}

UPDATE #1: Create new policy per url

In functional programming there is a well-known technique, called Memoization. It basically memorizes / caches the output of a method based on the received parameters. In other words if the method receives the same input then it emits the result from a cache rather than re-executing it.

This technique works fine for pure functions. They return the same value if they receive the exact same input in a side effect free way.

So, if we can have a solution which

  • creates a new policy if the url is new
  • or returns a cached policy if the url is an "already seen" one then we are good to go.

Here is a simple implementation, but you can find a lots of variations like 1, 2, 3, etc.

public interface IMemoizer<K, V> where K : IComparable
{
    Func<K, V> Memoize(Func<K, V> toBeMemoized);
}
public sealed class Memoizer<K, V> : IMemoizer<K, V> where K: IComparable
{
    public static readonly ConcurrentDictionary<K, Lazy<V>> memoizedValues;
    static Memoizer()
    {
        memoizedValues = new ConcurrentDictionary<K, Lazy<V>>();
    }

    public Func<K, V> Memoize(Func<K, V> toBeMemoized)
        => (K input) => memoizedValues.GetOrAdd(input, (K param) => new Lazy<V>(() => toBeMemoized(param))).Value;
}

Here is a sample for usage:

static void Main()
{
    var memoizer = new Memoizer<string, IAsyncPolicy<HttpResponseMessage>>();
    var policyMemoizer = memoizer.Memoize(GetSamplePolicy);

    var gPolicy1 = policyMemoizer("https://google.com");
    var soPolicy = policyMemoizer("https://stackoverflow.com");
    var gPolicy2 = policyMemoizer("https://google.com");

    Console.WriteLine(gPolicy1 == gPolicy2); //true
    Console.WriteLine(gPolicy1 == soPolicy); //false
    
    var policyMemoizer2 = memoizer.Memoize(GetSamplePolicy);
    var gPolicy3 = policyMemoizer2("https://google.com");

    Console.WriteLine(gPolicy1 == gPolicy3); //true
}

static IAsyncPolicy<HttpResponseMessage> GetSamplePolicy(string _)
        => Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(2));

You can register the memoizer as a singleton and you are ready to use it. :D

I hope it helps you to achieve the desired behaviour.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • But in this case , wont there be a global policy for type1 ? Wont my policy of `type1` aggregate the failures for all urls ? I do not want one policy instance for all of my url that use that type of policy. I want an instance of the policy for each url . So for `N` urls i would want `N` instances of `type1` policy. I do not want failures to be aggregated and counted in one instance of the policy for all urls. Hope its clear. – Bercovici Adrian Dec 14 '21 at 18:03
  • 1
    @BercoviciAdrian Ohh, I see. Do you know the urls upfront? For example you are using this httpclient against a handful of downstream services. If so you can register all policies upfront into the registry where the key can be url. If you don't know in advance then that's a different story. If that is your case please let me know and tomorrow I'll revise my proposed solution. – Peter Csala Dec 14 '21 at 20:35
  • Well currently the urls are not dynamic but in the future that is the plan. The scenario is the following : in my service that uses that registry and clientfactory , i receive a request that contains the url to which it should post. Therefore i need to select the desired policy and the desired httpclient in my service , but the policy should be applied for each unique url. So in my service its like : "give me a policy of type X " that i will apply for this `url` that i received as argument. – Bercovici Adrian Dec 15 '21 at 07:11
  • @BercoviciAdrian I've extended my post, please check it. – Peter Csala Dec 15 '21 at 18:52
  • Hello , thanks alot for the provided solution. I have used memoizers before and indeed here it looks like the solution to go. However i was wondering if there is something like this built in Polly directly so that i do not have to interact with this caching layer. My program currently is distributed and uses `Microsoft.Orleans` which is a shared nothing framework based on actors. I wanted to avoid at all costs adding synchronization primitives or concurrent collections to my code. – Bercovici Adrian Dec 15 '21 at 18:59
  • There is no option embedded inside the `Polly` framework that would be able degrevate you the user from this dynamic caching of policies (in my case based on the url) ? – Bercovici Adrian Dec 15 '21 at 19:20
  • 1
    @BercoviciAdrian Polly tries to provide a resilience library where the policy declaration and its application are separated. It tries to be generic enough to support all kinds of application models (like console, background worker, webapi, etc). It tries to provide primivites that can be combined to achieve more complex things. But because of these design decisions it has a couple of limitations that requires some tricks / combination with other tools to achieve something specific. So, in short no I'm not aware of any built-in solution for this :( – Peter Csala Dec 15 '21 at 20:31
  • what are policies? Will you explain them and how to setup them up – Golden Lion Dec 15 '21 at 20:46
  • @GoldenLion Policies are Polly primivites. Whenever you define a policy you specify under what circumstance what sort of resilient behaviour are you expected. For example in case of HttpRequestException retry at most 3times with 2 seconds delay between each probe. Or in case of return value -1 execute another method as a fallback. Did it give you clarity? – Peter Csala Dec 15 '21 at 21:59
  • @BercoviciAdrian I was thinking about your *built-in solution* question and it just come into my mind that Polly does have a [Cache policy](https://github.com/App-vNext/Polly/wiki/Cache) (which supports in memory and distributed cache providers). You might be able to use that but it feels a bit clumsy, something like this: `cachePolicy.ExecuteAsync(ctx => getUrlAwarePolicy(), new Context(url));` – Peter Csala Dec 17 '21 at 16:25
  • Thanks for posting. What i figured out i needed in the end was a way to add new `HttpClient`(s) at runtime. Is there a way to alter the `IServiceCollection` initialization ? – Bercovici Adrian Dec 20 '21 at 10:50
  • @BercoviciAdrian IServiceCollection does expose Add and TryAdd methods. Whereas IServiceProvider represent the "compiled" / assembled collection, so it provides API for retrieval. After you extended the Collection then you need to call the BuildServiceProvider to be able to retrieve the newly added instances. – Peter Csala Dec 20 '21 at 12:30
  • IServiceCollection does expose add operations but it seems it cannot be retrived after initialization. – Bercovici Adrian Dec 20 '21 at 13:00
  • @BercoviciAdrian Do you mean that the IServiceCollection can not be retrieved after Startup? – Peter Csala Dec 20 '21 at 13:35
  • Yes it seems like so. – Bercovici Adrian Dec 20 '21 at 14:04
  • @BercoviciAdrian Yes indeed, you can't retrieve that after initialization by design. If you register an XYZFactory class, then you can postpone the creation of a particular object. Depending on the Factory implementation it might utilize some caching mechanism. – Peter Csala Dec 20 '21 at 14:17