8

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.

Sergio0694
  • 4,447
  • 3
  • 31
  • 58

3 Answers3

9

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.

Sergio0694
  • 4,447
  • 3
  • 31
  • 58
4

Summary

You can pass custom JsonConverters to Refit to modify how it serializes or deserializes various types.

Detail

The RefitSettings class provides customization options including JSON serializer settings.

Beware that the RefitSettings class has changed somewhat in the past few releases. You should consult the appropriate documentation for your version of Refit.

From Refit's latest examples

var myConverters = new List<JsonConverter>();
myConverters += new myCustomADotBConverter();

var myApi = RestService.For<IMyApi>("https://api.example.com",
    new RefitSettings {
        ContentSerializer = new JsonContentSerializer( 
            new JsonSerializerSettings {
                ContractResolver = new CamelCasePropertyNamesContractResolver(),
                Converters = myConverters
        }
    )});

Here's a basic example of a custom JsonConverter from the JSON.Net docs.

public class VersionConverter : JsonConverter<Version>
{
    public override void WriteJson(JsonWriter writer, Version value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString());
    }

    public override Version ReadJson(JsonReader reader, Type objectType, Version existingValue, bool hasExistingValue, JsonSerializer serializer)
    {
        string s = (string)reader.Value;

        return new Version(s);
    }
}

public class NuGetPackage
{
    public string PackageId { get; set; }
    public Version Version { get; set; }
}

That example JsonConverter is designed to serialize or deserialize the "Version" field of a JSON payload that looks like this:

{
  "PackageId": "Newtonsoft.Json",
  "Version": "10.0.4"
}

You would have to write your own custom JsonConverter for the nested data structure you would like deserialize.

Community
  • 1
  • 1
jeyoor
  • 938
  • 1
  • 11
  • 20
  • 2
    Hi, thank you for your reply, but this doesn't actually answer my quesiton. As I showed in my example, I don't need to customize how the default deserialization works, but to perform additional processing on the deserialized data, instead of directly returning it to the caller. Or, to perform custom deserialization on return types that are variable (eg. a JSON array with objects of different types). Neither of these two cases can be easily handled with a custom deserializer. – Sergio0694 Mar 17 '19 at 14:21
  • 1
    Thank you for providing feedback and posting the solution you found! I've upvoted your answer in hopes it will appear ahead of mine going forward. – jeyoor Mar 24 '19 at 23:58
1

If you're using C# 8.0 or higher, you have the option to take the approach recommended by Refit's website:

Refit website explanation

  1. Write some transformation code in a private method on the interface
  2. Change the name of your Get method to be something different, like _Get() or GetInternal() and make it internal so it's not visible to the calling code.
  3. Create a new public method that has the original Get() name, and then give that a body that applies the transformation before returning.
trademark
  • 565
  • 4
  • 21