Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Cross-Technologies DTO & API Specifications #1378

Open
haimkastner opened this issue Mar 8, 2024 · 5 comments
Open

Support Cross-Technologies DTO & API Specifications #1378

haimkastner opened this issue Mar 8, 2024 · 5 comments

Comments

@haimkastner
Copy link
Contributor

haimkastner commented Mar 8, 2024

Is your feature request related to a problem? Please describe.

Request related

As part of the powerful and wide support in multiple languages in the unit definition, I think it can be cool to standardize the way how unit is represented in the API spec, and how it will be exposed/loaded.

Implementing in all UnitsNet-based libraries a similar API DTO to load/expose and the same JSON schema standard of how the unit is represented, will give the benefits of:

  1. Work with UnitsNet across various technologies transparently without any kind of manual conversions.
  2. Standart way how to represent units in API schemas.
  3. Clear OpenAPI specification (and automatically in case the spec is generated from the code object declarations)
  4. Human readable unit representation in the API schema, and easy to work with anyway.

See also haimkastner/unitsnet-js#29 for real-world, probably common use-case between C# backend and TypeScript frontend.

Currently, sterilizing is supported in the library, but not as a common, standard, unit dedicated and easy-to-use API.

Describe the solution you'd like

The JSON standart DTO will look like this:

{
   "value":100.01,
   "unit":"Meter"
}

As part of the full JSON API payload something like:

{
   "someInfo":"someValue",
   "someInfoLength":{
      "value":100.01,
      "unit":"Meter"
   }
}

See an OpenAPI unitsnet-openapi-spec example schema.

A basic and naive prototype of how it will be in C#

using System;
using UnitsNet;
using UnitsNet.Units;
using System.Text.Json;

public class LengthDto
{
	class JsonDto
	{
    	public double value { get; set; }
    	public string unit { get; set; }
	}
	
    public LengthDto(double value, LengthUnit unit)
    {
        this.value = value;
        this.unit = unit;
    }
	
    public double value { get; set; }
    public LengthUnit unit { get; set; }
	
	public string ToJson()
    {
		JsonDto jsonInterface = new JsonDto { value = this.value, unit = this.unit.ToString() };
		return JsonSerializer.Serialize(jsonInterface);
    }
	
	public static LengthDto fromJson(string json)
    {
		JsonDto jsonInterface = JsonSerializer.Deserialize<JsonDto>(json);
		Enum.TryParse<LengthUnit>(jsonInterface.unit, out LengthUnit newUnit);
		return new LengthDto(jsonInterface.value, newUnit);
    }
}


public static class LengthExtensions
{
    public static Length fromDto(this Length length, LengthDto lengthDto) 
    {
       return new Length(lengthDto.value, lengthDto.unit);
    }
	
	public static LengthDto toDto(this Length length, LengthUnit unit = LengthUnit.Meter) 
    {
       return new LengthDto(length.As(LengthUnit.Meter), unit);
    }
}

public class Program
{
	public static void Main()
	{
		Length lengthValue = Length.FromMeters(100.01);
		string json = lengthValue.toDto().ToJson();
		Console.WriteLine(json); // {"value":100.01,"unit":"Meter"}
		
		LengthDto newLengthDto = LengthDto.fromJson(json);
		Length newLengthValue = new Length().fromDto(newLengthDto);
		Console.WriteLine(newLengthValue.Meters); // 100.01
	}
}

A similar implementation I have created for the JS package haimkastner/unitsnet-js#32 & haimkastner/unitsnet-py#18

TypeScript usage (docs)

// Create a Length unit object
const length = Length.FromMeters(100.01);
// Obtain the DTO object, represented by the default unit - meter
const lengthDto: LengthDto = length.toDto(); // {"value":100.01,"unit":"Meter"}
// Obtain Length object from lengthDto
const newLength: Length = Length.FromDto(lengthDto);

Similar for the Python package (docs):

 # Create a Length unit object
length = Length.from_meters(100.01)
# Obtain the DTO object as json, represented by the default unit - meter
length_dto_json = length.to_dto_json() # {"value":100.01,"unit":"Meter"}
# Load JSON to DTO, and load
length_from_dto = Length.from_dto_json(length_dto_json)

Describe alternatives you've considered
To do it manually.

Additional context

See an example of the Python Length implementation of the DTO class and the unit API example

@angularsen
Copy link
Owner

angularsen commented Jul 8, 2024

Super late reply, sorry.

This is a good initiative, but I'm a bit hesitant to own the serialization and OpenAPI schema for this.

  1. We make breaking changes all the time; renaming/removing quantities and units. Handling this 100% for serialized data can be painful, so up until now this has been the application developer's responsibility.
  2. The OpenAPI schema should include information about the quantity type to disambiguate, since there are several quantities that share unit names, such as Luminosity.Watt and Power.Watt. See wiki for example JSON.

image

I mean, yes, we can probably land on an OpenAPI schema and store it in this repo to make it "official", as long as we don't handle migration of serialized data between major versions.

However, you would be the main driver for this project so it could also make sense for the OpenAPI schema to live in your github and we just refer to it in the readme. What do you think?

@lipchev
Copy link
Collaborator

lipchev commented Jul 8, 2024

By the way, at work, we have some extensions for the swagger builder that help us generate a fully specified swagger.json for the APIs that have quantities in the contract (at least for the AbbreviatedJsonConverter).
My colleague was supposed to create a PR (or at least a discussion) for it (I assume that would be an independent nuget).

Let's see if this would paste (I'm copying from the swagger UI):

MolarMass UnitsNet.MolarMass{
description:

Example: 1 kg/mol (Unit: KilogramPerMole, Dimensions: [Mass][Amount]). Click here to learn more.
Value* number($double)
example: 1
The quantity value (in the specified unit)

Unit* string($enum)
example: kg/mol
The quantity unit represented by it's default abbreviation
Enum:
[ cg/mol, dag/mol, dg/mol, g/mol, hg/mol, kg/mol, klb/mol, Mlb/mol, µg/mol, mg/mol, ng/mol, lb/mol, kg/kmol ]

Type* string($enum)
default: MolarMass
The quantity type discriminator
}

@angularsen
Copy link
Owner

Maybe I'm misreading this, but I'm not sure it's a good idea to use abbreviations as the enum value.

Abbreviations have historically seen a lot of breaking changes (renames, fixing syntax, disambiguating imperial/US units, or using a more common form as the primary abbreviation). More so than the unit enum names, although these change too.

Also, how does generating C# code for an OpenAPI spec with enum values like cg/mol work? Does it automatically set up JSON converters to map the strings?

@lipchev
Copy link
Collaborator

lipchev commented Jul 8, 2024

It's not really an Enum, those are still strings, but the declaration- string(enum) lists the available options- which is very helpful (and also fill's in the contracts examples).

{
  "Category": "string",
  "Diameter": {
    "Value": 1,
    "Unit": "m",
    "Type": "Length"
  },
  "Height": {
    "Value": 1,
    "Unit": "m",
    "Type": "Length"
  },
  "Weight": {
    "Value": 1,
    "Unit": "kg",
    "Type": "Mass"
  },
  "Option": "string",
  "Material": "string",
  "Name": "string",
  "Volume": {
    "Value": 1,
    "Unit": "m³",
    "Type": "Volume"
  },
  "ID": 0
}

As for the contract itself- It's every API's choice, I think there is a very strong argument to be made for the use of the abbreviations- when the client side isn't a .net application (like in my case).
So basically, the API declares what inputs it supports, which is completely independent of what is the latest version of Units.Net..

@haimkastner
Copy link
Contributor Author

haimkastner commented Jul 9, 2024

Thanks for your reply!

What I'm trying to solve, is not how to serialize units into DB, files, or any permanent storage, I'm trying to solve unit transfer between systems and microservices.

Let's say I have C# and Python services in my backend and JS frontend, which communicate via Rest/GraphQL.
Instead of converting any unit representation to a particular quantity (Meter, Centimeter, etc.) in the Rest/GraphQL schemas and making sure both sender and receiver services are aligned to do the right particular quantity from/to conversion.

With UnitsNet, all services can use the UnitsNet library, represent the unit DTO (Length DTO, Angle DTO, etc.) in the schema (whether using OpenAPI/similar tools or not), and send/load the data without conversion of quantity, just call to/from DTO in any service in the system.

To allow that easily, I suggest adding DTO interface representation and to/from DTO methods to each unit.

  1. We make breaking changes all the time; renaming/removing quantities and units. Handling this 100% for serialized data can be painful, so up until now this has been the application developer's responsibility.

I agree, but since I'm trying to solve only the communication process, I think it's reasonable to limit the communication between the services only if all of them are in the same major version.
This means that if the developer decides to update UnitsNet, needs to update the ALL UnitsNet libraries in all services to the latest.

From my side, regardless of this topic, I will make sure that any new major version in this project, will be reflected to a new major version in the JS and PY flavors as well haimkastner/unitsnet-js#45.

2. The OpenAPI schema should include information about the quantity type to disambiguate, since there are several quantities that share unit names, such as Luminosity.Watt and Power.Watt. See wiki for example JSON.

You are right, I will add the unit type (Power / Luminosity) as well like this:

{
   "value":100.01,
   "quantity":"Meter",
   "unit": "Length"
}

haimkastner/unitsnet-js#46

However, as a side note, I think that for most of the use cases, the send and the receiver services know the Unit, and I'm trying only to save them from converting back and forth the quantity, so once the service knows let's say that it's a Power unit, we don't care that Luminosity has also Watt is his quantities.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants