{"id":4703,"date":"2021-08-23T12:10:46","date_gmt":"2021-08-23T19:10:46","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4703"},"modified":"2021-08-23T12:12:12","modified_gmt":"2021-08-23T19:12:12","slug":"build-formatter-extensions-in-asp-net-core-odata-8-and-hooks-in-odataconnectedservice","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/build-formatter-extensions-in-asp-net-core-odata-8-and-hooks-in-odataconnectedservice\/","title":{"rendered":"Build formatter extensions in ASP.NET Core OData 8 and hooks in ODataConnectedService"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>In this post, I will create formatter extensions in an OData web service and request\/response hooks in an OData client application to generate\/consume ETag control information. ETag (aka entity tag), one of OData control information that may be applied to an entity or collection in response, can be used in a subsequent request to avoid overwriting if the entity or collection&#8217;s value has been changed by other requests.<\/p>\n<p>ETag value normally can be generated using concurrency properties automatically in OData, however, I\u2019d like to use this post to share with you the ideas of how to extend serialization and deserialization in ASP.NET Core OData 8 and how to use request\/response hook in OData Connected Service to customize the ETag functionality.<\/p>\n<p>Let\u2019s get started.<\/p>\n<h2>Scenarios &amp; Prerequisites<\/h2>\n<p>I want to generate\/consume the ETag control information (aka <a href=\"http:\/\/docs.oasis-open.org\/odata\/odata-json-format\/v4.01\/odata-json-format-v4.01.html#_Toc38457745\" target=\"_blank\" rel=\"noopener\">odata.etag<\/a>) using a hidden property on the service side, meanwhile, to generate\/consume the ETag using an extra property at the client side.<\/p>\n<p>So, let\u2019s create a blank solution named <strong>ODataETagExtensions<\/strong>, in which I create an ASP.NET Core Application called <strong>ODataETagWebApi<\/strong>, and a console application named <strong>ODataETagClient<\/strong>. I install the \u201cMicrosoft.AspNetCore.OData -version 8.0.1\u201d nuget package to <strong>ODataETagWebApi<\/strong>.<\/p>\n<p>I use <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=marketplace.ODataConnectedService\" target=\"_blank\" rel=\"noopener\">ODataConnectedService<\/a> in the console application <strong>ODataETagClient<\/strong>\u00a0to consume the ETag from the service. If you haven\u2019t installed ODataConnectedService, go to [Extensions]\ud83e\udc7a[Manage Extensions] menu from Visual studio, search and install<em> OData Connected Service<\/em>. You might need to close the VS to let the installation finish.<\/p>\n<p><img decoding=\"async\" width=\"935\" height=\"246\" class=\"wp-image-4704\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/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\/2021\/08\/graphical-user-interface-application-description.png 935w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/graphical-user-interface-application-description-300x79.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/graphical-user-interface-application-description-768x202.png 768w\" sizes=\"(max-width: 935px) 100vw, 935px\" \/><\/p>\n<h2>Build the model with hidden ETag property<\/h2>\n<p>In <strong>ODataETagWebApi, <\/strong>I create a simple C# class as our schema model as:<\/p>\n<pre class=\"lang:c# decode:true\">\/\/ Customer.cs\r\npublic class Customer\r\n{\r\n    public int Id { get; set; }\r\n\r\n    public string Name { get; set; }\r\n\r\n    public Guid Label { get; set; }\r\n}\r\n<\/pre>\n<p>Where, &#8220;<em>Label&#8221;<\/em> is the property containing ETag info but not included in the schema. So, when building the Edm model, let\u2019s ignore this property as:<\/p>\n<pre class=\"lang:c# decode:true\">private static IEdmModel GetEdmModel()\r\n{\r\n    var builder = new ODataConventionModelBuilder();\r\n\r\n    builder.EntitySet&lt;Customer&gt;(\"Customers\").EntityType.Ignore(c =&gt; c.Label);\r\n\r\n    return builder.GetEdmModel();\r\n}\r\n<\/pre>\n<p>Here\u2019s a part of the schema:<\/p>\n<pre class=\"prettyprint\">&lt;EntityType\u00a0Name=\"Customer\"&gt;\r\n  &lt;Key&gt;\r\n    &lt;PropertyRef\u00a0Name=\"Id\"\/&gt;\r\n  &lt;\/Key&gt;\r\n  &lt;Property\u00a0Name=\"Id\"\u00a0Type=\"Edm.Int32\"\u00a0Nullable=\"false\"\/&gt;\r\n  &lt;Property\u00a0Name=\"Name\"\u00a0Type=\"Edm.String\"\/&gt;\r\n&lt;\/EntityType&gt;\r\n<\/pre>\n<h2>Build controller<\/h2>\n<p>I create the following <strong>CustomersController <\/strong>to handle the request. For simplicity, I use memory data for customers:<\/p>\n<pre class=\"lang:c# decode:true\">public class CustomersController : ControllerBase\r\n{\r\n    private static IList&lt;Customer&gt; Customers = new List&lt;Customer&gt;\r\n    {\r\n        new Customer { Id = 1, Name = \"Freezing\", Label = new Guid(\"81B06CD0-3B66-4447-B193-94B11328A762\") },\r\n        new Customer { Id = 2, Name = \"Bracing\", Label = new Guid(\"629C11E1-1918-4978-AFE0-F90BA6A452C6\") },\r\n        new Customer { Id = 3, Name = \"Chilly\", Label = new Guid(\"CA02BF9C-8364-4320-B74F-CB8956C9A502\") },\r\n    };\r\n\r\n    [HttpGet]\r\n    [EnableQuery]\r\n    public IActionResult Get()\r\n    {\r\n        return Ok(Customers);\r\n    }\r\n\r\n    [HttpGet]\r\n    [EnableQuery]\r\n    public IActionResult Get(int key)\r\n    {\r\n        Customer c = Customers.FirstOrDefault(c =&gt; c.Id == key);\r\n        if (c == null)\r\n        {\r\n            return NotFound($\"Cannot find customer with Id={key}\");\r\n        }\r\n\r\n        return Ok(c);\r\n    }\r\n}\r\n<\/pre>\n<h2>Config and test service<\/h2>\n<p>In the <em>startup.cs<\/em>, let\u2019s configure OData as:<\/p>\n<pre class=\"lang:c# decode:true\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddControllers()\r\n        .AddOData(opt =&gt; opt.AddRouteComponents(\"odata\", GetEdmModel));\r\n}\r\n<\/pre>\n<p>Now, let\u2019s build, run <strong>ODataETagWebApi,<\/strong> and query `<a href=\"http:\/\/localhost:5000\/odata\/customers%60\">http:\/\/localhost:5000\/odata\/customers`<\/a>, we can get the following:<\/p>\n<pre class=\"lang:c# decode:true\">{\r\n  \"@odata.context\":\"http:\/\/localhost:5000\/odata\/$metadata#Customers\",\r\n  \"value\":[\r\n    {\r\n      \"Id\":1,\r\n      \"Name\":\"Freezing\"\r\n    },\r\n    ......\r\n    {\r\n      \"Id\":5,\r\n      \"Name\":\"Mild\"\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<h2>Customize serializer to return ETtag from service<\/h2>\n<p>From the above <em>Customers<\/em> query, you can find that each customer doesn\u2019t have \u201c<em>Label<\/em>\u201d property, neither <em>odata.etag<\/em> property. In order to generate \u201c<em>odata.etag<\/em>\u201d using the &#8220;<em>Label&#8221;<\/em> property, we can customize the resource serializer to generate it.<\/p>\n<p>I customize the serializer by deriving from <strong>ODataResourceSerializer<\/strong>. Below is my implementation:<\/p>\n<pre class=\"lang:c# decode:true\">public class ETagResourceSerializer : ODataResourceSerializer\r\n{\r\n    public ETagResourceSerializer(IODataSerializerProvider serializerProvider)\r\n        : base(serializerProvider)\r\n    { }\r\n\r\n    public override ODataResource CreateResource(SelectExpandNode selectExpandNode, ResourceContext resourceContext)\r\n    {\r\n        ODataResource resource = base.CreateResource(selectExpandNode, resourceContext);\r\n\r\n        if (resource.ETag == null &amp;&amp; resourceContext.ResourceInstance is Customer c)\r\n        {\r\n            resource.ETag = EncodeETag(c.Label);\r\n        }\r\n\r\n        return resource;\r\n    }\r\n\r\n    private static string EncodeETag(Guid guide)\r\n    {\r\n        byte[] bytes = Encoding.UTF8.GetBytes(guid.ToString());\r\n        return Convert.ToBase64String(bytes);\r\n    }\r\n}\r\n<\/pre>\n<p>I create the following serializer provider to inject the <strong>ETagResourceSerialize<\/strong>. Here\u2019s my implementation:<\/p>\n<pre class=\"lang:c# decode:true\">public class ETagSerializerProvider : ODataSerializerProvider\r\n{\r\n    public ETagSerializerProvider(IServiceProvider serviceProvider)\r\n        : base(serviceProvider)\r\n    { }\r\n\r\n    public override IODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)\r\n    {\r\n        if (edmType.IsEntity() || edmType.IsComplex())\r\n        {\r\n            return new ETagResourceSerializer(this);\r\n        }\r\n\r\n        return base.GetEdmTypeSerializer(edmType);\r\n    }\r\n}\r\n<\/pre>\n<p>Let\u2019s update <em>Startup.cs<\/em> by injecting the serializer provider into service collection:<\/p>\n<pre class=\"lang:c# decode:true\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddControllers()\r\n        .AddOData(opt =&gt; opt.AddRouteComponents(\"odata\", GetEdmModel(),\r\n            builder =&gt; builder.AddSingleton&lt;IODataSerializerProvider, ETagSerializerProvider&gt;()));\r\n}\r\n<\/pre>\n<p>Now, build, run <strong>ODataETagWebApi<\/strong> and query request: <a href=\"http:\/\/localhost:5000\/odata\/customers\/1\">http:\/\/localhost:5000\/odata\/customers\/1<\/a><\/p>\n<p>We can get the following response:<\/p>\n<pre class=\"lang:c# decode:true\">{\r\n  \"@odata.context\":\u00a0\"http:\/\/localhost:5000\/odata\/$metadata#Customers\/$entity\",\r\n  \"@odata.etag\":\u00a0\"ODFiMDZjZDAtM2I2Ni00NDQ3LWIxOTMtOTRiMTEzMjhhNzYy\",\r\n  \"Id\":\u00a01,\r\n  \"Name\":\u00a0\"Freezing\"\r\n}\r\n<\/pre>\n<h2>Consume ETag at OData client<\/h2>\n<p>Let\u2019s use ODataConnectedService to config the <strong>ODataETagClient <\/strong>project. First, Let\u2019s run the service <strong>ODataETagWebApi<\/strong> without debug. Once the service is running, right click <strong>ODataETagClient<\/strong> project from Solution Explorer in VS. Select [Add] \ud83e\udc7a [Connected Service] menu from the Pop-up menu. In the dialog, select OData Connected Service. In the <em>Configure endpoint <\/em>dialog below, input the Uri in the <strong>Address <\/strong>input box (be sure to use your service URI if different):<\/p>\n<p><img decoding=\"async\" width=\"752\" height=\"307\" class=\"wp-image-4705\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image.png 752w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-300x122.png 300w\" sizes=\"(max-width: 752px) 100vw, 752px\" \/><\/p>\n<p>Click <strong>next <\/strong>button and go to <em>Schema Types<\/em> page to make sure we have the schema loaded.<\/p>\n<p><img decoding=\"async\" width=\"777\" height=\"406\" class=\"wp-image-4706\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-1.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-1.png 777w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-1-300x157.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-1-768x401.png 768w\" sizes=\"(max-width: 777px) 100vw, 777px\" \/><\/p>\n<p>The above picture shows that OData connected service correctly retrieves the schema type from our service.<\/p>\n<p>Let\u2019s skip other configurations and click the <strong>Finish <\/strong>button to let the connected service generate the proxy classes.<\/p>\n<p>Now, let\u2019s change <em>Program.cs <\/em>in the <strong>ODataETagClient<\/strong> using the following codes:<\/p>\n<pre class=\"lang:c# decode:true\">class Program\r\n{\r\n    async static Task Main(string[] args)\r\n    {\r\n        await ListCustomers();\r\n    }\r\n\r\n    async static Task ListCustomers()\r\n    {\r\n        Console.WriteLine(\"List Customers: \");\r\n        Container context = GetContext();\r\n        IEnumerable&lt;Customer&gt; customers = await context.Customers.ExecuteAsync();\r\n        foreach (var customer in customers)\r\n        {\r\n            Console.WriteLine(\" {0}) Name={1}\", customer.Id, customer.Name);\r\n        }\r\n\r\n        Console.WriteLine();\r\n    }\r\n\r\n    private static Container GetContext()\r\n    {\r\n        var serviceRoot = \"http:\/\/localhost:5000\/odata\/\";\r\n        var context = new Container(new Uri(serviceRoot));\r\n        return context;\r\n    }\r\n}\r\n<\/pre>\n<p>To test, set <strong>ODataETagClient<\/strong> as startup project by right click <strong>ODataETagClient<\/strong> project in the solution explorer. Please make sure <strong>ODataETagWebApi<\/strong> is running, then [Ctrl+F5] to run <strong>ODataETagClient<\/strong>, we can get:<\/p>\n<p><img decoding=\"async\" width=\"627\" height=\"102\" class=\"wp-image-4707\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-2.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-2.png 627w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-2-300x49.png 300w\" sizes=\"(max-width: 627px) 100vw, 627px\" \/><\/p>\n<p>You can find OData connected service generates a proxy class named <strong>Customer<\/strong>, but it only has <strong>Id <\/strong>and <strong>Name <\/strong>properties.<\/p>\n<p>To use <em>Label<\/em>, let\u2019s add a new C# file \u201cCustomer.cs\u201d into <strong>ODataETagClient<\/strong> project, add the following:<\/p>\n<pre class=\"lang:c# decode:true\">\/\/ Customers.cs\r\npublic partial class Customer\r\n{\r\n    public Guid Label { get; set; }\r\n}\r\n<\/pre>\n<p>Change \u201c<strong>ListCustomers()<\/strong>\u201d in the <em>program.cs<\/em> class:<\/p>\n<pre class=\"lang:c# decode:true\">async static Task ListCustomers()\r\n{\r\n    \/\/ omit codes\r\n\r\n    foreach (var customer in customers)\r\n    {\r\n        Console.WriteLine(\" {0}) Name={1,-10}Label={2}\", customer.Id, customer.Name + \",\", customer.Label);\r\n    }\r\n\r\n    \/\/ omit codes\r\n}\r\n<\/pre>\n<p>Run the console application again, we can get an empty Guid Label.<\/p>\n<p><img decoding=\"async\" width=\"831\" height=\"101\" class=\"wp-image-4708\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-3.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-3.png 831w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-3-300x36.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-3-768x93.png 768w\" sizes=\"(max-width: 831px) 100vw, 831px\" \/><\/p>\n<p>Now, let\u2019s retrieve the <em>Label <\/em>from ETag. In order to retrieve the value, we can use OData client hook. OData client provides several ways to allow developers to hook into the client request and response. In the current scenario, we can use <strong>OnEntityMaterialized<\/strong> from response pipeline configuration. For other hooks, please refer to <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.odata.client.dataserviceclientresponsepipelineconfiguration?view=odata-client-7.0\">here.<\/a><\/p>\n<p><strong>OnEntityMaterialized<\/strong> response hook uses <strong>MaterializedEntityArgs<\/strong> as:<\/p>\n<pre class=\"lang:c# decode:true\">public sealed class MaterializedEntityArgs\r\n{\r\n    public MaterializedEntityArgs(ODataResource entry, object entity);\r\n    public ODataResource Entry { get; }\r\n    public object Entity { get; }\r\n}\r\n<\/pre>\n<p>Where,<\/p>\n<ul>\n<li><strong><em>Entry<\/em><\/strong> is the object holding the value from response.<\/li>\n<li><strong><em>Entity<\/em><\/strong> is the object to the client.<\/li>\n<\/ul>\n<p>I have the following codes to retrieve the ETag value from each customer and save it to <em>Label<\/em> as following.<\/p>\n<pre class=\"lang:c# decode:true\">private static Container GetContext()\r\n{\r\n    var serviceRoot = \"http:\/\/localhost:5000\/odata\/\";\r\n    var context = new Container(new Uri(serviceRoot));\r\n<span style=\"color: #0000ff;\">    context.Configurations.ResponsePipeline.OnEntityMaterialized(args =&gt;\r\n    {\r\n        if (args. Entity is Customer customer)\r\n        {\r\n            if (args.Entry.ETag != null)\r\n            {\r\n                customer.Label = DecodeETag(args.Entry.ETag);\r\n            }\r\n        }\r\n    });<\/span>\r\n\r\n    return context;\r\n}\r\n\r\nprivate static Guid DecodeETag(string etag)\r\n{\r\n    byte[] base64 = Convert.FromBase64String(etag);\r\n    string base64Str = Encoding.UTF8.GetString(base64);\r\n    return new Guid(base64Str);\r\n}\r\n<\/pre>\n<p>Now, run it again, we can get <em>Label <\/em>value:<\/p>\n<p><img decoding=\"async\" width=\"738\" height=\"102\" class=\"wp-image-4709\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-4.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-4.png 738w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-4-300x41.png 300w\" sizes=\"(max-width: 738px) 100vw, 738px\" \/><\/p>\n<h2>Customize deserializer to consume ETag at service<\/h2>\n<p>In the <strong>ODataETagWebApi <\/strong>project, Let\u2019s update the controller by adding the following methods to consume the ETag value from <em>Label <\/em>property:<\/p>\n<pre class=\"lang:c# decode:true\">public class CustomersController : ODataController\r\n{\r\n    \/\/ ...... omit others\r\n\r\n    [HttpPost]\r\n    public IActionResult Post([FromBody]Customer customer)\r\n    {\r\n        customer.Id = Customers.Last().Id + 1; \/\/ for simplicity\r\n        if (customer.Label == Guid.Empty)\r\n        {\r\n            customer.Label = Guid.NewGuid();\r\n        }\r\n\r\n        Customers.Add(customer);\r\n        return Created(customer);\r\n    }\r\n\r\n    [HttpPatch]\r\n    public IActionResult Patch(int key, Delta&lt;Customer&gt; patch)\r\n    {\r\n        Customer old = Customers.FirstOrDefault(c =&gt; c.Id == key);\r\n        if (old == null)\r\n        {\r\n            return NotFound($\"Cannot find customer with Id={key}\");\r\n        }\r\n\r\n        if (!patch.TryGetPropertyValue(\"Label\", out object value))\r\n        {\r\n            return BadRequest($\"Cannot find the ETag value\");\r\n        }\r\n\r\n        Guid labelValue = (Guid)value;\r\n        if (labelValue != old.Label)\r\n        {\r\n            \/\/ Compare Guid value for simplicity\r\n            return BadRequest($\"ETag value can not match!\");\r\n        }\r\n\r\n        patch.Patch(old);\r\n        return Updated(old);\r\n    }\r\n}\r\n<\/pre>\n<p>I change the base controller to <strong>ODataController<\/strong> to use &#8220;<strong>Created()<\/strong>&#8221; and &#8220;<strong>Updated()<\/strong>&#8221; methods.<\/p>\n<p>Same as serializer, I customize the deserializer by deriving form <strong>ODataResourceDeserializer<\/strong>:<\/p>\n<pre class=\"lang:c# decode:true\">public class ETagResourceDeserializer : ODataResourceDeserializer\r\n{\r\n    public ETagResourceDeserializer(IODataDeserializerProvider serializerProvider)\r\n        : base(serializerProvider)\r\n    { }\r\n\r\n    public override object ReadResource(ODataResourceWrapper resourceWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)\r\n    {\r\n        object resource = base.ReadResource(resourceWrapper, structuredType, readContext);\r\n        if (resourceWrapper.Resource.ETag != null)\r\n        {\r\n            Guid label = DecodeETag(resourceWrapper.Resource.ETag);\r\n            if (resource is Customer c)\r\n            {\r\n                \/\/ For POST scenario\r\n                c.Label = label;\r\n            }\r\n            else if (resource is IDelta delta)\r\n            {\r\n                \/\/ For PATCH scenario\r\n                \/\/ Have to use the reflection here to save the property \"Lavel\" into Hash-set.\r\n                Type deltaType = typeof(Delta&lt;&gt;).MakeGenericType(typeof(Customer));\r\n                var fieldInfo = deltaType.GetField(\"_updatableProperties\", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);\r\n                var updateProperties = fieldInfo.GetValue(delta) as HashSet&lt;string&gt;;\r\n                updateProperties.Add(\"Label\");\r\n                delta.TrySetPropertyValue(\"Label\", label);\r\n            }\r\n        }\r\n\r\n        return resource;\r\n    }\r\n\r\n    private static Guid DecodeETag(string etag)\r\n    {\r\n        \/\/ ...... same as above, Omit codes, \r\n    }\r\n}\r\n<\/pre>\n<p>In order to use <strong>ETagResourceDeserializer<\/strong>, I customize the deserializer provider to retrieve it:<\/p>\n<pre class=\"lang:c# decode:true\">public class ETagDeserializerProvider : ODataDeserializerProvider\r\n{\r\n    public ETagDeserializerProvider(IServiceProvider serviceProvider)\r\n        : base(serviceProvider)\r\n    { }\r\n\r\n    public override IODataEdmTypeDeserializer GetEdmTypeDeserializer(IEdmTypeReference edmType, bool isDelta = false)\r\n    {\r\n        if (edmType.IsEntity() || edmType.IsComplex())\r\n        {\r\n            return new ETagResourceDeserializer(this);\r\n        }\r\n\r\n        return base.GetEdmTypeDeserializer(edmType, isDelta);\r\n    }\r\n}\r\n<\/pre>\n<p>Add the deserializer provider into the service collection as:<\/p>\n<pre class=\"lang:c# decode:true\">    services.AddControllers()\r\n        .AddOData(opt =&gt; opt.AddRouteComponents(\"odata\", GetEdmModel(),\r\n            builder =&gt; builder.AddSingleton&lt;IODataSerializerProvider, ETagSerializerProvider&gt;()\r\n               .AddSingleton&lt;IODataDeserializerProvider, ETagDeserializerProvider&gt;()));\r\n<\/pre>\n<p>Now, build and run <strong>ODataETagWebApi <\/strong>project in debug model, put a break point in the <strong>Post()<\/strong> method, use Postman send request as:<\/p>\n<p><img decoding=\"async\" width=\"603\" height=\"213\" class=\"wp-image-4710\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-5.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-5.png 603w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-5-300x106.png 300w\" sizes=\"(max-width: 603px) 100vw, 603px\" \/><\/p>\n<p>We can get the following debug snapshot:<\/p>\n<p><img decoding=\"async\" width=\"952\" height=\"264\" class=\"wp-image-4711\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-6.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-6.png 952w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-6-300x83.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-6-768x213.png 768w\" sizes=\"(max-width: 952px) 100vw, 952px\" \/><\/p>\n<h2>Create entity using ETag from client<\/h2>\n<p>Let&#8217;s use OData client to replace Postman and send Post request to create a new customer with ETag value from <em>Label<\/em> property.<\/p>\n<p>First, create a new method in <strong>Program <\/strong>class of <strong>ODataETagClient <\/strong>project.<\/p>\n<pre class=\"lang:c# decode:true\">async static Task CreateCustomer(string name, Guid? label = null)\r\n{\r\n    Console.WriteLine($\"Create a new Customer: name={name}\");\r\n    Container context = GetContext();\r\n    Customer newCustomer = new Customer\r\n    {\r\n        Name = name,\r\n        Label = label ?? Guid.Empty\r\n    };\r\n\r\n    context.AddToCustomers(newCustomer);\r\n    DataServiceResponse responses = await context.SaveChangesAsync();\r\n    foreach (var response in responses)\r\n    {\r\n        Console.WriteLine($\" StatusCode={response.StatusCode}\");\r\n    }\r\n\r\n    Console.WriteLine();\r\n\r\n    \/\/ List Customers\r\n    await ListCustomers();\r\n}\r\n<\/pre>\n<p>Then, update &#8220;GetContext()&#8221; method to register a request Hook. I use &#8220;<strong>OnEntryStarting<\/strong>&#8221; request hook, for others, please refer to <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.odata.client.dataserviceclientrequestpipelineconfiguration?view=odata-client-7.0\" target=\"_blank\" rel=\"noopener\">here<\/a>:<\/p>\n<pre class=\"lang:c# decode:true\">private static Container GetContext()\r\n{\r\n    \/\/ ...... Omit codes\r\n\r\n    context.Configurations.RequestPipeline.OnEntryStarting(args =&gt;\r\n    {\r\n        Customer customer = args.Entity as Customer;\r\n        if (customer != null &amp;&amp; customer.Label != Guid.Empty)\r\n        {\r\n            args.Entry.ETag = EncodeETag(customer.Label);\r\n        }\r\n\r\n        args.Entry.Properties = args.Entry.Properties.ToList().Where(c =&gt; c.Name != \"Label\");\r\n    });\r\n\r\n    return context;\r\n}\r\n\r\nprivate static string EncodeETag(Guid guid)\r\n{\r\n    \/\/ ...... omit codes\r\n}\r\n<\/pre>\n<p>Now, let\u2019s change <strong>Main <\/strong>method as follows to test:<\/p>\n<pre class=\"lang:c# decode:true\">static void Main(string[] args)\r\n{\r\n    await ListCustomers();\r\n\r\n    await CreateCustomer(\"Sam\");\r\n\r\n    await CreateCustomer(\"Lucas\", new Guid(\"00000000-029B-484E-A257-111122223333\"));\r\n}\r\n<\/pre>\n<p>Let\u2019s config the startup project by right click on <strong>ODataETagExtensions <\/strong>as follows:<\/p>\n<p><img decoding=\"async\" width=\"782\" height=\"280\" class=\"wp-image-4712\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-7.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-7.png 782w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-7-300x107.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-7-768x275.png 768w\" sizes=\"(max-width: 782px) 100vw, 782px\" \/><\/p>\n<p>Open Fiddler, then build and run, we can see the following requests with the correct <em>odata.etag<\/em> value:<\/p>\n<p><img decoding=\"async\" width=\"1256\" height=\"512\" class=\"wp-image-4713\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-8.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-8.png 1256w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-8-300x122.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-8-1024x417.png 1024w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-8-768x313.png 768w\" sizes=\"(max-width: 1256px) 100vw, 1256px\" \/><\/p>\n<p>Here&#8217;s the console application output:\n<img decoding=\"async\" class=\"wp-image-4713\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/CreateCustomer2.png\" \/><\/p>\n<h2>Update entity using ETag from client<\/h2>\n<p>Let\u2019s create a new method in <strong>Program <\/strong>class of <strong>ODataETagClient<\/strong> project.<\/p>\n<pre class=\"lang:c# decode:true\">async static Task UpdateCustomer(int key, string name, Guid? label = null)\r\n{\r\n    Console.WriteLine($\"Update Customers({key}) with name={name}: \");\r\n\r\n    Container context = GetContext();\r\n\r\n    Customer customer = context.Customers.ByKey(key).GetValue();\r\n    customer.Name = name;\r\n\r\n    if (label != null)\r\n    {\r\n        customer.Label = label.Value;\r\n    }\r\n    context.UpdateObject(customer);\r\n\r\n    try\r\n    {\r\n        DataServiceResponse responses = await context.SaveChangesAsync();\r\n        foreach (var response in responses)\r\n        {\r\n            Console.WriteLine($\" StatusCode={response.StatusCode}\");\r\n        }\r\n    }\r\n    catch (DataServiceRequestException ex)\r\n    {\r\n        foreach (var response in ex.Response)\r\n        {\r\n            Console.WriteLine($\" StatusCode={response.StatusCode},\\n Message={ex.InnerException.Message}\");\r\n        }\r\n    }\r\n\r\n    Console.WriteLine();\r\n\r\n    \/\/ List Customers\r\n    await ListCustomers();\r\n}\r\n<\/pre>\n<p>Now, let\u2019s change Main method as:<\/p>\n<pre class=\"lang:c# decode:true\">static void Main(string[] args)\r\n{\r\n    Guid label = new Guid(\"00000000-029B-484E-A257-111122223333\");\r\n\r\n    await ListCustomers();\r\n\r\n    await CreateCustomer(\"Sam\");\r\n\r\n    await CreateCustomer(\"Lucas\", label);\r\n\r\n    await UpdateCustomer(1, \"Peter\");\r\n\r\n    await UpdateCustomer(2, \"John\", label);\r\n}\r\n<\/pre>\n<p>Let\u2019s run the <strong>ODataETagWebApi <\/strong>first again, then run the <strong>ODataETagClient <\/strong>console application.\nWe can get the following output:<\/p>\n<p><img decoding=\"async\" width=\"969\" height=\"333\" class=\"wp-image-4714\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-9.png\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-9.png 969w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-9-300x103.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/08\/word-image-9-768x264.png 768w\" sizes=\"(max-width: 969px) 100vw, 969px\" \/><\/p>\n<p>Where, you can see:<\/p>\n<ol>\n<li>Successfully update the name of Customer #1 from \u201cFreezing\u201d to \u201cPeter\u201d<\/li>\n<li>Can\u2019t update the name of Customer #2 from \u201cBracing\u201d to \u201cJohn\u201d because \u201c<code>ETag value cannot match!<\/code>\u201d.<\/li>\n<\/ol>\n<h2>Summary<\/h2>\n<p>This post went throw the steps on how to extend serializer and deserializer in ASP.NET Core OData 8 and how to use request and response hook in OData connected service. Hope it can help you with a similar requirement. Again, 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\/ODataETagExtensions\" target=\"_blank\" rel=\"noopener\">this<\/a>\u00a0repository.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction In this post, I will create formatter extensions in an OData web service and request\/response hooks in an OData client application to generate\/consume ETag control information. ETag (aka entity tag), one of OData control information that may be applied to an entity or collection in response, can be used in a subsequent request to [&hellip;]<\/p>\n","protected":false},"author":514,"featured_media":4144,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1472,1,117],"tags":[1474,1475],"class_list":["post-4703","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-odataconnectedservice"],"acf":[],"blog_post_summary":"<p>Introduction In this post, I will create formatter extensions in an OData web service and request\/response hooks in an OData client application to generate\/consume ETag control information. ETag (aka entity tag), one of OData control information that may be applied to an entity or collection in response, can be used in a subsequent request to [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4703","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=4703"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4703\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/4144"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=4703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}