diff --git a/AspNetCoreOData.sln b/AspNetCoreOData.sln
index 06a9b8f0a..a6e6b4a6c 100644
--- a/AspNetCoreOData.sln
+++ b/AspNetCoreOData.sln
@@ -29,6 +29,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ODataAlternateKeySample", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BenchmarkServer", "sample\BenchmarkServer\BenchmarkServer.csproj", "{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ODataMiniApi", "sample\ODataMiniApi\ODataMiniApi.csproj", "{53645862-60E1-403A-B9BB-911D3801010F}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +77,10 @@ Global
{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850}.Release|Any CPU.Build.0 = Release|Any CPU
+ {53645862-60E1-403A-B9BB-911D3801010F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {53645862-60E1-403A-B9BB-911D3801010F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {53645862-60E1-403A-B9BB-911D3801010F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {53645862-60E1-403A-B9BB-911D3801010F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -90,6 +96,7 @@ Global
{647EFCFA-55A7-4F0A-AD40-4B6EB1BFCFFA} = {B1F86961-6958-4617-ACA4-C231F95AE099}
{7B153669-A42F-4511-8BDB-587B3B27B2F3} = {B1F86961-6958-4617-ACA4-C231F95AE099}
{8346AD1B-00E3-462D-B6B1-9AA3C2FB2850} = {B1F86961-6958-4617-ACA4-C231F95AE099}
+ {53645862-60E1-403A-B9BB-911D3801010F} = {B1F86961-6958-4617-ACA4-C231F95AE099}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {540C9752-AAC0-49EA-BA60-78490C90FF86}
diff --git a/pipelines/azure_pipelines_nightly.yml b/pipelines/azure_pipelines_nightly.yml
index ae465ab28..36c75f05b 100644
--- a/pipelines/azure_pipelines_nightly.yml
+++ b/pipelines/azure_pipelines_nightly.yml
@@ -24,6 +24,8 @@ variables:
value: anycpu
- name: ProductBinPath
value: $(Build.SourcesDirectory)\bin\$(BuildPlatform)\$(BuildConfiguration)
+- group: OData-ESRP-CodeSigning
+
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
parameters:
@@ -125,10 +127,16 @@ extends:
displayName: Use .NET Core SDK 2.x
inputs:
version: 2.x
- - task: EsrpCodeSigning@1
+ - task: EsrpCodeSigning@5
displayName: ESRP CodeSigning - WebAPI OData Product Signing
inputs:
- ConnectedServiceName: 'ESRP CodeSigning - OData'
+ ConnectedServiceName: $(ODataEsrpConnectionServiceName)
+ AppRegistrationClientId: '$(ODataEsrpAppRegistrationClientId)'
+ AppRegistrationTenantId: '$(ODataEsrpAppRegistrationTenantId)'
+ AuthAKVName: $(ODataEsrpAuthAKVName)
+ AuthCertName: $(ODataEsrpAuthCertName)
+ AuthSignCertName: $(ODataEsrpAuthSignCertName)
+ ServiceEndpointUrl: '$(ODataEsrpServiceEndpointUrl)'
FolderPath: $(ProductBinPath)
Pattern: Microsoft.AspNetCore.OData.dll,Microsoft.AspNetCore.OData.NewtonsoftJson.dll
signConfigType: inlineSignParams
@@ -216,10 +224,16 @@ extends:
inputs:
command: custom
arguments: pack $(Build.SourcesDirectory)\src\Microsoft.AspNetCore.OData.NewtonsoftJson.Release.nuspec -NonInteractive -OutputDirectory $(Build.ArtifactStagingDirectory)\Nuget -Properties Configuration=$(BuildConfiguration);ProductRoot=$(ProductBinPath);SourcesRoot=$(Build.SourcesDirectory);VersionFullSemantic=$(VersionFullSemantic);NightlyBuildVersion=$(VersionNugetNightlyBuild);VersionNuGetSemantic=$(VersionNuGetSemantic) -Verbosity Detailed -Symbols -SymbolPackageFormat snupkg
- - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1
+ - task: EsrpCodeSigning@5
displayName: ESRP CodeSigning - WebAPI OData Packages Signing
inputs:
- ConnectedServiceName: ESRP CodeSigning - OData
+ ConnectedServiceName: $(ODataEsrpConnectionServiceName)
+ AppRegistrationClientId: '$(ODataEsrpAppRegistrationClientId)'
+ AppRegistrationTenantId: '$(ODataEsrpAppRegistrationTenantId)'
+ AuthAKVName: $(ODataEsrpAuthAKVName)
+ AuthCertName: $(ODataEsrpAuthCertName)
+ AuthSignCertName: $(ODataEsrpAuthSignCertName)
+ ServiceEndpointUrl: '$(ODataEsrpServiceEndpointUrl)'
FolderPath: $(Build.ArtifactStagingDirectory)\Nuget
Pattern: '*.nupkg'
signConfigType: inlineSignParams
diff --git a/sample/ODataMiniApi/AppDb.cs b/sample/ODataMiniApi/AppDb.cs
new file mode 100644
index 000000000..53cdebf2c
--- /dev/null
+++ b/sample/ODataMiniApi/AppDb.cs
@@ -0,0 +1,180 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.EntityFrameworkCore;
+
+namespace ODataMiniApi;
+
+public class AppDb : DbContext
+{
+ public AppDb(DbContextOptions options) : base(options) { }
+
+ public DbSet Schools => Set();
+
+ public DbSet Students => Set();
+
+ public DbSet Customers => Set();
+
+ public DbSet Orders => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+ modelBuilder.Entity().HasKey(x => x.SchoolId);
+ modelBuilder.Entity().HasKey(x => x.StudentId);
+ modelBuilder.Entity().OwnsOne(x => x.MailAddress);
+
+ modelBuilder.Entity().HasKey(x => x.Id);
+ modelBuilder.Entity().HasKey(x => x.Id);
+ modelBuilder.Entity().OwnsOne(x => x.Info);
+ }
+}
+
+static class AppDbExtension
+{
+ public static void MakeSureDbCreated(this WebApplication app)
+ {
+ using (var scope = app.Services.CreateScope())
+ {
+ var services = scope.ServiceProvider;
+ var context = services.GetRequiredService();
+
+ if (context.Schools.Count() == 0)
+ {
+ #region Students
+ var students = new List
+ {
+ // Mercury school
+ new Student { SchoolId = 1, StudentId = 10, FirstName = "Spens", LastName = "Alex", FavoriteSport = "Soccer", Grade = 87, BirthDay = new DateOnly(2009, 11, 15) },
+ new Student { SchoolId = 1, StudentId = 11, FirstName = "Jasial", LastName = "Eaine", FavoriteSport = "Basketball", Grade = 45, BirthDay = new DateOnly(1989, 8, 3) },
+ new Student { SchoolId = 1, StudentId = 12, FirstName = "Niko", LastName = "Rorigo", FavoriteSport = "Soccer", Grade = 78, BirthDay = new DateOnly(2019, 5, 5) },
+ new Student { SchoolId = 1, StudentId = 13, FirstName = "Roy", LastName = "Rorigo", FavoriteSport = "Tennis", Grade = 67, BirthDay = new DateOnly(1975, 11, 4) },
+ new Student { SchoolId = 1, StudentId = 14, FirstName = "Zaral", LastName = "Clak", FavoriteSport = "Basketball", Grade = 54, BirthDay = new DateOnly(2008, 1, 4) },
+
+ // Venus school
+ new Student { SchoolId = 2, StudentId = 20, FirstName = "Hugh", LastName = "Briana", FavoriteSport = "Basketball", Grade = 78, BirthDay = new DateOnly(1959, 5, 6) },
+ new Student { SchoolId = 2, StudentId = 21, FirstName = "Reece", LastName = "Len", FavoriteSport = "Basketball", Grade = 45, BirthDay = new DateOnly(2004, 2, 5) },
+ new Student { SchoolId = 2, StudentId = 22, FirstName = "Javanny", LastName = "Jay", FavoriteSport = "Soccer", Grade = 87, BirthDay = new DateOnly(2003, 6, 5) },
+ new Student { SchoolId = 2, StudentId = 23, FirstName = "Ketty", LastName = "Oak", FavoriteSport = "Tennis", Grade = 99, BirthDay = new DateOnly(1998, 7, 25) },
+
+ // Earth School
+ new Student { SchoolId = 3, StudentId = 30, FirstName = "Mike", LastName = "Wat", FavoriteSport = "Tennis", Grade = 93, BirthDay = new DateOnly(1999, 5, 15) },
+ new Student { SchoolId = 3, StudentId = 31, FirstName = "Sam", LastName = "Joshi", FavoriteSport = "Soccer", Grade = 78, BirthDay = new DateOnly(2000, 6, 23) },
+ new Student { SchoolId = 3, StudentId = 32, FirstName = "Kerry", LastName = "Travade", FavoriteSport = "Basketball", Grade = 89, BirthDay = new DateOnly(2001, 2, 6) },
+ new Student { SchoolId = 3, StudentId = 33, FirstName = "Pett", LastName = "Jay", FavoriteSport = "Tennis", Grade = 63, BirthDay = new DateOnly(1998, 11, 7) },
+
+ // Mars School
+ new Student { SchoolId = 4, StudentId = 40, FirstName = "Mike", LastName = "Wat", FavoriteSport = "Soccer", Grade = 64, BirthDay = new DateOnly(2011, 11, 15) },
+ new Student { SchoolId = 4, StudentId = 41, FirstName = "Sam", LastName = "Joshi", FavoriteSport = "Basketball", Grade = 98, BirthDay = new DateOnly(2005, 6, 6) },
+ new Student { SchoolId = 4, StudentId = 42, FirstName = "Kerry", LastName = "Travade", FavoriteSport = "Soccer", Grade = 88, BirthDay = new DateOnly(2011, 5, 13) },
+
+ // Jupiter School
+ new Student { SchoolId = 5, StudentId = 50, FirstName = "David", LastName = "Padron", FavoriteSport = "Tennis", Grade = 77, BirthDay = new DateOnly(2015, 12, 3) },
+ new Student { SchoolId = 5, StudentId = 53, FirstName = "Jeh", LastName = "Brook", FavoriteSport = "Basketball", Grade = 69, BirthDay = new DateOnly(2014, 10, 15) },
+ new Student { SchoolId = 5, StudentId = 54, FirstName = "Steve", LastName = "Johnson", FavoriteSport = "Soccer", Grade = 100, BirthDay = new DateOnly(1995, 3, 2) },
+
+ // Saturn School
+ new Student { SchoolId = 6, StudentId = 60, FirstName = "John", LastName = "Haney", FavoriteSport = "Soccer", Grade = 99, BirthDay = new DateOnly(2008, 12, 1) },
+ new Student { SchoolId = 6, StudentId = 61, FirstName = "Morgan", LastName = "Frost", FavoriteSport = "Tennis", Grade = 17, BirthDay = new DateOnly(2009, 11, 4) },
+ new Student { SchoolId = 6, StudentId = 62, FirstName = "Jennifer", LastName = "Viles", FavoriteSport = "Basketball", Grade = 54, BirthDay = new DateOnly(1989, 3, 15) },
+
+ // Uranus School
+ new Student { SchoolId = 7, StudentId = 72, FirstName = "Matt", LastName = "Dally", FavoriteSport = "Basketball", Grade = 77, BirthDay = new DateOnly(2011, 11, 4) },
+ new Student { SchoolId = 7, StudentId = 73, FirstName = "Kevin", LastName = "Vax", FavoriteSport = "Basketball", Grade = 93, BirthDay = new DateOnly(2012, 5, 12) },
+ new Student { SchoolId = 7, StudentId = 76, FirstName = "John", LastName = "Clarey", FavoriteSport = "Soccer", Grade = 95, BirthDay = new DateOnly(2008, 8, 8) },
+
+ // Neptune School
+ new Student { SchoolId = 8, StudentId = 81, FirstName = "Adam", LastName = "Singh", FavoriteSport = "Tennis", Grade = 92, BirthDay = new DateOnly(2006, 6, 23) },
+ new Student { SchoolId = 8, StudentId = 82, FirstName = "Bob", LastName = "Joe", FavoriteSport = "Soccer", Grade = 88, BirthDay = new DateOnly(1978, 11, 15) },
+ new Student { SchoolId = 8, StudentId = 84, FirstName = "Martin", LastName = "Dalton", FavoriteSport = "Tennis", Grade = 77, BirthDay = new DateOnly(2017, 5, 14) },
+
+ // Pluto School
+ new Student { SchoolId = 9, StudentId = 91, FirstName = "Michael", LastName = "Wu", FavoriteSport = "Soccer", Grade = 97, BirthDay = new DateOnly(2022, 9, 22) },
+ new Student { SchoolId = 9, StudentId = 93, FirstName = "Rachel", LastName = "Wottle", FavoriteSport = "Soccer", Grade = 81, BirthDay = new DateOnly(2022, 10, 5) },
+ new Student { SchoolId = 9, StudentId = 97, FirstName = "Aakash", LastName = "Aarav", FavoriteSport = "Soccer", Grade = 98, BirthDay = new DateOnly(2003, 3, 15) },
+
+ // Shyline high School
+ new Student { SchoolId = 10, StudentId = 101, FirstName = "Steve", LastName = "Chu", FavoriteSport = "Soccer", Grade = 77, BirthDay = new DateOnly(2002, 11, 12) },
+ new Student { SchoolId = 10, StudentId = 123, FirstName = "Wash", LastName = "Dish", FavoriteSport = "Tennis", Grade = 81, BirthDay = new DateOnly(2002, 12, 5) },
+ new Student { SchoolId = 10, StudentId = 106, FirstName = "Ren", LastName = "Wu", FavoriteSport = "Soccer", Grade = 88, BirthDay = new DateOnly(2003, 3, 15) }
+ };
+
+ foreach (var s in students)
+ {
+ context.Students.Add(s);
+ }
+ #endregion
+
+ #region Schools
+ var schools = new List
+ {
+ new School { SchoolId = 1, SchoolName = "Mercury Middle School", MailAddress = new Address { ApartNum = 241, City = "Kirk", Street = "156TH AVE", ZipCode = "98051" } },
+ new HighSchool { SchoolId = 2, SchoolName = "Venus High School", MailAddress = new Address { ApartNum = 543, City = "AR", Street = "51TH AVE PL", ZipCode = "98043" }, NumberOfStudents = 1187, PrincipalName = "Venus TT" },
+ new School { SchoolId = 3, SchoolName = "Earth University", MailAddress = new Address { ApartNum = 101, City = "Belly", Street = "24TH ST", ZipCode = "98029" } },
+ new School { SchoolId = 4, SchoolName = "Mars Elementary School ", MailAddress = new Address { ApartNum = 123, City = "Issaca", Street = "Mars Rd", ZipCode = "98023" } },
+ new School { SchoolId = 5, SchoolName = "Jupiter College", MailAddress = new Address { ApartNum = 443, City = "Redmond", Street = "Sky Freeway", ZipCode = "78123" } },
+ new School { SchoolId = 6, SchoolName = "Saturn Middle School", MailAddress = new Address { ApartNum = 11, City = "Moon", Street = "187TH ST", ZipCode = "68133" } },
+ new HighSchool { SchoolId = 7, SchoolName = "Uranus High School", MailAddress = new Address { ApartNum = 123, City = "Greenland", Street = "Sun Street", ZipCode = "88155" }, NumberOfStudents = 886, PrincipalName = "Uranus Sun" },
+ new School { SchoolId = 8, SchoolName = "Neptune Elementary School", MailAddress = new Address { ApartNum = 77, City = "BadCity", Street = "Moon way", ZipCode = "89155" } },
+ new School { SchoolId = 9, SchoolName = "Pluto University", MailAddress = new Address { ApartNum = 12004, City = "Sahamish", Street = "Universals ST", ZipCode = "10293" } },
+ new HighSchool { SchoolId =10, SchoolName = "Shyline High School", MailAddress = new Address { ApartNum = 4004, City = "Sammamish", Street = "8TH ST", ZipCode = "98029"}, NumberOfStudents = 976, PrincipalName = "Laly Fort" }
+ };
+
+ foreach (var s in schools)
+ {
+ s.Students = students.Where(std => std.SchoolId == s.SchoolId).ToList();
+
+ context.Schools.Add(s);
+ }
+ #endregion
+
+ context.SaveChanges();
+ }
+
+ if (context.Customers.Count() == 0)
+ {
+ #region Customers and Orders
+
+ var customers = new List
+ {
+ new Customer { Id = 1, Name = "Alice", Info = new Info { Email = "alice@example.com", Phone = "123-456-7819" },
+ Orders = [
+ new Order { Id = 11, Amount = 9},
+ new Order { Id = 12, Amount = 19},
+ ] },
+ new Customer { Id = 2, Name = "Johnson", Info = new Info { Email = "johnson@abc.com", Phone = "233-468-7289" },
+ Orders = [
+ new Order { Id = 21, Amount =8},
+ new Order { Id = 22, Amount = 76},
+ ] },
+ new Customer { Id = 3, Name = "Peter", Info = new Info { Email = "peter@earth.org", Phone = "223-656-7889" },
+ Orders = [
+ new Order { Id = 32, Amount = 7 }
+ ] },
+
+ new Customer { Id = 4, Name = "Sam", Info = new Info { Email = "sam@ms.edu", Phone = "245-876-0989" },
+ Orders = [
+ new Order { Id = 41, Amount = 5 },
+ new Order { Id = 42, Amount = 32}
+ ] }
+ };
+
+ foreach (var s in customers)
+ {
+ context.Customers.Add(s);
+ foreach (var o in s.Orders)
+ {
+ context.Orders.Add(o);
+ }
+ }
+ #endregion
+
+ context.SaveChanges();
+ }
+ }
+ }
+}
diff --git a/sample/ODataMiniApi/AppModels.cs b/sample/ODataMiniApi/AppModels.cs
new file mode 100644
index 000000000..59e26fd09
--- /dev/null
+++ b/sample/ODataMiniApi/AppModels.cs
@@ -0,0 +1,109 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace ODataMiniApi;
+
+public class EdmModelBuilder
+{
+ public static IEdmModel GetEdmModel()
+ {
+ var builder = new ODataConventionModelBuilder();
+ builder.EntitySet("Schools");
+ builder.ComplexType();
+ builder.ComplexType();
+
+ builder.EntitySet("Customers");
+ builder.EntitySet("Orders");
+ builder.ComplexType();
+
+ var action = builder.EntityType().Action("RateByName");
+ action.Parameter("name");
+ action.Parameter("age");
+ action.Returns();
+
+ builder.EntityType().Collection.Action("Rating").Parameter("p");
+
+ return builder.GetEdmModel();
+ }
+}
+
+public class School
+{
+ public int SchoolId { get; set; }
+
+ public string SchoolName { get; set; }
+
+ public Address MailAddress { get; set; }
+
+ public virtual IList Students { get; set; }
+}
+
+public class HighSchool : School
+{
+ // Additional properties specific to HighSchool
+ public int NumberOfStudents { get; set; }
+ public string PrincipalName { get; set; }
+}
+
+public class Student
+{
+ public int StudentId { get; set; }
+
+ public string FirstName { get; set; }
+
+ public string LastName { get; set; }
+
+ public string FavoriteSport { get; set; }
+
+ public int Grade { get; set; }
+
+ public int SchoolId { get; set; }
+
+ public DateOnly BirthDay { get; set; }
+}
+
+[ComplexType]
+public class Address
+{
+ public int ApartNum { get; set; }
+
+ public string City { get; set; }
+
+ public string Street { get; set; }
+
+ public string ZipCode { get; set; }
+}
+
+
+public class Customer
+{
+ public int Id { get; set; }
+
+ public string Name { get; set; }
+
+ public Info Info { get; set; }
+
+ //[AutoExpand]
+ public List Orders { get; set; }
+}
+
+public class Order
+{
+ public int Id { get; set; }
+
+ public int Amount { get; set; }
+}
+
+public class Info
+{
+ public string Email { get; set; }
+ public string Phone { get; set; }
+}
\ No newline at end of file
diff --git a/sample/ODataMiniApi/CustomersAndOrders.http b/sample/ODataMiniApi/CustomersAndOrders.http
new file mode 100644
index 000000000..85f620bd5
--- /dev/null
+++ b/sample/ODataMiniApi/CustomersAndOrders.http
@@ -0,0 +1,132 @@
+@ODataMiniApi_HostAddress = http://localhost:5177
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/customers
+
+### [AutoExpand]
+### 4.01 Delta Payload Get "odata." prefix even in 4.01?
+###
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/v0/customers
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/v00/customers?$select=name&$expand=info&$top=2&$orderby=id
+###
+
+GET {{ODataMiniApi_HostAddress}}/v1/customers?$select=name,info&$top=2&$orderby=id
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/customers?$select=name&$top=2&$orderby=id&$expand=info
+###
+GET {{ODataMiniApi_HostAddress}}/v1/customers?$select=name,info&$top=2&$orderby=id%20desc
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/v2/customers?$select=name&$top=2&$orderby=id
+
+###
+GET {{ODataMiniApi_HostAddress}}/v2/customers?$select=name&$top=2&$orderby=id&$expand=orders($select=amount)
+###
+GET {{ODataMiniApi_HostAddress}}/v2/customers?$select=name,info&$top=2&$orderby=id%20desc
+
+###
+# GET {{ODataMiniApi_HostAddress}}/v11/customers?$select=name&$expand=info // throw exception because 'info' is not navigation property
+GET {{ODataMiniApi_HostAddress}}/v11/customers?$select=name,info
+
+###
+GET {{ODataMiniApi_HostAddress}}/v3/customers
+
+###
+GET {{ODataMiniApi_HostAddress}}/v5/customers
+// throw exception since the delegate needs parameters, but we cannot easily provide the parameters.
+
+
+### $filter=amount gt 15
+GET {{ODataMiniApi_HostAddress}}/v0/?$filter=amount%20gt%2015
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/orders?$filter=amount%20gt%2015
+###
+GET {{ODataMiniApi_HostAddress}}/v2/orders?$filter=amount%20gt%2015
+
+
+###
+
+PATCH {{ODataMiniApi_HostAddress}}/v1/customers/1
+Content-Type: application/json
+
+{
+ "name": "kerry"
+}
+
+###
+
+POST {{ODataMiniApi_HostAddress}}/v1/customers/1/rateByName
+Content-Type: application/json
+
+{
+ "name": "kerry",
+ "age":16
+}
+
+###
+
+# You will get the following response:
+#
+#{
+# "@odata.context": "http://localhost:5177/$metadata#Edm.String",
+# "value": "EdmActionName: 'Rating': rate based on '8'"
+#}
+
+POST {{ODataMiniApi_HostAddress}}/v1/customers/rating
+Content-Type: application/json
+
+{
+ "p": 8
+}
+
+###
+
+# The following is the test case for patch deltaset (changes) to an entity set.
+# You will get the following response:
+#
+#{
+# "@odata.context": "http://localhost:5177/$metadata#Edm.String",
+# "value": "Patch : '4' to customers"
+#}
+
+PATCH {{ODataMiniApi_HostAddress}}/v1/customers
+Content-Type: application/json
+
+{
+ "@odata.context":"http://localhost/v1/$metadata#Customers/$delta",
+ "value":[
+ {
+ "@odata.id":"Customers(42)",
+ "Name":"Microsoft"
+ },
+ {
+ "@odata.context":"http://localhost/v1/$metadata#Customers/$deletedLink",
+ "source":"Customers(32)",
+ "relationship":"Orders",
+ "target":"Orders(12)"
+ },
+ {
+ "@odata.context":"http://localhost/v1/$metadata#Customers/$link",
+ "source":"Customers(22)",
+ "relationship":"Orders",
+ "target":"Orders(2)"
+ },
+ {
+ "@odata.context":"http://localhost/v1/$metadata#Customers/$deletedEntity",
+ "id":"Customers(12)",
+ "reason":"deleted"
+ }
+ ]
+}
+
+###
\ No newline at end of file
diff --git a/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs b/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs
new file mode 100644
index 000000000..28a90e423
--- /dev/null
+++ b/sample/ODataMiniApi/Endpoints/CustomersEndpoints.cs
@@ -0,0 +1,211 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData;
+using Microsoft.AspNetCore.OData.Deltas;
+using Microsoft.AspNetCore.OData.Formatter;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+using Microsoft.OData.UriParser;
+
+namespace ODataMiniApi.Students;
+
+///
+/// Add customers endpoint
+///
+public static class CustomersEndpoints
+{
+ public static IEndpointRouteBuilder MapCustomersEndpoints(this IEndpointRouteBuilder app, IEdmModel model)
+ {
+ app.MapGet("/customers", (AppDb db) => db.Customers.Include(s => s.Orders))
+ //.WithODa
+ .WithODataResult() // default: built the complex type property by default?
+ // If enable Query, define them as entity type
+ // If no query, define them as complex type?
+ .WithODataModel(model)
+ .WithODataVersion(ODataVersion.V401)
+ .WithODataBaseAddressFactory(c => new Uri("http://abc.com"));
+ //.WithODataServices(c => c.AddSingleton;
+
+ // app.MapGet("v0/customers", (AppDb db) => Results.Extensions.AsOData(db.Customers.Include(s => s.Orders)));
+
+ //app.MapGet("v00/customers", (AppDb db, ODataQueryOptions queryOptions) =>
+ //{
+ // db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // This line seems required otherwise it will throw exception
+ // var result = queryOptions.ApplyTo(db.Customers.Include(s => s.Orders));
+ // return Results.Extensions.AsOData(result);
+ //});
+
+ app.MapGet("v1/customers", (AppDb db, ODataQueryOptions queryOptions) =>
+ {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // This line seems required otherwise it will throw exception
+ return queryOptions.ApplyTo(db.Customers.Include(s => s.Orders));
+ })
+ .WithODataResult()
+ .WithODataModel(model)
+ ;
+
+ // Be noted, [ODataModelConfiguration] configures the 'Info' as complex type
+ // So, in this case, the 'Info' property on 'Customer' is a structural property, not a navigation property.
+ app.MapGet("v11/customers", (AppDb db, [ODataModelConfiguration] ODataQueryOptions queryOptions) =>
+ {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return queryOptions.ApplyTo(db.Customers.Include(s => s.Orders));
+ });
+
+ app.MapGet("v2/customers", (AppDb db) =>
+ {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return db.Customers.Include(s => s.Orders);
+ })
+ .AddODataQueryEndpointFilter()
+ .WithODataResult()
+ //.WithODataModel(model)
+ ;
+
+ //app.MapGet("v3/customers", (AppDb db) =>
+ //{
+ // db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ // return Results.Extensions.AsOData(db.Customers.Include(s => s.Orders));
+ //})
+ // .AddODataQueryEndpointFilter()
+
+ // //.WithODataModel(model)
+ // ;
+
+ // To discuss? To provide the MapODataGet, MapODataPost, MapODataPatch....
+ // It seems we cannot generate the Delegate easily.
+ //app.MapODataGet("v5/customers", (AppDb db) =>
+ //{
+ // db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ // return db.Customers.Include(s => s.Orders);
+ //});
+
+ app.MapPatch("v1/customers/{id}", (AppDb db, int id, Delta delta) =>
+ {
+ Customer customer = db.Customers.FirstOrDefault(s => s.Id == id);
+ if (customer == null)
+ {
+ return null; // should return Results.NotFound();
+ };
+
+ delta.Patch(customer);
+
+ return customer;
+ })
+ .WithODataResult()
+ .WithODataModel(model)
+ .WithODataPathFactory(
+ (h, t) =>
+ {
+ string idStr = h.GetRouteValue("id") as string;
+ int id = int.Parse(idStr);
+ IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");
+
+ IDictionary keysValues = new Dictionary();
+ keysValues["Id"] = id;
+ return new ODataPath(new EntitySetSegment(customers), new KeySegment(keysValues, customers.EntityType, customers));
+ });
+
+ app.MapPost("v1/customers/{id}/rateByName", (AppDb db, int id, ODataActionParameters parameters) =>
+ {
+ Customer customer = db.Customers.FirstOrDefault(s => s.Id == id);
+ if (customer == null)
+ {
+ return null; // should return Results.NotFound();
+ };
+
+ return $"{customer.Name}: {System.Text.Json.JsonSerializer.Serialize(parameters)}";
+ })
+ .WithODataResult()
+ .WithODataModel(model)
+ .WithODataPathFactory(
+ (h, t) =>
+ {
+ string idStr = h.GetRouteValue("id") as string;
+ int id = int.Parse(idStr);
+ IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");
+
+ IDictionary keysValues = new Dictionary();
+ keysValues["Id"] = id;
+
+ IEdmAction action = model.SchemaElements.OfType().First(a => a.Name == "RateByName");
+
+ return new ODataPath(new EntitySetSegment(customers),
+ new KeySegment(keysValues, customers.EntityType, customers),
+ new OperationSegment(action, null)
+ );
+ });
+
+ app.MapPost("v1/customers/rating", (AppDb db, ODataUntypedActionParameters parameters) =>
+ {
+ return $"EdmActionName: '{parameters.Action.Name}': rate based on '{parameters["p"]}'";
+ })
+ .WithODataResult()
+ .WithODataModel(model)
+ .WithODataPathFactory(
+ (h, t) =>
+ {
+ IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");
+ IEdmAction action = model.SchemaElements.OfType().First(a => a.Name == "Rating");
+
+ return new ODataPath(new EntitySetSegment(customers),
+ new OperationSegment(action, null)
+ );
+ });
+
+ // DeltaSet
+ app.MapPatch("v1/customers", (AppDb db, DeltaSet changes) =>
+ {
+ return $"Patch : '{changes.Count}' to customers";
+ })
+ .WithODataResult()
+ .WithODataModel(model)
+ .WithODataPathFactory(
+ (h, t) =>
+ {
+ IEdmEntitySet customers = model.FindDeclaredEntitySet("Customers");
+ return new ODataPath(new EntitySetSegment(customers));
+ });
+ return app;
+ }
+
+ // Scenarios using groups
+ public static IEndpointRouteBuilder MapOrdersEndpoints(this IEndpointRouteBuilder app, IEdmModel model)
+ {
+ var group = app.MapGroup("")
+ .WithODataResult()
+ .WithODataModel(model);
+
+ group.MapGet("v0/orders", (AppDb db) => db.Orders)
+ ;
+
+ group.MapGet("v1/orders", (AppDb db, ODataQueryOptions queryOptions) =>
+ {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; // This line seems required otherwise it will throw exception
+ return queryOptions.ApplyTo(db.Orders);
+ })
+ // .WithODataResult()
+ // .WithODataModel(model)
+ ;
+
+ group.MapGet("v2/orders", (AppDb db) =>
+ {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return db.Orders;
+ })
+ .AddODataQueryEndpointFilter()
+ //.WithODataResult()
+ //.WithODataModel(model)
+ ;
+
+ return app;
+ }
+
+}
diff --git a/sample/ODataMiniApi/MetadataHandler.cs b/sample/ODataMiniApi/MetadataHandler.cs
new file mode 100644
index 000000000..0ee73b0cf
--- /dev/null
+++ b/sample/ODataMiniApi/MetadataHandler.cs
@@ -0,0 +1,90 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Text;
+using System.Text.Json;
+using System.Xml;
+using Microsoft.AspNetCore.OData;
+using Microsoft.OData.Edm;
+using Microsoft.OData.Edm.Csdl;
+using Microsoft.OData.Edm.Validation;
+using System.Text.Encodings.Web;
+using Microsoft.AspNetCore.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+
+namespace ODataMiniApi;
+
+
+[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
+public class ODataModelConfigurationAttribute : Attribute, IODataModelConfiguration
+{
+ public ODataModelBuilder Apply(HttpContext context, ODataModelBuilder builder, Type clrType)
+ {
+ if (clrType == typeof(Customer))
+ {
+ builder.AddComplexType(typeof(Info));
+ }
+
+ return builder;
+ }
+}
+
+/*
+public class CustomizedMetadataHandler : ODataMetadataHandler
+{
+ protected override async ValueTask WriteAsJsonAsync(HttpContext context, IEdmModel model)
+ {
+ context.Response.ContentType = "application/json";
+
+ JsonWriterOptions options = new JsonWriterOptions
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ Indented = false,
+ SkipValidation = false
+ };
+
+ // we can't use response body directly since ODL writes the JSON CSDL using Synchronous operations.
+ using (MemoryStream memStream = new MemoryStream())
+ {
+ using (Utf8JsonWriter jsonWriter = new Utf8JsonWriter(memStream, options))
+ {
+ CsdlJsonWriterSettings settings = new CsdlJsonWriterSettings();
+ settings.IsIeee754Compatible = true;
+ IEnumerable errors;
+ bool ok = CsdlWriter.TryWriteCsdl(model, jsonWriter, settings, out errors);
+ jsonWriter.Flush();
+ }
+
+ memStream.Seek(0, SeekOrigin.Begin);
+ string output = new StreamReader(memStream).ReadToEnd();
+ await context.Response.WriteAsync(output).ConfigureAwait(false);
+ }
+ }
+
+ protected override async ValueTask WriteAsXmlAsync(HttpContext context, IEdmModel model)
+ {
+ context.Response.ContentType = "application/xml";
+
+ using (StringWriter sw = new StringWriter())
+ {
+ XmlWriterSettings settings = new XmlWriterSettings();
+ settings.Encoding = Encoding.UTF8;
+ settings.Indent = false;
+
+ using (XmlWriter xw = XmlWriter.Create(sw, settings))
+ {
+ IEnumerable errors;
+ CsdlWriter.TryWriteCsdl(model, xw, CsdlTarget.OData, out errors);
+ xw.Flush();
+ }
+
+ string output = sw.ToString();
+ await context.Response.WriteAsync(output).ConfigureAwait(false);
+ }
+ }
+}
+*/
\ No newline at end of file
diff --git a/sample/ODataMiniApi/ODataMetadataApi.http b/sample/ODataMiniApi/ODataMetadataApi.http
new file mode 100644
index 000000000..471b1a5f0
--- /dev/null
+++ b/sample/ODataMiniApi/ODataMetadataApi.http
@@ -0,0 +1,70 @@
+@ODataMiniApi_HostAddress = http://localhost:5177
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$document
+
+##
+// The above request gets the following result:
+//{
+// "@odata.context": "http://localhost:5177/$metadata",
+// "value": [
+// {
+// "name": "Schools",
+// "kind": "EntitySet",
+// "url": "Schools"
+// },
+// {
+// "name": "Customers",
+// "kind": "EntitySet",
+// "url": "Customers"
+// },
+// {
+// "name": "Orders",
+// "kind": "EntitySet",
+// "url": "Orders"
+// }
+// ]
+//}
+
+###
+GET {{ODataMiniApi_HostAddress}}/v2/$document
+
+##
+// The above request gets the following result (has customized context URL)
+//{
+// "@odata.context": "http://localhost:5177/v2/$metadata",
+// "value": [
+// {
+// "name": "Schools",
+// "kind": "EntitySet",
+// "url": "Schools"
+// },
+// {
+// "name": "Customers",
+// "kind": "EntitySet",
+// "url": "Customers"
+// },
+// {
+// "name": "Orders",
+// "kind": "EntitySet",
+// "url": "Orders"
+// }
+// ]
+//}
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$metadata
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$metadata?$format=application/xml
+
+# The above two requests get the CSDL-XML file (No-indent by default)
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$metadata?$format=application/json
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$metadata
+Accept: application/json
+
+# The above two requests get the CSDL-JSON file
\ No newline at end of file
diff --git a/sample/ODataMiniApi/ODataMiniApi.csproj b/sample/ODataMiniApi/ODataMiniApi.csproj
new file mode 100644
index 000000000..eb483896e
--- /dev/null
+++ b/sample/ODataMiniApi/ODataMiniApi.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ disable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/ODataMiniApi/ODataMiniApi.http b/sample/ODataMiniApi/ODataMiniApi.http
new file mode 100644
index 000000000..bcb21d0d3
--- /dev/null
+++ b/sample/ODataMiniApi/ODataMiniApi.http
@@ -0,0 +1,93 @@
+@ODataMiniApi_HostAddress = http://localhost:5177
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/giveschools?$select=SchoolName
+
+###
+
+# It's wrong to use $expand=MailAddress
+# because the MailAddress is complex type in the pre-built edm model.
+GET {{ODataMiniApi_HostAddress}}/myschools?$select=schoolName&$expand=MailAddress
+
+###
+
+# be noted: if use the model directly, the mailAddress is configured as complex property
+GET {{ODataMiniApi_HostAddress}}/myschools?$select=schoolName,MailAddress
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/schools?$select=schoolName,schoolid&$expand=MailAddress&$top=1
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/schools?$select=schoolName,schoolid,MailAddress&$top=1&$count=true
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/v1/$odata?$format=application/json
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$odata?$format=application/xml
+
+###
+GET {{ODataMiniApi_HostAddress}}/customized/$odata
+
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$document
+###
+GET {{ODataMiniApi_HostAddress}}/v1/$metadata?$format=application/json
+
+###
+
+# We need to figure out what payload should look like?
+# Since it's a collection, how to add '@odata.count' property into the collection?
+# Or use the PageResult
+GET {{ODataMiniApi_HostAddress}}/schoolspage?$select=schoolName,MailAddress&$count=true
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/schoolspage?$count=true
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/odata/students
+
+###
+GET {{ODataMiniApi_HostAddress}}/odata/students?$select=FirstName&$top=3
+
+###
+
+@id=1
+GET {{ODataMiniApi_HostAddress}}/schools/{{id}}?$select=SchoolName
+
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/odata/schools
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/giveschools1
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/giveschools2
+
+###
+GET {{ODataMiniApi_HostAddress}}/giveschools2?$select=schoolName
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/giveschools3
+
+###
+GET {{ODataMiniApi_HostAddress}}/giveschools3?$select=schoolName
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/getschools1
+
+###
+
+GET {{ODataMiniApi_HostAddress}}/odata/getschools1
\ No newline at end of file
diff --git a/sample/ODataMiniApi/Program.cs b/sample/ODataMiniApi/Program.cs
new file mode 100644
index 000000000..e7acc2fb6
--- /dev/null
+++ b/sample/ODataMiniApi/Program.cs
@@ -0,0 +1,171 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.AspNetCore.OData;
+using Microsoft.AspNetCore.OData.Query;
+using ODataMiniApi;
+using ODataMiniApi.Students;
+using Microsoft.OData.Edm;
+using Microsoft.AspNetCore.OData.Results;
+using System.Text.Json;
+using Microsoft.AspNetCore.OData.Query.Expressions;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddDbContext(options => options.UseInMemoryDatabase("SchoolStudentList"));
+
+// In pre-built OData, MailAddress and Student are complex types
+IEdmModel model = EdmModelBuilder.GetEdmModel();
+
+
+builder.Services.ConfigureHttpJsonOptions(options => {
+ options.SerializerOptions.WriteIndented = true;
+ options.SerializerOptions.IncludeFields = true;
+});
+
+builder.Services.AddOData(q => q.EnableAll());
+
+var app = builder.Build();
+app.MakeSureDbCreated();
+
+ODataMiniMetadata me = new ODataMiniMetadata();
+me.Services = services =>
+{
+ services.AddSingleton();
+};
+
+app.MapGet("test", () => "hello world").WithODataResult();
+
+// Group
+var group = app.MapGroup("")
+// .WithOData2(metadata => metadata.IsODataFormat = true, services => services.AddSingleton(new FilterBinder()))
+ ;
+
+group.MapGet("v1", () => "hello v1")
+ .AddEndpointFilter(async (efiContext, next) =>
+ {
+ var endpoint = efiContext.HttpContext.GetEndpoint();
+ app.Logger.LogInformation("----Before calling");
+ var result = await next(efiContext);
+ app.Logger.LogInformation($"----After calling, {result?.GetType().Name}");
+ return result;
+ }
+ ).Finally(v =>
+ {
+ v.Metadata.Add(new School());
+ });
+group.MapGet("v2", () => "hello v2");
+
+//
+app.MapGet("/giveschools1", (AppDb db) =>
+{
+ return db.Schools.Include(s => s.Students);
+});
+
+app.MapGet("/giveschools2", (AppDb db, ODataQueryOptions options) =>
+{
+ return options.ApplyTo(db.Schools);
+})
+ .WithODataResult();
+
+app.MapGet("/giveschools3", (AppDb db) =>
+{
+ return db.Schools.Include(s => s.Students);
+})
+ .WithODataResult()
+ .AddODataQueryEndpointFilter(querySetup: q => q.PageSize = 3, validationSetup: q => q.MaxSkip = 4);
+
+//app.MapGet("/odata/schools", (AppDb db) =>
+//{
+// return Results.Extensions.OData(db.Schools.Include(s => s.Students));
+//});
+
+var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
+{ WriteIndented = true };
+app.MapGet("/", () =>
+ Results.Json(new School { SchoolName = "Walk dog", SchoolId = 8 }, options));
+
+// OData $metadata, I use $odata for routing.
+#region ODataMetadata
+//app.MapODataMetadata1("/v1/$odata", model);
+//app.MapODataMetadata("/customized/$odata", model, new CustomizedMetadataHandler());
+
+app.MapODataServiceDocument("v1/$document", model);
+
+app.MapODataServiceDocument("v2/$document", model)
+ .WithODataBaseAddressFactory(c => new Uri("http://localhost:5177/v2"));
+
+app.MapODataMetadata("v1/$metadata", model);
+
+#endregion
+
+#region School Endpoints
+
+app.MapGet("/myschools", (AppDb db) =>
+{
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return db.Schools;
+})
+ .WithODataModel(model)
+ // .AddODataQueryEndpointFilter(new ODataQueryEndpointFilter(app.Services.GetRequiredService()));
+ .AddODataQueryEndpointFilter(querySetup: q => q.PageSize = 3, validationSetup: q => q.MaxSkip = 4);
+
+// use the server side pagesize?
+app.MapGet("/schoolspage", (AppDb db) =>
+{
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return new PageResult(db.Schools, new Uri("/schoolspage?$skip=2", UriKind.Relative), count: db.Schools.Count());
+ //return db.Schools;
+})
+ .WithODataModel(model)
+ .AddODataQueryEndpointFilter(querySetup: q => q.PageSize = 3);
+
+app.MapGet("/schools", (AppDb db, ODataQueryOptions options) => {
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return options.ApplyTo(db.Schools);
+}).WithODataModel(model);
+
+// Use the model built on the fly
+app.MapGet("/schools/{id}", async (int id, AppDb db, ODataQueryOptions options) => {
+
+ School school = await db.Schools.Include(c => c.MailAddress)
+ .Include(c => c.Students).FirstOrDefaultAsync(s => s.SchoolId == id);
+ if (school == null)
+ {
+ return Results.NotFound($"Cannot find school with id '{id}'");
+ }
+ else
+ {
+ return Results.Ok(options.ApplyTo(school, new ODataQuerySettings()));
+ }
+});
+
+// Use the model built on the fly
+app.MapGet("/customized/schools", (AppDb db, ODataQueryOptions options) =>
+{
+ db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
+ return options.ApplyTo(db.Schools);
+});
+
+#endregion
+
+app.MapCustomersEndpoints(model);
+
+app.MapOrdersEndpoints(model);
+
+app.MapSchoolEndpoints(model);
+
+// Endpoints for students
+app.MapStudentEndpoints();
+
+app.Run();
+
+///
+/// This is required in E2E test to identify the assembly.
+///
+public partial class Program
+{ }
diff --git a/sample/ODataMiniApi/Properties/launchSettings.json b/sample/ODataMiniApi/Properties/launchSettings.json
new file mode 100644
index 000000000..74ab92ec0
--- /dev/null
+++ b/sample/ODataMiniApi/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:27886",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5177",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/sample/ODataMiniApi/Schools/SchoolEndpoints.cs b/sample/ODataMiniApi/Schools/SchoolEndpoints.cs
new file mode 100644
index 000000000..c72291692
--- /dev/null
+++ b/sample/ODataMiniApi/Schools/SchoolEndpoints.cs
@@ -0,0 +1,35 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using Microsoft.AspNetCore.OData;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.OData.Edm;
+
+namespace ODataMiniApi.Students;
+
+///
+/// Add school endpoint
+///
+public static class SchoolEndpoints
+{
+ public static IEndpointRouteBuilder MapSchoolEndpoints(this IEndpointRouteBuilder app, IEdmModel model)
+ {
+ app.MapGet("/getschools1", (AppDb db) => db.Schools.Include(s => s.Students));
+
+ app.MapGet("/odata/getschools1", (AppDb db) => db.Schools.Include(s => s.Students))
+ .WithODataResult();
+
+ app.MapGet("/odata/getschools2", (AppDb db) => db.Schools.Include(s => s.Students))
+ .WithODataModel(model);
+
+ app.MapGet("/odata/getschools2", (AppDb db) => db.Schools.Include(s => s.Students))
+ .WithODataResult()
+ .WithODataModel(model);
+ return app;
+ }
+
+}
diff --git a/sample/ODataMiniApi/Students/StudentEndpoints.cs b/sample/ODataMiniApi/Students/StudentEndpoints.cs
new file mode 100644
index 000000000..058073802
--- /dev/null
+++ b/sample/ODataMiniApi/Students/StudentEndpoints.cs
@@ -0,0 +1,209 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.OData.Query;
+using Microsoft.EntityFrameworkCore;
+
+namespace ODataMiniApi.Students;
+
+///
+/// Add student endpoint to support CRUD operations.
+/// The codes are implemented as simple as possible. Please file issues to us for any issues.
+///
+public static class StudentEndpoints
+{
+ public static async Task GetStudentByIdAsync(int id, AppDb db, ODataQueryOptions options)
+ {
+ Student student = await db.Students.FirstOrDefaultAsync(s => s.StudentId == id);
+ if (student == null)
+ {
+ return Results.NotFound($"Cannot find student with id '{id}'");
+ }
+ else
+ {
+ return Results.Ok(options.ApplyTo(student, new ODataQuerySettings()));
+ }
+ }
+
+ public static async Task PatchStudentByIdAsync(int id, AppDb db, [FromBody] IDictionary properties)
+ {
+ // TODO: need to support Delta to replace IDictionary in the next step
+ Student oldStudent = await db.Students.FirstOrDefaultAsync(s => s.StudentId == id);
+ if (oldStudent == null)
+ {
+ return Results.NotFound($"Cannot find student with id '{id}'");
+ }
+
+ int oldSchoolId = oldStudent.SchoolId;
+
+ var studentProperties = typeof(Student).GetProperties();
+ foreach (var property in properties)
+ {
+ PropertyInfo propertyInfo = studentProperties.FirstOrDefault(p => string.Equals(p.Name, property.Key, StringComparison.OrdinalIgnoreCase));
+ if (propertyInfo == null)
+ {
+ return Results.BadRequest($"Cannot find property '{property.Key}' on student");
+ }
+
+ // For simplicity
+ if (propertyInfo.PropertyType == typeof(string))
+ {
+ propertyInfo.SetValue(oldStudent, property.Value.ConvertToString());
+ }
+ else if (propertyInfo.PropertyType == typeof(int))
+ {
+ propertyInfo.SetValue(oldStudent, property.Value.ConvertToInt());
+ }
+ else if (propertyInfo.PropertyType == typeof(DateOnly))
+ {
+ propertyInfo.SetValue(oldStudent, property.Value.ConvertToDateOnly());
+ }
+ }
+
+ if (oldSchoolId != oldStudent.SchoolId)
+ {
+ School school = await db.Schools.Include(c => c.Students).FirstOrDefaultAsync(c => c.SchoolId == oldSchoolId);
+ school.Students.Remove(oldStudent);
+
+ school = await db.Schools.Include(c => c.Students).FirstOrDefaultAsync(c => c.SchoolId == oldStudent.SchoolId);
+ if (school == null)
+ {
+ return Results.NotFound($"Cannot find school using the school Id '{oldStudent.SchoolId}' that the new student provides.");
+ }
+ else
+ {
+ school.Students.Add(oldStudent);
+ }
+ }
+
+ await db.SaveChangesAsync();
+
+ return Results.Ok(oldStudent);
+ }
+
+ public static async Task DeleteStudentByIdAsync(int id, AppDb db)
+ {
+ Student student = await db.Students.FirstOrDefaultAsync(s => s.StudentId == id);
+ if (student == null)
+ {
+ return Results.NotFound($"Cannot find student with id '{id}'");
+ }
+ else
+ {
+ db.Students.Remove(student);
+ School school = await db.Schools.Include(c => c.Students).FirstOrDefaultAsync(c => c.SchoolId == student.SchoolId);
+ school.Students.Remove(student);
+ await db.SaveChangesAsync();
+ return Results.Ok();
+ }
+ }
+
+ public static IEndpointRouteBuilder MapStudentEndpoints(this IEndpointRouteBuilder app)
+ {
+ // Let's use the group to build the student endpoints
+ var students = app.MapGroup("/odata");
+
+ // GET http://localhost:5177/odata/students?$select=lastName&$top=3
+ students.MapGet("/students", async (AppDb db, ODataQueryOptions options) =>
+ {
+ await db.Students.ToListAsync();
+ return options.ApplyTo(db.Students);
+ });
+
+ //GET http://localhost:5177/odata/students/12?select=lastName
+ students.MapGet("/students/{id}", GetStudentByIdAsync);
+
+ //GET http://localhost:5177/odata/students(12)?select=lastName
+ students.MapGet("/students({id})", GetStudentByIdAsync);
+
+ // POST http://localhost:5177/odata/students
+ // Content-Type: application/json
+ // BODY:
+ /*
+{
+
+"firstName": "Soda",
+"lastName": "Yu",
+"favoriteSport": "Soccer",
+"grade": 7,
+"schoolId": 3,
+"birthDay": "1977-11-04"
+}
+ */
+ students.MapPost("/students", async (Student student, AppDb db) =>
+ {
+ int studentId = db.Students.Max(s => s.StudentId) + 1;
+ student.StudentId = studentId;
+ School school = await db.Schools.Include(c => c.Students).FirstOrDefaultAsync(c => c.SchoolId == student.SchoolId);
+ if (school == null)
+ {
+ return Results.NotFound($"Cannot find school using the school Id '{student.SchoolId}' that the new student provides.");
+ }
+ else
+ {
+ school.Students.Add(student);
+ }
+
+ db.Students.Add(student);
+ await db.SaveChangesAsync();
+
+ return Results.Created($"/odata/students/{studentId}", student);
+ });
+
+ // PATCH http://localhost:5177/odata/students/10
+ // Content-Type: application/json
+ // BODY:
+ /*
+{
+ "firstName": "Sokuda",
+ "lastName": "Yu",
+ "schoolId": 4
+}
+ */
+ students.MapPatch("/students({id})", PatchStudentByIdAsync);
+ students.MapPatch("/students/{id}", PatchStudentByIdAsync);
+
+ // DELETE http://localhost:5177/odata/students/10
+ students.MapDelete("/students({id})", DeleteStudentByIdAsync);
+ students.MapDelete("/students/{id}", DeleteStudentByIdAsync);
+ return app;
+ }
+
+ private static string ConvertToString(this object value)
+ {
+ if (value is JsonElement json && json.ValueKind == JsonValueKind.String)
+ {
+ return json.GetString();
+ }
+
+ throw new InvalidCastException($"Cannot convert '{value}' to string");
+ }
+
+ private static int ConvertToInt(this object value)
+ {
+ if (value is JsonElement json && json.ValueKind == JsonValueKind.Number)
+ {
+ return json.GetInt32();
+ }
+
+ throw new InvalidCastException($"Cannot convert '{value}' to int");
+ }
+
+ private static DateOnly ConvertToDateOnly(this object value)
+ {
+ if (value is JsonElement json && json.ValueKind == JsonValueKind.String)
+ {
+ string str = json.GetString();
+ return DateOnly.Parse(str);
+ }
+
+ throw new InvalidCastException($"Cannot convert '{value}' to DateOnly");
+ }
+}
diff --git a/sample/ODataMiniApi/appsettings.Development.json b/sample/ODataMiniApi/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/sample/ODataMiniApi/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/sample/ODataMiniApi/appsettings.json b/sample/ODataMiniApi/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/sample/ODataMiniApi/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/sample/ODataMiniApi/readme.md b/sample/ODataMiniApi/readme.md
new file mode 100644
index 000000000..6516f02ef
--- /dev/null
+++ b/sample/ODataMiniApi/readme.md
@@ -0,0 +1,308 @@
+# ASP.NET Core OData (8.x) Minimal API Sample
+
+---
+This is an ASP.NET Core OData 8.x minimal API project.
+
+Minimal APIs are a simplified approach for building fast HTTP APIs with ASP.NET Core. You can build fully functioning REST endpoints with minimal code and configuration. Skip traditional scaffolding and avoid unnecessary controllers by fluently declaring API routes and actions.
+
+
+## Basic endpoints
+
+1) GET `http://localhost:5177/schools`
+
+```json
+[
+ {
+ "schoolId": 1,
+ "schoolName": "Mercury Middle School",
+ "mailAddress": {
+ "apartNum": 241,
+ "city": "Kirk",
+ "street": "156TH AVE",
+ "zipCode": "98051"
+ },
+ "students": null
+ },
+ {
+ "schoolId": 2,
+ ...
+ }
+ ...
+]
+```
+
+2) GET `http://localhost:5177/schools?$expand=mailaddress&$select=schoolName&$top=2`
+
+```json
+[
+ {
+ "MailAddress": {
+ "ApartNum": 241,
+ "City": "Kirk",
+ "Street": "156TH AVE",
+ "ZipCode": "98051"
+ },
+ "SchoolName": "Mercury Middle School"
+ },
+ {
+ "MailAddress": {
+ "ApartNum": 543,
+ "City": "AR",
+ "Street": "51TH AVE PL",
+ "ZipCode": "98043"
+ },
+ "SchoolName": "Venus High School"
+ }
+]
+```
+
+3) GET `http://localhost:5177/schools/5?$expand=students($top=1)&$select=schoolName`
+
+```json
+{
+ "Students": [
+ {
+ "StudentId": 50,
+ "FirstName": "David",
+ "LastName": "Padron",
+ "FavoriteSport": "Tennis",
+ "Grade": 77,
+ "SchoolId": 5,
+ "BirthDay": "2015-12-03"
+ }
+ ],
+ "SchoolName": "Jupiter College"
+}
+```
+
+4) GET `http://localhost:5177/customized/schools?$select=schoolName,mailAddress&$orderby=schoolName&$top=1`
+
+This endpoint uses the `OData model` from configuration, which builds `Address` as complex type, so we can use `$select`
+
+```json
+[
+ {
+ "SchoolName": "Earth University",
+ "MailAddress": {
+ "ApartNum": 101,
+ "City": "Belly",
+ "Street": "24TH ST",
+ "ZipCode": "98029"
+ }
+ }
+]
+```
+
+## Student CRUD endpoints
+
+I grouped student endpoints under `odata` group intentionally.
+
+1) GET `http://localhost:5177/odata/students?$select=lastName&$top=3`
+
+```json
+[
+ {
+ "LastName": "Alex"
+ },
+ {
+ "LastName": "Eaine"
+ },
+ {
+ "LastName": "Rorigo"
+ }
+]
+```
+
+2) POST `http://localhost:5177/odata/students` with the following body:
+Content-Type: application/json
+
+```json
+{
+
+ "firstName": "Sokuda",
+ "lastName": "Yu",
+ "favoriteSport": "Soccer",
+ "grade": 7,
+ "schoolId": 3,
+ "birthDay": "1977-11-04"
+}
+```
+
+Check using `http://localhost:5177/schools/3`, you can see a new student added:
+
+```json
+[
+ "schoolId": 3,
+ "schoolName": "Earth University",
+ "mailAddress": {
+ "apartNum": 101,
+ "city": "Belly",
+ "street": "24TH ST",
+ "zipCode": "98029"
+ },
+ "students": [
+ ...
+ {
+ "studentId": 98,
+ "firstName": "Sokuda",
+ "lastName": "Yu",
+ "favoriteSport": "Soccer",
+ "grade": 7,
+ "schoolId": 3,
+ "birthDay": "1977-11-04"
+ }
+ ]
+}
+```
+
+3) Patch `http://localhost:5177/odata/students/10`
+Content-Type: application/json
+
+```json
+{
+
+ "firstName": "Sokuda",
+ "lastName": "Yu",
+ "schoolId": 4
+}
+```
+
+This will change the student, and also move the student from `Schools(1)` to `Schools(4)`
+
+4) Delete `http://localhost:5177/odata/students/10`
+
+This will delete the `Students(10)`
+
+
+## OData CSDL metadata
+
+I built one metadata endpoint to return the CSDL representation of 'customized' OData.
+
+I use '$odata' to return the metadata.
+
+Try: GET http://localhost:5177/customized/$odata, You can get CSDL XML representation:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+You can use 'Accept' request header or '$format' or 'format' query option to specify JSON or XML format, by default it's XML format.
+
+Try: GET http://localhost:5177/customized/$odata, You can get CSDL XML representation:
+Request Header:
+Accept: application/json
+
+You can get:
+
+```json
+{
+ "$Version": "4.0",
+ "$EntityContainer": "Default.Container",
+ "ODataMiniApi": {
+ "School": {
+ "$Kind": "EntityType",
+ "$Key": [
+ "SchoolId"
+ ],
+ "SchoolId": {
+ "$Type": "Edm.Int32"
+ },
+ "SchoolName": {
+ "$Nullable": true
+ },
+ "MailAddress": {
+ "$Type": "ODataMiniApi.Address",
+ "$Nullable": true
+ },
+ "Students": {
+ "$Collection": true,
+ "$Type": "ODataMiniApi.Student",
+ "$Nullable": true
+ }
+ },
+ "Address": {
+ "$Kind": "ComplexType",
+ "ApartNum": {
+ "$Type": "Edm.Int32"
+ },
+ "City": {
+ "$Nullable": true
+ },
+ "Street": {
+ "$Nullable": true
+ },
+ "ZipCode": {
+ "$Nullable": true
+ }
+ },
+ "Student": {
+ "$Kind": "ComplexType",
+ "StudentId": {
+ "$Type": "Edm.Int32"
+ },
+ "FirstName": {
+ "$Nullable": true
+ },
+ "LastName": {
+ "$Nullable": true
+ },
+ "FavoriteSport": {
+ "$Nullable": true
+ },
+ "Grade": {
+ "$Type": "Edm.Int32"
+ },
+ "SchoolId": {
+ "$Type": "Edm.Int32"
+ },
+ "BirthDay": {
+ "$Type": "Edm.Date"
+ }
+ }
+ },
+ "Default": {
+ "Container": {
+ "$Kind": "EntityContainer",
+ "Schools": {
+ "$Collection": true,
+ "$Type": "ODataMiniApi.School"
+ }
+ }
+ }
+}
+```
diff --git a/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs b/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs
index 12976d088..1b3a5fd2d 100644
--- a/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs
+++ b/sample/ODataRoutingSample/Controllers/v1/CustomersController.cs
@@ -41,7 +41,7 @@ public CustomersController(MyDataContext context)
// For example: http://localhost:5000/v1/customers?$apply=groupby((Name), aggregate($count as count))&$orderby=name desc
[HttpGet]
- [EnableQuery]
+ [EnableQuery(PageSize = 2)]
public IActionResult Get()
{
return Ok(GetCustomers());
diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Nightly.nuspec b/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Nightly.nuspec
index 020c8713b..bb62202a6 100644
--- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Nightly.nuspec
+++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Nightly.nuspec
@@ -4,8 +4,8 @@
Microsoft.AspNetCore.OData.NewtonsoftJson
Microsoft ASP.NET Core OData opt-in component to support Newtonsoft.Json serializer converters
$VersionFullSemantic$-Nightly$NightlyBuildVersion$
- OData (.NET Foundation)
- © .NET Foundation and Contributors. All rights reserved.
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
This package contains customized Newtonsoft.Json serializer converters to support OData serialization.
This package contains customized Newtonsoft.Json serializer converters to support OData serialization.
en-US
diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Release.nuspec b/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Release.nuspec
index f306e2ecd..9032c0ada 100644
--- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Release.nuspec
+++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson.Release.nuspec
@@ -4,8 +4,8 @@
Microsoft.AspNetCore.OData.NewtonsoftJson
Microsoft ASP.NET Core OData opt-in component to support Newtonsoft.Json serializer converters
$VersionNuGetSemantic$
- OData (.NET Foundation)
- © .NET Foundation and Contributors. All rights reserved.
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
This package contains customized Newtonsoft.Json serializer converters to support OData serialization.
This package contains customized Newtonsoft.Json serializer converters to support OData serialization.
en-US
diff --git a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/Microsoft.AspNetCore.OData.NewtonsoftJson.csproj b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/Microsoft.AspNetCore.OData.NewtonsoftJson.csproj
index 155705a93..527ec6904 100644
--- a/src/Microsoft.AspNetCore.OData.NewtonsoftJson/Microsoft.AspNetCore.OData.NewtonsoftJson.csproj
+++ b/src/Microsoft.AspNetCore.OData.NewtonsoftJson/Microsoft.AspNetCore.OData.NewtonsoftJson.csproj
@@ -16,8 +16,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/Microsoft.AspNetCore.OData.Nightly.nuspec b/src/Microsoft.AspNetCore.OData.Nightly.nuspec
index 65c906528..005a3378f 100644
--- a/src/Microsoft.AspNetCore.OData.Nightly.nuspec
+++ b/src/Microsoft.AspNetCore.OData.Nightly.nuspec
@@ -4,8 +4,8 @@
Microsoft.AspNetCore.OData
Microsoft ASP.NET Core 8.x for OData v4.0
$VersionFullSemantic$-Nightly$NightlyBuildVersion$
- OData (.NET Foundation)
- © .NET Foundation and Contributors. All rights reserved.
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
This package contains everything you need to create OData v4.0 endpoints using ASP.NET Core MVC Core 8.x to support OData query syntax for your Web APIs.
This package contains everything you need to create OData v4.0 endpoints using ASP.NET Core.
en-US
diff --git a/src/Microsoft.AspNetCore.OData.Release.nuspec b/src/Microsoft.AspNetCore.OData.Release.nuspec
index 970c81ba4..ca94f8d9b 100644
--- a/src/Microsoft.AspNetCore.OData.Release.nuspec
+++ b/src/Microsoft.AspNetCore.OData.Release.nuspec
@@ -4,8 +4,8 @@
Microsoft.AspNetCore.OData
Microsoft ASP.NET Core 8.x for OData v4.0
$VersionNuGetSemantic$
- OData (.NET Foundation)
- © .NET Foundation and Contributors. All rights reserved.
+ Microsoft
+ © Microsoft Corporation. All rights reserved.
This package contains everything you need to create OData v4.0 endpoints using ASP.NET Core MVC Core 8.x to support OData query syntax for your Web APIs.
This package contains everything you need to create OData v4.0 endpoints using ASP.NET Core MVC.
en-US
diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs
index 4be2d89aa..e1d4b6ef9 100644
--- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs
+++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Linq;
@@ -40,7 +41,141 @@ public static bool IsDynamicTypeWrapper(this Type type)
public static bool IsSelectExpandWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(SelectExpandWrapper<>), type, out entityType);
- public static bool IsComputeWrapper(this Type type, out Type entityType) => IsTypeWrapper(typeof(ComputeWrapper<>), type, out entityType);
+ ///
+ /// Determines whether the specified type is a or a custom implementation
+ /// that inherits from and implements both
+ /// and .
+ ///
+ /// The type to check.
+ /// The entity type if the specified type is a or a custom implementation; otherwise, null.
+ ///
+ /// true if the specified type is a or a custom implementation
+ /// that meets the criteria; otherwise, false.
+ ///
+ public static bool IsComputeWrapper(this Type typeToCheck, out Type entityType)
+ {
+ entityType = null;
+ if (typeToCheck == null)
+ {
+ return false;
+ }
+
+ bool isComputeWrapper = false;
+
+ if (typeToCheck.IsGenericType)
+ {
+ Type genericTypeDefinition = typeToCheck.GetGenericTypeDefinition();
+
+ // Default implementation
+ if (genericTypeDefinition == typeof(ComputeWrapper<>))
+ {
+ Debug.Assert(
+ typeof(DynamicTypeWrapper).IsAssignableFrom(genericTypeDefinition)
+ && genericTypeDefinition.ImplementsInterface(typeof(IComputeWrapper<>))
+ && genericTypeDefinition.ImplementsInterface(typeof(IGroupByWrapper<,>)),
+ "ComputeWrapper must inherit from DynamicTypeWrapper and implement IComputeWrapper and IGroupByWrapper");
+
+ isComputeWrapper = true;
+ }
+ // Custom implementation
+ // Must inherit from DynamicTypeWrapper
+ // Must implement IComputeWrapper and IGroupByWrapper
+ else if (typeof(DynamicTypeWrapper).IsAssignableFrom(genericTypeDefinition)
+ && genericTypeDefinition.ImplementsInterface(typeof(IComputeWrapper<>))
+ && genericTypeDefinition.ImplementsInterface(typeof(IGroupByWrapper<,>)))
+ {
+ isComputeWrapper = true;
+ }
+
+ if (isComputeWrapper)
+ {
+ entityType = typeToCheck.GetGenericArguments()[0];
+ }
+ }
+
+ return isComputeWrapper;
+ }
+
+ ///
+ /// Determines whether the specified type is a or a custom implementation
+ /// that inherits from and implements both
+ /// and .
+ ///
+ /// The type to check.
+ ///
+ /// true if the specified type is a or a custom implementation
+ /// that meets the criteria; otherwise, false.
+ ///
+ public static bool IsFlatteningWrapper(this Type typeToCheck)
+ {
+ if (typeToCheck == null)
+ {
+ return false;
+ }
+
+ if (typeToCheck.IsGenericType)
+ {
+ Type genericTypeDefinition = typeToCheck.GetGenericTypeDefinition();
+
+ Func isFlatteningWrapperFunc = () => typeof(DynamicTypeWrapper).IsAssignableFrom(genericTypeDefinition)
+ && genericTypeDefinition.ImplementsInterface(typeof(IFlatteningWrapper<>))
+ && genericTypeDefinition.ImplementsInterface(typeof(IGroupByWrapper<,>));
+ // Default implementation
+ if (genericTypeDefinition == typeof(FlatteningWrapper<>))
+ {
+ Debug.Assert(
+ typeof(DynamicTypeWrapper).IsAssignableFrom(genericTypeDefinition)
+ && genericTypeDefinition.ImplementsInterface(typeof(IFlatteningWrapper<>))
+ && genericTypeDefinition.ImplementsInterface(typeof(IGroupByWrapper<,>)),
+ "FlatteningWrapper must inherit from DynamicTypeWrapper and implement IFlatteningWrapper and IGroupByWrapper");
+
+ return true;
+ }
+
+ // Custom implementation
+ // Must inherit from DynamicTypeWrapper
+ // Must implement IFlatteningWrapper and IGroupByWrapper
+ return typeof(DynamicTypeWrapper).IsAssignableFrom(genericTypeDefinition)
+ && genericTypeDefinition.ImplementsInterface(typeof(IFlatteningWrapper<>))
+ && genericTypeDefinition.ImplementsInterface(typeof(IGroupByWrapper<,>));
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified type is a or a custom implementation
+ /// that inherits from and implements .
+ ///
+ /// The type to check.
+ ///
+ /// true if the specified type is a or a custom implementation
+ /// that meets the criteria; otherwise, false.
+ ///
+ public static bool IsGroupByWrapper(this Type typeToCheck)
+ {
+ if (typeToCheck == null || typeToCheck.IsValueType || typeToCheck == typeof(string))
+ {
+ return false;
+ }
+
+ // Default implementation
+ if (typeof(GroupByWrapper).IsAssignableFrom(typeToCheck))
+ {
+ Debug.Assert(
+ typeof(DynamicTypeWrapper).IsAssignableFrom(typeToCheck)
+ && typeToCheck.ImplementsInterface(typeof(IGroupByWrapper<,>)),
+ "GroupByWrapper must inherit from DynamicTypeWrapper and implement IGroupByWrapper");
+
+ return true;
+ }
+
+ // Custom implementation
+ // Must inherit from DynamicTypeWrapper
+ // Must implement IGroupByWrapper
+ return typeof(DynamicTypeWrapper).IsAssignableFrom(typeToCheck) &&
+ typeToCheck.ImplementsInterface(typeof(IGroupByWrapper<,>));
+ }
private static bool IsTypeWrapper(Type wrappedType, Type type, out Type entityType)
{
@@ -250,6 +385,23 @@ Type collectionInterface
return false;
}
+ ///
+ /// Determines whether the specified represents an type.
+ ///
+ /// The to evaluate.
+ /// True if the type is an enumeration; false otherwise.
+ public static bool IsAsyncEnumerableType(Type clrType)
+ {
+ if (clrType == null)
+ {
+ throw Error.ArgumentNull(nameof(clrType));
+ }
+
+ return
+ (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) ||
+ (clrType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)));
+ }
+
internal static bool IsDictionary(Type clrType)
{
if (clrType == null)
@@ -382,6 +534,11 @@ internal static Type GetTaskInnerTypeOrSelf(Type type)
return type.GetGenericArguments().First();
}
+ if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>))
+ {
+ return type.GetGenericArguments().First();
+ }
+
return type;
}
@@ -431,4 +588,71 @@ private static Type GetInnerGenericType(Type interfaceType)
return null;
}
+
+ ///
+ /// Determines whether the specified type inherits from a given generic base type.
+ ///
+ /// The type to examine.
+ /// The open generic type definition to check against (e.g., typeof(Base<>)).
+ /// true if inherits from ; otherwise, false.
+ public static bool InheritsFromGenericBase(this Type typeToCheck, Type genericBaseType)
+ {
+ if (typeToCheck == null || genericBaseType == null || !genericBaseType.IsGenericTypeDefinition)
+ return false;
+
+ Type baseType = typeToCheck.BaseType;
+
+ while (baseType != null && baseType != typeof(object))
+ {
+ if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == genericBaseType)
+ {
+ return true;
+ }
+
+ baseType = baseType.BaseType;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified target type implements the given interface type.
+ ///
+ /// The type to check for implementation of the interface.
+ /// The interface type to check against.
+ ///
+ /// true if the target type implements the specified interface type; otherwise, false.
+ ///
+ ///
+ /// This method supports both generic and non-generic interfaces. For generic interfaces, it checks if the target type
+ /// implements any interface that matches the generic type definition of the specified interface type.
+ ///
+ public static bool ImplementsInterface(this Type targetType, Type interfaceType)
+ {
+ if (targetType == null || interfaceType == null)
+ {
+ return false;
+ }
+
+ if (interfaceType.IsGenericTypeDefinition) // Generic interface (e.g., I<>)
+ {
+ Type[] implementedInterfaces = targetType.GetInterfaces();
+ for (int i = 0; i < implementedInterfaces.Length; i++)
+ {
+ Type implementedInterface = implementedInterfaces[i];
+
+ if (implementedInterface.IsGenericType &&
+ implementedInterface.GetGenericTypeDefinition() == interfaceType)
+ {
+ return true;
+ }
+ }
+ }
+ else // Non-generic interface
+ {
+ return interfaceType.IsAssignableFrom(targetType);
+ }
+
+ return false;
+ }
}
diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs
index 84a0180c1..61e6b230a 100644
--- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs
+++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaOfT.cs
@@ -15,8 +15,11 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
+using Microsoft.AspNetCore.Http;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Abstracts;
using Microsoft.AspNetCore.OData.Common;
+using Microsoft.AspNetCore.OData.Extensions;
namespace Microsoft.AspNetCore.OData.Deltas;
@@ -426,6 +429,17 @@ public void CopyChangedValues(T original)
}
}
+ ///
+ /// Binds the and to generate the .
+ ///
+ /// The HttpContext.
+ /// The parameter info.
+ /// The built
+ public static async ValueTask> BindAsync(HttpContext context, ParameterInfo parameter)
+ {
+ return await context.BindODataParameterAsync>(parameter);
+ }
+
///
/// Copies the unchanged property values from the underlying entity (accessible via )
/// to the entity.
diff --git a/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs b/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs
index 1334a72b7..c6acce777 100644
--- a/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs
+++ b/src/Microsoft.AspNetCore.OData/Deltas/DeltaSetOfT.cs
@@ -8,7 +8,11 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Abstracts;
+using Microsoft.AspNetCore.OData.Extensions;
namespace Microsoft.AspNetCore.OData.Deltas;
@@ -29,6 +33,17 @@ public class DeltaSet : Collection, IDeltaSet, ITypedDelta whe
///
public Type ExpectedClrType => typeof(T);
+ ///
+ /// Binds the and to generate the .
+ ///
+ /// The HttpContext.
+ /// The parameter info.
+ /// The built
+ public static async ValueTask> BindAsync(HttpContext context, ParameterInfo parameter)
+ {
+ return await context.BindODataParameterAsync>(parameter);
+ }
+
#region Exclude unfinished APIs
#if false
///
@@ -100,5 +115,5 @@ protected virtual T GetOriginal(IDeltaSetItem deltaItem, IEnumerable originalSet
return null;
}
#endif
-#endregion
+ #endregion
}
diff --git a/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs b/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs
new file mode 100644
index 000000000..2ddedb38b
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs
@@ -0,0 +1,28 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System;
+using Microsoft.AspNetCore.Http;
+using Microsoft.OData.ModelBuilder;
+
+namespace Microsoft.AspNetCore.OData.Edm;
+
+///
+/// Defines a contract used to apply extra logic on the model builder.
+///
+public interface IODataModelConfiguration
+{
+ ///
+ /// Applies model configurations using the provided builder.
+ ///
+ /// The HttpContext.
+ /// The builder used to apply configurations.
+ /// The top CLR type.
+ /// The model builder or a totally new builder to use.
+ ODataModelBuilder Apply(HttpContext context, ODataModelBuilder builder, Type clrType);
+}
+
diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs
index 3f19f95eb..4e151c32d 100644
--- a/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs
+++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpContextExtensions.cs
@@ -5,10 +5,22 @@
//
//------------------------------------------------------------------------------
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.OData.Abstracts;
+using Microsoft.AspNetCore.OData.Edm;
+using Microsoft.AspNetCore.OData.Formatter;
+using Microsoft.AspNetCore.OData.Results;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Microsoft.OData;
+using Microsoft.OData.Edm;
+using Microsoft.OData.ModelBuilder;
+using Microsoft.OData.UriParser;
namespace Microsoft.AspNetCore.OData.Extensions;
@@ -75,4 +87,233 @@ public static ODataOptions ODataOptions(this HttpContext httpContext)
return httpContext.RequestServices?.GetService>()?.Value;
}
+
+ internal static bool IsMinimalEndpoint(this HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw Error.ArgumentNull(nameof(httpContext));
+ }
+
+ // Check if the endpoint is a minimal endpoint.
+ var endpoint = httpContext.GetEndpoint();
+ return endpoint?.Metadata.GetMetadata() != null;
+ }
+
+ internal static IEdmModel GetOrCreateEdmModel(this HttpContext httpContext, Type clrType, ParameterInfo parameter = null)
+ {
+ if (httpContext == null)
+ {
+ throw Error.ArgumentNull(nameof(httpContext));
+ }
+
+ // P1. Get model from the request if it's configured/cached, used it.
+ IODataFeature odataFeature = httpContext.ODataFeature();
+ IEdmModel model = odataFeature.Model;
+ if (model is not null)
+ {
+ return model;
+ }
+
+ // P2. Retrieve it from metadata if 'WithODataModel(model)' called/cached.
+ var endpoint = httpContext.GetEndpoint();
+ var odataMiniMetadata = endpoint.Metadata.GetMetadata();
+ model = odataMiniMetadata?.Model;
+ if (model is not null)
+ {
+ odataFeature.Model = model;
+ return model;
+ }
+
+ // 3.Ok, we don't have the model configured, let's build the model on the fly
+ bool isQueryCompositionMode = parameter != null ? true : false;
+
+ IAssemblyResolver resolver = httpContext.RequestServices.GetService() ?? new DefaultAssemblyResolver();
+ ODataModelBuilder builder = new ODataConventionModelBuilder(resolver, isQueryCompositionMode);
+
+ EntityTypeConfiguration entityTypeConfiguration = builder.AddEntityType(clrType);
+ builder.AddEntitySet(clrType.Name, entityTypeConfiguration);
+
+ // Do the model configuration if the configuration service is registered.
+ // First, let's check the configuration on the parameter as attribute (provided using parameterInfo)
+ var modelConfig = parameter?.GetCustomAttributes()
+ .FirstOrDefault(c => c is IODataModelConfiguration) as IODataModelConfiguration;
+
+ // Then, check the configuration on the globle
+ modelConfig = modelConfig ?? httpContext.RequestServices.GetService();
+ if (modelConfig is not null)
+ {
+ builder = modelConfig.Apply(httpContext, builder, clrType);
+ }
+
+ model = builder.GetEdmModel();
+
+ // Add the model into the cache
+ if (odataMiniMetadata is not null)
+ {
+ // make sure the 'ServiceProvider' is built after the model configuration.
+ odataMiniMetadata.Model = model;
+ }
+
+ // Cached it into the ODataFeature()
+ odataFeature.Model = model;
+ return model;
+ }
+
+ internal static ODataPath GetOrCreateODataPath(this HttpContext httpContext, Type clrType)
+ {
+ if (httpContext == null)
+ {
+ throw Error.ArgumentNull(nameof(httpContext));
+ }
+
+ // 1. Get model for the request if it's configured/cached, used it.
+ IODataFeature odataFeature = httpContext.ODataFeature();
+ if (odataFeature.Path is not null)
+ {
+ return odataFeature.Path;
+ }
+
+ IEdmModel model = httpContext.GetOrCreateEdmModel(clrType);
+
+ // 2. Retrieve it from metadata?
+ var endpoint = httpContext.GetEndpoint();
+ var odataMiniMetadata = endpoint.Metadata.GetMetadata();
+ var pathFactory = odataMiniMetadata?.PathFactory ?? ODataMiniMetadata.DefaultPathFactory;
+
+ var path = pathFactory.Invoke(httpContext, clrType);
+ odataFeature.Path = path;
+
+ return path;
+ }
+
+ internal static IServiceProvider GetOrCreateServiceProvider(this HttpContext httpContext)
+ {
+ if (httpContext == null)
+ {
+ throw Error.ArgumentNull(nameof(httpContext));
+ }
+
+ // 1. Get service provider for the request if it's configured/cached, used it.
+ IODataFeature odataFeature = httpContext.ODataFeature();
+ if (odataFeature.Services is not null)
+ {
+ return odataFeature.Services;
+ }
+
+ // 2. Retrieve it from metadata?
+ var endpoint = httpContext.GetEndpoint();
+ var odataMiniMetadata = endpoint.Metadata.GetMetadata();
+ if (odataMiniMetadata is not null)
+ {
+ odataFeature.Services = odataMiniMetadata.ServiceProvider;
+ return odataFeature.Services;
+ }
+
+ return null;
+ }
+
+ internal static async ValueTask BindODataParameterAsync(this HttpContext httpContext, ParameterInfo parameter)
+ where T : class
+ {
+ ArgumentNullException.ThrowIfNull(httpContext, nameof(httpContext));
+ ArgumentNullException.ThrowIfNull(parameter, nameof(parameter));
+
+ var endpoint = httpContext.GetEndpoint();
+ ODataMiniMetadata metadata = endpoint.Metadata.GetMetadata();
+ if (metadata is null || metadata.Model is null || metadata.PathFactory is null)
+ {
+ throw new ODataException(SRResources.ODataMustBeSetOnMinimalAPIEndpoint);
+ }
+
+ Type parameterType = parameter.ParameterType;
+
+ IEdmModel model = metadata.Model;
+
+ IODataFeature oDataFeature = httpContext.ODataFeature();
+ oDataFeature.Model = model;
+ oDataFeature.Services = httpContext.GetOrCreateServiceProvider();
+ oDataFeature.Path = metadata.PathFactory(httpContext, parameterType);
+ HttpRequest request = httpContext.Request;
+ IList toDispose = new List();
+ Uri baseAddress = httpContext.GetInputBaseAddress(metadata);
+
+ ODataVersion version = ODataResult.GetODataVersion(request, metadata);
+
+ object result = null;
+ try
+ {
+ result = await ODataInputFormatter.ReadFromStreamAsync(
+ parameterType,
+ defaultValue: null,
+ baseAddress,
+ version,
+ request,
+ toDispose).ConfigureAwait(false);
+
+ foreach (IDisposable obj in toDispose)
+ {
+ obj.Dispose();
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new ODataException(Error.Format(SRResources.BindParameterFailedOnMinimalAPIEndpoint, parameter.Name, ex.Message));
+ }
+
+ return result as T;
+ }
+
+ internal static Uri GetInputBaseAddress(this HttpContext httpContext, ODataMiniMetadata options)
+ {
+ if (httpContext == null)
+ {
+ throw Error.ArgumentNull(nameof(httpContext));
+ }
+
+ if (options.BaseAddressFactory is not null)
+ {
+ return options.BaseAddressFactory(httpContext);
+ }
+ else
+ {
+ return ODataInputFormatter.GetDefaultBaseAddress(httpContext.Request);
+ }
+ }
+
+ internal static IServiceProvider BuildDefaultServiceProvider(this HttpContext httpContext, IEdmModel model)
+ {
+ ArgumentNullException.ThrowIfNull(httpContext);
+ ArgumentNullException.ThrowIfNull(model);
+
+ ODataMiniOptions miniOptions = httpContext.RequestServices.GetService>()?.Value
+ ?? new ODataMiniOptions();
+
+ IServiceCollection services = new ServiceCollection();
+
+ // Inject the core odata services.
+ services.AddDefaultODataServices(miniOptions.Version);
+
+ // Inject the default query configuration from this options.
+ services.AddSingleton(sp => miniOptions.QueryConfigurations);
+
+ // Inject the default Web API OData services.
+ services.AddDefaultWebApiServices();
+
+ // Set Uri resolver to by default enabling unqualified functions/actions and case insensitive match.
+ services.AddSingleton(sp =>
+ new UnqualifiedODataUriResolver
+ {
+ EnableCaseInsensitive = miniOptions.EnableCaseInsensitive, // by default to enable case insensitive
+ EnableNoDollarQueryOptions = miniOptions.EnableNoDollarQueryOptions // retrieve it from global setting
+ });
+
+ // Inject the Edm model.
+ // From Current ODL implement, such injection only be used in reader and writer if the input
+ // model is null.
+ // How about the model is null?
+ services.AddSingleton(sp => model);
+
+ return services.BuildServiceProvider();
+ }
}
diff --git a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs
index d786ef7ec..bd79485ce 100644
--- a/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs
+++ b/src/Microsoft.AspNetCore.OData/Extensions/HttpRequestExtensions.cs
@@ -14,6 +14,7 @@
using Microsoft.AspNetCore.OData.Formatter.Deserialization;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.OData;
using Microsoft.OData.Edm;
@@ -98,6 +99,19 @@ public static TimeZoneInfo GetTimeZoneInfo(this HttpRequest request)
throw Error.ArgumentNull(nameof(request));
}
+ bool isMinimalApi = request.HttpContext.IsMinimalEndpoint();
+ if (isMinimalApi)
+ {
+ // In minimal API, we don't have ODataOptions, so we check the ODataMiniOptions directly.
+ ODataMiniOptions miniOptions = request.HttpContext.RequestServices?.GetService>()?.Value;
+ if (miniOptions is not null)
+ {
+ return miniOptions.TimeZone;
+ }
+
+ return null;
+ }
+
return request.ODataOptions()?.TimeZone;
}
@@ -113,6 +127,19 @@ public static bool IsNoDollarQueryEnable(this HttpRequest request)
throw Error.ArgumentNull(nameof(request));
}
+ bool isMinimalApi = request.HttpContext.IsMinimalEndpoint();
+ if (isMinimalApi)
+ {
+ // In minimal API, we don't have ODataOptions, so we check the ODataMiniOptions directly.
+ ODataMiniOptions miniOptions = request.HttpContext.RequestServices?.GetService>()?.Value;
+ if (miniOptions is not null)
+ {
+ return miniOptions.EnableNoDollarQueryOptions;
+ }
+
+ return false;
+ }
+
return request.ODataOptions()?.EnableNoDollarQueryOptions ?? false;
}
diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ODataQueryFilterInvocationContext.cs b/src/Microsoft.AspNetCore.OData/Extensions/ODataQueryFilterInvocationContext.cs
new file mode 100644
index 000000000..12b41a77e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.OData/Extensions/ODataQueryFilterInvocationContext.cs
@@ -0,0 +1,32 @@
+//-----------------------------------------------------------------------------
+//
+// Copyright (c) .NET Foundation and Contributors. All rights reserved.
+// See License.txt in the project root for license information.
+//
+//------------------------------------------------------------------------------
+
+using System.Reflection;
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.OData.Extensions;
+
+///
+/// Provides an abstraction for wrapping the and the associated with a route handler.
+///
+public class ODataQueryFilterInvocationContext
+{
+ ///
+ /// The associated with the current route handler.
+ ///
+ public MethodInfo MethodInfo { get; init; }
+
+ ///
+ /// The associated with the current route filter.
+ ///
+ public EndpointFilterInvocationContext InvocationContext { get; init; }
+
+ ///
+ /// Gets the .
+ ///
+ public HttpContext HttpContext => InvocationContext.HttpContext;
+}
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs
index d97c16413..ca4f6c549 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataActionParameters.cs
@@ -7,7 +7,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
+using System.Reflection;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.OData.Abstracts;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.OData.Deltas;
+using Microsoft.AspNetCore.OData.Extensions;
namespace Microsoft.AspNetCore.OData.Formatter;
@@ -19,4 +24,14 @@ namespace Microsoft.AspNetCore.OData.Formatter;
[SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "ODataActionParameters is more appropriate here.")]
public class ODataActionParameters : Dictionary
{
+ ///
+ /// Binds the and to generate the .
+ ///
+ /// The HttpContext.
+ /// The parameter info.
+ /// The built
+ public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter)
+ {
+ return await context.BindODataParameterAsync(parameter);
+ }
}
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs
index baa00320f..b75d9db40 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatterHelper.cs
@@ -159,6 +159,7 @@ internal static async Task WriteToStreamAsync(
writeContext.QueryOptions = queryOptions;
writeContext.SetComputedProperties(queryOptions?.Compute?.ComputeClause);
writeContext.Type = type;
+ writeContext.IsDelta = serializer.ODataPayloadKind == ODataPayloadKind.Delta;
//Set the SelectExpandClause on the context if it was explicitly specified.
if (selectExpandDifferentFromQueryOptions != null)
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs
index 286e28a2c..d13de3617 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/ODataUntypedActionParameters.cs
@@ -10,6 +10,11 @@
using System.Collections.Generic;
using Microsoft.OData.Edm;
using Microsoft.AspNetCore.OData.Abstracts;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.OData.Deltas;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.OData.Extensions;
namespace Microsoft.AspNetCore.OData.Formatter;
@@ -34,4 +39,15 @@ public ODataUntypedActionParameters(IEdmAction action)
/// Gets the OData action of this parameters.
///
public IEdmAction Action { get; }
+
+ ///
+ /// Binds the and to generate the .
+ ///
+ /// The HttpContext.
+ /// The parameter info.
+ /// The built
+ public static async ValueTask BindAsync(HttpContext context, ParameterInfo parameter)
+ {
+ return await context.BindODataParameterAsync(parameter);
+ }
}
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs
index c90a5d92f..90be5aded 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataEnumSerializer.cs
@@ -201,7 +201,7 @@ private static string GetFlagsEnumValue(Enum graphEnum, ClrEnumMemberAnnotation
long flagValue = Convert.ToInt64(flag);
// Using bitwise operations to check if a flag is set, which is more efficient than Enum.HasFlag
- if ((graphValue & flagValue) != 0 && flagValue != 0)
+ if (flagValue != 0 && (graphValue & flagValue) == flagValue)
{
IEdmEnumMember flagMember = memberMapAnnotation.GetEdmEnumMember(flag);
if (flagMember != null)
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs
index aa6050cd3..39b7b4152 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSerializer.cs
@@ -134,7 +134,7 @@ private async Task WriteDeltaComplexPropertiesAsync(SelectExpandNode selectExpan
IEnumerable complexProperties = selectExpandNode.SelectedComplexProperties.Keys;
- if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource())
+ if (resourceContext.EdmObject != null && resourceContext.SerializerContext.IsDelta)
{
IDelta deltaObject = null;
if (resourceContext.EdmObject is TypedEdmEntityObject obj)
@@ -477,7 +477,7 @@ private async Task WriteResourceAsync(object graph, ODataWriter writer, ODataSer
}
await writer.WriteStartAsync(odataDeletedResource).ConfigureAwait(false);
- await WriteResourceContent(writer, selectExpandNode, resourceContext, /*isDelta*/ true).ConfigureAwait(false);
+ await WriteResourceContent(writer, selectExpandNode, resourceContext).ConfigureAwait(false);
await writer.WriteEndAsync().ConfigureAwait(false);
}
else
@@ -494,9 +494,8 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink
}
else
{
- bool isDelta = graph is IDelta || graph is IEdmChangedObject;
await writer.WriteStartAsync(resource).ConfigureAwait(false);
- await WriteResourceContent(writer, selectExpandNode, resourceContext, isDelta).ConfigureAwait(false);
+ await WriteResourceContent(writer, selectExpandNode, resourceContext).ConfigureAwait(false);
await writer.WriteEndAsync().ConfigureAwait(false);
}
}
@@ -512,11 +511,10 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink
/// The to use to write the resource contents
/// The describing the response graph.
/// The context for the resource instance being written.
- /// Whether to only write changed properties of the resource
- private async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext, bool isDelta)
+ private async Task WriteResourceContent(ODataWriter writer, SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
// TODO: These should be aligned; do we need different methods for delta versus non-delta complex/navigation properties?
- if (isDelta)
+ if (resourceContext.SerializerContext.IsDelta)
{
await WriteUntypedPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false);
await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer).ConfigureAwait(false);
@@ -1130,7 +1128,7 @@ private async Task WriteComplexPropertiesAsync(SelectExpandNode selectExpandNode
return;
}
- if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource())
+ if (resourceContext.EdmObject != null && resourceContext.SerializerContext.IsDelta)
{
IDelta deltaObject = resourceContext.EdmObject as IDelta;
IEnumerable changedProperties = deltaObject.GetChangedPropertyNames();
@@ -1400,7 +1398,7 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode
{
IEnumerable structuralProperties = selectExpandNode.SelectedStructuralProperties;
- if (null != resourceContext.EdmObject && resourceContext.EdmObject.IsDeltaResource())
+ if (resourceContext.EdmObject != null && resourceContext.SerializerContext.IsDelta)
{
IDelta deltaObject = null;
if (resourceContext.EdmObject is TypedEdmEntityObject obj)
@@ -1412,8 +1410,8 @@ private IEnumerable CreateStructuralPropertyBag(SelectExpandNode
deltaObject = resourceContext.EdmObject as IDelta;
}
- if(deltaObject != null)
- {
+ if (deltaObject != null)
+ {
IEnumerable changedProperties = deltaObject.GetChangedPropertyNames();
structuralProperties = structuralProperties.Where(p => changedProperties.Contains(p.Name) || p.IsKey());
}
diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs
index 46b61d1a1..4ef338938 100644
--- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs
+++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs
@@ -9,6 +9,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
+using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
@@ -97,9 +98,8 @@ public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReferenc
throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet));
}
- if (writeContext.Type != null &&
- writeContext.Type.IsGenericType &&
- writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) &&
+ if (writeContext.Type != null &&
+ TypeHelper.IsAsyncEnumerableType(writeContext.Type) &&
graph is IAsyncEnumerable