So I'm building a client for a third API and I want to be able to Get() and GetMany() for each type this controller can provide.

So I've built this adapter, that provides generically typed methods for these.

public interface IClientAdapter<T>
    public Task<T?> Get(string predicates = "");
    public Task<List<T>?> GetMany(string predicates = "");

And this is the primary interface of the actual client.

public interface IBasicSpaceDataClient
    public IClientAdapter<Announcement> Announcements { get; }
    public IClientAdapter<BoxScore> BoxScores { get; }
    public IClientAdapter<Conjunction> Conjunctions { get; }
    public IClientAdapter<Decay> Decays { get; }
    public IClientAdapter<GeneralPerturbation> GeneralPerturbations { get; }
    public IClientAdapter<GeneralPerturbation> GeneralPerturbationHistory { get; }
    public IClientAdapter<LaunchSite> LaunchSites { get; } 
    public IClientAdapter<SatCatChange> SatCatChanges { get; }
    public IClientAdapter<SatCatEntry> SatCatEntries { get; }
    public IClientAdapter<SatCatEntry> SatCatDebuts { get; }
    public IClientAdapter<TrackingAndImpactPrediction> TrackingAndImpactPredictions { get; }

This means that you can do:

List<Announcement> announcements = await myClient.Announcements.GetMany();

Which I think is quite nice.

In terms of actual implementation, the adapter looks like this:

internal class HttpClientAdapter<T>: IClientAdapter<T>
    private readonly HttpClient _client;
    private readonly string _endpoint;

    public HttpClientAdapter(HttpClient client, string endpoint)
        _client = client;
        _endpoint = endpoint;

    public async Task<T?> Get(string predicates)
        T[]? arrayOfOne = await _client.GetFromJsonAsync<T[]>($"{_endpoint}/limit/1/{predicates}");
        return arrayOfOne is null ? default : arrayOfOne.Single();
    public async Task<List<T>?> GetMany(string predicates) => await _client.GetFromJsonAsync<List<T>>($"{_endpoint}/{predicates}");

And all of this works quite well.

The thing I'm not sure about creeps in in the BasicSpaceData concrete client constructor.

public BasicSpaceDataClient(HttpClient client) {
    Announcements = new HttpClientAdapter<Announcement>(client, $"{BasicSpaceDataEndpoint}announcement");
    BoxScores = new HttpClientAdapter<BoxScore>(client, $"{BasicSpaceDataEndpoint}boxscore");
    Conjunctions = new HttpClientAdapter<Conjunction>(client, $"{BasicSpaceDataEndpoint}cdm_public");
    Decays = new HttpClientAdapter<Decay>(client, $"{BasicSpaceDataEndpoint}decay");
    GeneralPerturbations = new HttpClientAdapter<GeneralPerturbation>(client, $"{BasicSpaceDataEndpoint}gp");
    GeneralPerturbationHistory = new HttpClientAdapter<GeneralPerturbation>(client, $"{BasicSpaceDataEndpoint}gp_history");
    LaunchSites = new HttpClientAdapter<LaunchSite>(client, $"{BasicSpaceDataEndpoint}launch_site");
    SatCatChanges = new HttpClientAdapter<SatCatChange>(client, $"{BasicSpaceDataEndpoint}satcat_change");
    SatCatEntries = new HttpClientAdapter<SatCatEntry>(client, $"{BasicSpaceDataEndpoint}satcat");
    SatCatDebuts = new HttpClientAdapter<SatCatEntry>(client, $"{BasicSpaceDataEndpoint}satcat_debut");
    TrackingAndImpactPredictions = new HttpClientAdapter<TrackingAndImpactPrediction>(client, $"{BasicSpaceDataEndpoint}tip");

Currently, all adapters share one client (which I think makes sense given they're all pointing at one controller server-side) and they're all newed up in the client constructor.

It's that second part I'm not so sure about. On the one hand, they're very simple objects and all the functionality comes through the injected HttpClient. On the other hand, this sort of thing always makes me think about whether they'd be better off injected. Usually I'd opt with the latter, but in this case, I'm wondering if there's actually any benefit to this and all it would do would be increase the complexity of my DI setup.

What are people's thoughts on this?

1 Answer 1


My thoughts are...

  1. As soon as one end point is different in some way you are going to lose your savings in terms of LoC

  2. Given a client class for some API, I would automatically assume that all the methods are endpoints of that same API. If I have to inject each resource client though.. It raises the question of whether they all point at different APIs. If I call AddAddress("Glasgow") can I later call GetCity("Glasgow")?

So. No, I wouldn't inject the resource clients. Plus! Unless there were many resources with the exact same pattern of endpoint for some reason, I would be having resource clients with the methods hardcoded, rather than using a generic.

My worry would be that having made this generic (in both senses) approach, I would hit different resource endpoints or requirements that wouldn't match the pattern and be beguiled into further complicating the generic case to cope with these rather than give up on it and do special cases.

The danger would be in ending up with an overly complex solution with more LoC than the simple, hardcode every endpoint, one.

  • Thanks for your answer, thankfully, the API should be extremely stable as it's owned by the USSC and used by commercial space operators, so they're not going to be making a lot of changes Jan 2, 2023 at 20:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.