October 7th, 2025
0 reactions

Announcing a new OData.NET serializer

Clément Habinshuti
Senior Software Engineer

One of the major, recurring complaints of the OData.NET libraries is the performance overhead of the serialization stack. We have done a lot of work to improve the serialization performance, but the existing architecture limits how far we can go. For this reason, we have started work on a new serialization stack for OData.NET libraries that addresses major performance concerns and also makes general improvements to usability. We plan to ship this new serializer as part of Microsoft.OData.Core 9.x library (the next major release) under a new namespace as an alternative to the existing ODataMessageWriter and ODataMessageReader.

But you don’t have to wait for Microsoft.OData.Core 9.x release, we have shipped the preview release to NuGet of the serializer as standalone package Microsoft.OData.Serializer so you can start testing it and sharing feedback with us. Let’s write a quick demo to show how it works.

  • To get started, create a new .NET Console application with .NET 8 or later.
  • AddMicrosoft.OData.Core package from NuGet. You can use either versions 8.x or 9.x preview.
  • Depending on your version of Microsoft.OData.Core, you may also need to add the Microsoft.Extensions.DependencyInjection.Abstractions package from NuGet.
  • Add Microsoft.OData.Serializer package from NuGet. The latest version is 0.1.0-preview.1 at the time of this writing. If using Visual Studio, make sure to **Include prerelease** versions in your package search.

Then write the following code in your Program.cs file:

// Setup EDM model that describes the OData service
var csdlSchema =
"""
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
  <edmx:DataServices>
    <Schema Namespace="ODataDemo" xmlns="http://docs.oasis-open.org/odata/ns/edm">
      <EntityType Name="Product">
        <Key>
          <PropertyRef Name="ID"/>
        </Key>
        <Property Name="ID" Type="Edm.Int32" Nullable="false"/>
        <Property Name="Name" Type="Edm.String"/>
        <Property Name="Description" Type="Edm.String"/>
        <Property Name="InStock" Type="Edm.Boolean"/>
        <Property Name="Price" Type="Edm.Decimal"/>
      </EntityType>
      <EntityContainer Name="DemoService">
        <EntitySet Name="Products" EntityType="ODataDemo.Product"/>
      </EntityContainer>
    </Schema>
 </edmx:DataServices>
</edmx:Edmx>
""";
var xmlReader = XmlReader.Create(new StringReader(csdlSchema));
IEdmModel model = CsdlReader.Parse(xmlReader);

// OData URI determines the kind of payload being requested
var odataUri = new ODataUriParser(
    model,
    new Uri("http://localhost/odata"),
    new Uri("Products", UriKind.Relative))
  .ParseUri();


// Prepare payload to write
List<Product> products = [
    new Product
    {
        ID = 1,
        Name = "Laptop",
        Description = "A high-performance laptop.",
        IsAvailable = false,
        Price = 999.99m
    },
    new Product
    {
        ID = 2,
        Name = "Smartphone",
        Description = "A latest model smartphone.",
        IsAvailable = true,
        Price = 699.99m
    }
];

// Initialize serializer options
var serializerOptions = new ODataSerializerOptions();

// Write the output to the console
using var outputStream = Console.OpenStandardOutput();
await ODataSerializer.WriteAsync(products, outputStream, odataUri, model, serializerOptions);

Console.ReadKey();

[ODataType("ODataDemo.Product")]
class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }

    [ODataPropertyName("InStock")]
    public bool IsAvailable { get; set; }
    public decimal Price { get; set; }

    public int Version { get; set; } // Skipped because it's not in the schema
}

When you run the application, it should print output similar to the following (pretty-printed for clarity):

{
  "@odata.context": "http://localhost/odata/$metadata#Products",
  "value": [
    {
      "ID": 1,
      "Name": "Laptop",
      "Description": "A high-performance laptop.",
      "InStock": false,
      "Price": 999.99
    },
    {
      "ID": 2,
      "Name": "Smartphone",
      "Description": "A latest model smartphone.",
      "InStock": true,
      "Price": 699.99
    }
  ]
}

Understanding the sample code

The above sample application creates an OData schema containing a single entity type Product and a single entity set Products that returns a collection of Product entity. The class also defines a .NET class called Product that maps to the entity type in the service schema. This demonstrates built-in support for serializing plain CLR types (POCO classes). We create a list of products and use ODataSerializer.WriteAsync() to serialize the product list and write to the output stream, which is the console standard output in this case. The WriteAsync method receives the payload, the output stream, the IEdmModel, the ODataUri and ODataSerializerOptions instance. The ODataUri helps the serializer know the kind of payload and @odata.context URL to write.

The ODataSerializerOptions class is used to customize various settings of the serializer, such as the buffer size or how to handle specific types. It’s intended for use a singleton per OData service, shared by all requests of the same OData service. In this example we create a new instance without modifying any setting. Instead of customizing the ODataSerializerOptions directly, we used the new OData-specific attribute [ODataType] to tell the serializer which entity type in the IEdmModel the .NET class maps to. We also used [ODataPropertyName] attribute to tell serialize which OData property a CLR property maps to when they don’t have the same names.

How it works

The new ODataSerializer works very differently from the existing ODataMessageWriter. ODataSerializer works by creating metadata that maps each input type like List<Product> and the Product class to specialized writer that knows how to serialize that type. In the example above, the metadata was generated automatically by the serializer using reflection, relying on the [ODataType] attribute to find the right entity type to map it to. But we could also define this mapping manually using the ODataSerializerOptions.AddTypeInfo() method. Here’s how the code would look like if we defined the mapping manually:

Category
ODataODL

Author

Clément Habinshuti
Senior Software Engineer

0 comments