[Tutorial & Sample] Using Unsigned Integers in OData
Unsigned integers can be useful in many ways, such as representing data sizes, resource handles and so on. Though OData V4 only supports signed integer natively, the protocol offers a flexible way called type definition to allow users to ‘define’ unsigned integer types themselves.
As an example, we know that any UInt16 integer can be represented by the primitive type Edm.Int32. Thus by type definition, we can define a new type named MyNamespace.UInt16 whose underlying type is Edm.Int32. By doing so, we can store and serialize UInt16 integers as Edm.Int32 ones. There are three advantages of leveraging type definition here:
(1) Prevent breaking clients who conform to the protocol (to recognize type definitions) but are unaware of unsigned integer types.
(2) Give the underlying type a different name that is meaningful to the context.
(3) Enable the flexibility to change the underlying type without breaking the ‘unsigned integer’ type semantics.
From version 6.5.0, our OData library starts to provide built-in support for unsigned integer types (including UInt16, UInt32 and UInt64 for now) as a protocol extension. Generally a user would have to write very little code to gain a workable model with default implementation of unsigned integers. Meanwhile, the library is flexible that users are allowed to use their customized implementation as well.
Introducing Type Definition
Before diving into unsigned integers, let’s first take a look at how to define and use type definitions in OData. Suppose we want to define a new type Height whose underlying type is Edm.Double and use it to define a property MyHeight in an entity type Person (it is the same for complex type).
We can write the following CSDL for the model:
Or write the equivalent code in C#:
The following code demonstrates the creation of a Person entry:
You can serialize/deserialize the above entry to/from payload with the above model as usual.
The underlying type of a type definition must be a primitive type rather than an entity type, a complex type, an enumeration type, or even a type definition. However, two type definitions with the same underlying type along with the underlying primitive type itself are treated assignable from (or equivalent to) each other so all the expressions below evaluate to true:
This means the three types are type-comparable and exchangeable to each other in model definition, serialization and deserialization. Take entry deserialization for example, you can post Person entities specifying the type of MyHeight as SomeNamespace.Length or Edm.Double, which should both work perfectly.
Using Unsigned Integers
Unsigned integers are supported based on type definition. Say if a user wants to use UInt32 as property type in his model, a corresponding type definition should be added to the model so that a compliant client can recognize the UInt32 type.
This is done automatically if using the default implementation of unsigned integers of our library. Or you can add your own type definition if you want to override the underlying type.
But this is just about model definition. The next thing to consider should be how to serialize/deserialize entries with unsigned integers. Suppose we have the following Employeeentry:
Since OData V4 only supports signed integers, we have to convert UInt32 value to the underlying type Int64 known to the protocol before serializing it to the payload. Thus we may obtain an entry payload like:
If we want to deserialize the above payload to an Employee entry, we first get an Int64 value of StockQuantity directly from the payload. Then we need to convert the value from the underlying type Int64 to UInt32.
These two kinds of conversion are defined by the interface IPrimitiveValueConverter and its implementing classes:
Each model has an internal dictionary that maps each type definition within the model to a primitive value converter, which converts value between the user type (e.g., UInt32 here) and its underlying type (e.g., Int64 here). The library also offers a DefaultPrimitiveValueConverter used to handle the default conversions of unsigned integers. If a type definition is not associated with a converter in the model, the library uses the internal PassThroughPrimitiveValueConverter to directly pass though the value without conversion.
Default Implementation
The default implementation of unsigned integers enables users to write the least code to support unsigned integers in their models. It consists of two parts: (1).default type definitions of unsigned integer types; (2).default primitive value converter for unsigned integers.
For the first part, the default type definitions are listed below:
Type Definition Name | Default Underlying Type |
SomeNamspace.UInt16 | Edm.Int32 |
SomeNamspace.UInt32 | Edm.Int64 |
SomeNamspace.UInt64 | Edm.Decimal |
For the second part, the default conversions of unsigned integers are listed below:
User Type | Underlying Type | Type Definition |
System.UInt16 | System.Int32 | SomeNamespace.UInt16 |
System.UInt32 | System.Int64 | SomeNamespace.UInt32 |
System.UInt64 | System.Decimal | SomeNamespace.UInt64 |
The following example illustrates the usage of the default implementation. Suppose we want to create an entity type Product with Quantity of UInt16, StockQuantity of UInt32 and LifeTimeSeconds of UInt64, we can simply write the following code:
You can then serialize/deserialize the entry with the model as usual and the default primitive value converter will automatically handle all the underlying conversions.
User Customization
In case you want to override the underlying type and the conversions of an unsigned integer type, you can define your own type definition and primitive value converter.
Say if you want to use Edm.String as the underlying type of UInt64, you first need to create a new type definition along with the types that need it.
Secondly define a custom converter between UInt64 and String.
Thirdly associate a MyConverter instance with that type definition in the model.
Then you will be able to serialize an entry with UInt64:
You may get the payload like:
If you want to get the corresponding converter for a type definition, you can do as follows:
Querying Unsigned Integer Properties
You can query unsigned integer properties just as querying other primitive ones. Regarding the above sample, the following queries are supported:
For query options, support of custom unsigned integer types is NOT guaranteed. Currently only unsigned integers of default implementation are well supported. Here are a few examples: