Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 735
Sergio0694 Avatar asked Jan 14 '19 14:01

Sergio0694


People also ask

How do I return a value from a procedure?

A procedure can return an integer value called a return code to indicate the execution status of a procedure. You specify the return code for a procedure using the RETURN statement. As with OUTPUT parameters, you must save the return code in a variable when the procedure is executed in order to use the return code value in the calling program.

How to change the return value of the getdepartmentinfo procedure?

The first result set shows the result set of the select statement inside the code of the GetDepartmentInfo procedure. The second result set displays the stored procedure return value. We can change the return value inside the procedure code using the RETURN command.

What does refit actually do?

Among the many things Refit does for you is: Construct the infrastructure to make HTTP requests and receive the responses URI encoding of parameters Serialization of responses into typed objects Under the hood it generates some code as part of the build process.

Is it OK to return something in a post/put response body?

It is OK (and most likely desirable) to return something in a POST/PUT response body. How this is done is application-specific and almost impossible to generalize. You do not want to return large-size "context" by default (traffic-noise that defeats the whole reason of moving away from POST-followed-by-GETs.)


2 Answers

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.

like image 81
Sergio0694 Avatar answered Oct 07 '22 23:10

Sergio0694


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.

like image 23
jeyoor Avatar answered Oct 08 '22 00:10

jeyoor