From e302c8c19f3a72ffe7f82014187ae398e62b555a Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Thu, 14 Dec 2023 20:07:07 -0800 Subject: [PATCH 1/3] fixes #578: Support minimal API --- AspNetCoreOData.sln | 7 + sample/ODataMiniApi/AppDb.cs | 125 +++++++++++ sample/ODataMiniApi/AppModels.cs | 65 ++++++ sample/ODataMiniApi/ODataMiniApi.csproj | 17 ++ sample/ODataMiniApi/Program.cs | 55 +++++ .../Properties/launchSettings.json | 29 +++ .../ODataMiniApi/Students/StudentEndpoints.cs | 209 ++++++++++++++++++ .../ODataMiniApi/appsettings.Development.json | 8 + sample/ODataMiniApi/appsettings.json | 9 + sample/ODataMiniApi/readme.md | 130 +++++++++++ .../Edm/DefaultODataEndpointModelMapper.cs | 18 ++ .../Edm/IODataEndpointModelMapper.cs | 25 +++ .../Edm/IODataModelConfiguration.cs | 25 +++ ...DataEndpointConventionBuilderExtensions.cs | 44 ++++ .../Extensions/ODataPrefixMetadata.cs | 31 +++ .../Microsoft.AspNetCore.OData.xml | 132 +++++++++-- .../ODataServiceCollectionExtensions.cs | 65 ++++++ .../PublicAPI.Unshipped.txt | 19 ++ .../Query/ODataQueryOptionsOfT.cs | 95 ++++++++ .../Query/Wrapper/AggregationWrapper.cs | 1 + .../Query/Wrapper/ComputeWrapperOfT.cs | 1 + .../Wrapper/DynamicTypeWrapperConverter.cs | 6 +- .../Wrapper/EntitySetAggregationWrapper.cs | 1 + .../Query/Wrapper/FlatteningWrapperOfT.cs | 1 + .../Query/Wrapper/GroupByWrapper.cs | 1 + .../Wrapper/NoGroupByAggregationWrapper.cs | 1 + .../Query/Wrapper/NoGroupByWrapper.cs | 1 + .../Query/Wrapper/SelectAllAndExpandOfT.cs | 1 + .../Query/Wrapper/SelectAllOfT.cs | 1 + .../Query/Wrapper/SelectExpandWrapper.cs | 2 + .../Wrapper/SelectExpandWrapperConverter.cs | 14 +- .../Query/Wrapper/SelectExpandWrapperOfT.cs | 1 + .../Wrapper/SelectSomeAndInheritanceOfT.cs | 1 + .../Query/Wrapper/SelectSomeOfT.cs | 1 + .../Results/PageResultOfT.cs | 2 + .../Results/PageResultValueConverter.cs | 8 +- .../Results/SingleResultOfT.cs | 2 + .../Results/SingleResultValueConverter.cs | 8 +- ...rosoft.AspNetCore.OData.PublicApi.Net6.bsl | 58 +++++ ...t.AspNetCore.OData.PublicApi.NetCore31.bsl | 57 +++++ 40 files changed, 1242 insertions(+), 35 deletions(-) create mode 100644 sample/ODataMiniApi/AppDb.cs create mode 100644 sample/ODataMiniApi/AppModels.cs create mode 100644 sample/ODataMiniApi/ODataMiniApi.csproj create mode 100644 sample/ODataMiniApi/Program.cs create mode 100644 sample/ODataMiniApi/Properties/launchSettings.json create mode 100644 sample/ODataMiniApi/Students/StudentEndpoints.cs create mode 100644 sample/ODataMiniApi/appsettings.Development.json create mode 100644 sample/ODataMiniApi/appsettings.json create mode 100644 sample/ODataMiniApi/readme.md create mode 100644 src/Microsoft.AspNetCore.OData/Edm/DefaultODataEndpointModelMapper.cs create mode 100644 src/Microsoft.AspNetCore.OData/Edm/IODataEndpointModelMapper.cs create mode 100644 src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs create mode 100644 src/Microsoft.AspNetCore.OData/Extensions/ODataEndpointConventionBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.OData/Extensions/ODataPrefixMetadata.cs diff --git a/AspNetCoreOData.sln b/AspNetCoreOData.sln index 06a9b8f0a..05e018987 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", "{986A48E0-FA19-49D0-B716-4E60740D243C}" +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 + {986A48E0-FA19-49D0-B716-4E60740D243C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {986A48E0-FA19-49D0-B716-4E60740D243C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {986A48E0-FA19-49D0-B716-4E60740D243C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {986A48E0-FA19-49D0-B716-4E60740D243C}.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} + {986A48E0-FA19-49D0-B716-4E60740D243C} = {B1F86961-6958-4617-ACA4-C231F95AE099} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {540C9752-AAC0-49EA-BA60-78490C90FF86} diff --git a/sample/ODataMiniApi/AppDb.cs b/sample/ODataMiniApi/AppDb.cs new file mode 100644 index 000000000..09d54f307 --- /dev/null +++ b/sample/ODataMiniApi/AppDb.cs @@ -0,0 +1,125 @@ +//----------------------------------------------------------------------------- +// +// 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(); + + 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); + } +} + +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) } + }; + + 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 School { SchoolId = 2, SchoolName = "Venus High School", MailAddress = new Address { ApartNum = 543, City = "AR", Street = "51TH AVE PL", ZipCode = "98043" } }, + 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 School { SchoolId = 7, SchoolName = "Uranus High School", MailAddress = new Address { ApartNum = 123, City = "Greenland", Street = "Sun Street", ZipCode = "88155" } }, + 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" } } + }; + + foreach (var s in schools) + { + s.Students = students.Where(std => std.SchoolId == s.SchoolId).ToList(); + + context.Schools.Add(s); + } + #endregion + + context.SaveChanges(); + } + } + } +} diff --git a/sample/ODataMiniApi/AppModels.cs b/sample/ODataMiniApi/AppModels.cs new file mode 100644 index 000000000..6f51143ba --- /dev/null +++ b/sample/ODataMiniApi/AppModels.cs @@ -0,0 +1,65 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.EntityFrameworkCore; +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(); + 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 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; } +} diff --git a/sample/ODataMiniApi/ODataMiniApi.csproj b/sample/ODataMiniApi/ODataMiniApi.csproj new file mode 100644 index 000000000..a99c9f211 --- /dev/null +++ b/sample/ODataMiniApi/ODataMiniApi.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + disable + enable + + + + + + + + + + + diff --git a/sample/ODataMiniApi/Program.cs b/sample/ODataMiniApi/Program.cs new file mode 100644 index 000000000..e4ebef909 --- /dev/null +++ b/sample/ODataMiniApi/Program.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------------- +// +// 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 Microsoft.AspNetCore.OData.Extensions; +using ODataMiniApi; +using ODataMiniApi.Students; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddDbContext(options => options.UseInMemoryDatabase("SchoolStudentList")); +builder.Services.AddOData(opt => opt.EnableQueryFeatures()// This line is required + .AddRouteComponents("customized", EdmModelBuilder.GetEdmModel()) // This line is used to test 'UseOData' extension +); + +var app = builder.Build(); +app.MakeSureDbCreated(); + +#region School Endpoints + +app.MapGet("/schools", (AppDb db, ODataQueryOptions options) => { + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return options.ApplyTo(db.Schools); +}); + +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())); + } +}); + +app.MapGet("/customized/schools", (AppDb db, ODataQueryOptions options) => { + db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return options.ApplyTo(db.Schools); +}).UseOData("customized"); // In customized OData, MailAddress and Student are complex type, you can also use 'IODataModelConfiguration' + +#endregion + +// Endpoints for students +app.MapStudentEndpoints(); + +app.Run(); + 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/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..7cef2bf77 --- /dev/null +++ b/sample/ODataMiniApi/readme.md @@ -0,0 +1,130 @@ +# 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" +} +``` \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataEndpointModelMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataEndpointModelMapper.cs new file mode 100644 index 000000000..b3eca2de5 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataEndpointModelMapper.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNetCore.OData.Edm +{ + internal class DefaultODataEndpointModelMapper : IODataEndpointModelMapper + { + public ConcurrentDictionary Maps { get; } = new ConcurrentDictionary(); + } +} diff --git a/src/Microsoft.AspNetCore.OData/Edm/IODataEndpointModelMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/IODataEndpointModelMapper.cs new file mode 100644 index 000000000..52cf5fa23 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Edm/IODataEndpointModelMapper.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Concurrent; +using Microsoft.AspNetCore.Http; +using Microsoft.OData.Edm; + +namespace Microsoft.AspNetCore.OData.Edm +{ + /// + /// A cache for and mapping. + /// It's typically used for minimal API scenario. + /// + public interface IODataEndpointModelMapper + { + /// + /// Gets the map between and + /// + ConcurrentDictionary Maps { get; } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs b/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs new file mode 100644 index 000000000..b0b166682 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Edm/IODataModelConfiguration.cs @@ -0,0 +1,25 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +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. + void Apply(HttpContext context, ODataModelBuilder builder); + } +} diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ODataEndpointConventionBuilderExtensions.cs b/src/Microsoft.AspNetCore.OData/Extensions/ODataEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000..f7a52a053 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Extensions/ODataEndpointConventionBuilderExtensions.cs @@ -0,0 +1,44 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OData.Edm; + +namespace Microsoft.AspNetCore.OData.Extensions +{ + /// + /// Extension methods for annotating OData metadata on an . + /// + public static class ODataEndpointConventionBuilderExtensions + { + /// + /// Adds an OData prefix metadata to associated with the current endpoint. + /// The prefix should be same as the defined prefix when calling 'AddRouteComponents' + /// This method typically is used in Minimal API scenarios. + /// + /// The . + /// The route component prefix. + /// A that can be used to further customize the endpoint. + /// + /// When we target on .NET 7 or above, we can get the 'ServiceProvider' from Endpoint builder, + /// Then, we should check the 'AddOData' is called and associated route with given prefix registered. + /// + public static TBuilder UseOData(this TBuilder builder, string prefix) where TBuilder : IEndpointConventionBuilder + => builder.WithMetadata(new ODataPrefixMetadata(prefix ?? string.Empty)); + + /// + /// Adds an OData model configuration metadata to associated with the current endpoint. + /// This method typically is used in Minimal API scenarios. + /// + /// The . + /// The model configuration. + /// A that can be used to further customize the endpoint. + public static TBuilder UseOData(this TBuilder builder, IODataModelConfiguration config) where TBuilder : IEndpointConventionBuilder + => builder.WithMetadata(config); + } +} diff --git a/src/Microsoft.AspNetCore.OData/Extensions/ODataPrefixMetadata.cs b/src/Microsoft.AspNetCore.OData/Extensions/ODataPrefixMetadata.cs new file mode 100644 index 000000000..460a7d978 --- /dev/null +++ b/src/Microsoft.AspNetCore.OData/Extensions/ODataPrefixMetadata.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OData.Extensions +{ + /// + /// Defines a contract use to specify the OData prefix metadata in . + /// + public sealed class ODataPrefixMetadata + { + /// + /// Initializes a new instance of the class. + /// + /// The route component prefix + public ODataPrefixMetadata(string prefix) + { + Prefix = prefix ?? string.Empty; + } + + /// + /// Gets the route component prefix. + /// + public string Prefix { get; } + } +} diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 31f29577c..2226b8863 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -1102,6 +1102,20 @@ The type to test. True if the type is a DateTime; false otherwise. + + + Determine if a type is a . + + The type to test. + True if the type is a DateOnly; false otherwise. + + + + Determine if a type is a . + + The type to test. + True if the type is a TimeOnly; false otherwise. + Determine if a type is a TimeSpan. @@ -2486,6 +2500,29 @@ Gets the whole expand path. + + + A cache for and mapping. + It's typically used for minimal API scenario. + + + + + Gets the map between and + + + + + Defines a contract used to apply extra logic on the model builder. + + + + + Applies model configurations using the provided builder. + + The HttpContext. + The builder used to apply configurations. + Provides the mapping between CLR type and Edm type. @@ -3108,6 +3145,50 @@ The OData path segments. The generated OData link. + + + Extension methods for annotating OData metadata on an . + + + + + Adds an OData prefix metadata to associated with the current endpoint. + The prefix should be same as the defined prefix when calling 'AddRouteComponents' + This method typically is used in Minimal API scenarios. + + The . + The route component prefix. + A that can be used to further customize the endpoint. + + When we target on .NET 7 or above, we can get the 'ServiceProvider' from Endpoint builder, + Then, we should check the 'AddOData' is called and associated route with given prefix registered. + + + + + Adds an OData model configuration metadata to associated with the current endpoint. + This method typically is used in Minimal API scenarios. + + The . + The model configuration. + A that can be used to further customize the endpoint. + + + + Defines a contract use to specify the OData prefix metadata in . + + + + + Initializes a new instance of the class. + + The route component prefix + + + + Gets the route component prefix. + + Provides extension methods for the class. @@ -4763,7 +4844,7 @@ Checks whether a navigation link should be written or not. - The navigation link to be written + The navigation link to be written. The resource context for the resource whose navigation link is being written. true if navigation link should be written; otherwise false. @@ -6586,6 +6667,31 @@ Provides extension methods to add OData services. + + + Adds essential OData services to the specified . + + The to add services to. + A that can be used to further configure the OData services. + + + + Adds essential OData services to the specified . + + The to add services to. + The OData options to configure the services with, + including access to a service provider which you can resolve services from. + A that can be used to further configure the OData services. + + + + Adds essential OData services to the specified . + + The to add services to. + The OData options to configure the services with, + including access to a service provider which you can resolve services from. + A that can be used to further configure the OData services. + Enables query support for actions with an or return @@ -10482,6 +10588,14 @@ The settings to use in query composition. The new after the query has been applied to. + + + Bind the and to generate the . + + The HttpContext. + The parameter info. + The built + A to bind parameters of type to the OData query from the incoming request. @@ -15239,19 +15353,3 @@ -ummary> - The value segment. - - - - Gets the value segment. - - - - - - - - - - diff --git a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs index 4481291fa..9a540caf6 100644 --- a/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.OData/ODataServiceCollectionExtensions.cs @@ -5,10 +5,12 @@ // //------------------------------------------------------------------------------ +using System; using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Parser; @@ -26,6 +28,62 @@ namespace Microsoft.AspNetCore.OData /// public static class ODataServiceCollectionExtensions { + /// + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// A that can be used to further configure the OData services. + public static IServiceCollection AddOData(this IServiceCollection services) + { + return services.AddOData(opt => { }); + } + + /// + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IServiceCollection AddOData(this IServiceCollection services, Action setupAction) + { + if (services == null) + { + throw Error.ArgumentNull(nameof(services)); + } + + if (setupAction == null) + { + throw Error.ArgumentNull(nameof(setupAction)); + } + + services.AddODataCore().Configure(setupAction); + return services; + } + + /// + /// Adds essential OData services to the specified . + /// + /// The to add services to. + /// The OData options to configure the services with, + /// including access to a service provider which you can resolve services from. + /// A that can be used to further configure the OData services. + public static IServiceCollection AddOData(this IServiceCollection services, Action setupAction) + { + if (services == null) + { + throw Error.ArgumentNull(nameof(services)); + } + + if (setupAction == null) + { + throw Error.ArgumentNull(nameof(setupAction)); + } + + services.AddODataCore().AddOptions().Configure(setupAction); + return services; + } + /// /// Enables query support for actions with an or return /// type. To avoid processing unexpected or malicious queries, use the validation settings on @@ -80,6 +138,8 @@ internal static IServiceCollection AddODataCore(this IServiceCollection services services.TryAddEnumerable( ServiceDescriptor.Transient, ODataMvcOptionsSetup>()); + // For Minimal API, we should call 'ConfigureHttpJsonOptions' to config the JsonConverter, + // But, this extension has been introduced since .NET 7 services.TryAddEnumerable( ServiceDescriptor.Transient, ODataJsonOptionsSetup>()); @@ -103,6 +163,11 @@ internal static IServiceCollection AddODataCore(this IServiceCollection services services.TryAddSingleton(); + // + // For Minimal API endpoint and model mapping cache + // + services.TryAddSingleton(); + return services; } } diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index c1fe66ff6..efa24eeee 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -238,6 +238,10 @@ Microsoft.AspNetCore.OData.Edm.EntitySelfLinks.IdLink.get -> System.Uri Microsoft.AspNetCore.OData.Edm.EntitySelfLinks.IdLink.set -> void Microsoft.AspNetCore.OData.Edm.EntitySelfLinks.ReadLink.get -> System.Uri Microsoft.AspNetCore.OData.Edm.EntitySelfLinks.ReadLink.set -> void +Microsoft.AspNetCore.OData.Edm.IODataEndpointModelMapper +Microsoft.AspNetCore.OData.Edm.IODataEndpointModelMapper.Maps.get -> System.Collections.Concurrent.ConcurrentDictionary +Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration +Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration.Apply(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.OData.ModelBuilder.ODataModelBuilder builder) -> void Microsoft.AspNetCore.OData.Edm.IODataTypeMapper Microsoft.AspNetCore.OData.Edm.IODataTypeMapper.GetClrPrimitiveType(Microsoft.OData.Edm.IEdmPrimitiveType primitiveType, bool nullable) -> System.Type Microsoft.AspNetCore.OData.Edm.IODataTypeMapper.GetClrType(Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmType edmType, bool nullable, Microsoft.OData.ModelBuilder.IAssemblyResolver assembliesResolver) -> System.Type @@ -275,6 +279,10 @@ Microsoft.AspNetCore.OData.Extensions.HttpContextExtensions Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions Microsoft.AspNetCore.OData.Extensions.HttpResponseExtensions Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers +Microsoft.AspNetCore.OData.Extensions.ODataEndpointConventionBuilderExtensions +Microsoft.AspNetCore.OData.Extensions.ODataPrefixMetadata +Microsoft.AspNetCore.OData.Extensions.ODataPrefixMetadata.ODataPrefixMetadata(string prefix) -> void +Microsoft.AspNetCore.OData.Extensions.ODataPrefixMetadata.Prefix.get -> string Microsoft.AspNetCore.OData.Extensions.SerializableErrorExtensions Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys Microsoft.AspNetCore.OData.Formatter.Deserialization.IODataDeserializer @@ -1212,6 +1220,8 @@ Microsoft.AspNetCore.OData.Query.Wrapper.DynamicTypeWrapper.TryGetPropertyValue( Microsoft.AspNetCore.OData.Query.Wrapper.ISelectExpandWrapper Microsoft.AspNetCore.OData.Query.Wrapper.ISelectExpandWrapper.ToDictionary() -> System.Collections.Generic.IDictionary Microsoft.AspNetCore.OData.Query.Wrapper.ISelectExpandWrapper.ToDictionary(System.Func propertyMapperProvider) -> System.Collections.Generic.IDictionary +Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter +Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.SelectExpandWrapperConverter() -> void Microsoft.AspNetCore.OData.Results.BadRequestODataResult Microsoft.AspNetCore.OData.Results.BadRequestODataResult.BadRequestODataResult(Microsoft.OData.ODataError odataError) -> void Microsoft.AspNetCore.OData.Results.BadRequestODataResult.BadRequestODataResult(string message) -> void @@ -1553,6 +1563,8 @@ override Microsoft.AspNetCore.OData.Query.ETag.TrySetMember(System.Dynamic.SetMe override Microsoft.AspNetCore.OData.Query.ETag.ApplyTo(System.Linq.IQueryable query) -> System.Linq.IQueryable override Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(System.Linq.IQueryable query) -> System.Linq.IQueryable override Microsoft.AspNetCore.OData.Query.ODataQueryOptions.ApplyTo(System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) -> System.Linq.IQueryable +override Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.CanConvert(System.Type typeToConvert) -> bool +override Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.CreateConverter(System.Type type, System.Text.Json.JsonSerializerOptions options) -> System.Text.Json.Serialization.JsonConverter override Microsoft.AspNetCore.OData.Results.BadRequestODataResult.ExecuteResultAsync(Microsoft.AspNetCore.Mvc.ActionContext context) -> System.Threading.Tasks.Task override Microsoft.AspNetCore.OData.Results.ConflictODataResult.ExecuteResultAsync(Microsoft.AspNetCore.Mvc.ActionContext context) -> System.Threading.Tasks.Task override Microsoft.AspNetCore.OData.Results.CreatedODataResult.ExecuteResultAsync(Microsoft.AspNetCore.Mvc.ActionContext context) -> System.Threading.Tasks.Task @@ -1710,6 +1722,8 @@ static Microsoft.AspNetCore.OData.Extensions.HttpRequestExtensions.ODataOptions( static Microsoft.AspNetCore.OData.Extensions.HttpResponseExtensions.IsSuccessStatusCode(this Microsoft.AspNetCore.Http.HttpResponse response) -> bool static Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers.CreateODataLink(this Microsoft.AspNetCore.Http.HttpRequest request, params Microsoft.OData.UriParser.ODataPathSegment[] segments) -> string static Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers.CreateODataLink(this Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IList segments) -> string +static Microsoft.AspNetCore.OData.Extensions.ODataEndpointConventionBuilderExtensions.UseOData(this TBuilder builder, Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration config) -> TBuilder +static Microsoft.AspNetCore.OData.Extensions.ODataEndpointConventionBuilderExtensions.UseOData(this TBuilder builder, string prefix) -> TBuilder static Microsoft.AspNetCore.OData.Extensions.SerializableErrorExtensions.CreateODataError(this Microsoft.AspNetCore.Mvc.SerializableError serializableError) -> Microsoft.OData.ODataError static Microsoft.AspNetCore.OData.Formatter.LinkGenerationHelpers.GenerateActionLink(this Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, Microsoft.OData.Edm.IEdmOperation action) -> System.Uri static Microsoft.AspNetCore.OData.Formatter.LinkGenerationHelpers.GenerateActionLink(this Microsoft.AspNetCore.OData.Formatter.ResourceSetContext resourceSetContext, Microsoft.OData.Edm.IEdmOperation action) -> System.Uri @@ -1737,6 +1751,9 @@ static Microsoft.AspNetCore.OData.ODataMvcBuilderExtensions.AddOData(this Micros static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder static Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder builder, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IMvcCoreBuilder +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddOData(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action setupAction) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddODataQueryFilter(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions.AddODataQueryFilter(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.AspNetCore.Mvc.Filters.IActionFilter queryFilter) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.AspNetCore.OData.ODataUriFunctions.AddCustomUriFunction(string functionName, Microsoft.OData.UriParser.FunctionSignatureWithReturnType functionSignature, System.Reflection.MethodInfo methodInfo) -> void @@ -1758,6 +1775,7 @@ static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.IsSystemQueryOption(st static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.IsSystemQueryOption(string queryOptionName, bool isDollarSignOptional) -> bool static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults(System.Linq.IQueryable queryable, int limit, bool parameterize, out bool resultsLimited) -> System.Linq.IQueryable static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.LimitResults(System.Linq.IQueryable queryable, int limit, out bool resultsLimited) -> System.Linq.IQueryable +static Microsoft.AspNetCore.OData.Query.ODataQueryOptions.BindAsync(Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) -> System.Threading.Tasks.ValueTask> static Microsoft.AspNetCore.OData.Query.OrderByNode.CreateCollection(Microsoft.OData.UriParser.OrderByClause orderByClause) -> System.Collections.Generic.IList static Microsoft.AspNetCore.OData.Results.SingleResult.Create(System.Linq.IQueryable queryable) -> Microsoft.AspNetCore.OData.Results.SingleResult static Microsoft.AspNetCore.OData.Routing.Conventions.OperationRoutingConvention.AddSelector(Microsoft.AspNetCore.OData.Routing.Conventions.ODataControllerActionContext context, Microsoft.OData.Edm.IEdmOperation edmOperation, bool hasKeyParameter, Microsoft.OData.Edm.IEdmEntityType entityType, Microsoft.OData.Edm.IEdmNavigationSource navigationSource, Microsoft.OData.Edm.IEdmEntityType castType) -> void @@ -1778,6 +1796,7 @@ static readonly Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys.Mess static readonly Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys.MessageLanguageKey -> string static readonly Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys.ModelStateKey -> string static readonly Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys.StackTraceKey -> string +static readonly Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter.MapperProvider -> System.Func virtual Microsoft.AspNetCore.OData.Batch.DefaultODataBatchHandler.ExecuteRequestMessagesAsync(System.Collections.Generic.IEnumerable requests, Microsoft.AspNetCore.Http.RequestDelegate handler) -> System.Threading.Tasks.Task> virtual Microsoft.AspNetCore.OData.Batch.DefaultODataBatchHandler.ParseBatchRequestsAsync(Microsoft.AspNetCore.Http.HttpContext context) -> System.Threading.Tasks.Task> virtual Microsoft.AspNetCore.OData.Batch.ODataBatchHandler.CreateResponseMessageAsync(System.Collections.Generic.IEnumerable responses, Microsoft.AspNetCore.Http.HttpRequest request) -> System.Threading.Tasks.Task diff --git a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs index b19bcbce9..0a3c647f8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/ODataQueryOptionsOfT.cs @@ -5,11 +5,19 @@ // //------------------------------------------------------------------------------ +using System; using System.Linq; +using System.Reflection; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Common; +using Microsoft.AspNetCore.OData.Edm; +using Microsoft.AspNetCore.OData.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; namespace Microsoft.AspNetCore.OData.Query { @@ -95,6 +103,93 @@ public override IQueryable ApplyTo(IQueryable query, ODataQuerySettings querySet return base.ApplyTo(query, querySettings); } +#if NET6_0_OR_GREATER + /// + /// Bind the and to generate the . + /// + /// The HttpContext. + /// The parameter info. + /// The built + public static ValueTask> BindAsync(HttpContext context, ParameterInfo parameter) + { + if (context == null) + { + throw Error.ArgumentNull(nameof(context)); + } + + if (parameter == null) + { + throw Error.ArgumentNull(nameof(parameter)); + } + + Type entityClrType = typeof(TEntity); + IEdmModel model = GetEdmModel(context); + ODataQueryContext entitySetContext = new ODataQueryContext(model, entityClrType, context.ODataFeature().Path); + var result = new ODataQueryOptions(entitySetContext, context.Request); + + return ValueTask.FromResult(result); + } + + private static IEdmModel GetEdmModel(HttpContext context) + { + Endpoint endpoint = context.GetEndpoint(); + + // 1. If customer calls "WithOData({prefix})", let's use it to fetch the model associated when calling 'AddOData(...)' + ODataPrefixMetadata prefixMetadata = endpoint.Metadata.GetMetadata(); + if (prefixMetadata != null) + { + ODataOptions options = context.RequestServices.GetService>()?.Value; + if (options != null) + { + if (options.RouteComponents.TryGetValue(prefixMetadata.Prefix, out var routeComponents)) + { + var odataFeature = context.ODataFeature(); + odataFeature.RoutePrefix = prefixMetadata.Prefix; + odataFeature.Model = routeComponents.EdmModel; + odataFeature.Services = routeComponents.ServiceProvider; + return routeComponents.EdmModel; + } + } + + throw new InvalidOperationException($"The '{prefixMetadata.Prefix}' when calling WithOData() is not registered in AddOData()."); + } + + // 2. Let's retrieve the model from the cache + IODataEndpointModelMapper endpointModelMapper = context.RequestServices.GetService(); + IEdmModel model; + if (endpointModelMapper != null && endpointModelMapper.Maps.TryGetValue(endpoint, out model)) + { + return model; + } + + // 3. Let's build the model on the fly + Type entityClrType = typeof(TEntity); + IAssemblyResolver resolver = context.RequestServices.GetService(); + ODataConventionModelBuilder builder = resolver != null ? + new ODataConventionModelBuilder(resolver, true) : + new ODataConventionModelBuilder(); + + EntityTypeConfiguration entityTypeConfiguration = builder.AddEntityType(entityClrType); + builder.AddEntitySet(entityClrType.Name, entityTypeConfiguration); + + var modelConfigs = endpoint.Metadata.OfType(); + foreach (var config in modelConfigs) + { + config.Apply(context, builder); + } + + model = builder.GetEdmModel(); + + // Add the model into the cache + if (endpointModelMapper != null) + { + endpointModelMapper.Maps[endpoint] = model; + } + + return model; + } +#endif + private static void ValidateQuery(IQueryable query) { if (query == null) diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs index 5548f582a..8fdfbc5da 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/AggregationWrapper.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class AggregationWrapper : GroupByWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs index 01b8baff7..9db6b09fa 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/ComputeWrapperOfT.cs @@ -18,6 +18,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class ComputeWrapper : GroupByWrapper, IEdmEntityObject { public T Instance { get; set; } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs index 82c376a45..656a842e2 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/DynamicTypeWrapperConverter.cs @@ -47,15 +47,15 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o if (type.IsGenericType) { // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); + Type genericType = type.GetGenericTypeDefinition(); Type elementType = type.GetGenericArguments()[0]; - if (generaticType == typeof(ComputeWrapper<>)) + if (genericType == typeof(ComputeWrapper<>)) { return (JsonConverter)Activator.CreateInstance(typeof(ComputeWrapperConverter<>).MakeGenericType(new Type[] { elementType })); } - if (generaticType == typeof(FlatteningWrapper<>)) + if (genericType == typeof(FlatteningWrapper<>)) { return (JsonConverter)Activator.CreateInstance(typeof(FlatteningWrapperConverter<>).MakeGenericType(new Type[] { elementType })); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs index a02c08c93..1582cbabb 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/EntitySetAggregationWrapper.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class EntitySetAggregationWrapper : GroupByWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs index 4cf37eb9e..784a5c6e5 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/FlatteningWrapperOfT.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class FlatteningWrapper : GroupByWrapper { // TODO: how to use 'Source'? diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs index 5bc0ff162..b4c32aea8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/GroupByWrapper.cs @@ -15,6 +15,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class GroupByWrapper : DynamicTypeWrapper { private Dictionary _values; diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs index 972b817e2..8dfbc8aba 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByAggregationWrapper.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class NoGroupByAggregationWrapper : GroupByWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs index 9d90c7a7a..41a24dbf1 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/NoGroupByWrapper.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(DynamicTypeWrapperConverter))] internal class NoGroupByWrapper : GroupByWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs index 2325c243b..c23163aa2 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllAndExpandOfT.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal class SelectAllAndExpand : SelectExpandWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs index c4d5b22b0..5fdec4322 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectAllOfT.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal class SelectAll : SelectExpandWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs index 194155a62..deb400316 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapper.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; using Microsoft.AspNetCore.OData.Edm; using Microsoft.AspNetCore.OData.Formatter.Value; using Microsoft.AspNetCore.OData.Query.Container; @@ -14,6 +15,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal abstract class SelectExpandWrapper : IEdmEntityObject, ISelectExpandWrapper { private static readonly IPropertyMapper DefaultPropertyMapper = new IdentityPropertyMapper(); diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs index 332bedd32..c9a386dc0 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperConverter.cs @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper /// /// Supports converting types by using a factory pattern. /// - internal class SelectExpandWrapperConverter : JsonConverterFactory + public class SelectExpandWrapperConverter : JsonConverterFactory { public static readonly Func MapperProvider = (IEdmModel model, IEdmStructuredType type) => new JsonPropertyNameMapper(model, type); @@ -65,30 +65,30 @@ public override JsonConverter CreateConverter(Type type, JsonSerializerOptions o } // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); + Type genericType = type.GetGenericTypeDefinition(); Type entityType = type.GetGenericArguments()[0]; - if (generaticType == typeof(SelectSome<>)) + if (genericType == typeof(SelectSome<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeConverter<>).MakeGenericType(new Type[] { entityType })); } - if (generaticType == typeof(SelectSomeAndInheritance<>)) + if (genericType == typeof(SelectSomeAndInheritance<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SelectSomeAndInheritanceConverter<>).MakeGenericType(new Type[] { entityType })); } - if (generaticType == typeof(SelectAll<>)) + if (genericType == typeof(SelectAll<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SelectAllConverter<>).MakeGenericType(new Type[] { entityType })); } - if (generaticType == typeof(SelectAllAndExpand<>)) + if (genericType == typeof(SelectAllAndExpand<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SelectAllAndExpandConverter<>).MakeGenericType(new Type[] { entityType })); } - if (generaticType == typeof(SelectExpandWrapper<>)) + if (genericType == typeof(SelectExpandWrapper<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SelectExpandWrapperConverter<>).MakeGenericType(new Type[] { entityType })); } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs index 22ce62991..d53cd26d7 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectExpandWrapperOfT.cs @@ -25,6 +25,7 @@ property selection combination possible. */ /// Represents a container class that contains properties that are either selected or expanded using $select and $expand. /// /// The element being selected and expanded. + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal class SelectExpandWrapper : SelectExpandWrapper { /// diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs index 24b78bddb..082110130 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeAndInheritanceOfT.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal class SelectSomeAndInheritance : SelectExpandWrapper { } diff --git a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs index 0f224f59e..d4e557bb8 100644 --- a/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Query/Wrapper/SelectSomeOfT.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.OData.Query.Wrapper { + [JsonConverter(typeof(SelectExpandWrapperConverter))] internal class SelectSome : SelectAllAndExpand { } diff --git a/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs b/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs index 277509909..bd02f1737 100644 --- a/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Results/PageResultOfT.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.Serialization; +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.OData.Results { @@ -23,6 +24,7 @@ namespace Microsoft.AspNetCore.OData.Results /// [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "Collection suffix not appropriate")] [DataContract] + [JsonConverter(typeof(PageResultValueConverter))] public class PageResult : PageResult, IEnumerable { /// diff --git a/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs b/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs index 0eea8aea8..3bd0bbf48 100644 --- a/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Results/PageResultValueConverter.cs @@ -21,8 +21,8 @@ public override bool CanConvert(Type typeToConvert) return false; } - Type generaticType = typeToConvert.GetGenericTypeDefinition(); - return generaticType == typeof(PageResult<>); + Type genericType = typeToConvert.GetGenericTypeDefinition(); + return genericType == typeof(PageResult<>); } /// @@ -34,10 +34,10 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) { // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); + Type genericType = type.GetGenericTypeDefinition(); Type entityType = type.GetGenericArguments()[0]; - if (generaticType == typeof(PageResult<>)) + if (genericType == typeof(PageResult<>)) { return (JsonConverter)Activator.CreateInstance(typeof(PageResultConverter<>).MakeGenericType(new Type[] { entityType })); } diff --git a/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs b/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs index 45f25ca58..264b6a791 100644 --- a/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs +++ b/src/Microsoft.AspNetCore.OData/Results/SingleResultOfT.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System.Linq; +using System.Text.Json.Serialization; namespace Microsoft.AspNetCore.OData.Results { @@ -14,6 +15,7 @@ namespace Microsoft.AspNetCore.OData.Results /// [EnableQuery]. /// /// The type of the data in the data source. + [JsonConverter(typeof(SingleResultValueConverter))] public sealed class SingleResult : SingleResult { /// diff --git a/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs b/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs index 089bd5420..55669c67a 100644 --- a/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs +++ b/src/Microsoft.AspNetCore.OData/Results/SingleResultValueConverter.cs @@ -23,8 +23,8 @@ public override bool CanConvert(Type typeToConvert) return false; } - Type generaticType = typeToConvert.GetGenericTypeDefinition(); - return generaticType == typeof(SingleResult<>); + Type genericType = typeToConvert.GetGenericTypeDefinition(); + return genericType == typeof(SingleResult<>); } /// @@ -36,10 +36,10 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) { // Since 'type' is tested in 'CanConvert()', it must be a generic type - Type generaticType = type.GetGenericTypeDefinition(); + Type genericType = type.GetGenericTypeDefinition(); Type entityType = type.GetGenericArguments()[0]; - if (generaticType == typeof(SingleResult<>)) + if (genericType == typeof(SingleResult<>)) { return (JsonConverter)Activator.CreateInstance(typeof(SingleResultConverter<>).MakeGenericType(new Type[] { entityType })); } diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl index 1ea6ff77d..af8c89039 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.Net6.bsl @@ -67,6 +67,21 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { ExtensionAttribute(), ] public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`1[[Microsoft.AspNetCore.OData.ODataOptions]] setupAction) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`2[[Microsoft.AspNetCore.OData.ODataOptions],[System.IServiceProvider]] setupAction) + [ ExtensionAttribute(), ] @@ -550,6 +565,14 @@ public class Microsoft.AspNetCore.OData.Deltas.DeltaSet`1 : System.Collections.O System.Type StructuredType { public virtual get; } } +public interface Microsoft.AspNetCore.OData.Edm.IODataEndpointModelMapper { + System.Collections.Concurrent.ConcurrentDictionary`2[[Microsoft.AspNetCore.Http.Endpoint],[Microsoft.OData.Edm.IEdmModel]] Maps { public abstract get; } +} + +public interface Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration { + void Apply (Microsoft.AspNetCore.Http.HttpContext context, Microsoft.OData.ModelBuilder.ODataModelBuilder builder) +} + public interface Microsoft.AspNetCore.OData.Edm.IODataTypeMapper { System.Type GetClrPrimitiveType (Microsoft.OData.Edm.IEdmPrimitiveType primitiveType, bool nullable) System.Type GetClrType (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmType edmType, bool nullable, Microsoft.OData.ModelBuilder.IAssemblyResolver assembliesResolver) @@ -932,6 +955,21 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers { public static string CreateODataLink (Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IList`1[[Microsoft.OData.UriParser.ODataPathSegment]] segments) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.Extensions.ODataEndpointConventionBuilderExtensions { + [ + ExtensionAttribute(), + ] + public static TBuilder UseOData (TBuilder builder, Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration config) + + [ + ExtensionAttribute(), + ] + public static TBuilder UseOData (TBuilder builder, string prefix) +} + [ EditorBrowsableAttribute(), ExtensionAttribute(), @@ -955,6 +993,12 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys public static readonly string StackTraceKey = "StackTrace" } +public sealed class Microsoft.AspNetCore.OData.Extensions.ODataPrefixMetadata { + public ODataPrefixMetadata (string prefix) + + string Prefix { public get; } +} + public enum Microsoft.AspNetCore.OData.Formatter.ODataMetadataLevel : int { Full = 1 Minimal = 0 @@ -1427,6 +1471,7 @@ public class Microsoft.AspNetCore.OData.Query.ODataQueryOptions`1 : Microsoft.As public virtual System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query) public virtual System.Linq.IQueryable ApplyTo (System.Linq.IQueryable query, Microsoft.AspNetCore.OData.Query.ODataQuerySettings querySettings) + public static ValueTask`1 BindAsync (Microsoft.AspNetCore.Http.HttpContext context, System.Reflection.ParameterInfo parameter) internal virtual Microsoft.AspNetCore.OData.Query.ETag GetETag (Microsoft.Net.Http.Headers.EntityTagHeaderValue etagHeaderValue) } @@ -1688,6 +1733,7 @@ public class Microsoft.AspNetCore.OData.Results.ODataErrorResult : Microsoft.Asp [ DataContractAttribute(), +JsonConverterAttribute(), ] public class Microsoft.AspNetCore.OData.Results.PageResult`1 : Microsoft.AspNetCore.OData.Results.PageResult, IEnumerable`1, IEnumerable { public PageResult`1 (IEnumerable`1 items, System.Uri nextPageLink, System.Nullable`1[[System.Int64]] count) @@ -1737,6 +1783,9 @@ public class Microsoft.AspNetCore.OData.Results.UpdatedODataResult`1 : Microsoft public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) } +[ +JsonConverterAttribute(), +] public sealed class Microsoft.AspNetCore.OData.Results.SingleResult`1 : Microsoft.AspNetCore.OData.Results.SingleResult { public SingleResult`1 (IQueryable`1 queryable) @@ -3116,6 +3165,15 @@ public abstract class Microsoft.AspNetCore.OData.Query.Wrapper.DynamicTypeWrappe public virtual bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public class Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter : System.Text.Json.Serialization.JsonConverterFactory { + public static readonly System.Func`3[[Microsoft.OData.Edm.IEdmModel],[Microsoft.OData.Edm.IEdmStructuredType],[Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper]] MapperProvider = System.Func`3[Microsoft.OData.Edm.IEdmModel,Microsoft.OData.Edm.IEdmStructuredType,Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper] + + public SelectExpandWrapperConverter () + + public virtual bool CanConvert (System.Type typeToConvert) + public virtual System.Text.Json.Serialization.JsonConverter CreateConverter (System.Type type, System.Text.Json.JsonSerializerOptions options) +} + [ AttributeUsageAttribute(), ] diff --git a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl index 1ea6ff77d..0978f45cd 100644 --- a/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl +++ b/test/Microsoft.AspNetCore.OData.Tests/PublicApi/Microsoft.AspNetCore.OData.PublicApi.NetCore31.bsl @@ -67,6 +67,21 @@ public sealed class Microsoft.AspNetCore.OData.ODataMvcCoreBuilderExtensions { ExtensionAttribute(), ] public sealed class Microsoft.AspNetCore.OData.ODataServiceCollectionExtensions { + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`1[[Microsoft.AspNetCore.OData.ODataOptions]] setupAction) + + [ + ExtensionAttribute(), + ] + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddOData (Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action`2[[Microsoft.AspNetCore.OData.ODataOptions],[System.IServiceProvider]] setupAction) + [ ExtensionAttribute(), ] @@ -550,6 +565,14 @@ public class Microsoft.AspNetCore.OData.Deltas.DeltaSet`1 : System.Collections.O System.Type StructuredType { public virtual get; } } +public interface Microsoft.AspNetCore.OData.Edm.IODataEndpointModelMapper { + System.Collections.Concurrent.ConcurrentDictionary`2[[Microsoft.AspNetCore.Http.Endpoint],[Microsoft.OData.Edm.IEdmModel]] Maps { public abstract get; } +} + +public interface Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration { + void Apply (Microsoft.AspNetCore.Http.HttpContext context, Microsoft.OData.ModelBuilder.ODataModelBuilder builder) +} + public interface Microsoft.AspNetCore.OData.Edm.IODataTypeMapper { System.Type GetClrPrimitiveType (Microsoft.OData.Edm.IEdmPrimitiveType primitiveType, bool nullable) System.Type GetClrType (Microsoft.OData.Edm.IEdmModel edmModel, Microsoft.OData.Edm.IEdmType edmType, bool nullable, Microsoft.OData.ModelBuilder.IAssemblyResolver assembliesResolver) @@ -932,6 +955,21 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.LinkGeneratorHelpers { public static string CreateODataLink (Microsoft.AspNetCore.Http.HttpRequest request, System.Collections.Generic.IList`1[[Microsoft.OData.UriParser.ODataPathSegment]] segments) } +[ +ExtensionAttribute(), +] +public sealed class Microsoft.AspNetCore.OData.Extensions.ODataEndpointConventionBuilderExtensions { + [ + ExtensionAttribute(), + ] + public static TBuilder UseOData (TBuilder builder, Microsoft.AspNetCore.OData.Edm.IODataModelConfiguration config) + + [ + ExtensionAttribute(), + ] + public static TBuilder UseOData (TBuilder builder, string prefix) +} + [ EditorBrowsableAttribute(), ExtensionAttribute(), @@ -955,6 +993,12 @@ public sealed class Microsoft.AspNetCore.OData.Extensions.SerializableErrorKeys public static readonly string StackTraceKey = "StackTrace" } +public sealed class Microsoft.AspNetCore.OData.Extensions.ODataPrefixMetadata { + public ODataPrefixMetadata (string prefix) + + string Prefix { public get; } +} + public enum Microsoft.AspNetCore.OData.Formatter.ODataMetadataLevel : int { Full = 1 Minimal = 0 @@ -1688,6 +1732,7 @@ public class Microsoft.AspNetCore.OData.Results.ODataErrorResult : Microsoft.Asp [ DataContractAttribute(), +JsonConverterAttribute(), ] public class Microsoft.AspNetCore.OData.Results.PageResult`1 : Microsoft.AspNetCore.OData.Results.PageResult, IEnumerable`1, IEnumerable { public PageResult`1 (IEnumerable`1 items, System.Uri nextPageLink, System.Nullable`1[[System.Int64]] count) @@ -1737,6 +1782,9 @@ public class Microsoft.AspNetCore.OData.Results.UpdatedODataResult`1 : Microsoft public virtual System.Threading.Tasks.Task ExecuteResultAsync (Microsoft.AspNetCore.Mvc.ActionContext context) } +[ +JsonConverterAttribute(), +] public sealed class Microsoft.AspNetCore.OData.Results.SingleResult`1 : Microsoft.AspNetCore.OData.Results.SingleResult { public SingleResult`1 (IQueryable`1 queryable) @@ -3116,6 +3164,15 @@ public abstract class Microsoft.AspNetCore.OData.Query.Wrapper.DynamicTypeWrappe public virtual bool TryGetPropertyValue (string propertyName, out System.Object& value) } +public class Microsoft.AspNetCore.OData.Query.Wrapper.SelectExpandWrapperConverter : System.Text.Json.Serialization.JsonConverterFactory { + public static readonly System.Func`3[[Microsoft.OData.Edm.IEdmModel],[Microsoft.OData.Edm.IEdmStructuredType],[Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper]] MapperProvider = System.Func`3[Microsoft.OData.Edm.IEdmModel,Microsoft.OData.Edm.IEdmStructuredType,Microsoft.AspNetCore.OData.Query.Container.IPropertyMapper] + + public SelectExpandWrapperConverter () + + public virtual bool CanConvert (System.Type typeToConvert) + public virtual System.Text.Json.Serialization.JsonConverter CreateConverter (System.Type type, System.Text.Json.JsonSerializerOptions options) +} + [ AttributeUsageAttribute(), ] From 182f203c8bc774abbe8b6aeaa0735b048b6ea8c7 Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Thu, 14 Dec 2023 20:12:07 -0800 Subject: [PATCH 2/3] add more scenarios into readme.md --- sample/ODataMiniApi/readme.md | 48 +++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/sample/ODataMiniApi/readme.md b/sample/ODataMiniApi/readme.md index 7cef2bf77..7e41df023 100644 --- a/sample/ODataMiniApi/readme.md +++ b/sample/ODataMiniApi/readme.md @@ -6,7 +6,6 @@ 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` @@ -127,4 +126,49 @@ Content-Type: application/json "schoolId": 3, "birthDay": "1977-11-04" } -``` \ No newline at end of file +``` + +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)` \ No newline at end of file From a673bede173418724480ed6c06d82ace5bbe398d Mon Sep 17 00:00:00 2001 From: Sam Xu Date: Tue, 26 Dec 2023 19:05:06 -0800 Subject: [PATCH 3/3] Enable OData metadata endpoint. --- sample/ODataMiniApi/MetadataHandler.cs | 151 +++++++++++++++++++++++++ sample/ODataMiniApi/Program.cs | 5 +- sample/ODataMiniApi/readme.md | 136 +++++++++++++++++++++- 3 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 sample/ODataMiniApi/MetadataHandler.cs diff --git a/sample/ODataMiniApi/MetadataHandler.cs b/sample/ODataMiniApi/MetadataHandler.cs new file mode 100644 index 000000000..783bfd39a --- /dev/null +++ b/sample/ODataMiniApi/MetadataHandler.cs @@ -0,0 +1,151 @@ +//----------------------------------------------------------------------------- +// +// 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.AspNetCore.OData.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.OData.Edm; +using Microsoft.OData.Edm.Csdl; +using Microsoft.OData.Edm.Validation; +using System.Text.Encodings.Web; + +namespace ODataMiniApi; + +public class MetadataHandler +{ + public static async Task HandleMetadata(HttpContext context) + { + IEdmModel model = GetEdmModel(context); + if (IsJson(context)) + { + await WriteAsJson(context, model); + } + else + { + await WriteAsXml(context, model); + } + } + + internal static async Task WriteAsJson(HttpContext context, IEdmModel model) + { + context.Response.ContentType = "application/json"; + + JsonWriterOptions options = new JsonWriterOptions + { + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Indented = true, + 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); + } + } + + internal static async Task WriteAsXml(HttpContext context, IEdmModel model) + { + context.Response.ContentType = "application/xml"; + + // we can't use response body directly since ODL writes the XML CSDL using Synchronous operations. + //XmlWriterSettings settings = new XmlWriterSettings(); + //settings.Encoding = Encoding.UTF8; + //settings.Indent = true; // for better readability + + //using (XmlWriter xw = XmlWriter.Create(context.Response.Body, settings)) + //{ + // IEnumerable errors; + // CsdlWriter.TryWriteCsdl(model, xw, CsdlTarget.OData, out errors); + // xw.Flush(); + //} + + //await Task.CompletedTask; + + using (StringWriter sw = new StringWriter()) + { + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Encoding = Encoding.UTF8; + settings.Indent = true; // for better readability + + 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); + } + } + + internal static bool IsJson(HttpContext context) + { + var acceptHeaders = context.Request.Headers.Accept; + if (acceptHeaders.Any(h => h.Contains("application/json", StringComparison.OrdinalIgnoreCase))) + { + // If Accept header set on Request, we use it. + return true; + } + else if (acceptHeaders.Any(h => h.Contains("application/xml", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + StringValues formatValues; + bool dollarFormat = context.Request.Query.TryGetValue("$format", out formatValues) || context.Request.Query.TryGetValue("format", out formatValues); + if (dollarFormat) + { + if (formatValues.Any(h => h.Contains("application/json", StringComparison.OrdinalIgnoreCase))) + { + return true; + } + else if (formatValues.Any(h => h.Contains("application/xml", StringComparison.OrdinalIgnoreCase))) + { + return false; + } + } + + return false; + } + + private static IEdmModel GetEdmModel(HttpContext context) + { + // You can retrieve/create the Edm model by yourself, or create and use the Model Provider service + Endpoint endpoint = context.GetEndpoint(); + ODataPrefixMetadata prefixMetadata = endpoint.Metadata.GetMetadata(); + if (prefixMetadata != null) + { + ODataOptions options = context.RequestServices.GetService>()?.Value; + if (options != null) + { + if (options.RouteComponents.TryGetValue(prefixMetadata.Prefix, out var routeComponents)) + { + return routeComponents.EdmModel; + } + } + } + + throw new InvalidOperationException($"Please calling WithOData() to register the EdmModel."); + } +} diff --git a/sample/ODataMiniApi/Program.cs b/sample/ODataMiniApi/Program.cs index e4ebef909..bde3b97d3 100644 --- a/sample/ODataMiniApi/Program.cs +++ b/sample/ODataMiniApi/Program.cs @@ -11,6 +11,7 @@ using Microsoft.AspNetCore.OData.Extensions; using ODataMiniApi; using ODataMiniApi.Students; +using System.Runtime.CompilerServices; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(options => options.UseInMemoryDatabase("SchoolStudentList")); @@ -44,7 +45,9 @@ app.MapGet("/customized/schools", (AppDb db, ODataQueryOptions options) => { db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; return options.ApplyTo(db.Schools); -}).UseOData("customized"); // In customized OData, MailAddress and Student are complex type, you can also use 'IODataModelConfiguration' +}).UseOData("customized"); // In customized OData, MailAddress and Student are complex types, you can also use 'IODataModelConfiguration' + +app.MapGet("/customized/$odata", MetadataHandler.HandleMetadata).UseOData("customized"); #endregion diff --git a/sample/ODataMiniApi/readme.md b/sample/ODataMiniApi/readme.md index 7e41df023..6516f02ef 100644 --- a/sample/ODataMiniApi/readme.md +++ b/sample/ODataMiniApi/readme.md @@ -171,4 +171,138 @@ This will change the student, and also move the student from `Schools(1)` to `Sc 4) Delete `http://localhost:5177/odata/students/10` -This will delete the `Students(10)` \ No newline at end of file +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" + } + } + } +} +```