{"id":5189,"date":"2023-03-09T15:49:39","date_gmt":"2023-03-09T22:49:39","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=5189"},"modified":"2024-10-01T12:06:18","modified_gmt":"2024-10-01T19:06:18","slug":"customize-odata-payload-serialization-format-within-asp-net-core-odata","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/customize-odata-payload-serialization-format-within-asp-net-core-odata\/","title":{"rendered":"Customize OData payload serialization format within ASP.NET Core OData"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>JSON (aka, JavaScript Object Notation), a standard text-based format for representing structured data based on JavaScript object syntax, is the default representation\u00a0for the OData requests and responses payload, see OData JSON format <a href=\"http:\/\/docs.oasis-open.org\/odata\/odata-json-format\/v4.01\/odata-json-format-v4.01.html\">here<\/a>. It\u2019s a very popular format and widely used in most scenarios. However, there are customers who want to build services following OData conventions and want to get OData payload using formats other than JSON, such as CSV (aka, Comma Separated Value) format, or YAML (aka, YAML Ain&#8217;t Markup Language) format, etc.<\/p>\n<p>OData .NET libraries are designed to empower customers to customize\/extend other payload formats besides JSON. There\u2019s a post <a href=\"https:\/\/devblogs.microsoft.com\/odata\/tutorial-sample-odatalib-custom-payload-format\/\">here<\/a> mentioning a way to customize CSV format using an old version of OData APIs. In this post, I\u2019d like to guide you through the steps about how to customize\/extend OData payload serialization formats with ASP.NET Core OData 8.x version. I mainly use CSV to introduce the detail customization process, but you can follow the same pattern to implement any other payload format in your OData service, for example, YAML.<\/p>\n<p>Let\u2019s get started by overviewing the OData payload serialization\/writing process within ASP.NET Core OData.<\/p>\n<h2>OData Payload Serialization Overview<\/h2>\n<p>Below is a simple picture describing the components used to serialize OData payload within ASP.NET Core OData 8.<\/p>\n<p><img decoding=\"async\" width=\"1430\" height=\"1034\" class=\"wp-image-5190\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/diagram-description-automatically-generated.png\" alt=\"Diagram Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/diagram-description-automatically-generated.png 1430w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/diagram-description-automatically-generated-300x217.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/diagram-description-automatically-generated-1024x740.png 1024w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/diagram-description-automatically-generated-768x555.png 768w\" sizes=\"(max-width: 1430px) 100vw, 1430px\" \/><\/p>\n<p>Where <strong>ODataOutputFormatter<\/strong> is the formatter to serialize the OData payload into the response body. It creates <strong>ODataMessageWriter<\/strong> using the <strong>ODataMessageWriterSettings<\/strong> from the service container and a response message. <strong>ODataMessageWriter<\/strong> would internally delegate <strong>ODataMediaTypeResolver <\/strong>from the service container also to figure out the proper payload format (aka, <strong>ODataFormat<\/strong>), which in turn creates the corresponding output context (aka, <strong>ODataOutputContext<\/strong>), finally, the output context creates the <strong>ODataWriter<\/strong> to perform output writing. The serializers, such as <strong>ODataResourceSerializer<\/strong>, use the <strong>ODataWriter<\/strong> to write the resources, properties, nested properties, etc.<\/p>\n<p><strong>ODataMediaTypeResolver<\/strong>, <strong>ODataFormat<\/strong>, <strong>ODataOutputContext,<\/strong> and <strong>ODataWriter<\/strong> are key components that we should implement by ourselves to customize the payload format. I will use a sample application in the following sections to share a demo about how to customize them to get the CSV format payload.<\/p>\n<h2>Prerequisites<\/h2>\n<p>Let\u2019s start to create an ASP.NET Core Web API application named \u2018<strong><em>ODataCustomizePayloadFormat\u2019<\/em><\/strong> with\u00a0<a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.OData&quot; \\t &quot;_blank\">Microsoft.AspNetCore.OData<\/a>\u00a0(version-8.0.12) installed. You can follow up on my previous posts to build the project.<\/p>\n<p>Within this application, I have two types of entities:<\/p>\n<ol>\n<li><strong>Book<\/strong>: only contains primitive type properties<\/li>\n<li><strong>Customer<\/strong>: contains properties of primitive, enum, complex type and collection of them<\/li>\n<\/ol>\n<p>I have the corresponding controllers for both entity types and keep them as simple as possible, for example:<\/p>\n<pre class=\"lang:c# decode:true\">public class BooksController : ControllerBase\r\n{\r\n    private static IList&lt;Book&gt; _books = GetBooks();\r\n\r\n    [HttpGet]\r\n    [EnableQuery]\r\n    public IActionResult Get()\r\n    {\r\n        return Ok(_books);\r\n    }\r\n\r\n    [HttpGet]\r\n    [EnableQuery]\r\n    public Book Get(int key)\r\n    {\r\n        Book b = _books.FirstOrDefault(c =&gt; c.Id == key);\r\n        return b;\r\n    }\r\n\r\n    \/\/ ......\r\n}\r\n<\/pre>\n<p>I&#8217;m omitting other codes (such as Edm model builder, etc) from the post. Please refer to the sample <a href=\"https:\/\/github.com\/xuzhg\/MyAspNetCore\/tree\/master\/src\/ODataCustomizePayloadFormat\">here<\/a> for details.<\/p>\n<h2>Implement CSV Format<\/h2>\n<p>In order to support the CSV format, we need to create the following four classes derived from the classes listed in the above overview section, respectively.<\/p>\n<ol>\n<li><code>public class CsvMediaTypeResolver : ODataMediaTypeResolver<\/code>This class is used to build a mapping between the media type and an OData format. For example, it builds a mapping between <strong>text\/csv<\/strong> media type and <strong>CsvFormat<\/strong>.<\/li>\n<li><code>public class CsvFormat : ODataFormat<\/code>This class is used to create the output context for writing and the input context for reading. In this post, I only cover the writing context.<\/li>\n<li><code>public class CsvOutputContext : ODataOutputContext<\/code>This class is used to create the CsvWriter, it acts as a bridge between <strong>ODataMessageWriter<\/strong> and the specific OData writer.<\/li>\n<li><code>public class CsvWriter : ODataWriter<\/code>This class is used to perform the &#8216;real&#8217; writing process.<\/li>\n<\/ol>\n<p>Let\u2019s implement them one by one in detail.<\/p>\n<h2>CsvMediaTypeResolver<\/h2>\n<p>OData media type resolving is designed to get an <strong>ODataFormat<\/strong> based on the request metadata, such as Content-Type header, Accept header, etc. The resolving process depends on an <strong>ODataMediaTypeResolver<\/strong> which is registered as a service in the dependency injection service container. OData reader and writer would first call the <em>GetMediaTypeFormats<\/em> method as below from this service to get a list of supported <strong>ODataMediaTypeFormat<\/strong> based on the given <strong>ODataPayloadKind<\/strong>, then resolve media type information from request message to get the best matched <strong>ODataFormat<\/strong>.<\/p>\n<pre class=\"lang:c# decode:true\">public class ODataMediaTypeResolver\r\n{\r\n    public virtual IEnumerable&lt;ODataMediaTypeFormat&gt; GetMediaTypeFormats(ODataPayloadKind payloadKind)\r\n    { ......}\r\n}\r\n<\/pre>\n<p>The default implementation of <strong>ODataMediaTypeResolver<\/strong> would return JSON format for data requests and XML format for metadata requests. To make media type resolver understand <strong>text\/csv<\/strong> media type and return the corresponding <strong>CsvFormat<\/strong>, we can derive from <strong>ODataMediaTypeResolver<\/strong> and override <em>GetMediaTypeFormats<\/em> to inject our own behavior. Here\u2019s the sample implementation:<\/p>\n<pre class=\"lang:c# decode:true\">public class CsvMediaTypeResolver : ODataMediaTypeResolver\r\n{\r\n    private readonly ODataMediaTypeFormat[] _mediaTypeFormats =\r\n    {\r\n        new ODataMediaTypeFormat(new ODataMediaType(\"text\", \"csv\"), new CsvFormat()),\r\n    };\r\n\r\n    public override IEnumerable&lt;ODataMediaTypeFormat&gt; GetMediaTypeFormats(ODataPayloadKind payloadKind)\r\n    {\r\n        if (payloadKind == ODataPayloadKind.Resource || payloadKind == ODataPayloadKind.ResourceSet)\r\n        {\r\n            return _mediaTypeFormats.Concat(base.GetMediaTypeFormats(payloadKind));\r\n        }\r\n\r\n        return base.GetMediaTypeFormats(payloadKind);\r\n    }\r\n}\r\n<\/pre>\n<p>Where, I have a private field to hold a mapping between media type <strong>text\/csv<\/strong> and an instance of <strong>CsvFormat<\/strong>, which is inserted into the list of <strong>ODataMediaTypeFormat<\/strong> in the overridden method <em>GetMediaTypeFormats<\/em>.<\/p>\n<p>We should register the new media type resolver into the service container. I will share it in the following section.<\/p>\n<h2>CsvFormat<\/h2>\n<p>As mentioned above, we need an <strong>ODataFormat<\/strong> in the <strong>CsvMeiaTypeResolver<\/strong> to create the output context for writing and the input context for reading. <strong>ODataFormat<\/strong> is defined as an abstract class as below, so we should implement it.<\/p>\n<pre class=\"lang:c# decode:true\">public abstract class ODataFormat\r\n{}\r\n<\/pre>\n<p>Below is the <strong>CsvFormat<\/strong> implementation:<\/p>\n<pre class=\"lang:c# decode:true\">public class CsvFormat : ODataFormat\r\n{\r\n    public override Task&lt;ODataOutputContext&gt; CreateOutputContextAsync(\r\n        ODataMessageInfo messageInfo, ODataMessageWriterSettings messageWriterSettings)\r\n    {\r\n        return Task.FromResult&lt;ODataOutputContext&gt;(\r\n            new CsvOutputContext(this, messageWriterSettings, messageInfo));\r\n    }\r\n\r\n    \/\/ \u2026\u2026\r\n    \/\/ We don't need other overrides for writing, just throw <strong>NotImplementedException<\/strong> and omit them here\u2026\r\n\r\n}\r\n<\/pre>\n<p>In this post, we only need to implement the <em>CreateOutputContextAsync<\/em> method in <strong>CsvFormat<\/strong> to return a <strong>CsvOutputContext<\/strong>. For other overrides, let\u2019s simply throw <strong>NotImplementedException<\/strong> exceptions since we don\u2019t use them.<\/p>\n<h2>CsvOutputContext<\/h2>\n<p>As mentioned, output context acts as a bridge between <strong>ODataMessageWriter<\/strong> and the specific <strong>ODataWriter<\/strong>. <strong>ODataOutputContext<\/strong> is also defined as an abstract class, so we should implement it by ourselves. Here\u2019s the <strong>CsvOutputContext<\/strong> implementation:<\/p>\n<pre class=\"lang:c# decode:true\">public class CsvOutputContext : ODataOutputContext\r\n{\r\n    private Stream stream;\r\n\r\n    public CsvOutputContext(ODataFormat format, ODataMessageWriterSettings settings, ODataMessageInfo messageInfo)\r\n        : base(format, messageInfo, settings)\r\n    {\r\n        stream = messageInfo.MessageStream;\r\n        Writer = new StreamWriter(stream);\r\n    }\r\n\r\n    public TextWriter Writer { get; private set; }\r\n\r\n    public override Task&lt;ODataWriter&gt; CreateODataResourceSetWriterAsync(IEdmEntitySetBase entitySet, IEdmStructuredType resourceType)\r\n        =&gt; Task.FromResult&lt;ODataWriter&gt;(new CsvWriter(this, resourceType));\r\n\r\n    public override Task&lt;ODataWriter&gt; CreateODataResourceWriterAsync(IEdmNavigationSource navigationSource, IEdmStructuredType resourceType)\r\n        =&gt; Task.FromResult&lt;ODataWriter&gt;(new CsvWriter(this, resourceType));\r\n\r\n    public void Flush() =&gt; stream.Flush();\r\n\r\n    protected override void Dispose(bool disposing)\r\n    {\r\n        \/\/ ...... Omits the disposing codes\r\n    }\r\n}\r\n<\/pre>\n<p>The implementation is simple. In the constructor, I create a <strong>StreamWriter<\/strong> to wrap the writing stream. Within the class, we only override the needed methods to create a writer for a resource set and a single resource. <strong>Be noted<\/strong>, to dispose the stream and the writer in the Dispose(bool) is important otherwise the content may be truncated, or no content is return at all. Thanks <a href=\"mailto:sifernan@microsoft.com\">sifernan@microsoft.com<\/a><\/p>\n<h2>CsvWriter<\/h2>\n<p><strong>CsvOutputContext<\/strong> is responsible for returning the OData writer to finish the \u2018real\u2019 writing operations. <strong>CsvWriter<\/strong> is such a class used to finish the CSV format writing. It is a class derived from abstract class <strong>ODataWriter<\/strong>, typically we should override the following four virtual methods to finish the resource set or resource writing:<\/p>\n<pre class=\"lang:c# decode:true\">public class CsvWriter : ODataWriter\r\n{\r\n    public override Task WriteStartAsync(ODataResourceSet resourceSet)\r\n    { }\r\n\r\n    public override Task WriteStartAsync(ODataResource resource)\r\n    { }\r\n\r\n    public override Task WriteStartAsync(ODataNestedResourceInfo nestedResourceInfo)\r\n    { }\r\n\r\n    public override Task WriteEndAsync()\r\n    { }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>, I use <em>WriteEndAsync<\/em> in the above class for consistency. Actually, I override the abstract method <em>WriteEnd<\/em>() in my sample, not the <em>WriteEndAsync<\/em>(), since WriteEndAsync() in the base class calls <em>WriteEnd<\/em>() directly.<\/p>\n<p>It could be a little bit hard to understand the writing flow, for example, which method is called first, which is next, etc. Let me use the following example to illustrate it.<\/p>\n<p>Supposed I want to write and get the below-left payload, I need a couple of OData objects (for example, <strong>ODataResoruceSet<\/strong>, <strong>ODataResource<\/strong>, etc) to call <em>WriteStartAsync<\/em> method. In the below-right picture, I list all OData objects related. The top level is an <strong>ODataResourceSet<\/strong>, it\u2019s a collection of <strong>ODataResource<\/strong>, each <strong>ODataResource<\/strong> contains properties, and may contain a collection of <strong>ODataNestedResourceInfo<\/strong>.<\/p>\n<p><img decoding=\"async\" width=\"1429\" height=\"901\" class=\"wp-image-5191\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-2.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-2.png 1429w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-2-300x189.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-2-1024x646.png 1024w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-2-768x484.png 768w\" sizes=\"(max-width: 1429px) 100vw, 1429px\" \/><\/p>\n<p>Here\u2019s the simplified writing process for the above OData payload:<\/p>\n<p><img decoding=\"async\" width=\"1430\" height=\"1569\" class=\"wp-image-5192\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated.png\" alt=\"Text, letter Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated.png 1430w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated-273x300.png 273w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated-933x1024.png 933w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated-768x843.png 768w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-letter-description-automatically-generated-1400x1536.png 1400w\" sizes=\"(max-width: 1430px) 100vw, 1430px\" \/><\/p>\n<p>Where you can see that for each OData object, a <em>WriteStartAsync<\/em> is called first, then a <em>WriteEndAsync<\/em> is called at the end. We can embed more writing processes within it, and each writing process also starts calling <em>WriteStartAsync<\/em>, ends calling <em>WriteEndAsync<\/em>.<\/p>\n<p>The implementation of <strong>CsvWriter<\/strong> depends on your requirement. You can refer to <a href=\"https:\/\/devblogs.microsoft.com\/odata\/tutorial-sample-odatalib-custom-payload-format\/\">this post<\/a> for a simple scenario, or you can refer to my implementation <a href=\"https:\/\/github.com\/xuzhg\/MyAspNetCore\/blob\/master\/src\/ODataCustomizePayloadFormat\/ODataCustomizePayloadFormat\/Extensions\/Csv\/CsvWriter.cs\">here<\/a> for a little bit complex scenario, in which I have codes to write the nested properties.<\/p>\n<h2>Register CSV format<\/h2>\n<p>In the old version, we must register the <strong>CsvMediaTypeResolver<\/strong> on <strong>MessageWriterSettings<\/strong>. With ASP.NET Core OData 8.x version, it\u2019s easy to inject the <strong>CsvMediaTypeResolver<\/strong> into the service container through dependency injection as below.<\/p>\n<pre class=\"lang:c# decode:true\">builder.Services.AddControllers().\r\n    AddOData(opt =&gt;\r\n        opt.EnableQueryFeatures()\r\n        .AddRouteComponents(\"odata\", EdmModelBuilder.GetEdmModel(),\r\n            service =&gt; service.AddSingleton&lt;ODataMediaTypeResolver&gt;(sp =&gt; new CsvMediaTypeResolver())));\r\n<\/pre>\n<p>Besides, we should let the existing OData formatter understand the new &#8220;<strong>text\/csv<\/strong>&#8221; media type. We can achieve it using following codes:<\/p>\n<pre class=\"lang:c# decode:true\">builder.Services.AddControllers(opt =&gt;\r\n{\r\n    var odataFormatter = opt.OutputFormatters.OfType&lt;ODataOutputFormatter&gt;().First();\r\n    odataFormatter.SupportedMediaTypes.Add(\"text\/csv\");\r\n});\r\n<\/pre>\n<h2>Test<\/h2>\n<p>We finished all implementations. Let\u2019s run and test it.<\/p>\n<p>First, we send the \u201cGET <a href=\"http:\/\/localhost:5296\/odata\/books\">http:\/\/localhost:5296\/odata\/books<\/a>\u201d request without any other header settings. You can get the default JSON-formatted OData payload as:<\/p>\n<p><img decoding=\"async\" width=\"928\" height=\"1675\" class=\"wp-image-5193\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated.png\" alt=\"Text Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated.png 928w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated-166x300.png 166w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated-567x1024.png 567w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated-768x1386.png 768w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/text-description-automatically-generated-851x1536.png 851w\" sizes=\"(max-width: 928px) 100vw, 928px\" \/><\/p>\n<p>Resend the request \u201cGET <a href=\"http:\/\/localhost:5296\/odata\/books\">http:\/\/localhost:5296\/odata\/books<\/a>\u201d again with request header <strong>Accept=text\/csv<\/strong>, we can get the OData CSV-formatted response payload as:<\/p>\n<p><img decoding=\"async\" width=\"904\" height=\"628\" class=\"wp-image-5194\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-5.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-5.png 904w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-5-300x208.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-5-768x534.png 768w\" sizes=\"(max-width: 904px) 100vw, 904px\" \/><\/p>\n<p>It also works with the OData query options such as:<\/p>\n<p><img decoding=\"async\" width=\"926\" height=\"552\" class=\"wp-image-5195\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email.png\" alt=\"Graphical user interface, text, application, email Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email.png 926w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-300x179.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-768x458.png 768w\" sizes=\"(max-width: 926px) 100vw, 926px\" \/><\/p>\n<p>My <strong>CsvWriter<\/strong> implementation supports writing the nested resource (complex or collection of the complex). For simplicity, I write the nested single-value resource as <em>&#8220;{propertyName=PropertyValue,\u2026}&#8221;<\/em>, and collection-valued nested resource as <em>&#8220;[{nested resource},{\u2026},\u2026]&#8221;. <\/em>You can change it to get any format for the nested resource.<\/p>\n<p>Send the request \u201cGET <a id=\"post-5189-OLE_LINK1\"><\/a><a href=\"http:\/\/localhost:5296\/odata\/customers\">http:\/\/localhost:5296\/odata\/customers<\/a>\u201d with request header \u201c<strong>Accept=text\/csv<\/strong>\u201d, we can get a little complex CSV-formatted OData payload.<\/p>\n<p><img decoding=\"async\" width=\"1489\" height=\"576\" class=\"wp-image-5196\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-application-description.png\" alt=\"Graphical user interface, application Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-application-description.png 1489w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-application-description-300x116.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-application-description-1024x396.png 1024w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-application-description-768x297.png 768w\" sizes=\"(max-width: 1489px) 100vw, 1489px\" \/><\/p>\n<p>It also supports querying the simple property and writing it as a CSV.<\/p>\n<p><img decoding=\"async\" width=\"933\" height=\"493\" class=\"wp-image-5197\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-1.png\" alt=\"Graphical user interface, text, application, email Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-1.png 933w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-1-300x159.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-1-768x406.png 768w\" sizes=\"(max-width: 933px) 100vw, 933px\" \/><\/p>\n<p>Again, we can also query the CSV-formatted collection complex property such as:<\/p>\n<p><img decoding=\"async\" width=\"911\" height=\"521\" class=\"wp-image-5198\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-2.png\" alt=\"Graphical user interface, text, application, email Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-2.png 911w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-2-300x172.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-2-768x439.png 768w\" sizes=\"(max-width: 911px) 100vw, 911px\" \/><\/p>\n<h2>Yaml format<\/h2>\n<p>It\u2019s easy to repeat the same process to customize the OData serialization payload to another format, for example, YAML. What we need is:<\/p>\n<ul>\n<li>a <strong>YamlWriter<\/strong>, which writes the resource, resource set as YAML format.<\/li>\n<li>a <strong>YamlOutputContext<\/strong>, which returns the instance of <strong>YamlWriter<\/strong><\/li>\n<li>a <strong>YamlFormat<\/strong>, which returns <strong>YamlOutputContext<\/strong><\/li>\n<li>a mapping within the media type resolver as:<\/li>\n<\/ul>\n<pre class=\"lang:c# decode:true\">private readonly ODataMediaTypeFormat[] _mediaTypeFormats =\r\n{\r\n    new ODataMediaTypeFormat(new ODataMediaType(\"text\", \"csv\"), new CsvFormat()),\r\n    new ODataMediaTypeFormat(new ODataMediaType(\"application\", \"yaml\"), YamlFormat()),\r\n};\r\n<\/pre>\n<ul>\n<li>Finally, we must let <strong>ODataOutputFormatter<\/strong> understand the \u201c<strong>application\/yaml<\/strong>\u201d media type.<\/li>\n<\/ul>\n<pre class=\"lang:c# decode:true\">builder.Services.AddControllers(opt =&gt;\r\n{\r\n    var odataFormatter = opt.OutputFormatters.OfType&lt;ODataOutputFormatter&gt;().First();\r\n    odataFormatter.SupportedMediaTypes.Add(\"text\/csv\");\r\n    odataFormatter.SupportedMediaTypes.Add(\"application\/yaml\");\r\n});\r\n<\/pre>\n<p>You can find detailed YAML format implementation from my sample <a href=\"https:\/\/github.com\/xuzhg\/MyAspNetCore\/tree\/master\/src\/ODataCustomizePayloadFormat\/ODataCustomizePayloadFormat\/Extensions\/Yaml\">here<\/a>.<\/p>\n<p>Ok. Once we finish the YAML implementation, let\u2019s send the request &#8220;GET <a href=\"http:\/\/localhost:5296\/odata\/customers\/1\">http:\/\/localhost:5296\/odata\/customers\/1<\/a>&#8221; with the request header &#8220;<strong>Accept=application\/yaml<\/strong>&#8220;, so we can get a YAML-formatted OData payload.<\/p>\n<p><img decoding=\"async\" width=\"925\" height=\"915\" class=\"wp-image-5199\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10.png 925w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10-300x297.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10-768x760.png 768w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10-24x24.png 24w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10-48x48.png 48w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/word-image-5189-10-96x96.png 96w\" sizes=\"(max-width: 925px) 100vw, 925px\" \/><\/p>\n<p>Of course, it supports all scenarios mentioned in CSV. For example, It supports the query options:<\/p>\n<p><img decoding=\"async\" width=\"993\" height=\"722\" class=\"wp-image-5200\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-3.png\" alt=\"Graphical user interface, text, application, email Description automatically generated\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-3.png 993w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-3-300x218.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2023\/03\/graphical-user-interface-text-application-email-3-768x558.png 768w\" sizes=\"(max-width: 993px) 100vw, 993px\" \/><\/p>\n<p>Finally, Let\u2019s use an example to put three formats together as a summary.<\/p>\n<p>&nbsp;<\/p>\n<table>\n<tbody>\n<tr>\n<td colspan=\"3\" align=\"center\"><strong>GET http:\/\/localhost:5296\/odata\/books\/2?$select=Title,Author<\/strong><\/td>\n<\/tr>\n<tr>\n<td align=\"center\"><strong>Accept=application\/json;odata.metadata=none<\/strong><\/td>\n<td align=\"center\"><strong>Accept=text\/csv<\/strong><\/td>\n<td align=\"center\"><strong>Accept=application\/yaml<\/strong><\/td>\n<\/tr>\n<tr>\n<td>\n<pre class=\"lang:json decode:true\">{\r\n  \"Title\":\u00a0\"Animal\u00a0Farm\",\r\n  \"Author\":\u00a0\"George\u00a0Orwell\"\r\n}<\/pre>\n<\/td>\n<td>\n<pre class=\"lang:txt decode:true\">Title,Author\r\nAnimal Farm,George Orwell<\/pre>\n<pre><\/pre>\n<\/td>\n<td>\n<pre class=\"lang:yaml decode:true\">Title:\u00a0Animal\u00a0Farm\r\nAuthor:\u00a0George\u00a0Orwell<\/pre>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Summary<\/h2>\n<p>This post went through the steps to customize OData payload serialization as CSV, and YAML format within ASP.NET Core OData 8.x. Hope the contents and implementations in this post can give you an idea\/direction to implement your own payload format customization. Please do not hesitate to leave your comments below or let me know your thoughts through\u00a0<a href=\"mailto:saxu@microsoft.com\">saxu@microsoft.com<\/a>. Thanks.<\/p>\n<p>I uploaded the whole project to\u00a0<a href=\"https:\/\/github.com\/xuzhg\/MyAspNetCore\/tree\/master\/src\/ODataCustomizePayloadFormat\" target=\"_blank\" rel=\"noopener\">this<\/a>\u00a0repository.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction JSON (aka, JavaScript Object Notation), a standard text-based format for representing structured data based on JavaScript object syntax, is the default representation\u00a0for the OData requests and responses payload, see OData JSON format here. It\u2019s a very popular format and widely used in most scenarios. However, there are customers who want to build services following [&hellip;]<\/p>\n","protected":false},"author":514,"featured_media":3253,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1472,1,117],"tags":[1474,1479,48,1480],"class_list":["post-5189","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-asp-net-core","category-odata","category-webapi","tag-asp-net-core-odata","tag-csv","tag-odata","tag-yaml"],"acf":[],"blog_post_summary":"<p>Introduction JSON (aka, JavaScript Object Notation), a standard text-based format for representing structured data based on JavaScript object syntax, is the default representation\u00a0for the OData requests and responses payload, see OData JSON format here. It\u2019s a very popular format and widely used in most scenarios. However, there are customers who want to build services following [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5189","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/users\/514"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/comments?post=5189"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5189\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/3253"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=5189"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=5189"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=5189"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}