20

I am using Refit to call an API using a Typed Client in asp.net core 2.2 which is currently bootstrapped using a single BaseAddress from our configuration Options:

services.AddRefitClient<IMyApi>()
        .ConfigureHttpClient(c => { c.BaseAddress = new Uri(myApiOptions.BaseAddress);})
        .ConfigurePrimaryHttpMessageHandler(() => NoSslValidationHandler)
        .AddPolicyHandler(pollyOptions);

In our Configuration json:

"MyApiOptions": {
    "BaseAddress": "https://server1.domain.com",
}

In our IMyApi interface:

public IMyAPi interface {
        [Get("/api/v1/question/")]
        Task<IEnumerable<QuestionResponse>> GetQuestionsAsync([AliasAs("document_type")]string projectId);
}

Example Current Service:

public class MyProject {
     private IMyApi _myApi;
     public MyProject (IMyApi myApi) {
        _myApi = myApi;
     }

    public Response DoSomething(string projectId) {
        return _myApi.GetQuestionsAsync(projectId);
    }
}

I now have the requirement to use different BaseAddresses based on data at runtime. My understanding is that Refit adds a single Instance of the HttpClient into DI and so switching BaseAddresses at runtime won't directly work in a multithreaded app. Right now it's really simple to inject an instance of IMyApi and call the interface method GetQuestionsAsync. At that point it is too late to set the BaseAddress. If I have multiple BaseAddresses, is there an easy way to dynamically select one?

Example config:

    "MyApiOptions": {
        "BaseAddresses": {
            "BaseAddress1": "https://server1.domain.com",
            "BaseAddress2": "https://server2.domain.com"
        }
}

Example Future Service:

public class MyProject {
     private IMyApi _myApi;
     public MyProject (IMyApi myApi) {
        _myApi = myApi;
     }

    public Response DoSomething(string projectId) {
        string baseUrl = SomeOtherService.GetBaseUrlByProjectId(projectId);

        return _myApi.UseBaseUrl(baseUrl).GetQuestionsAsync(projectId);
    }
}

UPDATE Based on the accepted answer I ended up with the following:

public class RefitHttpClientFactory<T> : IRefitHttpClientFactory<T>
{
    private readonly IHttpClientFactory _clientFactory;

    public RefitHttpClientFactory(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public T CreateClient(string baseAddressKey)
    {
        var client = _clientFactory.CreateClient(baseAddressKey);

        return RestService.For<T>(client);
    }
}
sarin
  • 5,227
  • 3
  • 34
  • 63

2 Answers2

9

Inject a ClientFactory instead of a client:

public class ClientFactory
{
    public IMyApi CreateClient(string url) => RestService.For<IMyApi>(url);
}

public class MyProject {
     private ClientFactory _factory;
     public MyProject (ClientFactory factory) {
        _factory = factory;
     }

    public Response DoSomething(string projectId) {
        string baseUrl = SomeOtherService.GetBaseUrlByProjectId(projectId);
        var client = _factory.CreateClient(baseUrl);

        return client.GetQuestionsAsync(projectId);
    }
}
Rik
  • 28,507
  • 14
  • 48
  • 67
  • Thanks. 2 questions. 1) how does this impact the startup bootstrap i.e. do I just remove the .ConfigureHttpClient() call? 2) Perhaps I should have said this in the question, but there will be many functions like DoSomething() calling various endpoints on IMyApi. Is there a way to refactor the Client Creation out of MyProject and cleverly inject it in, otherwise I'm going to get duplication of the first two lines in DoSomething() across each of those methods? – sarin Oct 29 '19 at 17:36
  • I'm not sure about the bootstrapping, it depends on the DI approach you're using. One possibility is to drop the bootstrapping completely and registering the `ClientFactory` in you DI container – Rik Oct 30 '19 at 14:43
  • You can change the factory's `Create` method to `CreateForProject(string projectId)` and look up the URL in that method. – Rik Oct 30 '19 at 14:48
  • 2
    One thing I noticed is RestService.For(url) creates a New HttpClient on each request. My preference is to use a static instance for each baseAddress to reduce memory/cpu/threads etc. I've noticed that the IHttpClientFactory can add Named instances, but then unsure how this would then work... – sarin Oct 30 '19 at 14:58
  • @sarin Did you solve problem with creation of new HttpClients for each instance taken from ClientFactory? – Hooch Jan 15 '20 at 10:42
  • @Hooch and sarin - Were you able to use the static instance of HttpClient? I am also in need of using dynamic base address as the OP. I thought I found a solution when I came across this SO question but not using the static httpclient is giving me a pause. – Stack Undefined May 22 '22 at 05:27
  • @StackUndefined - I ended up using the factory method. It works much better. For me, base URL changed based on request scope variable. So I created factory for Refit Clients that was aware of this variable. This way, the class that depended on Refit clients wasn't aware of the fact that HttpClients were constructed strangely. Only the factory was aware of this. This preserved separation of concerns as only the factory knew how and why to create new HttpClients and users just got it. – Hooch May 23 '22 at 07:06
1

Another option is to assign a fake base address in the configs and then override it using a DelegatingHandler

services
    .AddRefitClient<IMyApi>()
    .ConfigureHttpClient((sp, c) => c.BaseAddress = new Uri(MyDelegatingHandler.FakeBaseAddress))
    .AddHttpMessageHandler<MyDelegatingHandler>()
public class MyDelegatingHandler : DelegatingHandler
{
    public const string FakeBaseAddress = "http://fake.to.replace";

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var relativeUrl = request.RequestUri!.AbsoluteUri.Substring(FakeBaseAddress.Length);
        var newUri = Combine(GetDynamicBaseAddress(), relativeUrl);
        request.RequestUri = new Uri(newUri);
        return await base.SendAsync(request, cancellationToken);
    }

    private static string Combine(string baseUrl, string relativeUrl) =>
        return $"{baseUrl.TrimEnd('/')}/{relativeUrl.TrimStart('/')}";
}