{"id":5932,"date":"2025-10-07T09:22:28","date_gmt":"2025-10-07T16:22:28","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/odata\/?p=5932"},"modified":"2025-11-25T17:48:40","modified_gmt":"2025-11-26T00:48:40","slug":"enable-odata-functionalities-on-asp-net-core-minimal-api","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/odata\/enable-odata-functionalities-on-asp-net-core-minimal-api\/","title":{"rendered":"Enable OData functionalities on ASP.NET Core Minimal API"},"content":{"rendered":"<h1><strong>Introduction<\/strong><\/h1>\n<p>Minimal API is a simplified approach for building HTTP APIs fast within ASP.NET Core, compared to the controller-based APIs. Developers can build fully functioning REST endpoints with minimal code and configuration, especially without controller, action and even the formatters. See ASP.NET Core Minimal API details at <a href=\"https:\/\/learn.microsoft.com\/en-us\/aspnet\/core\/fundamentals\/apis?view=aspnetcore-10.0\" target=\"_blank\" rel=\"noopener\">here<\/a>.<\/p>\n<p>Since <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.AspNetCore.OData\/9.4.0\">Microsoft.AspNetCore.OData version 9.4.0<\/a>, it enables developers to achieve OData functionalities on ASP.NET Core minimal APIs. In this post, I\u2019d like to go through the basics of enabling OData functionalities on a Minimal API application. Let\u2019s get started.<\/p>\n<h1><strong>Prerequisites<\/strong><\/h1>\n<p>First of first, let&#8217;s create an ASP.NET Core Empty application named <strong><em>ODataMinimalAPI<\/em><\/strong> with\u00a0following Nuget packages installed.<\/p>\n<pre class=\"lang:c# decode:true\">&lt;ItemGroup&gt;\r\n    &lt;PackageReference Include=\"Microsoft.AspNetCore.OData\" Version=\"9.4.1\" \/&gt;\r\n    &lt;PackageReference Include=\"Microsoft.EntityFrameworkCore.InMemory\" Version=\"9.0.11\" \/&gt;\r\n&lt;\/ItemGroup&gt;\r\n\r\n<\/pre>\n<p>The <em>Program.cs<\/em> contains the following codes:<\/p>\n<pre class=\"lang:c# decode:true\">var builder = WebApplication.CreateBuilder(args);\r\n\r\nvar app = builder.Build();\r\napp.MapGet(\"\/\", () =&gt; \"Hello World!\");\r\n\r\napp.Run();\r\n<\/pre>\n<p>The lambda expression in preceding\u00a0<em>MapGet<\/em> is called\u00a0<strong>Route Handler<\/strong>. They are methods that execute when the route matches. Route handlers can be:<\/p>\n<ul>\n<li>A lambda expression,<\/li>\n<li>A local function,<\/li>\n<li>An instance method<\/li>\n<li>A static method<\/li>\n<li>Or a RequestDelegate<\/li>\n<\/ul>\n<p>Basically, ASP.NET Core OData decorates the endpoint with metadata or even changes the route handler to enable OData functionalities.<\/p>\n<h1><strong>Models and database context<\/strong><\/h1>\n<p>In the project, Let&#8217;s create the following classes to represent data managed in the application:<\/p>\n<pre class=\"lang:c# decode:true\">public class Customer\r\n{\r\n    public int Id { get; set; }\r\n    public string Name { get; set; }\r\n    public Address Location { get; set; }\r\n    public Info Info { get; set; }\r\n    public List&lt;Order&gt; Orders { get; set; }\r\n}\r\n\r\npublic class Order\r\n{\r\n    public int Id { get; set; }\r\n    public int Amount { get; set; }\r\n}\r\n\r\npublic class Address\r\n{\r\n    public string City { get; set; }\r\n    public string Street { get; set; }\r\n}\r\n\r\npublic class Info\r\n{\r\n    public string Email { get; set; }\r\n    public string Phone { get; set; }\r\n}\r\n\r\n<\/pre>\n<p>Let&#8217;s also create a <em><strong>DbContext<\/strong><\/em> as below:<\/p>\n<pre class=\"lang:c# decode:true\">using Microsoft.EntityFrameworkCore;\r\n\r\npublic class ApplicationDb : DbContext\r\n{\r\n    public ApplicationDb(DbContextOptions&lt;ApplicationDb&gt; options) : base(options) { }\r\n\r\n    public DbSet&lt;Customer&gt; Customers=&gt; Set&lt;Customer&gt;();\r\n\r\n    public DbSet&lt;Order&gt; Orders =&gt; Set&lt;Order&gt;();\r\n\r\n    protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n    {\r\n        base.OnModelCreating(modelBuilder);\r\n        modelBuilder.Entity&lt;Customer&gt;().HasKey(x =&gt; x.Id);\r\n        modelBuilder.Entity&lt;Order&gt;().HasKey(x =&gt; x.Id);\r\n        modelBuilder.Entity&lt;Customer&gt;().OwnsOne(x =&gt; x.Info);\r\n        modelBuilder.Entity&lt;Customer&gt;().OwnsOne(x =&gt; x.Location);\r\n    }\r\n}\r\n<\/pre>\n<p>Then, update <em>Program.cs<\/em> as below:<\/p>\n<pre class=\"lang:c# decode:true\">using Microsoft.EntityFrameworkCore;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddDbContext&lt;ApplicationDb&gt;(options =&gt; options.UseInMemoryDatabase(\"CustomerOrderList\"));\r\n\r\nvar app = builder.Build();\r\n\r\napp.MakeSureDbCreated(); \/\/ Create sample data ahead\r\n\r\napp.MapGet(\"\/json\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n\r\napp.Run();\r\n\r\n<\/pre>\n<p>Run the application. Open <strong>&#8216;Endpoints Explorer&#8217;<\/strong> by clicking [<strong>View<\/strong>]-&gt; [<strong>Other Windows<\/strong>]-&gt; [<strong>Endpoints Explorer<\/strong>] menu, you may see an endpoint listed as:<\/p>\n<p><img decoding=\"async\" class=\"wp-image-5352\" src=\"https:\/\/devblogs.microsoft.com\/odata\/wp-content\/uploads\/sites\/23\/2025\/10\/json_customers_request.webp\" alt=\"Endpoints\" \/><\/p>\n<p>Right-click on the <strong><em>\/json\/customers<\/em><\/strong> and select [<strong>Generate Request<\/strong>] menu, a <em>ODataMinimalApi.http<\/em> is created with a Http Request. We can send a request by clicking [<strong>Send request<\/strong>] on the endpoint in the http file and check the response. Below is the sample data created ahead by sending request to <strong><em>\/json\/customers<\/em><\/strong>.<\/p>\n<pre class=\"lang:json decode:true\">[\r\n  {\r\n    \"id\": 1,\r\n    \"name\": \"Alice\",\r\n    \"location\": {\r\n      \"city\": \"Redmond\",\r\n      \"street\": \"123 Main St\"\r\n    },\r\n    \"info\": {\r\n      \"email\": \"alice@example.com\",\r\n      \"phone\": \"123-456-7819\"\r\n    },\r\n    \"orders\": [\r\n      {\r\n        \"id\": 11,\r\n        \"amount\": 9\r\n      },\r\n      {\r\n        \"id\": 12,\r\n        \"amount\": 19\r\n      }\r\n    ]\r\n  },\r\n  {\r\n    \"id\": 2,\r\n    \"name\": \"Johnson\",\r\n    \"location\": {\r\n      \"city\": \"Sammamish\",\r\n      \"street\": \"228 Ave NE\"\r\n    },\r\n    \"info\": {\r\n      \"email\": \"johnson@abc.com\",\r\n      \"phone\": \"233-468-7289\"\r\n    },\r\n    \"orders\": [\r\n      {\r\n        \"id\": 21,\r\n        \"amount\": 8\r\n      },\r\n      {\r\n        \"id\": 22,\r\n        \"amount\": 76\r\n      }\r\n    ]\r\n  }\r\n]\r\n<\/pre>\n<h1><strong>OData Edm Model<\/strong><\/h1>\n<p>Let&#8217;s create the following OData Edm model ahead which is used in some scenarios below. Be noted, I mark <strong><em>Address<\/em><\/strong> as entity type, and <strong><em>Info<\/em><\/strong> as complex type explicitly.<\/p>\n<pre class=\"lang:c# decode:true\">public class EdmModelBuilder\r\n{\r\n    public static IEdmModel GetEdmModel()\r\n    {\r\n        var builder = new ODataConventionModelBuilder();\r\n\r\n        builder.EntitySet&lt;Customer&gt;(\"Customers\");\r\n        builder.EntitySet&lt;Order&gt;(\"Orders\");\r\n\r\n        builder.EntityType&lt;Address&gt;(); \/\/ Intentionally built it as entity type\r\n        builder.ComplexType&lt;Info&gt;(); \/\/ Intentionally built it as complex type\r\n\r\n        return builder.GetEdmModel();\r\n    }\r\n}\r\n<\/pre>\n<h1><strong>OData basic scenarios<\/strong><\/h1>\n<h2><strong>WithODataResult()<\/strong><\/h2>\n<p>We enable developers to call an extension method named\u00a0<strong><em>WithODataResult()<\/em><\/strong>\u00a0on the endpoint to get &#8216;<em>OData-formatted<\/em>&#8216; response. Let\u2019s add a new endpoint as below:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"odata\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .WithODataResult();\r\n<\/pre>\n<p>Then, generate a request in <em>ODataMinimalApi.http<\/em> as <strong>GET {{ODataMinimalApi_HostAddress}}\/odata\/customers<\/strong>.\u00a0Run the application, click the [<strong>Send request<\/strong>], (I will omit these steps in the following section), we can get the OData response payload as below:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer\",\r\n  \"value\": [\r\n    {\r\n      \"Id\": 1,\r\n      \"Name\": \"Alice\",\r\n      \"Location\": {\r\n        \"City\": \"Redmond\",\r\n        \"Street\": \"123 Main St\"\r\n      },\r\n      \"Info\": {\r\n        \"Email\": \"alice@example.com\",\r\n        \"Phone\": \"123-456-7819\"\r\n      }\r\n    },\r\n    {\r\n      \"Id\": 2,\r\n      \"Name\": \"Johnson\",\r\n      \"Location\": {\r\n        \"City\": \"Sammamish\",\r\n        \"Street\": \"228 Ave NE\"\r\n      },\r\n      \"Info\": {\r\n        \"Email\": \"johnson@abc.com\",\r\n        \"Phone\": \"233-468-7289\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, <strong>Location <\/strong>and <strong>Info <\/strong>are returned as complex type properties (without an explicit key property), meanwhile <strong>Orders <\/strong>is not presented by default since it\u2019s a navigation property (with a key property).<\/p>\n<h2><strong>IODataModelConfiguration<\/strong><\/h2>\n<p>One way to change the model is to provide an implementation of <em><strong>IODataModelConfiguration<\/strong><\/em> interface. For example, we can create a class <strong><em>MyODataModelConfiguration<\/em><\/strong> implemented <strong><em>IODataModelConfiguration<\/em><\/strong> interface to config the <strong>Info <\/strong> type as an entity type. As a result, the <strong><em>Info <\/em><\/strong>property on <strong><em>Customer <\/em><\/strong>is built as a navigation property.<\/p>\n<pre class=\"lang:c# decode:true\">public class MyODataModelConfiguration : IODataModelConfiguration\r\n{\r\n    public ODataModelBuilder Apply(HttpContext context, ODataModelBuilder builder, Type clrType)\r\n    {\r\n        if (context.Request.Path.StartsWithSegments(\"\/modelconfig\/customers\", StringComparison.OrdinalIgnoreCase))\r\n        {\r\n            builder.EntityType&lt;Info&gt;();\r\n        }\r\n\r\n        return builder;\r\n    }\r\n}\r\n<\/pre>\n<p>Register the configuration as a global service into the Dependency Injection container as below:<\/p>\n<pre class=\"lang:c# decode:true\">builder.Services.AddSingleton&lt;IODataModelConfiguration, MyODataModelConfiguration&gt;();\r\n<\/pre>\n<p>Meanwhile, add a new endpoint as:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"modelconfig\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .WithODataResult();\r\n<\/pre>\n<p>Then, run the application and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/modelconfig\/customers<\/em><\/strong>, we can get a different payload as:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer\",\r\n  \"value\": [\r\n    {\r\n      \"Id\": 1,\r\n      \"Name\": \"Alice\",\r\n      \"Location\": {\r\n        \"City\": \"Redmond\",\r\n        \"Street\": \"123 Main St\"\r\n      }\r\n    },\r\n    {\r\n      \"Id\": 2,\r\n      \"Name\": \"Johnson\",\r\n      \"Location\": {\r\n        \"City\": \"Sammamish\",\r\n        \"Street\": \"228 Ave NE\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, <strong><em>Info <\/em><\/strong> property is not presented by default since it\u2019s a navigation property.<\/p>\n<h2><strong>WithODataModel()<\/strong><\/h2>\n<p>Another way to configure the model for an endpoint to call <strong><em>WithODataModel(model)<\/em><\/strong> extension method as below example:<\/p>\n<pre class=\"lang:c# decode:true\">IEdmModel model = EdmModelBuilder.GetEdmModel();\r\napp.MapGet(\"\/withmodel\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .WithODataResult()\r\n    .WithODataModel(model);\r\n<\/pre>\n<p>Then, run and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/withmodel\/customers<\/em><\/strong>, we can get the following payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customers\",\r\n  \"value\": [\r\n    {\r\n      \"Id\": 1,\r\n      \"Name\": \"Alice\",\r\n      \"Info\": {\r\n        \"Email\": \"alice@example.com\",\r\n        \"Phone\": \"123-456-7819\"\r\n      }\r\n    },\r\n    {\r\n      \"Id\": 2,\r\n      \"Name\": \"Johnson\",\r\n      \"Info\": {\r\n        \"Email\": \"johnson@abc.com\",\r\n        \"Phone\": \"233-468-7289\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, in the given model, <strong><em>Info <\/em><\/strong>is built as complex type meanwhile <strong><em>Address <\/em><\/strong>is built as entity type explicitly.<\/p>\n<h2><strong>WithODataVersion()<\/strong><\/h2>\n<p>We can change the OData spec version by calling <strong><em>WithODataVersion(version)<\/em><\/strong> extension method as:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"\/v401\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .WithODataResult()\r\n    .WithODataModel(model)\r\n    .WithODataVersion(ODataVersion.V401);\r\n<\/pre>\n<p>Then, run and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/v401\/customers<\/em><\/strong>, we can get an OData v4.01 payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@context\": \"http:\/\/localhost:5134\/$metadata#Customers\",\r\n  \"value\": [\r\n    {\r\n       ...\r\n    },\r\n    {\r\n       ...\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>We can see the <strong><em>odata.<\/em><\/strong> prefix is omitted in the context URL by default in OData v4.01 version.<\/p>\n<h2><strong>WithODataBaseAddressFactory()<\/strong><\/h2>\n<p>We can change the base address in the payload by calling <strong><em>WithODataBaseAddressFactory(lambda)<\/em><\/strong>. For example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"\/baseaddress\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .WithODataResult()\r\n    .WithODataModel(model)\r\n    .WithODataBaseAddressFactory(c =&gt; new Uri(\"http:\/\/abc.com\"));\r\n<\/pre>\n<p>Then, run and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/baseaddress\/customers<\/em><\/strong>, we can get the following payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/abc.com\/$metadata#Customers\",\r\n  \"value\": [\r\n    {\r\n       ...\r\n    },\r\n    {\r\n      ...\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, the context URL is changed using the URL specified in the extension method.<\/p>\n<h1><strong>OData query option scenarios<\/strong><\/h1>\n<p>OData is so powerful owing to its richful querying capabilities. Let\u2019s see how to enable OData query options on the Minimal API endpoint.<\/p>\n<h2>Configure OData query options<\/h2>\n<p>For security reason, OData query options are disabled globally by default. If we want to enable them, just call the following method:<\/p>\n<pre class=\"lang:c# decode:true\">builder.Services.AddOData(q =&gt; q.EnableAll());\r\n<\/pre>\n<p>It enables all query options. Of course, we can enable a certain query option by calling its corresponding method only, for example, <strong><em>Expand()<\/em><\/strong> only enables <strong>$expand<\/strong>, etc.<\/p>\n<h2><strong>ODataQueryOptions&lt;T&gt;<\/strong><\/h2>\n<p>One way to enable OData query functionalities on an endpoint is to add a <strong><em>ODataQueryOptions&lt;T&gt;<\/em><\/strong> parameter explicitly on the route handler. Here\u2019s the example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"\/queryoptions\/customers\", (ApplicationDb db, ODataQueryOptions&lt;Customer&gt; queryOptions) =&gt;\r\n    queryOptions.ApplyTo(db.Customers.Include(s =&gt; s.Orders)))\r\n    .WithODataResult();\r\n\r\n<\/pre>\n<p>Then run and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/queryoptions\/customers<\/em><\/strong>, we can get:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer\",\r\n  \"value\": [\r\n    {\r\n      \"Id\": 1,\r\n      \"Name\": \"Alice\"\r\n    },\r\n    {\r\n      \"Id\": 2,\r\n      \"Name\": \"Johnson\"\r\n    }\r\n  ]\r\n}\r\n\r\n<\/pre>\n<p>Be noted, in query option pattern, the <strong><em>Address <\/em><\/strong>and <strong><em>Info <\/em><\/strong>are built as entity type by default. It\u2019s a little bit different compared to the non-query option scenario mentioned above.<\/p>\n<p>We can change the model by implementing the <em><strong>IODataModelConfiguration<\/strong> <\/em>interface or calling <em><strong>WithODataModel(model)<\/strong><\/em> extension method. If we mark the model configuration class as attribute, we can also decorate the configuration on the parameter as below.<\/p>\n<pre class=\"lang:c# decode:true\">(ApplicationDb db, [MyModelConfiguration] ODataQueryOptions&lt;Customer&gt; queryOptions) =&gt; {}\r\n<\/pre>\n<p>Now, let\u2019s run and test the query options on this endpoint as:<strong><em>GET {{ODataMinimalApi_HostAddress}}\/queryoptions\/customers?$expand=info($select=Phone)&amp;$filter=startswith(Name,&#8217;A&#8217;)&amp;$select=Name<\/em><\/strong>, we can get the following payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer(Name,Info(Phone))\",\r\n  \"value\": [\r\n    {\r\n      \"Name\": \"Alice\",\r\n      \"Info\": {\r\n        \"Phone\": \"123-456-7819\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<h2><strong>ODataQueryEndpointFilter<\/strong><\/h2>\n<p>The other way to enable OData query on an endpoint is to apply <em><strong>ODataQueryEndpointFilter<\/strong><\/em> on the endpoint.<\/p>\n<p>Here\u2019s an example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"queryfilter\/customers\", (ApplicationDb db) =&gt; db.Customers.Include(s =&gt; s.Orders))\r\n    .AddODataQueryEndpointFilter()\r\n    .WithODataResult();\r\n<\/pre>\n<p>We can test this endpoint using the following request:<\/p>\n<p><strong><em>GET {{ODataMinimalApi_HostAddress}}\/queryfilter\/customers?$select=location($select=street)&amp;$orderby=Id&amp;$select=Name<\/em><\/strong><\/p>\n<p>Send the request and we can get the following payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer(Location\/Street,Name)\",\r\n  \"value\": [\r\n    {\r\n      \"Name\": \"Alice\",\r\n      \"Location\": {\r\n        \"Street\": \"123 Main St\"\r\n      }\r\n    },\r\n    {\r\n      \"Name\": \"Johnson\",\r\n      \"Location\": {\r\n        \"Street\": \"228 Ave NE\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, the <strong><em>Address <\/em><\/strong>and <strong><em>Info <\/em><\/strong>are built as complex type again in this scenario. You can use the <em><strong>IODataModelConfiguration<\/strong><\/em> or <em><strong>WithODataModel(model)<\/strong><\/em> to specify the model.<\/p>\n<h1><strong>OData advanced scenarios<\/strong><\/h1>\n<h2><strong>Service document and Metadata<\/strong><\/h2>\n<p>We can call <strong><em>MapODataServiceDocument()<\/em><\/strong> extension method to register an endpoint which returns the service document of an Edm model.<\/p>\n<p>For example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapODataServiceDocument(\"\/$document\", model);\r\n<\/pre>\n<p>Run the application and send request to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/$document<\/em><\/strong>, we can get the result as:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata\",\r\n  \"value\": [\r\n    {\r\n      \"name\": \"Customers\",\r\n      \"kind\": \"EntitySet\",\r\n      \"url\": \"Customers\"\r\n    },\r\n    {\r\n      \"name\": \"Orders\",\r\n      \"kind\": \"EntitySet\",\r\n      \"url\": \"Orders\"\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Meanwhile, we can call <strong><em>MapODataMetadata() <\/em><\/strong>extension method to register an endpoint which returns the schema document of an Edm model.<\/p>\n<p>For example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapODataMetadata(\"\/$metadata\", model);\r\n<\/pre>\n<p>Run the application and send reqquest to <strong><em>GET {{ODataMinimalApi_HostAddress}}\/$metadata<\/em><\/strong>, we can get the result as:<\/p>\n<pre class=\"lang:xml decode:true\">&lt;?xml version=\"1.0\" encoding=\"utf-8\"?&gt;\r\n&lt;edmx:Edmx Version=\"4.0\" xmlns:edmx=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edmx\"&gt;\r\n  &lt;edmx:DataServices&gt;\r\n    &lt;Schema Namespace=\"ODataMinimalApi.Models\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n      &lt;EntityType Name=\"Customer\"&gt;\r\n        &lt;Key&gt;\r\n          &lt;PropertyRef Name=\"Id\" \/&gt;\r\n        &lt;\/Key&gt;\r\n        &lt;Property Name=\"Id\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n        &lt;Property Name=\"Name\" Type=\"Edm.String\" \/&gt;\r\n        &lt;Property Name=\"Info\" Type=\"ODataMinimalApi.Models.Info\" \/&gt;\r\n        &lt;NavigationProperty Name=\"Location\" Type=\"ODataMinimalApi.Models.Address\" \/&gt;\r\n        &lt;NavigationProperty Name=\"Orders\" Type=\"Collection(ODataMinimalApi.Models.Order)\" \/&gt;\r\n      &lt;\/EntityType&gt;\r\n      &lt;EntityType Name=\"Order\"&gt;\r\n        &lt;Key&gt;\r\n          &lt;PropertyRef Name=\"Id\" \/&gt;\r\n        &lt;\/Key&gt;\r\n        &lt;Property Name=\"Id\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n        &lt;Property Name=\"Amount\" Type=\"Edm.Int32\" Nullable=\"false\" \/&gt;\r\n      &lt;\/EntityType&gt;\r\n      &lt;EntityType Name=\"Address\"&gt;\r\n        &lt;Property Name=\"City\" Type=\"Edm.String\" \/&gt;\r\n        &lt;Property Name=\"Street\" Type=\"Edm.String\" \/&gt;\r\n      &lt;\/EntityType&gt;\r\n      &lt;ComplexType Name=\"Info\"&gt;\r\n        &lt;Property Name=\"Email\" Type=\"Edm.String\" \/&gt;\r\n        &lt;Property Name=\"Phone\" Type=\"Edm.String\" \/&gt;\r\n      &lt;\/ComplexType&gt;\r\n    &lt;\/Schema&gt;\r\n    &lt;Schema Namespace=\"Default\" xmlns=\"http:\/\/docs.oasis-open.org\/odata\/ns\/edm\"&gt;\r\n      &lt;EntityContainer Name=\"Container\"&gt;\r\n        &lt;EntitySet Name=\"Customers\" EntityType=\"ODataMinimalApi.Models.Customer\"&gt;\r\n          &lt;NavigationPropertyBinding Path=\"Orders\" Target=\"Orders\" \/&gt;\r\n        &lt;\/EntitySet&gt;\r\n        &lt;EntitySet Name=\"Orders\" EntityType=\"ODataMinimalApi.Models.Order\" \/&gt;\r\n      &lt;\/EntityContainer&gt;\r\n    &lt;\/Schema&gt;\r\n  &lt;\/edmx:DataServices&gt;\r\n&lt;\/edmx:Edmx&gt;\r\n<\/pre>\n<h2><strong>Function and action<\/strong><\/h2>\n<p>We can build endpoint to handle OData function and action. Let\u2019s define the following edm function and action in the model builder as examples:<\/p>\n<pre class=\"lang:c# decode:true\">public static IEdmModel GetEdmModel()\r\n{\r\n    \/\/ \u2026\r\n\r\n    var function = builder.EntityType&lt;Customer&gt;().Collection.Function(\"AddNameSuffixForAllCustomer\");\r\n    function.Parameter&lt;string&gt;(\"suffix\");\r\n    function.ReturnsCollectionFromEntitySet&lt;Customer&gt;(\"Customers\");\r\n\r\n    var action = builder.EntityType&lt;Customer&gt;().Action(\"RateByName\");\r\n    action.Parameter&lt;string&gt;(\"name\");\r\n    action.Parameter&lt;int&gt;(\"age\");\r\n    action.Returns&lt;string&gt;();\r\n\r\n    return builder.GetEdmModel();\r\n}\r\n<\/pre>\n<ul>\n<li>Edm Function<\/li>\n<\/ul>\n<p>We can define the following endpoint to handle the edm function:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapGet(\"function\/customers\/addsuffix(suffix={suffix})\", (ApplicationDb db, string suffix) =&gt;\r\n{\r\n    var customers = db.Customers.ToList();\r\n    foreach (var customer in customers)\r\n    {\r\n        customer.Name += suffix;\r\n    }\r\n\r\n    return customers;\r\n})\r\n    .WithODataResult()\r\n    .WithODataModel(model)\r\n    .AddODataQueryEndpointFilter()\r\n    .WithODataPathFactory(\r\n        (h, t) =&gt;\r\n        {\r\n            string suffix = h.GetRouteValue(\"suffix\") as string;\r\n            IEdmEntitySet customers = model.FindDeclaredEntitySet(\"Customers\");\r\n            IEdmOperation function = model.FindDeclaredOperations(\"Default.AddNameSuffixForAllCustomer\").Single();\r\n            IList&lt;OperationSegmentParameter&gt; parameters = new List&lt;OperationSegmentParameter&gt;\r\n            {\r\n                new OperationSegmentParameter(\"suffix\", suffix)\r\n            };\r\n\r\n            return new ODataPath(new EntitySetSegment(customers), new OperationSegment(function, parameters, customers));\r\n});\r\n<\/pre>\n<p>Then generate the following request and send request to:<\/p>\n<p><strong><em>GET {{ODataMinimalApi_HostAddress}}\/function\/customers\/addsuffix(suffix=&#8217;%20Sr&#8217;)?$select=name<\/em><\/strong><\/p>\n<p>We can get the following payload:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customers(Name)\",\r\n  \"value\": [\r\n    {\r\n      \"Name\": \"Johnson' Sr'\"\r\n    },\r\n    {\r\n      \"Name\": \"Alice' Sr'\"\r\n    }\r\n  ]\r\n}\r\n<\/pre>\n<p>Be noted, the parameter <strong><em>suffix <\/em><\/strong>value is binded directly from the route data. Therefore, developers should take responsibility to convert the raw value to OData value, for example, to remove the single quote for the string value. We\u2019ll figure out a better solution to improve this scenario in the next version.<\/p>\n<ul>\n<li>Edm Action<\/li>\n<\/ul>\n<p>We can add <strong><em>ODataActionParameters<\/em><\/strong> as a parameter the route handler to access the Edm action parameters as follows:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapPost(\"action\/customers\/{id}\/rateByName\", (ApplicationDb db, int id, ODataActionParameters parameters) =&gt;\r\n{\r\n    Customer customer = db.Customers.FirstOrDefault(s =&gt; s.Id == id);\r\n    if (customer == null)\r\n    {\r\n        return null;\r\n    }\r\n\r\n    return $\"{customer.Name}: {System.Text.Json.JsonSerializer.Serialize(parameters)}\"; \/\/ for test only\r\n})\r\n    .WithODataResult()\r\n    .WithODataModel(model)\r\n    .WithODataPathFactory(\r\n        (h, t) =&gt;\r\n        {\r\n            string idStr = h.GetRouteValue(\"id\") as string;\r\n            int id = int.Parse(idStr);\r\n            IEdmEntitySet customers = model.FindDeclaredEntitySet(\"Customers\");\r\n            IDictionary&lt;string, object&gt; keysValues = new Dictionary&lt;string, object&gt;();\r\n            keysValues[\"Id\"] = id;\r\n\r\n            IEdmAction action = model.SchemaElements.OfType&lt;IEdmAction&gt;().First(a =&gt; a.Name == \"RateByName\");\r\n\r\n            return new ODataPath(new EntitySetSegment(customers), \r\n                        new KeySegment(keysValues, customers.EntityType, customers), \r\n                       new OperationSegment(action, null)\r\n        );\r\n    });\r\n\r\n<\/pre>\n<p>Then generate the following POST request to test:<\/p>\n<pre class=\"lang:c# decode:true\">@id=1\r\n\r\nPOST {{ODataMinimalApi_HostAddress}}\/action\/customers\/{{id}}\/rateByName\r\n\r\nContent-Type: application\/json\r\n\r\n{\r\n  \"name\": \"kerry\",\r\n  \"age\":16\r\n}\r\n<\/pre>\n<p>We can get the following response:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Edm.String\",\r\n  \"value\": \"Alice: {\\\"name\\\":\\\"kerry\\\",\\\"age\\\":16}\"\r\n}\r\n<\/pre>\n<p>Be noted, <strong><em>WithODataPathFactory()<\/em><\/strong> is necessary so far to provide an OData path to get the response from edm function and action.<\/p>\n<h2><strong>Delta request<\/strong><\/h2>\n<p>We can add <strong><em>Detal&lt;T&gt;<\/em><\/strong> parameter on a router handler to change the single entity. For example:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapPatch(\"delta\/customers\/{id}\", (ApplicationDb db, int id, Delta&lt;Customer&gt; delta) =&gt;\r\n{\r\n    Customer customer = db.Customers.FirstOrDefault(s =&gt; s.Id == id);\r\n    if (customer == null)\r\n    {\r\n        return null;\r\n    }\r\n    delta.Patch(customer);\r\n    return customer;\r\n})\r\n    .WithODataResult()\r\n    .WithODataModel(model)\r\n    .AddODataQueryEndpointFilter()\r\n    .WithODataPathFactory(\r\n        (h, t) =&gt;\r\n        {\r\n            string idStr = h.GetRouteValue(\"id\") as string;\r\n            int id = int.Parse(idStr);\r\n            IEdmEntitySet customers = model.FindDeclaredEntitySet(\"Customers\");\r\n            IDictionary&lt;string, object&gt; keysValues = new Dictionary&lt;string, object&gt;();\r\n            keysValues[\"Id\"] = id;\r\n            return new ODataPath(new EntitySetSegment(customers), new KeySegment(keysValues, customers.EntityType, customers));\r\n\r\n        });\r\n\r\n<\/pre>\n<p>Generate the patch request as:<\/p>\n<pre class=\"lang:c# decode:true\">PATCH {{ODataMinimalApi_HostAddress}}\/delta\/customers\/1?$expand=location;$select=id,name,location\r\nContent-Type: application\/json\r\n\r\n\u00a0\r\n{\r\n  \"Name\": \"kerry\",\r\n  \"Location@odata.delta\": {\r\n    \"street\": \"new street\"\r\n  }\r\n}\r\n<\/pre>\n<p>Then run the application and send the request, we can get an updated customer as:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customers(Id,Name,Location,Location())\/$entity\",\r\n  \"Id\": 1,\r\n  \"Name\": \"kerry\",\r\n  \"Location\": {\r\n    \"City\": \"Redmond\",\r\n    \"Street\": \"new street\"\r\n  }\r\n}\r\n<\/pre>\n<p>We can also use<strong> <em>DeltaSet&lt;T&gt;<\/em><\/strong> to handle the changes for an entity set as:<\/p>\n<pre class=\"lang:c# decode:true\">app.MapPatch(\"deltaset\/customers\", (ApplicationDb  db, DeltaSet&lt;Customer&gt; changes) =&gt;\r\n{\r\n    \/\/ omit others \u2026\r\n})\r\n<\/pre>\n<p>I\u2019d omit codes here for simplicity.<\/p>\n<h2><strong>Batch request<\/strong><\/h2>\n<p>We can call <strong><em>UseODataMiniBatching()<\/em><\/strong> to enable the batch request on the Minimal API. For example:<\/p>\n<pre class=\"lang:c# decode:true\">app.UseODataMiniBatching(\"odata\/$batch\", model);\r\n<\/pre>\n<p>Be noted, we should call this method as early as possible to support OData batching.<\/p>\n<p>Let&#8217;s use the following request to test the <strong>$batch<\/strong>:<\/p>\n<pre class=\"lang:c# decode:true\">POST {{ODataMinimalApi_HostAddress}}\/odata\/$batch\r\nContent-Type: application\/json\r\nAccept: application\/json\r\n\r\n\u00a0\r\n{\r\n  \"requests\": [\r\n  {\r\n    \"id\": \"1\",\r\n    \"atomicityGroup\": \"f7de7314-2f3d-4422-b840-ada6d6de0f18\",\r\n    \"method\": \"POST\",\r\n    \"url\": \"http:\/\/localhost:5134\/action\/customers\/2\/rateByName\",\r\n    \"headers\": {\r\n      \"OData-Version\": \"4.0\",\r\n      \"Content-Type\": \"application\/json\",\r\n      \"Accept\": \"application\/json\"\r\n    },\r\n    \"body\": {\r\n      \"name\": \"wu\",\r\n      \"age\": 20\r\n    }\r\n  },\r\n  {\r\n    \"id\": \"2\",\r\n    \"atomicityGroup\": \"f7de7314-2f3d-4422-b840-ada6d6de0f18\",\r\n    \"method\": \"GET\",\r\n    \"url\": \"http:\/\/localhost:5134\/queryoptions\/customers?$expand=info($select=Phone)&amp;$filter=startswith(Name,'A')&amp;$select=Name\",\r\n    \"headers\": {\r\n      \"OData-Version\": \"4.0\",\r\n      \"Content-Type\": \"application\/json;odata.metadata=minimal\",\r\n      \"Accept\": \"application\/json;odata.metadata=minimal\"\r\n      }\r\n    }\r\n  ]\r\n}\r\n\r\n<\/pre>\n<p>Run the application and send the request, we can get the following response:<\/p>\n<pre class=\"lang:json decode:true\">{\r\n  \"responses\": [\r\n    {\r\n      \"id\": \"1\",\r\n      \"atomicityGroup\": \"fd7fb5df-a27f-4dde-a72e-71c142028037\",\r\n      \"status\": 200,\r\n      \"headers\": {\r\n        \"content-type\": \"application\/json\",\r\n        \"odata-version\": \"4.0\"\r\n      },\r\n      \"body\": {\r\n        \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Edm.String\",\r\n        \"value\": \"Johnson: {\\\"name\\\":\\\"wu\\\",\\\"age\\\":20}\"\r\n      }\r\n    },\r\n    {\r\n      \"id\": \"2\",\r\n      \"atomicityGroup\": \"fd7fb5df-a27f-4dde-a72e-71c142028037\",\r\n      \"status\": 200,\r\n      \"headers\": {\r\n        \"content-type\": \"application\/json\",\r\n        \"odata-version\": \"4.0\"\r\n      },\r\n      \"body\": {\r\n        \"@odata.context\": \"http:\/\/localhost:5134\/$metadata#Customer(Name,Info(Phone))\",\r\n        \"value\": [\r\n          {\r\n            \"Name\": \"Alice\",\r\n            \"Info\": {\r\n              \"Phone\": \"123-456-7819\"\r\n             }\r\n          }\r\n        ]\r\n      }\r\n    }\r\n  ]\r\n}<\/pre>\n<p>That&#8217;s all.<\/p>\n<h1>Summary<\/h1>\n<p>This post went throw the scenarios to enable OData functionalities on ASP.NET Core Minimal API. For new simple projects, OData recommend using Minimal APIs to build the OData endpoints\u00a0as they provide a simplified, high-performance approach for building APIs with minimal code and configuration. Of course, 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\/ODataMinimalApi\">this<\/a>\u00a0repository.<\/p>\n<pre><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Introduction Minimal API is a simplified approach for building HTTP APIs fast within ASP.NET Core, compared to the controller-based APIs. Developers can build fully functioning REST endpoints with minimal code and configuration, especially without controller, action and even the formatters. See ASP.NET Core Minimal API details at here. Since Microsoft.AspNetCore.OData version 9.4.0, it enables developers [&hellip;]<\/p>\n","protected":false},"author":514,"featured_media":5992,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1472,1,116],"tags":[],"class_list":["post-5932","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-asp-net-core","category-odata","category-odl"],"acf":[],"blog_post_summary":"<p>Introduction Minimal API is a simplified approach for building HTTP APIs fast within ASP.NET Core, compared to the controller-based APIs. Developers can build fully functioning REST endpoints with minimal code and configuration, especially without controller, action and even the formatters. See ASP.NET Core Minimal API details at here. Since Microsoft.AspNetCore.OData version 9.4.0, it enables developers [&hellip;]<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5932","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=5932"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/posts\/5932\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media\/5992"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/media?parent=5932"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/categories?post=5932"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/odata\/wp-json\/wp\/v2\/tags?post=5932"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}