From 99139b7bc081e1c9f99b162528295d47441e2510 Mon Sep 17 00:00:00 2001 From: Elizabeth Okerio Date: Thu, 13 Jul 2023 11:06:57 +0300 Subject: [PATCH] support IAsyncenumerable --- .../Common/TypeHelper.cs | 3 +- .../Edm/DefaultODataTypeMapper.cs | 15 +- .../Formatter/ResourceSetContext.cs | 19 +- .../ODataResourceSetSerializer.cs | 306 ++++++++++++++---- .../Microsoft.AspNetCore.OData.xml | 27 ++ .../PublicAPI.Unshipped.txt | 1 + .../IAsyncEnumerableController.cs | 128 ++++++++ .../IAsyncEnumerableDataModel.cs | 54 ++++ .../IAsyncEnumerableEdmModel.cs | 24 ++ .../IAsyncEnumerableTests.cs | 88 +++++ ...rosoft.AspNetCore.OData.PublicApi.Net6.bsl | 1 + ...t.AspNetCore.OData.PublicApi.NetCore31.bsl | 1 + 12 files changed, 595 insertions(+), 72 deletions(-) create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs create mode 100644 test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs diff --git a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs index f0f79f0f4..360e80b8c 100644 --- a/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs +++ b/src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs @@ -240,7 +240,8 @@ Type collectionInterface .Union(new[] { clrType }) .FirstOrDefault( t => t.IsGenericType - && t.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + && (t.GetGenericTypeDefinition() == typeof(IEnumerable<>) + || t.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))); if (collectionInterface != null) { diff --git a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs index dfc5ba44d..f6de16954 100644 --- a/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs +++ b/src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs @@ -222,9 +222,20 @@ private IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCollectio } Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>)); - if (enumerableOfT != null) + Type asyncEnumerableOfT = ExtractGenericInterface(clrType, typeof(IAsyncEnumerable<>)); + + if (enumerableOfT != null || asyncEnumerableOfT != null) { - Type elementClrType = enumerableOfT.GetGenericArguments()[0]; + Type elementClrType = null; + + if (enumerableOfT != null) + { + elementClrType = enumerableOfT.GetGenericArguments()[0]; + } + else + { + elementClrType = asyncEnumerableOfT.GetGenericArguments()[0]; + } // IEnumerable> is a collection of T. if (elementClrType.IsSelectExpandWrapper(out entityType)) diff --git a/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs b/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs index ac4383d83..ecdfa244b 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs @@ -6,6 +6,7 @@ //------------------------------------------------------------------------------ using System.Collections; +using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OData.Extensions; using Microsoft.AspNetCore.OData.Formatter.Serialization; @@ -55,11 +56,27 @@ internal static ResourceSetContext Create(ODataSerializerContext writeContext, I { Request = writeContext.Request, EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, - // Url = writeContext.Url, ResourceSetInstance = resourceSetInstance }; return resourceSetContext; } + + /// + /// Create a from an and . + /// + /// The serializer context. + /// The instance representing the resourceSet being written. + /// A new . + /// This signature uses types that are AspNetCore-specific. + internal static ResourceSetContext Create(ODataSerializerContext writeContext, IAsyncEnumerable resourceSetInstance) + { + return new ResourceSetContext + { + Request = writeContext.Request, + EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase, + ResourceSetInstance = resourceSetInstance + }; + } } } diff --git a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs index 1dcfc45c2..09a02049d 100644 --- a/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs +++ b/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs @@ -25,7 +25,6 @@ using Microsoft.Extensions.Options; using Microsoft.OData; using Microsoft.OData.Edm; -using Microsoft.OData.ModelBuilder.Capabilities.V1; using Microsoft.OData.UriParser; namespace Microsoft.AspNetCore.OData.Formatter.Serialization @@ -97,14 +96,22 @@ public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReferenc throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); } - IEnumerable enumerable = graph as IEnumerable; // Data to serialize - if (enumerable == null) + if (writeContext.Type != null && + writeContext.Type.IsGenericType && + writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) && + graph is IAsyncEnumerable asyncEnumerable) + { + await WriteResourceSetAsync(asyncEnumerable, expectedType, writer, writeContext).ConfigureAwait(false); + } + else if (graph is IEnumerable enumerable) + { + await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); + } + else { throw new SerializationException( Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName)); } - - await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false); } private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer, @@ -120,6 +127,73 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, enumerable, writeContext); + WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); + + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); + object lastResource = null; + + foreach (object item in enumerable) + { + lastResource = item; + + await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); + } + + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), + // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if + // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet + // object before calling WriteEnd(), the next page link will be written at the end, as required for + // odata.streaming=true support. + + resourceSet.NextPageLink = nextLinkGenerator(lastResource); + + await writer.WriteEndAsync().ConfigureAwait(false); + } + + private async Task WriteResourceSetAsync(IAsyncEnumerable asyncEnumerable, IEdmTypeReference resourceSetType, ODataWriter writer, + ODataSerializerContext writeContext) + { + Contract.Assert(writer != null); + Contract.Assert(writeContext != null); + Contract.Assert(asyncEnumerable != null); + Contract.Assert(resourceSetType != null); + + IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType); + ODataResourceSet resourceSet = CreateResourceSet(asyncEnumerable, resourceSetType.AsCollection(), writeContext); + + Func nextLinkGenerator = GetNextLinkGenerator(resourceSet, asyncEnumerable, writeContext); + + WriteResourceSetInternal(resourceSet, elementType, resourceSetType, writeContext, out bool isUntypedCollection, out IODataEdmTypeSerializer resourceSerializer); + + await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); + object lastResource = null; + + await foreach (object item in asyncEnumerable) + { + lastResource = item; + + await WriteResourceSetItemAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext).ConfigureAwait(false); + } + + // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), + // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if + // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet + // object before calling WriteEnd(), the next page link will be written at the end, as required for + // odata.streaming=true support. + + resourceSet.NextPageLink = nextLinkGenerator(lastResource); + + await writer.WriteEndAsync().ConfigureAwait(false); + } + + private void WriteResourceSetInternal( + ODataResourceSet resourceSet, + IEdmStructuredTypeReference elementType, + IEdmTypeReference resourceSetType, + ODataSerializerContext writeContext, + out bool isUntypedCollection, + out IODataEdmTypeSerializer resourceSerializer) + { if (resourceSet == null) { throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet)); @@ -137,52 +211,47 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen }); } - IODataEdmTypeSerializer resourceSerializer = SerializerProvider.GetEdmTypeSerializer(elementType); + resourceSerializer = SerializerProvider.GetEdmTypeSerializer(elementType); if (resourceSerializer == null) { throw new SerializationException( Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName())); } - bool isUntypedCollection = resourceSetType.IsCollectionUntyped(); + isUntypedCollection = resourceSetType.IsCollectionUntyped(); // set the nextpagelink to null to support JSON odata.streaming. - resourceSet.NextPageLink = null; - await writer.WriteStartAsync(resourceSet).ConfigureAwait(false); - object lastResource = null; - foreach (object item in enumerable) - { - lastResource = item; - if (item == null || item is NullEdmComplexObject) - { - if (elementType.IsEntity()) - { - throw new SerializationException(SRResources.NullElementInCollection); - } + resourceSet.NextPageLink = null; + } - // for null complex element, it can be serialized as "null" in the collection. - await writer.WriteStartAsync(resource: null).ConfigureAwait(false); - await writer.WriteEndAsync().ConfigureAwait(false); - } - else if (isUntypedCollection) - { - await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false); - } - else + private async Task WriteResourceSetItemAsync( + object item, + IEdmStructuredTypeReference elementType, + bool isUntypedCollection, + IEdmTypeReference resourceSetType, + ODataWriter writer, + IODataEdmTypeSerializer resourceSerializer, + ODataSerializerContext writeContext) + { + if (item == null || item is NullEdmComplexObject) + { + if (elementType.IsEntity()) { - await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false); + throw new SerializationException(SRResources.NullElementInCollection); } - } - // Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet), - // the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if - // the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet - // object before calling WriteEnd(), the next page link will be written at the end, as required for - // odata.streaming=true support. - - resourceSet.NextPageLink = nextLinkGenerator(lastResource); - - await writer.WriteEndAsync().ConfigureAwait(false); + // for null complex element, it can be serialized as "null" in the collection. + await writer.WriteStartAsync(resource: null).ConfigureAwait(false); + await writer.WriteEndAsync().ConfigureAwait(false); + } + else if (isUntypedCollection) + { + await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false); + } + else + { + await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false); + } } private async Task WriteUntypedResourceSetItemAsync(object item, IEdmTypeReference parentSetType, ODataWriter writer, ODataSerializerContext writeContext) @@ -352,44 +421,59 @@ public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstanc if (writeContext.NavigationSource != null && structuredType.IsEntity()) { ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); - IEdmEntityType entityType = structuredType.AsEntity().EntityDefinition(); - var operations = writeContext.Model.GetAvailableOperationsBoundToCollection(entityType); - var odataOperations = CreateODataOperations(operations, resourceSetContext, writeContext); - foreach (var odataOperation in odataOperations) - { - ODataAction action = odataOperation as ODataAction; - if (action != null) - { - resourceSet.AddAction(action); - } - else - { - resourceSet.AddFunction((ODataFunction)odataOperation); - } - } + WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); } if (writeContext.ExpandedResource == null) { // If we have more OData format specific information apply it now, only if we are the root feed. PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; - if (odataResourceSetAnnotations != null) + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); + } + else + { + ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection; + if (countOptionCollection != null && countOptionCollection.TotalCount != null) { - resourceSet.Count = odataResourceSetAnnotations.Count; - resourceSet.NextPageLink = odataResourceSetAnnotations.NextPageLink; + resourceSet.Count = countOptionCollection.TotalCount; } - else if (writeContext.Request != null) - { - IODataFeature odataFeature = writeContext.Request.ODataFeature(); - resourceSet.NextPageLink = odataFeature.NextLink; - resourceSet.DeltaLink = odataFeature.DeltaLink; + } - long? countValue = odataFeature.TotalCount; - if (countValue.HasValue) - { - resourceSet.Count = countValue.Value; - } - } + return resourceSet; + } + + /// + /// Create the to be written for the given resourceSet instance. + /// + /// The instance representing the resourceSet being written. + /// The EDM type of the resourceSet being written. + /// The serializer context. + /// The created object. + public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable resourceSetInstance, IEdmCollectionTypeReference resourceSetType, + ODataSerializerContext writeContext) + { + if (writeContext == null) + { + throw Error.ArgumentNull(nameof(writeContext)); + } + + ODataResourceSet resourceSet = new ODataResourceSet + { + TypeName = resourceSetType.FullName() + }; + + IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured(); + if (writeContext.NavigationSource != null && structuredType.IsEntity()) + { + ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance); + WriteEntityTypeOperations(resourceSet, resourceSetContext, structuredType, writeContext); + } + + if (writeContext.ExpandedResource == null) + { + // If we have more OData format specific information apply it now, only if we are the root feed. + PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult; + ApplyODataResourceSetAnnotations(resourceSet, odataResourceSetAnnotations, writeContext); } else { @@ -403,6 +487,53 @@ public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstanc return resourceSet; } + private void WriteEntityTypeOperations( + ODataResourceSet resourceSet, + ResourceSetContext resourceSetContext, + IEdmStructuredTypeReference structuredType, + ODataSerializerContext writeContext) + { + IEdmEntityType entityType = structuredType.AsEntity().EntityDefinition(); + IEnumerable operations = writeContext.Model.GetAvailableOperationsBoundToCollection(entityType); + var odataOperations = CreateODataOperations(operations, resourceSetContext, writeContext); + foreach (var odataOperation in odataOperations) + { + ODataAction action = odataOperation as ODataAction; + if (action != null) + { + resourceSet.AddAction(action); + } + else + { + resourceSet.AddFunction((ODataFunction)odataOperation); + } + } + } + + private void ApplyODataResourceSetAnnotations( + ODataResourceSet resourceSet, + PageResult odataResourceSetAnnotations, + ODataSerializerContext writeContext) + { + if (odataResourceSetAnnotations != null) + { + resourceSet.Count = odataResourceSetAnnotations.Count; + resourceSet.NextPageLink = odataResourceSetAnnotations.NextPageLink; + } + else if (writeContext.Request != null) + { + IODataFeature odataFeature = writeContext.Request.ODataFeature(); + resourceSet.NextPageLink = odataFeature.NextLink; + resourceSet.DeltaLink = odataFeature.DeltaLink; + + long? countValue = odataFeature.TotalCount; + if (countValue.HasValue) + { + resourceSet.Count = countValue.Value; + } + } + } + /// /// Creates a function that takes in an object and generates nextlink uri. /// @@ -440,6 +571,45 @@ internal static Func GetNextLinkGenerator(ODataResourceSetBase reso return (obj) => { return null; }; } + /// + /// Creates a function that takes in an object and generates a nextlink uri. + /// + /// The resource set describing a collection of structured objects. + /// The instance representing the resourceSet being written. + /// The serializer context. + /// The function that generates the NextLink from an object. + internal static Func GetNextLinkGenerator(ODataResourceSetBase resourceSet, IAsyncEnumerable resourceSetInstance, ODataSerializerContext writeContext) + { + if (resourceSet != null && resourceSet.NextPageLink != null) + { + Uri defaultUri = resourceSet.NextPageLink; + return (obj) => { return defaultUri; }; + } + + if (writeContext.ExpandedResource == null) + { + if (writeContext.Request != null && writeContext.QueryContext != null) + { + SkipTokenHandler handler = writeContext.QueryContext.GetSkipTokenHandler(); + return (obj) => { + return handler.GenerateNextPageLink(new System.Uri(writeContext.Request.GetEncodedUrl()), + (writeContext.Request.ODataFeature() as ODataFeature).PageSize, obj, writeContext); + }; + } + } + else + { + // nested resourceSet + ITruncatedCollection truncatedCollection = resourceSetInstance as ITruncatedCollection; + if (truncatedCollection != null && truncatedCollection.IsTruncated) + { + return (obj) => { return GetNestedNextPageLink(writeContext, truncatedCollection.PageSize, obj); }; + } + } + + return (obj) => { return null; }; + } + /// /// Creates an to be written for the given operation and the resourceSet instance. /// diff --git a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml index 9317c3ab6..90f8e731c 100644 --- a/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml +++ b/src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml @@ -4309,6 +4309,15 @@ A new . This signature uses types that are AspNetCore-specific. + + + Create a from an and . + + The serializer context. + The instance representing the resourceSet being written. + A new . + This signature uses types that are AspNetCore-specific. + Represents an that serializes instances of objects backed by an . @@ -4914,6 +4923,15 @@ The serializer context. The created object. + + + Create the to be written for the given resourceSet instance. + + The instance representing the resourceSet being written. + The EDM type of the resourceSet being written. + The serializer context. + The created object. + Creates a function that takes in an object and generates nextlink uri. @@ -4923,6 +4941,15 @@ The serializer context. The function that generates the NextLink from an object. + + + Creates a function that takes in an object and generates a nextlink uri. + + The resource set describing a collection of structured objects. + The instance representing the resourceSet being written. + The serializer context. + The function that generates the NextLink from an object. + Creates an to be written for the given operation and the resourceSet instance. diff --git a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt index 1819f7059..f918ce0cf 100644 --- a/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt +++ b/src/Microsoft.AspNetCore.OData/PublicAPI.Unshipped.txt @@ -1842,6 +1842,7 @@ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializ virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.CreateUntypedPropertyValue(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, Microsoft.AspNetCore.OData.Formatter.ResourceContext resourceContext, out Microsoft.OData.Edm.IEdmTypeReference actualType) -> object virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSerializer.WriteDeltaObjectInlineAsync(object graph, Microsoft.OData.Edm.IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.CreateODataOperation(Microsoft.OData.Edm.IEdmOperation operation, Microsoft.AspNetCore.OData.Formatter.ResourceSetContext resourceSetContext, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataOperation +virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.CreateResourceSet(System.Collections.Generic.IAsyncEnumerable resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataResourceSet virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.CreateResourceSet(System.Collections.IEnumerable resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> Microsoft.OData.ODataResourceSet virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteEnumItemAsync(object enumValue, Microsoft.OData.Edm.IEdmTypeReference enumType, Microsoft.OData.Edm.IEdmTypeReference parentSetType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task virtual Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WritePrimitiveItemAsync(object primitiveValue, Microsoft.OData.Edm.IEdmTypeReference primitiveType, Microsoft.OData.Edm.IEdmTypeReference parentSetType, Microsoft.OData.ODataWriter writer, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) -> System.Threading.Tasks.Task diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs new file mode 100644 index 000000000..7633e5a3e --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableController.cs @@ -0,0 +1,128 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.AspNetCore.OData.Routing.Controllers; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +{ + public class CustomersController : ODataController + { + private readonly IAsyncEnumerableContext _context; + + public CustomersController(IAsyncEnumerableContext context) + { + context.Database.EnsureCreated(); + _context = context; + + if (!_context.Customers.Any()) + { + Generate(); + } + } + + [EnableQuery] + [HttpGet("v1/Customers")] + public IAsyncEnumerable CustomersData() + { + IAsyncEnumerable customers = CreateCollectionAsync(); + + return customers; + } + + [EnableQuery] + [HttpGet("odata/Customers")] + public IAsyncEnumerable Get() + { + return _context.Customers.AsAsyncEnumerable(); + } + + public async IAsyncEnumerable CreateCollectionAsync() + { + await Task.Delay(5); + // Yield the items one by one asynchronously + yield return new Customer + { + Id = 1, + Name = "Customer1", + Orders = new List { + new Order { + Name = "Order1", + Price = 25 + }, + new Order { + Name = "Order2", + Price = 75 + } + }, + Address = new Address + { + Name = "City1", + Street = "Street1" + } + }; + + await Task.Delay(5); + + yield return new Customer + { + Id = 2, + Name = "Customer2", + Orders = new List { + new Order { + Name = "Order1", + Price = 35 + }, + new Order { + Name = "Order2", + Price = 65 + } + }, + Address = new Address + { + Name = "City2", + Street = "Street2" + } + }; + } + + public void Generate() + { + for (int i = 1; i <= 3; i++) + { + var customer = new Customer + { + Name = "Customer" + (i + 1) % 2, + Orders = + new List { + new Order { + Name = "Order" + 2*i, + Price = i * 25 + }, + new Order { + Name = "Order" + 2*i+1, + Price = i * 75 + } + }, + Address = new Address + { + Name = "City" + i % 2, + Street = "Street" + i % 2, + } + }; + + _context.Customers.Add(customer); + } + + _context.SaveChanges(); + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs new file mode 100644 index 000000000..8aff1a864 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableDataModel.cs @@ -0,0 +1,54 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +{ + public class IAsyncEnumerableContext : DbContext + { + public IAsyncEnumerableContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(c => c.Address).WithOwner(); + } + } + + public class Customer + { + public int Id { get; set; } + + public string Name { get; set; } + + public Address Address { get; set; } + + public IList Orders { get; set; } + } + + public class Order + { + public int Id { get; set; } + + public string Name { get; set; } + + public int Price { get; set; } + } + + public class Address + { + public string Name { get; set; } + + public string Street { get; set; } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs new file mode 100644 index 000000000..3e76c333a --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableEdmModel.cs @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using Microsoft.OData.Edm; +using Microsoft.OData.ModelBuilder; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +{ + public class IAsyncEnumerableEdmModel + { + public static IEdmModel GetEdmModel() + { + var builder = new ODataConventionModelBuilder(); + builder.EntitySet("Customers"); + builder.EntitySet("Orders"); + IEdmModel model = builder.GetEdmModel(); + return model; + } + } +} diff --git a/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs new file mode 100644 index 000000000..82784d1c3 --- /dev/null +++ b/test/Microsoft.AspNetCore.OData.E2E.Tests/IAsyncEnumerableTests/IAsyncEnumerableTests.cs @@ -0,0 +1,88 @@ +//----------------------------------------------------------------------------- +// +// Copyright (c) .NET Foundation and Contributors. All rights reserved. +// See License.txt in the project root for license information. +// +//------------------------------------------------------------------------------ + +using System.Collections.Generic; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.TestCommon; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Microsoft.AspNetCore.OData.E2E.Tests.IAsyncEnumerableTests +{ + public class IAsyncEnumerableTests : WebODataTestBase + { + public class TestsStartup : TestStartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(opt => opt.UseInMemoryDatabase("IAsyncEnumerableTest")); + + services.ConfigureControllers(typeof(CustomersController)); + + IEdmModel edmModel = IAsyncEnumerableEdmModel.GetEdmModel(); + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("odata", edmModel)); + + services.AddControllers().AddOData(opt => opt.Count().Filter().Expand().Select().OrderBy().SetMaxTop(null) + .AddRouteComponents("v1", edmModel)); + } + } + + public IAsyncEnumerableTests(WebODataTestFixture factory) + : base(factory) + { + } + + [Fact] + public async Task UsingAsAsyncEnumerableWorks() + { + // Arrange + string queryUrl = "odata/Customers"; + var expectedResult = "{\"@odata.context\":\"http://localhost/odata/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City0\",\"Street\":\"Street0\"}},{\"Id\":3,\"Name\":\"Customer0\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultObject = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResult, resultObject); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + Assert.Equal(3, customers.Count); + } + + [Fact] + public async Task UsingAsAsyncEnumerableWorksWithoutEFCore() + { + // Arrange + string queryUrl = "v1/Customers"; + var expectedResult = "{\"@odata.context\":\"http://localhost/v1/$metadata#Customers\",\"value\":[{\"Id\":1,\"Name\":\"Customer1\",\"Address\":{\"Name\":\"City1\",\"Street\":\"Street1\"}},{\"Id\":2,\"Name\":\"Customer2\",\"Address\":{\"Name\":\"City2\",\"Street\":\"Street2\"}}]}"; + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, queryUrl); + request.Headers.Accept.Add(MediaTypeWithQualityHeaderValue.Parse("application/json")); + + // Act + HttpResponseMessage response = await Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var resultObject = await response.Content.ReadAsStringAsync(); + Assert.Equal(expectedResult, resultObject); + + List customers = JToken.Parse(await response.Content.ReadAsStringAsync())["value"].ToObject>(); + Assert.Equal(2, customers.Count); + } + } +} 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 f5c40287a..d7a0a7edb 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 @@ -2287,6 +2287,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSet public ODataResourceSetSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataOperation CreateODataOperation (Microsoft.OData.Edm.IEdmOperation operation, Microsoft.AspNetCore.OData.Formatter.ResourceSetContext resourceSetContext, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + public virtual Microsoft.OData.ODataResourceSet CreateResourceSet (System.Collections.Generic.IAsyncEnumerable`1[[System.Object]] resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataResourceSet CreateResourceSet (System.Collections.IEnumerable resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) [ AsyncStateMachineAttribute(), 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 f5c40287a..d7a0a7edb 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 @@ -2287,6 +2287,7 @@ public class Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSet public ODataResourceSetSerializer (Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializerProvider serializerProvider) public virtual Microsoft.OData.ODataOperation CreateODataOperation (Microsoft.OData.Edm.IEdmOperation operation, Microsoft.AspNetCore.OData.Formatter.ResourceSetContext resourceSetContext, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) + public virtual Microsoft.OData.ODataResourceSet CreateResourceSet (System.Collections.Generic.IAsyncEnumerable`1[[System.Object]] resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) public virtual Microsoft.OData.ODataResourceSet CreateResourceSet (System.Collections.IEnumerable resourceSetInstance, Microsoft.OData.Edm.IEdmCollectionTypeReference resourceSetType, Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext writeContext) [ AsyncStateMachineAttribute(),