Search code examples
c#.net.net-core.net-standardrefit

How to properly post-process Refit return values?


I'm writing some APIs using Refit, which works wonders, and I'm having some trouble figuring out a good (as in "clean", "proper") way to perform some arbitrary processing on the returned data.

As an example, consider this code:

public interface ISomeService
{
    [Get("/someurl/{thing}.json")]
    Task<Data> GetThingAsync([AliasAs("thing")] string thing);
}

Now, a lot of REST APIs I've seen have the unfortunate habit of packing the actual data (as in "useful" data) deep into the JSON response. Say, the actual JSON has this structure:

{
    "a" = {
        "b" = {
            "data" = {
...
}

Now, typically I'd just map all the necessary models, which would allow Refit to correctly deserialize the response. This though makes the API a bit clunky to use, as every time I use it I have to do something like:

var response = await SomeService.GetThingAsync("foo");
var data = response.A.B.Data;

What I'm saying is that those two outer models are really just containers, that don't need to be exposed to the user. Or, say the Data property is a model that has another property that is an IEnumerable, I might very well just want to directly return that to the user.

I have no idea on how to do this without having to write useless wrapper classes for each service, where each one would also have to obviously repeat all the XML comments in the interfaces etc., resulting in even more useless code floating around.

I'd just like to have some simple, optional Func<T, TResult> equivalent that gets called on the result of a given Refit API, and does some modifications on the returned data before presenting it to the user.


Solution

  • I've found that a clean enough solution for this problem is to use extension methods to extend the Refit services. For instance, say I have a JSON mapping like this:

    public class Response
    {
        [JsonProperty("container")]
        public DataContainer Container { get; set; }
    }
    
    public class DataContainer
    {
        [JsonProperty("data")]
        public Data Data { get; set; }
    }
    
    public class Data
    {
        [JsonProperty("ids")]
        public IList<string> Ids { get; set; }
    }
    

    And then I have a Refit API like this instead:

    public interface ISomeService
    {
        [Get("/someurl/{thing}.json")]
        [EditorBrowsable(EditorBrowsableState.Never)]
        [Obsolete("use extension " + nameof(ISomeService) + "." + nameof(SomeServiceExtensions.GetThingAsync))]
        Task<Response> _GetThingAsync(string thing);
    }
    

    I can just define an extension method like this, and use this one instead of the API exposed by the Refit service:

    #pragma warning disable 612, 618
    
    public static class SomeServiceExtensions
    {
        public static Task<Data> GetThingAsync(this ISomeService service, string thing)
        {
            var response = await service._GetThingAsync(thing);
            return response.Container.Data.Ids;
        }
    }
    

    This way, whenever I call the GetThingAsync API, I'm actually using the extension method that can take care of all the additional deserialization for me.