Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSwag: How do you Use Custom Value Object Types in C# -> Swagger -> C# client?

I have an API that uses Noda Time types in both input and output. The types are serialized to strings in the JSON using the default Noda Time serialization format (which basically is the ISO-8601 format).

I have an object looking something like this:

public class NodaTimeDataStructure
{
    public System.DateTime DateTime { get; set; }
    public DateInterval DateInterval { get; set; }
    public DateTimeZone DateTimeZone { get; set; }
    public Duration Duration { get; set; }
    public Instant Instant { get; set; }
    public Interval Interval { get; set; }
    public IsoDayOfWeek IsoDayOfWeek { get; set; }
    public LocalDate LocalDate { get; set; }
    public LocalDateTime LocalDateTime { get; set; }
    public LocalTime LocalTime { get; set; }
    public Offset Offset { get; set; }
    public OffsetDate OffsetDate { get; set; }
    public OffsetDateTime OffsetDateTime { get; set; }
    public OffsetTime OffsetTime { get; set; }
    public Period Period { get; set; }
    public ZonedDateTime ZonedDateTime { get; set; }
}

This will normally result in the following Swagger JSON:

"NodaTimeDataStructure": {
  "type": "object",
  "additionalProperties": false,
  "required": [
    "dateTime", "duration", "instant", "interval", "isoDayOfWeek", "localDate", "localDateTime",
    "localTime", "offset", "offsetDate", "offsetDateTime", "offsetTime", "zonedDateTime"
  ],
  "properties": {
    "dateTime":       { "type": "string", "format": "date-time" },
    "instant":        { "type": "string", "format": "date-time" },
    "zonedDateTime":  { "type": "string", "format": "date-time" },
    "offsetDateTime": { "type": "string", "format": "date-time" },
    "localDateTime":  { "type": "string", "format": "date-time" },
    "localDate":      { "type": "string", "format": "date" },
    "localTime":      { "type": "string", "format": "time" },
    "duration":       { "type": "string", "format": "time-span" },
    "dateInterval":   { "type": "array", "items": { "type": "string", "format": "date" } },
    "dateTimeZone":   { "$ref": "#/definitions/DateTimeZone" },
    "interval":       { "$ref": "#/definitions/Interval" },
    "isoDayOfWeek":   { "$ref": "#/definitions/IsoDayOfWeek" },
    "offset":         { "$ref": "#/definitions/Offset" },
    "offsetDate":     { "$ref": "#/definitions/OffsetDate" },
    "offsetTime":     { "$ref": "#/definitions/OffsetTime" },
    "period":         { "$ref": "#/definitions/Period" }
  }
}

This makes it impossible to convert back to the right Noda Time types in a C# client. Apart from the many different types having the exact same format ("date-time") making a mapping impossible, certain types have unfortunate definitions. A DateInterval results in an array of "date", since it's an enumerable of LocalDate, but a simple start/end date format would work much better. Other methods are created with a $ref to very elaborate objects containing fields of absolutely no interest. Be aware that all of these should be serialized as simple strings (arguably not the intervals).

I am able to create my own Type Mappers and adding them to a AspNetCoreToSwaggerGeneratorSettings like this:

var nodaTimeTypeMappers = new[]
{
    CreateTypeMapper(typeof(DateInterval), "date-interval"),
    CreateTypeMapper(typeof(DateTimeZone), "date-time-zone"),
    CreateTypeMapper(typeof(Duration), "duration"),
    CreateTypeMapper(typeof(Instant), "instant"),
    CreateTypeMapper(typeof(Interval), "interval"),
    CreateTypeMapper(typeof(IsoDayOfWeek), "iso-day-of-week"),
    CreateTypeMapper(typeof(LocalDate), "local-date"),
    CreateTypeMapper(typeof(LocalDateTime), "local-date-time"),
    CreateTypeMapper(typeof(LocalTime), "local-time"),
    CreateTypeMapper(typeof(Offset), "offset"),
    CreateTypeMapper(typeof(OffsetDate), "offset-date"),
    CreateTypeMapper(typeof(OffsetDateTime), "offset-date-time"),
    CreateTypeMapper(typeof(OffsetTime), "offset-time"),
    CreateTypeMapper(typeof(Period), "period"),
    CreateTypeMapper(typeof(ZonedDateTime), "zoned-date-time"),
};

foreach (var typeMapper in nodaTimeTypeMappers)
{
    settings.TypeMappers.Add(typeMapper);
}

PrimitiveTypeMapper CreateTypeMapper(Type type, string name)
{
    return new PrimitiveTypeMapper(type, s =>
    {
        s.Type = JsonObjectType.String;
        s.Format = "noda-time-" + name;
    });
}

to get something like this:

"NodaTimeRequest": {
  "type": "object",
  "additionalProperties": false,
  "required": [
    "dateTime", "duration", "instant", "interval", "isoDayOfWeek", "localDate", "localDateTime",
    "localTime", "offset", "offsetDate", "offsetDateTime", "offsetTime", "zonedDateTime"
  ],
  "properties": {
    "dateTime":       { "type": "string", "format": "date-time" },
    "dateInterval":   { "type": "string", "format": "noda-time-date-interval" },
    "dateTimeZone":   { "type": "string", "format": "noda-time-date-time-zone" },
    "duration":       { "type": "string", "format": "noda-time-duration" },
    "instant":        { "type": "string", "format": "noda-time-instant" },
    "interval":       { "type": "string", "format": "noda-time-interval" },
    "isoDayOfWeek":   { "type": "string", "format": "noda-time-iso-day-of-week" },
    "localDate":      { "type": "string", "format": "noda-time-local-date" },
    "localDateTime":  { "type": "string", "format": "noda-time-local-date-time" },
    "localTime":      { "type": "string", "format": "noda-time-local-time" },
    "offset":         { "type": "string", "format": "noda-time-offset" },
    "offsetDate":     { "type": "string", "format": "noda-time-offset-date" },
    "offsetDateTime": { "type": "string", "format": "noda-time-offset-date-time" },
    "offsetTime":     { "type": "string", "format": "noda-time-offset-time" },
    "period":         { "type": "string", "format": "noda-time-period" },
    "zonedDateTime":  { "type": "string", "format": "noda-time-zoned-date-time" }
  }
}

This allows the formats to be used just like the existing formats ("date-time", "date", "time", "time-span"), but I can't for the love of God figure out how to make the swagger2csclient use those formats to properly convert back to the corresponding Noda Time types. Am I generally missing something or is this currently not possible?

like image 457
Mikkel R. Lund Avatar asked Sep 19 '19 08:09

Mikkel R. Lund


1 Answers

I don't have a solution for the Swagger json problem, but I can help with the C# client generation part.

Instead of generating the client from NSwag json, what we will do is have NSwagStudio generate the client using reflection. I'm using "Web API via reflection" with Runtime set to "Default":

enter image description here

This generator "uses .NET reflection to analyze ASP.NET Web API or ASP.NET Core controllers". Your mileage may vary of course - there is also a ".NET assembly" option and/or you might need to set the Runtime explicitly.

On the right hand pane, click "CSharp Client" and switch to the "CSharp Client" tab:

enter image description here

The first serving of secret sauce is visible in the above screenshot: we add NodaTime as an additional namespace.

Further down, we need to have NSwagStudio generate DTO classes and - here's the really important thing - not generate any of the NodaTime types by adding them to the "Excluded Type Names" list:

enter image description here

The type exclusion string I used is: DateInterval,DateTimeZone,Duration,Instant,Interval,IsoDayOfWeek,LocalDate,LocalDateTime,LocalTime,Offset,OffsetDate,OffsetDateTime,OffsetTime,Period,ZonedDateTime,CalendarSystem,Era.

There are numerous other options you will want to look at. Once you've done that, press Generate Outputs and your C# client will be generated.


Pasting the client in to a project which references NodaTime, we can see that the NodaTime types are used:

enter image description here

My test utilised your class NodaTimeDataStructure and this controller:

[Route("api/[controller]")]
[ApiController]
public class NodaTimeController
{
    [HttpGet]
    public NodaTimeDataStructure Get() => new NodaTimeDataStructure();
}

For the purposes of this test/demo, I built this into a library which targets .NET 4.8 and uses ASP.NET Core 2.2.

like image 196
Stephen Kennedy Avatar answered Oct 13 '22 10:10

Stephen Kennedy