{"id":4499,"date":"2021-07-09T11:36:20","date_gmt":"2021-07-09T18:36:20","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=4499"},"modified":"2021-07-09T11:36:20","modified_gmt":"2021-07-09T18:36:20","slug":"api-versioning-extension-with-asp-net-core-odata-8","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/api-versioning-extension-with-asp-net-core-odata-8\/","title":{"rendered":"API versioning extension with ASP.NET Core OData 8"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>API versioning can help evolving our APIs without changing or breaking the existing API services. URL segment, request header, and query string are three ways to achieve API versioning in ASP.NET Core application.<\/p>\n<p><a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.OData\/8.0.1\" rel=\"noopener\" target=\"_blank\">ASP.NET Core OData 8<\/a>, built upon ASP.NET Core, has the built-in API versioning functionality via route URL prefix template. For instance, the following code configures a <strong>version<\/strong> template in the route URL prefix to achieve URL based API versioning:<\/p>\n<pre class=\"lang:c# decode:true\">  services.AddControllers()\r\n    .AddOData(opt =&gt; opt.AddRouteComponents(\"v{version}\", edmModel));\r\n<\/pre>\n<p>Based on this configuration, it supports API versioning using URL segment as:<\/p>\n<ul>\n<li>http:\/\/localhost:5000\/<strong>v1.0<\/strong>\/Customers<\/li>\n<li>http:\/\/localhost:5000\/<strong>v2.0<\/strong>\/Customers<\/li>\n<\/ul>\n<p>ASP.NET Core OData 8 doesn&#8217;t have the built-in API versioning based on query string and request header. However, it&#8217;s easy to extend the package to achieve these two API versionings. This post will create the extensions to build the query string API versioning with ASP.NET Core OData 8.x and share with you the ideas of how easy to extend ASP.NET Core OData 8. The same way also applies to the request header.<\/p>\n<p>Let&#8217;s get started.<\/p>\n<h2>Scenarios<\/h2>\n<p>We want to build an API which can return the different version of <strong>Customers<\/strong> data based on <strong>api-version<\/strong> query string using the <strong>same<\/strong> request URL, for example:<\/p>\n<ul>\n<li><a href=\"http:\/\/localhost:5000\/Customers?api-version=1.0\">http:\/\/localhost:5000\/Customers?api-version=1.0<\/a> returns &#8220;<strong>Customers<\/strong>&#8221; for version 1.0<\/li>\n<li><a href=\"http:\/\/localhost:5000\/Customers?api-version=2.0\">http:\/\/localhost:5000\/Customers?api-version=2.0<\/a> returns &#8220;<strong>Customers<\/strong>&#8221; for version 2.0<\/li>\n<li><a href=\"http:\/\/localhost:5000\/Customers?api-version=3.0\">http:\/\/localhost:5000\/Customers?api-version=3.0<\/a> returns &#8220;<strong>Version 3.0 NotSupported exception<\/strong>&#8220;.<\/li>\n<\/ul>\n<p><strong>Be noted<\/strong>, v1 and v2 use the same HTTP request path.<\/p>\n<h2>Prerequisites<\/h2>\n<p>Let us create an ASP.NET Core Application called &#8220;<strong>ODataApiVersion<\/strong>&#8221; using Visual Studio 2019. You can follow up any guide or refer to <a href=\"https:\/\/devblogs.microsoft.com\/odata\/asp-net-odata-8-0-preview-for-net-5\/\">ASP.NET Core OData 8.0 Preview for .NET 5<\/a> to create this application.<\/p>\n<p>We install the following nuget packages:<\/p>\n<ul>\n<li>Microsoft.AspNetCore.OData -version 8.0.1<\/li>\n<li>Microsoft.AspNetCore.Mvc.Versioning -version 5.0<\/li>\n<\/ul>\n<h2>CLR Model<\/h2>\n<p>Once the application is created, let\u2019s create a folder named &#8220;Models&#8221; in the solution explorer. In this folder, let&#8217;s create the following three C# classes for our CLR model:<\/p>\n<pre class=\"lang:c# decode:true\">Namespace ODataApiVersion.Models\r\n{\r\n    public abstract class CustomerBase\r\n    {\r\n        public int Id { get; set; }\r\n        public string ApiVersion { get; set; }\r\n     }\r\n}\r\n\r\nNamespace ODataApiVersion.Models.<strong>v1<\/strong>\r\n{\r\n    public class Customer : CustomerBase\r\n    {\r\n        public string Name { get; set; }\r\n        public string PhoneNumber { get; set; }\r\n    }\r\n}\r\n\r\nNamespace ODataApiVersion.Models.<strong>v2<\/strong>\r\n{\r\n    public class Customer : CustomerBase\r\n    {\r\n        public string FirstName { get; set; }\r\n        public string LastName { get; set; }\r\n        public string Email { get; set; }\r\n    }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>: the two concrete classes have the same name &#8220;<strong>Customer<\/strong>&#8221; but in different namespace.<\/p>\n<h2>Edm Model provider<\/h2>\n<p>We need an Edm model provider to provide the Edm model based on the API version.<\/p>\n<p>Let&#8217;s create the following interface and use it as a service in the dependency injection:<\/p>\n<pre class=\"lang:c# decode:true\">public interface IODataModelProvider\r\n{\r\n    IEdmModel GetEdmModel(string apiVersion);\r\n}\r\n<\/pre>\n<p>We create a default implementation for the model provider interface as<\/p>\n<pre class=\"lang:c# decode:true\">public class MyODataModelProvider : IODataModelProvider\r\n{\r\n    private IDictionary&lt;string, IEdmModel&gt; _cached = new Dictionary&lt;string, IEdmModel&gt;();\r\n    public IEdmModel GetEdmModel(string apiVersion)\r\n    {\r\n        if (_cached.TryGetValue(apiVersion, out IEdmModel model))\r\n        {\r\n            return model;\r\n        }\r\n\r\n        model = BuildEdmModel(apiVersion);\r\n        _cached[apiVersion] = model;\r\n        return model;\r\n    }\r\n\r\n    private static IEdmModel BuildEdmModel(string version)\r\n    {\r\n        switch (version)\r\n        {\r\n            case \"1.0\": return BuildV1Model();\r\n            case \"2.0\": return BuildV2Model();\r\n        }\r\n\r\n        throw new NotSupportedException($\"The input version '{version}' is not supported!\");\r\n    }\r\n\r\n    private static IEdmModel BuildV1Model()\r\n    {\r\n        var builder = new ODataConventionModelBuilder();\r\n        builder.EntitySet&lt;Models.v1.Customer&gt;(\"Customers\");\r\n        return builder.GetEdmModel();\r\n    }\r\n\r\n    private static IEdmModel BuildV2Model()\r\n    {\r\n        var builder = new ODataConventionModelBuilder();\r\n        builder.EntitySet&lt;Models.v2.Customer&gt;(\"Customers\");\r\n        return builder.GetEdmModel();\r\n    }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>: v1 and v2 Edm model have the same entity set named &#8220;<strong>Customers<\/strong>&#8220;.<\/p>\n<h2>CustomersController<\/h2>\n<p>We need two controllers to handle the same request for different API versions. In the &#8220;Controllers&#8221; folder, add two controllers using the same name &#8220;<strong>CustomersController<\/strong>&#8221; but different namespace.<\/p>\n<pre class=\"lang:c# decode:true\">namespace ODataApiVersion.Controllers.<strong>v1<\/strong>\r\n{\r\n    [ApiVersion(\"1.0\")]\r\n    public class CustomersController : ODataController\r\n    {\r\n        private Customer[] customers = new Customer[]\r\n        {\r\n            \/\/ ...... Omit the codes, you can find them from the project\r\n        };\r\n\r\n        [EnableQuery]\r\n        public IActionResult Get()\r\n        {\r\n            return Ok(customers);\r\n        }\r\n\r\n        [EnableQuery]\r\n        public IActionResult Get(int key)\r\n        {\r\n            var customer = customers.FirstOrDefault(c =&gt; c.Id == key);\r\n            if (customer == null)\r\n            {\r\n                return NotFound($\"Cannot find customer with Id={key}.\");\r\n            }\r\n\r\n            return Ok(customer);\r\n\r\n         }\r\n    }\r\n}\r\n\r\nnamespace ODataApiVersion.Controllers.<strong>v2<\/strong>\r\n{\r\n    [ApiVersion(\"2.0\")]\r\n    public class CustomersController : ODataController\r\n    {\r\n        private Customer[] _customers = new Customer[]\r\n        {\r\n            \/\/ ...... Omit the codes, you can find them from the project\r\n        };\r\n\r\n        [EnableQuery]\r\n        public IActionResult Get()\r\n        {\r\n            return Ok(customers);\r\n        }\r\n\r\n        [EnableQuery]\r\n        public IActionResult Get(int key)\r\n        {\r\n            \/\/ ...... Omit the codes, you can find them from the project\r\n         }\r\n    }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>: Each controller has [<strong>ApiVersionAttribute<\/strong>] decorated using different version string.<\/p>\n<h2>Construct the routing template<\/h2>\n<p>We need to build the routing template for the action in the controller, which is used to match the coming request. The built-in OData routing convention cannot meet this requirement. So, we have to build the routing template using an <strong>IApplicationModelProvider <\/strong>.<\/p>\n<pre class=\"lang:c# decode:true\">public class MyODataRoutingApplicationModelProvider : IApplicationModelProvider\r\n{\r\n    public int Order =&gt; 90;\r\n    public void OnProvidersExecuted(ApplicationModelProviderContext context)\r\n    {\r\n        IEdmModel model = EdmCoreModel.Instance; \/\/ just for place holder\r\n        string prefix = string.Empty;\r\n        foreach (var controllerModel in context.Result.Controllers)\r\n        {\r\n            \/\/ CustomersController\r\n            if (controllerModel.ControllerName == \"Customers\")\r\n            {\r\n                ProcessCustomersController(prefix, model, controllerModel);\r\n                continue;\r\n            }\r\n\r\n            \/\/ MetadataController\r\n            if (controllerModel.ControllerName == \"Metadata\")\r\n            {\r\n                ProcessMetadata(prefix, model, controllerModel);\r\n                continue;\r\n            }\r\n        }\r\n    }\r\n\r\n    public void OnProvidersExecuting(ApplicationModelProviderContext context)\r\n    {}\r\n\r\n    private static void ProcessCustomersController(string prefix, IEdmModel model, ControllerModel controllerModel)\r\n    {\r\n        foreach (var actionModel in controllerModel.Actions)\r\n        {\r\n            \/\/ For simplicity, only check the parameter count\r\n            if (actionModel.ActionName == \"Get\")\r\n            {\r\n                if (actionModel.Parameters.Count == 0)\r\n                {\r\n                    ODataPathTemplate path = new ODataPathTemplate(new EntitySetCustomersSegment());\r\n                    actionModel.AddSelector(\"get\", prefix, model, path);\r\n                }\r\n                else\r\n                {\r\n                   ODataPathTemplate path = new ODataPathTemplate(\r\n                        new EntitySetCustomersSegment(),\r\n                        new EntitySetWithKeySegment());\r\n                   actionModel.AddSelector(\"get\", prefix, model, path);\r\n                }\r\n            }\r\n        }\r\n    }\r\n\r\n    private static void ProcessMetadata(string prefix, IEdmModel model, ControllerModel controllerModel)\r\n    {\r\n        \/\/ ...... Omit the codes, you can find them from the project\r\n    }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>: the above codes handle the actions on:<\/p>\n<ul>\n<li><strong>v1.CustomersController<\/strong><\/li>\n<li><strong>v2.CustomersController<\/strong><\/li>\n<li><strong>MetadataController<\/strong><\/li>\n<\/ul>\n<p>Besides, <strong>EntitySetCustomersSegment<\/strong> has the following codes:<\/p>\n<pre class=\"lang:c# decode:true\">public class EntitySetCustomersSegment : ODataSegmentTemplate\r\n{\r\n    public override IEnumerable&lt;string&gt; GetTemplates(ODataRouteOptions options)\r\n    {\r\n        yield return \"\/Customers\";\r\n    }\r\n\r\n    public override bool TryTranslate(ODataTemplateTranslateContext context)\r\n    {\r\n        \/\/ Support case-insenstivie\r\n        var edmEntitySet = context.Model.EntityContainer.EntitySets()\r\n            .FirstOrDefault(e =&gt; string.Equals(\"Customers\", e.Name, StringComparison.OrdinalIgnoreCase));\r\n\r\n        if (edmEntitySet != null)\r\n        {\r\n            EntitySetSegment segment = new EntitySetSegment(edmEntitySet);\r\n            context.Segments.Add(segment);\r\n            return true;\r\n        }\r\n\r\n        return false;\r\n    }\r\n}\r\n<\/pre>\n<p>And <strong>EntitySetWithKeySegment<\/strong> has the following codes:<\/p>\n<pre class=\"lang:c# decode:true\">public class EntitySetWithKeySegment : ODataSegmentTemplate\r\n{\r\n    public override IEnumerable&lt;string&gt; GetTemplates(ODataRouteOptions options)\r\n    {\r\n        yield return \"\/{key}\";\r\n     \/\/ yield return \"({key})\"; enable it if you want to support key in parenthesis\r\n    }\r\n\r\n    public override bool TryTranslate(ODataTemplateTranslateContext context)\r\n    {\r\n        \/\/ ...... Omit the codes, you can find them from the project\r\n    }\r\n}\r\n<\/pre>\n<h2>Routing matcher policy<\/h2>\n<p>We need a routing matcher policy to select the best endpoint. Here&#8217;s our implementation<\/p>\n<pre class=\"lang:c# decode:true\">internal class MyODataRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy\r\n{\r\n    private readonly IODataTemplateTranslator _translator;\r\n    private readonly IODataModelProvider _provider;\r\n    private readonly ODataOptions _options;\r\n\r\n    public MyODataRoutingMatcherPolicy(IODataTemplateTranslator translator,\r\n        IODataModelProvider provider,\r\n        IOptions&lt;ODataOptions&gt; options)\r\n    {\r\n        _translator = translator;\r\n        _provider = provider;\r\n        _options = options.Value;\r\n    }\r\n\r\n    public override int Order =&gt; 900 - 1; \/\/ minus 1 to make sure it's running before built-in OData matcher policy\r\n\r\n    public bool AppliesToEndpoints(IReadOnlyList&lt;Endpoint&gt; endpoints)\r\n    {\r\n        return endpoints.Any(e =&gt; e.Metadata.OfType&lt;ODataRoutingMetadata&gt;().FirstOrDefault() != null);\r\n    }\r\n\r\n    public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)\r\n    {\r\n        \/\/ ...... omit some checking codes\r\n\r\n        for (var i = 0; i &lt; candidates.Count; i++)\r\n        {\r\n            ref CandidateState candidate = ref candidates[i];\r\n            if (!candidates.IsValidCandidate(i))\r\n            {\r\n                continue;\r\n            }\r\n\r\n            IODataRoutingMetadata metadata = candidate.Endpoint.Metadata.OfType&lt;IODataRoutingMetadata&gt;().FirstOrDefault();\r\n            if (metadata == null)\r\n            {\r\n                continue;\r\n            }\r\n\r\n            \/\/ Get api-version query from HttpRequest?\r\n            QueryStringApiVersionReader reader = new QueryStringApiVersionReader(\"api-version\");\r\n            string apiVersionStr = reader.Read(httpContext.Request);\r\n            if (apiVersionStr == null)\r\n            {\r\n                candidates.SetValidity(i, false);\r\n                continue;\r\n            }\r\n\r\n            ApiVersion apiVersion = ApiVersion.Parse(apiVersionStr);\r\n            IEdmModel model = GetEdmModel(apiVersion);\r\n            if (model == null)\r\n            {\r\n                candidates.SetValidity(i, false);\r\n                continue;\r\n            }\r\n\r\n            if (!IsApiVersionMatch(candidate.Endpoint.Metadata, apiVersion))\r\n            {\r\n                candidates.SetValidity(i, false);\r\n                continue;\r\n            }\r\n\r\n            ODataTemplateTranslateContext translatorContext\r\n                = new ODataTemplateTranslateContext(httpContext, candidate.Endpoint, candidate.Values, model);\r\n\r\n            try\r\n            {\r\n                ODataPath odataPath = _translator.Translate(metadata.Template, translatorContext);\r\n                if (odataPath != null)\r\n                {\r\n                    odataFeature.RoutePrefix = metadata.Prefix;\r\n                    odataFeature.Model = model;\r\n                    odataFeature.Path = odataPath;\r\n\r\n                    ODataOptions options = new ODataOptions();\r\n                    UpdateQuerySetting(options);\r\n                    options.AddRouteComponents(model);\r\n                    odataFeature.Services = options.GetRouteServices(string.Empty);\r\n\r\n                    MergeRouteValues(translatorContext.UpdatedValues, candidate.Values);\r\n                }\r\n                else\r\n                {\r\n                    candidates.SetValidity(i, false);\r\n                }\r\n            }\r\n            catch\r\n            {\r\n                candidates.SetValidity(i, false);\r\n            }\r\n        }\r\n\r\n        return Task.CompletedTask;\r\n    }\r\n\r\n    private void UpdateQuerySetting(ODataOptions options)\r\n    {\r\n        \/\/ ...... omit the setting copy codes\r\n    }\r\n\r\n    private static void MergeRouteValues(RouteValueDictionary updates, RouteValueDictionary source)\r\n    {\r\n        foreach (var data in updates)\r\n        {\r\n            source[data.Key] = data.Value;\r\n        }\r\n    }\r\n\r\n    private IEdmModel GetEdmModel(ApiVersion apiVersion)\r\n    {\r\n        return _provider.GetEdmModel(apiVersion.ToString());\r\n    }\r\n\r\n    private static bool IsApiVersionMatch(EndpointMetadataCollection metadata, ApiVersion apiVersion)\r\n    {\r\n        var apiVersions = metadata.OfType&lt;ApiVersionAttribute&gt;().ToArray();\r\n        if (apiVersions.Length == 0)\r\n        {\r\n            \/\/ If no [ApiVersion] on the controller,\r\n            \/\/ Let's simply return true, it means it can work the input version or any version.\r\n            return true;\r\n        }\r\n\r\n        foreach (var item in apiVersions)\r\n        {\r\n            if (item.Versions.Contains(apiVersion))\r\n            {\r\n                return true;\r\n            }\r\n        }\r\n\r\n        return false;\r\n    }\r\n}\r\n<\/pre>\n<p><strong>Be noted<\/strong>: The order value is &#8220;900 &#8211; 1&#8221; to make sure this policy is applied before the built-in OData routing match policy.<\/p>\n<h2>Config the services<\/h2>\n<p>Now, let\u2019s configure the above extensions as services into the service collection in the <strong>Startup<\/strong> class as below:<\/p>\n<pre class=\"lang:c# decode:true\">public void ConfigureServices(IServiceCollection services)\r\n{\r\n    services.AddControllers().AddOData();\r\n    services.TryAddSingleton&lt;IODataModelProvider, MyODataModelProvider&gt;();\r\n    services.TryAddEnumerable(\r\n        ServiceDescriptor.Transient&lt;IApplicationModelProvider, MyODataRoutingApplicationModelProvider&gt;());\r\n    services.TryAddEnumerable(ServiceDescriptor.Singleton&lt;MatcherPolicy, MyODataRoutingMatcherPolicy&gt;());\r\n}\r\n\r\npublic void Configure(IApplicationBuilder app, IWebHostEnvironment env)\r\n{\r\n    \/\/ ......\r\n    app.UseODataRouteDebug();\r\n\r\n    app.UseRouting();\r\n    \/\/ ......\r\n}\r\n<\/pre>\n<h2>Routing Debug<\/h2>\n<p>You may noticed we call <strong>app.UseODataRouteDebug()<\/strong> in <strong>Configure(&#8230;)<\/strong> method. This function enables <strong>\/$odata<\/strong> middleware. Run the application and send <a href=\"http:\/\/localhost:5000\/$odata\">http:\/\/localhost:5000\/$odata<\/a> in any internet browser, you can get the following html page:<\/p>\n<p><img decoding=\"async\" class=\"aligncenter wp-image-4512 size-full\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/07\/dollarOData.png\" alt=\"Image dollarOData\" width=\"1167\" height=\"562\" srcset=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/07\/dollarOData.png 1167w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/07\/dollarOData-300x144.png 300w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/07\/dollarOData-1024x493.png 1024w, https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2021\/07\/dollarOData-768x370.png 768w\" sizes=\"(max-width: 1167px) 100vw, 1167px\" \/><\/p>\n<p>This HTML page gives you a whole routing template picture. You can see we have the same routing templates for different actions. For example, <strong>Get(int key)<\/strong> action in <strong>v1.CustomersController<\/strong> and <strong>v2.CustomersController<\/strong> have the same routing template as <strong>~\/Customers\/{key}<\/strong>.<\/p>\n<h2>Run and test the functionalities<\/h2>\n<p>Now, we can run and test the API versioning functionalities.<\/p>\n<h2>Query metadata<\/h2>\n<p>As mentioned, we have the codes in <strong>MyODataRoutingApplicationModelProvider<\/strong> to process the <strong>MetadataController<\/strong>, it supports the metadata versioning.<\/p>\n<p>Send Http request <a href=\"http:\/\/localhost:5000\/$metadata?api-version=1.0\">http:\/\/localhost:5000\/$metadata?api-version=1.0<\/a>, you can get:<\/p>\n<p><script type=\"text\/javascript\" src=\"https:\/\/gist.github.com\/xuzhg\/2c87730f30c88303beb6adbffe233b12.js\"><\/script><\/p>\n<p>It is also working with <a href=\"http:\/\/localhost:5000\/$metadata?api-version=2.0\">http:\/\/localhost:5000\/$metadata?api-version=2.0<\/a> Http request, it will return the following metadata.<\/p>\n<p><script type=\"text\/javascript\" src=\"https:\/\/gist.github.com\/xuzhg\/30c1c3d885a0dc13f2568f44e4d37858.js\"><\/script><\/p>\n<p>If you send Http request using unsupported version, for example, <a href=\"http:\/\/localhost:5000\/$metadata?api-version=3.0\">http:\/\/localhost:5000\/$metadata?api-version=3.0<\/a>, you will get<\/p>\n<p><strong>System.NotSupportedException: The input version 3.0 is not supported!<\/strong><\/p>\n<h2>Query Customers<\/h2>\n<p>We can query the entity set <strong>Customers<\/strong> using a different API version.<\/p>\n<p>Send Http request <a href=\"http:\/\/localhost:5000\/Customers?api-version=1.0\">http:\/\/localhost:5000\/Customers?api-version=1.0<\/a>, you can get the following JSON response:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\":\u00a0\"http:\/\/localhost:5000\/$metadata#Customers\",\r\n  \"value\":\u00a0[\r\n    {\r\n      \"Name\":\u00a0\"Sam\",\r\n      \"PhoneNumber\":\u00a0\"111-222-3333\",\r\n      \"Id\":\u00a01,\r\n      \"ApiVersion\":\u00a0\"v1.0\"\r\n    },\r\n    {\r\n      \"Name\":\u00a0\"Peter\",\r\n      \"PhoneNumber\":\u00a0\"456-ABC-8888\",\r\n      \"Id\":\u00a02,\r\n      \"ApiVersion\":\u00a0\"v1.0\"\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Send <a href=\"http:\/\/localhost:5000\/Customers?api-version=2.0\">http:\/\/localhost:5000\/Customers?api-version=2.0<\/a> request, you can get the following JSON response:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\":\u00a0\"http:\/\/localhost:5000\/$metadata#Customers\",\r\n  \"value\":\u00a0[\r\n    {\r\n      \"FirstName\":\u00a0\"YXS\",\r\n      \"LastName\":\u00a0\"WU\",\r\n      \"Email\":\u00a0\"yxswu@abc.com\",\r\n      \"Id\":\u00a011,\r\n      \"ApiVersion\":\u00a0\"v2.0\"\r\n    },\r\n    {\r\n      \"FirstName\":\u00a0\"KIO\",\r\n      \"LastName\":\u00a0\"XU\",\r\n      \"Email\":\u00a0\"kioxu@efg.com\",\r\n      \"Id\":\u00a012,\r\n      \"ApiVersion\":\u00a0\"v2.0\"\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Send <a href=\"http:\/\/localhost:5000\/Customers?api-version=3.0\">http:\/\/localhost:5000\/ShipmentContracts?api-version=3.0<\/a> request, you will get the same error:<\/p>\n<p><strong>System.NotSupportedException: The input version 3.0 is not supported!<\/strong><\/p>\n<p>Since we have created the single entity route template, the following URLs also work as expected.<\/p>\n<ul>\n<li>http:\/\/localhost:5000\/Customers\/2?api-version=1.0<\/li>\n<li>http:\/\/localhost:5000\/Customers\/12?api-version=2.0<\/li>\n<\/ul>\n<h2>Using OData query option<\/h2>\n<p>You can use the config methods on <strong>ODataOptions <\/strong>to enable OData query option. For instance, you can call &#8220;<strong>Select<\/strong>()&#8221; to enable <strong>$select<\/strong> OData query option.<\/p>\n<pre class=\"lang:c# decode:true\">\r\nservices.AddControllers().AddOData(opt =&gt; opt.Select());\r\n<\/pre>\n<p>Now, we have the $select functionality enabled.<\/p>\n<p>Send <a href=\"http:\/\/localhost:5000\/Customers?api-version=2.0&amp;$select=Email,LastName\">http:\/\/localhost:5000\/Customers?api-version=2.0&amp;$select=Email,ApiVersion<\/a><\/p>\n<p>You can get the following response payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n    \"@odata.context\": \"http:\/\/localhost:5000\/$metadata#Customers(Email,ApiVersion)\",\r\n    \"value\": [\r\n        {\r\n            \"Email\": \"yxswu@abc.com\",\r\n            \"ApiVersion\": \"v2.0\"\r\n        },\r\n        {\r\n            \"Email\": \"kioxu@efg.com\",\r\n            \"ApiVersion\": \"v2.0\"\r\n        }\r\n    ]\r\n}\r\n<\/pre>\n<p>Please try other config methods in <strong>ODataOptions<\/strong> to enable more OData query option functionalities.<\/p>\n<h2>Summary<\/h2>\n<p>This post went throw the steps on how to enable API query string versioning with ASP.NET Core OData 8. Hope the ideas and implementations in this post can help you understand how to extend the functionality for ASP.NET Core OData. Please do not hesitate to leave your comments below or let me know your thoughts through <a href=\"mailto:saxu@microsoft.com\">saxu@microsoft.com<\/a>. Thanks.<\/p>\n<p>I uploaded the whole project to <a href=\"https:\/\/github.com\/xuzhg\/MyAspNetCore\/tree\/master\/src\/ODataApiVersion\">this<\/a> repository.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction API versioning can help evolving our APIs without changing or breaking the existing API services. URL segment, request header, and query string are three ways to achieve API versioning in ASP.NET Core application. ASP.NET Core OData 8, built upon ASP.NET Core, has the built-in API versioning functionality via route URL prefix template. For instance, [&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":[1473,502,48],"class_list":["post-4499","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-asp-net-core","category-odata","category-webapi","tag-api-versioning","tag-asp-net-core","tag-odata"],"acf":[],"blog_post_summary":"<p>Introduction API versioning can help evolving our APIs without changing or breaking the existing API services. URL segment, request header, and query string are three ways to achieve API versioning in ASP.NET Core application. ASP.NET Core OData 8, built upon ASP.NET Core, has the built-in API versioning functionality via route URL prefix template. For instance, [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4499","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=4499"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/4499\/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=4499"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=4499"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=4499"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}