When we need to tell NSwag and the generated OpenAPI 3.0 document that an abstract class should be converted to some concrete type on the client side, I use [KnownType] with a discriminator and that works really well:
using Newtonsoft.Json;
using NJsonSchema.Converters;
namespace Xyz.common.objects
{
[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[KnownType(typeof(DeviceTemplate))]
[KnownType(typeof(PersonelTemplate))]
public abstract class AbstractResourceTemplate
{
}
}
The problem I've run into is that [KnownType] cannot be used on an interface. So this fails to compile:
using Newtonsoft.Json;
using NJsonSchema.Converters;
namespace Xyz.common.objects
{
[JsonConverter(typeof(JsonInheritanceConverter), "discriminator")]
[KnownType(typeof(DeviceTemplate))] // Can't put KnownType on interfaces
[KnownType(typeof(PersonelTemplate))]
public interface IResourceTemplate
{
}
}
Is there equivalent code that can be placed on the interface to let NSwag know what the discriminating objects should be?
For context, this is how the interface is used:
public class DeviceTemplateReponse {
// In this class, Template is always going to be a DeviceTemplate, if that helps.
[WhatGoesHere???(typeof(DeviceTemplate))]
public IResourceTemplate Template { get; set; }
}
I imagine there is a way to inform NSwag that Template is always going to be the concrete type DeviceTemplate when in DeviceTemplateReponse, so don't use IResourceTemplate in the OpenAPI docuement. This is important because otherwise I get this error on the client side:
The abstract class 'IResourceTemplate' cannot be instantiated.
Also, it seems like a TypeMapper would help here but I'm not able to find documentation or examples on how something like that would be implemented here.
Some workarounds I found is to convert the interfaces to abstract classes, or to write my own versions of objects that only use concrete classes. But I'd really rather not be messing with code like that if I don't have to.
The problem with interfaces is that a single type can implement multiple interfaces, but the type itself will still implement a single common base. What is more, we can explicitly overload interface properties and expose multiple values for the same property.
At the client side, if we allowed this, what are the default values for the fields that are not defined in the interface? Is the serializer supposed to allow all metadata through other end for the base type, or should it be masked and restricted to only the properties that are defined on the interface. When a server side type has explicit properties for the interface, that have different values, which value do you want to send to the client, which one should be sent?
In NSwag, polymorphism is simplified and managed through direct inheritance, not through composition of interfaces. See NJsonSchema - Inheritance and the associated PRs, internally only direct base types are inspected in the current implementation, so your attributes on the interface definition are ignored completely.
It hasn't come up often for me, out of habit I try to map all my API endpoints to specific DTOs for that resource and have different endpoints for different structures if they are needed, any aggregate endpoints deliberately use direct inheritance mainly because that is a requirement of other frameworks I use like EF and OData.
When it does come up, I just create an abstract class that implements the interface, use it for this endpoint then in the logic we can still cast it back, it then becomes a very minimal intervention.
Is this annoying, well yes, when it comes up... But you can use implicit type casting or an explicit helper method or tools like AutoMapper is it comes up enough to simplify setting your logical instances into the
Templateproperty of the response, should be very minimal overheads to your codebase and you only write this once.