using System.Text; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.OData.Deltas; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Query.Validator; using Microsoft.AspNetCore.OData.Routing; using Microsoft.AspNetCore.OData.Routing.Parser; using Microsoft.AspNetCore.Routing.Matching; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; using Microsoft.OData.UriParser; using Project.DotNet.New.WebApi; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers() .AddOData(options => { var conventionModelBuilder = new ODataConventionModelBuilder(); conventionModelBuilder.EntityType(); conventionModelBuilder.EntitySet("forcasts"); conventionModelBuilder.EnableLowerCamelCase(); var model = conventionModelBuilder.GetEdmModel(); options.AddRouteComponents("api/v1.0", model); }); builder.Services.AddODataMinimalApi(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); var testData = Enumerable.Range(1, 1000).Select(index => new WeatherForecast { Id = index, Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = "" }).ToList().AsQueryable(); app.MapODataGet("api/v1.0", "forcasts", () => ODataResults.ODataResult(testData)); app.MapODataGet("api/v1.0", "forcasts/{forcastId:int}", async (int forcastId) => { return ODataResults.ODataResult(await Task.FromResult(testData.Where(o => o.Id == forcastId))); }); app.MapODataGet("api/v1.0", "forcasts({forcastId:int})", async (int forcastId) => { return ODataResults.ODataResult(await Task.FromResult(testData.Where(o => o.Id == forcastId))); }); app.MapODataDelete("api/v1.0", "forcasts({forcastId:int})", (int forcastId) => Results.NoContent()); app.MapODataPost("api/v1.0", "forcasts", async (ODataRequestBody oDataRequest) => { return ODataResults.ODataResult(await Task.FromResult(oDataRequest.Entity)); }); app.MapODataPatch("api/v1.0", "forcasts/{forcastId:int}", async (ODataRequestBody> oDataRequest) => { oDataRequest.Entity?.Patch(testData.First()); return ODataResults.ODataResult(await Task.FromResult(testData.AsQueryable())); }); app.Run(); public class ODataRequestBody { public T? Entity { get; set; } public static async ValueTask> BindAsync(HttpContext context) { var inputDeserializer = context.RequestServices.GetRequiredService(); return new ODataRequestBody { Entity = await inputDeserializer.ReadAsync(context) }; } } public class ODataModelResolver { private readonly IOptions _options; public ODataModelResolver(IOptions options) { _options = options; } public IEdmModel? GetModel(string? prefix = null) { var routeComponent = _options.Value.RouteComponents[prefix ?? string.Empty]; return routeComponent.EdmModel; } } public class ODataOutputSerializer { private readonly ODataOutputFormatter _outputFormatter; public ODataOutputSerializer() { _outputFormatter = ODataOutputFormatterFactory.Create() .Reverse() .First(o => o.SupportedMediaTypes.Contains("application/json")); } public async Task WriteAsync(HttpContext httpContext, object output) { var outputFormatterWriteContext = new OutputFormatterWriteContext( httpContext, (stream, encoding) => new StreamWriter(stream, encoding), output.GetType(), output ) { ContentType = "application/json" }; _outputFormatter.WriteResponseHeaders(outputFormatterWriteContext); await _outputFormatter.WriteResponseBodyAsync(outputFormatterWriteContext, Encoding.UTF8); } } public class ODataInputDeserializer { private readonly IModelMetadataProvider _modelMetadataProvider; private readonly ICompositeMetadataDetailsProvider _compositeMetadataProvider; private readonly ODataInputFormatter _inputFormatter; public ODataInputDeserializer(IModelMetadataProvider modelMetadataProvider, ICompositeMetadataDetailsProvider compositeMetadataProvider) { _modelMetadataProvider = modelMetadataProvider; _compositeMetadataProvider = compositeMetadataProvider; _inputFormatter = ODataInputFormatterFactory.Create().Reverse() .First(o => o.SupportedMediaTypes.Contains("application/json")); } public async ValueTask ReadAsync(HttpContext context) { var result = await _inputFormatter.ReadAsync( new InputFormatterContext( context, "", new ModelStateDictionary(), new DefaultModelMetadata(_modelMetadataProvider, _compositeMetadataProvider, new DefaultMetadataDetails(ModelMetadataIdentity.ForType(typeof(T)), ModelAttributes.GetAttributesForType(typeof(T)))), (stream, encoding) => new StreamReader(stream, encoding))); return (T)(result.Model ?? default(T)); } } public static class ODataResults { public static ODataResult ODataResult(IQueryable result) { return new ODataResult(result); } public static ODataResult ODataResult(T result) { return new ODataResult(new EnumerableQuery(new [] { result })); } } public class ODataResult : IResult { public ODataResult(IQueryable result) { Result = result; } public IQueryable Result { get; set; } public async Task ExecuteAsync(HttpContext httpContext) { if (httpContext.GetEndpoint()?.Metadata.FirstOrDefault(o => o is ODataRoutingMetadata) is not ODataRoutingMetadata endPointMetaData) throw new InvalidOperationException( "OData endpoint metadata not found maybe this is not a valid endpoint."); var serializer = httpContext.RequestServices.GetRequiredService(); var odataFeature = httpContext.ODataFeature(); var queryOptions = new ODataQueryOptions( new ODataQueryContext(endPointMetaData.Model, typeof(T), odataFeature.Path), httpContext.Request ); if (httpContext.GetEndpoint()?.Metadata.FirstOrDefault(o => o is ODataValidationSettings) is ODataValidationSettings validationSettings) { queryOptions.Validate(validationSettings); } object? result = httpContext.GetEndpoint()?.Metadata .FirstOrDefault(o => o is ODataQuerySettings) is not ODataQuerySettings querySettings ? queryOptions.ApplyTo(Result) : queryOptions.ApplyTo(Result, querySettings); if (odataFeature.Path.LastSegment is KeySegment) { var enumerator = ((IQueryable)result).GetEnumerator(); try { result = enumerator.MoveNext() ? enumerator.Current : null; } finally { // Ensure any active/open database objects that were created // iterating over the IQueryable object are properly closed. var disposable = enumerator as IDisposable; disposable?.Dispose(); } } if (result == null) { httpContext.Response.StatusCode = 404; return; } await serializer.WriteAsync(httpContext, result); } } public static class WebApplicationExtension { public static RouteHandlerBuilder MapODataGet(this WebApplication webApplication, string routePrefix, string odataPath, Delegate requestDelegate) { return webApplication.MapODataMethod("GET", routePrefix, odataPath, requestDelegate); } public static RouteHandlerBuilder MapODataPost(this WebApplication webApplication, string routePrefix, string odataPath, Delegate requestDelegate) { return webApplication.MapODataMethod("POST", routePrefix, odataPath, requestDelegate); } public static RouteHandlerBuilder MapODataPut(this WebApplication webApplication, string routePrefix, string odataPath, Delegate requestDelegate) { return webApplication.MapODataMethod("PUT", routePrefix, odataPath, requestDelegate); } public static RouteHandlerBuilder MapODataPatch(this WebApplication webApplication, string routePrefix, string odataPath, Delegate requestDelegate) { return webApplication.MapODataMethod("PATCH", routePrefix, odataPath, requestDelegate); } public static RouteHandlerBuilder MapODataDelete(this WebApplication webApplication, string routePrefix, string odataPath, Delegate requestDelegate) { return webApplication.MapODataMethod("DELETE", routePrefix, odataPath, requestDelegate); } private static RouteHandlerBuilder MapODataMethod(this WebApplication webApplication, string method, string routePrefix, string odataPath, Delegate requestDelegate) { var modelResolver = webApplication.Services.GetRequiredService(); var pathTemplateParser = webApplication.Services.GetRequiredService(); if (routePrefix.EndsWith("/")) routePrefix = routePrefix[..^1]; var model = modelResolver.GetModel(routePrefix); var odataPathTemplate = pathTemplateParser.Parse(model, odataPath, null); return webApplication.MapMethods(routePrefix + "/" + odataPath, new[] { method }, requestDelegate) .WithMetadata(new ODataRoutingMetadata(routePrefix, model, odataPathTemplate)); } } public static class ServiceCollectionExtensions { public static IServiceCollection AddODataMinimalApi(this IServiceCollection services) { services.AddSingleton((Func)(_ => new UnqualifiedODataUriResolver { EnableCaseInsensitive = true })); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.TryAddEnumerable(ServiceDescriptor.Singleton()); return services; } } public class CustomRoutingMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy { public override int Order => 800; public bool AppliesToEndpoints(IReadOnlyList endpoints) { return true; } public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { for (var i = 0; i < candidates.Count; i++) { var candidate = candidates[i]; if (candidate.Values == null) candidates.ReplaceEndpoint(i, candidate.Endpoint, new RouteValueDictionary()); } return Task.CompletedTask; } }