Using composition over inheritance to create an OpenAPI client

Andy BlackledgeAndy Blackledge
8 min read

In the previous parts in this series, I discovered the OpenAPI package, used it to verify REST API payloads, and then used it to create a class that can use an OpenAPI specification to become a dynamic API client. At the end of the last part, I compared the resulting client with a static client generated by Visual Studio. I liked the strong-typing of the static approach, but didn't like the quantity of generated code I would have to own.

With this in mind, I wondered if I could create a client class that is a hybrid of the two. An example usage is shown below.

var client =
    await PetstoreHybridOpenApiClient.CreateAsync(
        new Uri("http://petstore.swagger.io"));

await client.AddPetAsync(new Pet { Name = "Luna" });

Pet pet = await client.GetPetByIdAsync(1);

ICollection<Pet> pets = await client.FindPetsByStatusAsync([Anonymous.Sold]);

As an additional challenge, I decided to try implementing it all without resorting to inheritance. As someone who has used object orientation for a long time, my first thought is often to start creating subclasses. However, I am aware of the option to use composition over inheritance. So I thought it would be interesting to try that and see how it felt.

Creating the constructors and factory methods

The dynamic client class that I developed has a factory method that is used as follows.

var client = await OpenApiClientV2.CreateAsync(openApiJson, domainUri);

This relies on the calling code reading the OpenAPI document into a JSON string and passing it to the method. For the hybrid client, I decided that I would use a convention instead. The OpenAPI document would be an embedded resource, with the same name as the client class, but with a .OpenAPI.json extension.

Properties of the OpenAPI document file

This meant that the factory method calling code would be simplified, with just the domain URI being required.

var client =
    await PetstoreHybridOpenApiClient.CreateAsync(
        new Uri("http://petstore.swagger.io"));

With this usage in mind, I created the skeleton for the PetstoreHybridClient class, wrapping an OpenApiClientV2 instance in a 'has-a' relationship.

public class PetstoreHybridClient
{
    private readonly OpenApiClientV2 _client;

    private PetstoreHybridClient(OpenApiClientV2 client)
    {
        _client = client;

    public static async Task<PetstoreHybridClient> CreateAsync(Uri domainUri)
    {
        // Instantiate an instance of PetstoreHybridClient...
    }
}

The next task was for the factory method to create and return an instance. To do this, it delegates to a method on a new static class HybridOpenApiClient. The method takes the domain and a function that receives an OpenApiClientV2 instance and instantiates a new class. In this case, the PetstoreHybridClient class.

public static async Task<PetstoreHybridClient> CreateAsync(Uri domainUri)
{
    return
        await HybridOpenApiClient.CreateAsync(
            domainUri, (client) => new PetstoreHybridClient(client));
}

The HybridOpenApiClient class was then implemented as follows. Note that, the createHybridClient function would not be necessary if C# type constraints could specify anything other that a default constructor.

public static class HybridOpenApiClient
{
    public static async Task<T> CreateAsync<T>(
        Uri domainUri, Func<OpenApiClientV2, T> createHybridClient) where T : class
    {
        var openApiJson = LoadOpenApiJsonForType<T>();

        var client = await OpenApiClientV2.CreateAsync(openApiJson, domainUri);

        return createHybridClient(client);
    }
}

I have omitted the details for the LoadOpenApiJsonForType, but the full code can be found on the accompanying GitHub repo.

Adding the API methods

public async Task AddPetAsync(
    Pet body)
{
    await _client.PerformAsync(
        "addPet",
        [
            ("body", JsonConvert.Serialize(body)),
        ]);
}

In the previous post in the series, I used Visual Studio to generate a client and a set of model classes. These model classes are annotated with attributes that implement the validation in the OpenAPI document. For example, the Name property on the Pet class is annotated as follows.

public partial class Pet
{
    // <snip>

    [Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Always)]
    [System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = true)]
    public string Name { get; set; }

    // <snip>
}

The problem with this was that the JsonConvert.Serialize(body) results in a JsonSerializationException, which I didn't want. I wanted all failures to be handled the same. However, on the other hand, I wanted to use the generated classes.

The solution was to create a pair of serialization helpers as follows:

private static readonly JsonSerializerSettings _serializerSettings =
    new()
    {
        MissingMemberHandling = MissingMemberHandling.Ignore,
        NullValueHandling = NullValueHandling.Ignore,
        Converters = [new StringEnumConverter()],
    };

public static string Serialize(object value)
{
    if (value == null) return null;

    var valueJson = JsonConvert.SerializeObject(value, _serializerSettings);

    var isJsonString = valueJson.StartsWith("\"");
    if (isJsonString) return valueJson.Trim('"');

    return valueJson;
}

public static T Deserialize<T>(JsonResponse response)
{
    return JsonConvert.DeserializeObject<T>(response.Payload, _serializerSettings);
}

These could then be used as follows in the hybrid client.

  public async Task<Pet> GetPetByIdAsync(
      long petId)
  {
      var response =
          await _client.PerformAsync(
              "getPetById",
              [
                  ("petId", HybridOpenApiClient.Serialize(petId)),
              ]);

      return HybridOpenApiClient.Deserialize<Pet>(response);
  }

Adding and overriding default behaviour

The OpenApiClientV2 class allows custom error handling. This is done by supplying an OnFailure function. For the hybrid client, I wanted the base implementation to throw an exception for any failure. To do this, I extended HybridOpenApiClient with the following code.

public static class HybridOpenApiClient
{
    public static async Task<T> CreateAsync<T>(
        Uri domainUri, Func<OpenApiClientV2, T> createHybridClient) where T : class
    {
        // <snip>

        client.OnFailure = OnFailure;

        return createHybridClient(client);
    }

    public static void OnFailure(
        string operationId,
        IEnumerable<(string, string)> parameters,
        JsonResponse response)
    {
        if (response.HttpStatusCode.HasValue)
        {
            throw new OpenApiException(
                $"{operationId} received {(int)response.HttpStatusCode}: " +
                    $"{string.Join(", ", response.FailureReasons)}",
                response.HttpStatusCode.Value);
        }

        throw new OpenApiException(
            $"{operationId} failed: " +
            $"{string.Join(", ", response.FailureReasons)}",
            response.Exception);
    }
}

So now, any failure results in a OpenApiException being thrown with a description of the failure reason. As an exercise, I wanted the specific implementation, PetstoreHybridClient, to also log the failure details.

This was possible by supplying a custom OnFailure implementation as follows.

private PetstoreHybridClient(OpenApiClientV2 client)
{
    _client = client;

    _client.OnFailure =
        (o, p, r) =>
            {
                LogFailure(o, r);
                HybridOpenApiClient.OnFailure(o, p, r);
            };
}

public static void LogFailure(string operationId, JsonResponse response)
{
    Console.WriteLine(
        $"{operationId} failed in {response.ElapsedMilliseconds}ms");
}

With this, the PetstoreHybridClient logs the failure, but retains the behaviour of throwing exceptions.

Comparison with a base client

For comparison, I also created an abstract class HybridOpenApiClientBase. Below is an abbreviated version of the resulting subclass.

public class PetstoreHybridClientSubclass : HybridOpenApiClientBase
{
    // Base overrides

    protected override void OnFailure(
        string operationId,
        IEnumerable<(string, string)> parameters,
        JsonResponse response)
    {
        LogFailure(operationId, response);
        base.OnFailure(operationId, parameters, response);
    }

    private static void LogFailure(string operationId, JsonResponse response)
    {
        // ... as before ...
    }

    // API methods as before...

    public async Task AddPetAsync(
        Pet body)
    {
        await Client.PerformAsync(
            "addPet",
            [
                ("body", Serialize(body)),
            ]);
    }
}

The result was less code than the composition-based version, as there was no need to implement a factory method. Another nice feature was that Visual Studio prompts for overrides, which provides guidance as to what can be customised.

Summary

It was an educational experience to explore creating the client class without using inheritance. In this case, I actually favoured the inheritance-based result. I guess, in this case, the relationship was more of an 'is-a' than a 'has-a'. What was interesting, was that the base class was built using the composition-based implementation. This does show the flexibility of composition.

What I definitely did learn, was that I should keep an open mind when designing classes and not immediately start thinking of class hierarchies. To start with composition, and then wait for the need for inheritance to emerge. As the Thoughtworks Composition vs. Inheritance: How to Choose? article says:

If you find that you are using a component to provide the vast majority of your functionality, creating forwarding methods on your class to call the component's methods, exposing the component's fields, etc., consider whether inheritance - for some or all of the desired behavior - might be more appropriate.

Which is echoed by the Code Maze article Composition vs Inheritance in C#:

So, we should use inheritance if:

  • There is an "is-a" relationship between classes (X is a Y)
  • The derived class can have all the functionality of the base class

For all other instances, the composition is the preferred choice.

  • Wikipedia - Composition over inheritance

    • Composition over inheritance (or composite reuse principle) in object-oriented programming (OOP) is the principle that classes should favor polymorphic behavior and code reuse by their composition (by containing instances of other classes that implement the desired functionality) over inheritance from a base or parent class.

    • To favor composition over inheritance is a design principle that gives the design higher flexibility. It is more natural to build business-domain classes out of various components than trying to find commonality between them and creating a family tree. In other words, it is better to compose what an object can do (has-a) than extend what it is (is-a).

    • One common drawback of using composition instead of inheritance is that methods being provided by individual components may have to be implemented in the derived type, even if they are only forwarding methods. In contrast, inheritance does not require all of the base class's methods to be re-implemented within the derived class.

      • C# provides default interface methods since version 8.0 which allows to define body to interface member.
  • Thoughtworks - Composition vs. Inheritance: How to Choose?

    • Composition is fairly easy to understand - we can see composition in everyday life: a chair has legs, a wall is composed of bricks and mortar, and so on. While the definition of inheritance is simple, it can become a complicated, tangled thing when used unwisely. Inheritance is more of an abstraction that we can only talk about, not touch directly. Though it is possible to mimic inheritance using composition in many situations, it is often unwieldy to do so. The purpose of composition is obvious: make wholes out of parts. The purpose of inheritance is a bit more complex because inheritance serves two purposes, semantics and mechanics.

    • Inheritance captures semantics (meaning) in a classification hierarchy (a taxonomy), arranging concepts from generalized to specialized, grouping related concepts in subtrees, and so on. Inheritance captures mechanics by encoding the representation of the data (fields) and behavior (methods) of a class and making it available for reuse and augmentation in subclasses. Mechanically, the subclass will inherit the implementation of the superclass and thus also its interface.

  • Code Maze - Composition vs Inheritance in C#

0
Subscribe to my newsletter

Read articles from Andy Blackledge directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andy Blackledge
Andy Blackledge