Skip to content

Commit cdc77e0

Browse files
support IAsyncenumerable
1 parent e8f1e24 commit cdc77e0

12 files changed

+540
-26
lines changed

src/Microsoft.AspNetCore.OData/Common/TypeHelper.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ Type collectionInterface
240240
.Union(new[] { clrType })
241241
.FirstOrDefault(
242242
t => t.IsGenericType
243-
&& t.GetGenericTypeDefinition() == typeof(IEnumerable<>));
243+
&& (t.GetGenericTypeDefinition() == typeof(IEnumerable<>)
244+
|| t.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)));
244245

245246
if (collectionInterface != null)
246247
{

src/Microsoft.AspNetCore.OData/Edm/DefaultODataTypeMapper.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,20 @@ private IEdmType GetEdmType(IEdmModel edmModel, Type clrType, bool testCollectio
222222
}
223223

224224
Type enumerableOfT = ExtractGenericInterface(clrType, typeof(IEnumerable<>));
225-
if (enumerableOfT != null)
225+
Type asyncEnumerableOfT = ExtractGenericInterface(clrType, typeof(IAsyncEnumerable<>));
226+
227+
if (enumerableOfT != null || asyncEnumerableOfT != null)
226228
{
227-
Type elementClrType = enumerableOfT.GetGenericArguments()[0];
229+
Type elementClrType = null;
230+
231+
if (enumerableOfT != null)
232+
{
233+
elementClrType = enumerableOfT.GetGenericArguments()[0];
234+
}
235+
else
236+
{
237+
elementClrType = asyncEnumerableOfT.GetGenericArguments()[0];
238+
}
228239

229240
// IEnumerable<SelectExpandWrapper<T>> is a collection of T.
230241
if (elementClrType.IsSelectExpandWrapper(out entityType))

src/Microsoft.AspNetCore.OData/Formatter/ResourceSetContext.cs

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//------------------------------------------------------------------------------
77

88
using System.Collections;
9+
using System.Collections.Generic;
910
using Microsoft.AspNetCore.Http;
1011
using Microsoft.AspNetCore.OData.Extensions;
1112
using Microsoft.AspNetCore.OData.Formatter.Serialization;
@@ -61,5 +62,25 @@ internal static ResourceSetContext Create(ODataSerializerContext writeContext, I
6162

6263
return resourceSetContext;
6364
}
65+
66+
/// <summary>
67+
/// Create a <see cref="ResourceSetContext"/> from an <see cref="ODataSerializerContext"/> and <see cref="IAsyncEnumerable{T}"/>.
68+
/// </summary>
69+
/// <param name="resourceSetInstance">The instance representing the resourceSet being written.</param>
70+
/// <param name="writeContext">The serializer context.</param>
71+
/// <returns>A new <see cref="ResourceSetContext"/>.</returns>
72+
/// <remarks>This signature uses types that are AspNetCore-specific.</remarks>
73+
internal static ResourceSetContext Create(ODataSerializerContext writeContext, IAsyncEnumerable<object> resourceSetInstance)
74+
{
75+
ResourceSetContext resourceSetContext = new ResourceSetContext
76+
{
77+
Request = writeContext.Request,
78+
EntitySetBase = writeContext.NavigationSource as IEdmEntitySetBase,
79+
// Url = writeContext.Url,
80+
ResourceSetInstance = resourceSetInstance
81+
};
82+
83+
return resourceSetContext;
84+
}
6485
}
6586
}

src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs

+189-23
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
using Microsoft.Extensions.Options;
2626
using Microsoft.OData;
2727
using Microsoft.OData.Edm;
28-
using Microsoft.OData.ModelBuilder.Capabilities.V1;
2928
using Microsoft.OData.UriParser;
3029

3130
namespace Microsoft.AspNetCore.OData.Formatter.Serialization
@@ -97,14 +96,30 @@ public override async Task WriteObjectInlineAsync(object graph, IEdmTypeReferenc
9796
throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet));
9897
}
9998

100-
IEnumerable enumerable = graph as IEnumerable; // Data to serialize
101-
if (enumerable == null)
99+
if (writeContext.Type != null && writeContext.Type.IsGenericType && writeContext.Type?.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>))
102100
{
103-
throw new SerializationException(
104-
Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
101+
IAsyncEnumerable<object> asyncEnumerable = graph as IAsyncEnumerable<object>; // Data to serialize
102+
103+
if (asyncEnumerable == null)
104+
{
105+
throw new SerializationException(
106+
Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
107+
}
108+
109+
await WriteResourceSetAsync(asyncEnumerable, expectedType, writer, writeContext).ConfigureAwait(false);
105110
}
111+
else
112+
{
113+
IEnumerable enumerable = graph as IEnumerable; // Data to serialize
106114

107-
await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false);
115+
if (enumerable == null)
116+
{
117+
throw new SerializationException(
118+
Error.Format(SRResources.CannotWriteType, GetType().Name, graph.GetType().FullName));
119+
}
120+
121+
await WriteResourceSetAsync(enumerable, expectedType, writer, writeContext).ConfigureAwait(false);
122+
}
108123
}
109124

110125
private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReference resourceSetType, ODataWriter writer,
@@ -150,28 +165,74 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen
150165
resourceSet.NextPageLink = null;
151166
await writer.WriteStartAsync(resourceSet).ConfigureAwait(false);
152167
object lastResource = null;
168+
153169
foreach (object item in enumerable)
154170
{
155171
lastResource = item;
156-
if (item == null || item is NullEdmComplexObject)
157-
{
158-
if (elementType.IsEntity())
159-
{
160-
throw new SerializationException(SRResources.NullElementInCollection);
161-
}
162172

163-
// for null complex element, it can be serialized as "null" in the collection.
164-
await writer.WriteStartAsync(resource: null).ConfigureAwait(false);
165-
await writer.WriteEndAsync().ConfigureAwait(false);
166-
}
167-
else if (isUntypedCollection)
168-
{
169-
await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false);
170-
}
171-
else
173+
await WriteResourceSetAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext);
174+
}
175+
176+
// Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet),
177+
// the next page link will be written early in a manner not compatible with odata.streaming=true. Instead, if
178+
// the next page link is not set when calling WriteStart(resourceSet) but is instead set later on that resourceSet
179+
// object before calling WriteEnd(), the next page link will be written at the end, as required for
180+
// odata.streaming=true support.
181+
182+
resourceSet.NextPageLink = nextLinkGenerator(lastResource);
183+
184+
await writer.WriteEndAsync().ConfigureAwait(false);
185+
}
186+
187+
private async Task WriteResourceSetAsync(IAsyncEnumerable<object> asyncEnumerable, IEdmTypeReference resourceSetType, ODataWriter writer,
188+
ODataSerializerContext writeContext)
189+
{
190+
Contract.Assert(writer != null);
191+
Contract.Assert(writeContext != null);
192+
Contract.Assert(asyncEnumerable != null);
193+
Contract.Assert(resourceSetType != null);
194+
195+
IEdmStructuredTypeReference elementType = GetResourceType(resourceSetType);
196+
ODataResourceSet resourceSet = CreateResourceSet(asyncEnumerable, resourceSetType.AsCollection(), writeContext);
197+
198+
Func<object, Uri> nextLinkGenerator = GetNextLinkGenerator(resourceSet, null, writeContext);
199+
200+
if (resourceSet == null)
201+
{
202+
throw new SerializationException(Error.Format(SRResources.CannotSerializerNull, ResourceSet));
203+
}
204+
205+
IEdmEntitySetBase entitySet = writeContext.NavigationSource as IEdmEntitySetBase;
206+
if (entitySet == null)
207+
{
208+
resourceSet.SetSerializationInfo(new ODataResourceSerializationInfo
172209
{
173-
await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false);
174-
}
210+
IsFromCollection = true,
211+
NavigationSourceEntityTypeName = elementType.FullName(),
212+
NavigationSourceKind = EdmNavigationSourceKind.UnknownEntitySet,
213+
NavigationSourceName = null
214+
});
215+
}
216+
217+
IODataEdmTypeSerializer resourceSerializer = SerializerProvider.GetEdmTypeSerializer(elementType);
218+
if (resourceSerializer == null)
219+
{
220+
throw new SerializationException(
221+
Error.Format(SRResources.TypeCannotBeSerialized, elementType.FullName()));
222+
}
223+
224+
bool isUntypedCollection = resourceSetType.IsCollectionUntyped();
225+
226+
// set the nextpagelink to null to support JSON odata.streaming.
227+
resourceSet.NextPageLink = null;
228+
await writer.WriteStartAsync(resourceSet).ConfigureAwait(false);
229+
object lastResource = null;
230+
231+
await foreach (object item in asyncEnumerable)
232+
{
233+
lastResource = item;
234+
235+
await WriteResourceSetAsync(item, elementType, isUntypedCollection, resourceSetType, writer, resourceSerializer, writeContext);
175236
}
176237

177238
// Subtle and surprising behavior: If the NextPageLink property is set before calling WriteStart(resourceSet),
@@ -185,6 +246,36 @@ private async Task WriteResourceSetAsync(IEnumerable enumerable, IEdmTypeReferen
185246
await writer.WriteEndAsync().ConfigureAwait(false);
186247
}
187248

249+
private async Task WriteResourceSetAsync(
250+
object item,
251+
IEdmStructuredTypeReference elementType,
252+
bool isUntypedCollection,
253+
IEdmTypeReference resourceSetType,
254+
ODataWriter writer,
255+
IODataEdmTypeSerializer resourceSerializer,
256+
ODataSerializerContext writeContext)
257+
{
258+
if (item == null || item is NullEdmComplexObject)
259+
{
260+
if (elementType.IsEntity())
261+
{
262+
throw new SerializationException(SRResources.NullElementInCollection);
263+
}
264+
265+
// for null complex element, it can be serialized as "null" in the collection.
266+
await writer.WriteStartAsync(resource: null).ConfigureAwait(false);
267+
await writer.WriteEndAsync().ConfigureAwait(false);
268+
}
269+
else if (isUntypedCollection)
270+
{
271+
await WriteUntypedResourceSetItemAsync(item, resourceSetType, writer, writeContext).ConfigureAwait(false);
272+
}
273+
else
274+
{
275+
await resourceSerializer.WriteObjectInlineAsync(item, elementType, writer, writeContext).ConfigureAwait(false);
276+
}
277+
}
278+
188279
private async Task WriteUntypedResourceSetItemAsync(object item, IEdmTypeReference parentSetType, ODataWriter writer, ODataSerializerContext writeContext)
189280
{
190281
Contract.Assert(item != null); // "item == null" is handled.
@@ -403,6 +494,81 @@ public virtual ODataResourceSet CreateResourceSet(IEnumerable resourceSetInstanc
403494
return resourceSet;
404495
}
405496

497+
/// <summary>
498+
/// Create the <see cref="ODataResourceSet"/> to be written for the given resourceSet instance.
499+
/// </summary>
500+
/// <param name="resourceSetInstance">The instance representing the resourceSet being written.</param>
501+
/// <param name="resourceSetType">The EDM type of the resourceSet being written.</param>
502+
/// <param name="writeContext">The serializer context.</param>
503+
/// <returns>The created <see cref="ODataResourceSet"/> object.</returns>
504+
public virtual ODataResourceSet CreateResourceSet(IAsyncEnumerable<object> resourceSetInstance, IEdmCollectionTypeReference resourceSetType,
505+
ODataSerializerContext writeContext)
506+
{
507+
if (writeContext == null)
508+
{
509+
throw Error.ArgumentNull(nameof(writeContext));
510+
}
511+
512+
ODataResourceSet resourceSet = new ODataResourceSet
513+
{
514+
TypeName = resourceSetType.FullName()
515+
};
516+
517+
IEdmStructuredTypeReference structuredType = GetResourceType(resourceSetType).AsStructured();
518+
if (writeContext.NavigationSource != null && structuredType.IsEntity())
519+
{
520+
ResourceSetContext resourceSetContext = ResourceSetContext.Create(writeContext, resourceSetInstance);
521+
IEdmEntityType entityType = structuredType.AsEntity().EntityDefinition();
522+
var operations = writeContext.Model.GetAvailableOperationsBoundToCollection(entityType);
523+
var odataOperations = CreateODataOperations(operations, resourceSetContext, writeContext);
524+
foreach (var odataOperation in odataOperations)
525+
{
526+
ODataAction action = odataOperation as ODataAction;
527+
if (action != null)
528+
{
529+
resourceSet.AddAction(action);
530+
}
531+
else
532+
{
533+
resourceSet.AddFunction((ODataFunction)odataOperation);
534+
}
535+
}
536+
}
537+
538+
if (writeContext.ExpandedResource == null)
539+
{
540+
// If we have more OData format specific information apply it now, only if we are the root feed.
541+
PageResult odataResourceSetAnnotations = resourceSetInstance as PageResult;
542+
if (odataResourceSetAnnotations != null)
543+
{
544+
resourceSet.Count = odataResourceSetAnnotations.Count;
545+
resourceSet.NextPageLink = odataResourceSetAnnotations.NextPageLink;
546+
}
547+
else if (writeContext.Request != null)
548+
{
549+
IODataFeature odataFeature = writeContext.Request.ODataFeature();
550+
resourceSet.NextPageLink = odataFeature.NextLink;
551+
resourceSet.DeltaLink = odataFeature.DeltaLink;
552+
553+
long? countValue = odataFeature.TotalCount;
554+
if (countValue.HasValue)
555+
{
556+
resourceSet.Count = countValue.Value;
557+
}
558+
}
559+
}
560+
else
561+
{
562+
ICountOptionCollection countOptionCollection = resourceSetInstance as ICountOptionCollection;
563+
if (countOptionCollection != null && countOptionCollection.TotalCount != null)
564+
{
565+
resourceSet.Count = countOptionCollection.TotalCount;
566+
}
567+
}
568+
569+
return resourceSet;
570+
}
571+
406572
/// <summary>
407573
/// Creates a function that takes in an object and generates nextlink uri.
408574
/// </summary>

src/Microsoft.AspNetCore.OData/Microsoft.AspNetCore.OData.xml

+18
Original file line numberDiff line numberDiff line change
@@ -4309,6 +4309,15 @@
43094309
<returns>A new <see cref="T:Microsoft.AspNetCore.OData.Formatter.ResourceSetContext"/>.</returns>
43104310
<remarks>This signature uses types that are AspNetCore-specific.</remarks>
43114311
</member>
4312+
<member name="M:Microsoft.AspNetCore.OData.Formatter.ResourceSetContext.Create(Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext,System.Collections.Generic.IAsyncEnumerable{System.Object})">
4313+
<summary>
4314+
Create a <see cref="T:Microsoft.AspNetCore.OData.Formatter.ResourceSetContext"/> from an <see cref="T:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext"/> and <see cref="T:System.Collections.Generic.IAsyncEnumerable`1"/>.
4315+
</summary>
4316+
<param name="resourceSetInstance">The instance representing the resourceSet being written.</param>
4317+
<param name="writeContext">The serializer context.</param>
4318+
<returns>A new <see cref="T:Microsoft.AspNetCore.OData.Formatter.ResourceSetContext"/>.</returns>
4319+
<remarks>This signature uses types that are AspNetCore-specific.</remarks>
4320+
</member>
43124321
<member name="T:Microsoft.AspNetCore.OData.Formatter.Serialization.IODataEdmTypeSerializer">
43134322
<summary>
43144323
Represents an <see cref="T:Microsoft.AspNetCore.OData.Formatter.Serialization.IODataSerializer"/> that serializes instances of objects backed by an <see cref="T:Microsoft.OData.Edm.IEdmType"/>.
@@ -4914,6 +4923,15 @@
49144923
<param name="writeContext">The serializer context.</param>
49154924
<returns>The created <see cref="T:Microsoft.OData.ODataResourceSet"/> object.</returns>
49164925
</member>
4926+
<member name="M:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.CreateResourceSet(System.Collections.Generic.IAsyncEnumerable{System.Object},Microsoft.OData.Edm.IEdmCollectionTypeReference,Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext)">
4927+
<summary>
4928+
Create the <see cref="T:Microsoft.OData.ODataResourceSet"/> to be written for the given resourceSet instance.
4929+
</summary>
4930+
<param name="resourceSetInstance">The instance representing the resourceSet being written.</param>
4931+
<param name="resourceSetType">The EDM type of the resourceSet being written.</param>
4932+
<param name="writeContext">The serializer context.</param>
4933+
<returns>The created <see cref="T:Microsoft.OData.ODataResourceSet"/> object.</returns>
4934+
</member>
49174935
<member name="M:Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.GetNextLinkGenerator(Microsoft.OData.ODataResourceSetBase,System.Collections.IEnumerable,Microsoft.AspNetCore.OData.Formatter.Serialization.ODataSerializerContext)">
49184936
<summary>
49194937
Creates a function that takes in an object and generates nextlink uri.

0 commit comments

Comments
 (0)