Skip to content

Commit

Permalink
Support write resource with stream property and Support query stream …
Browse files Browse the repository at this point in the history
…property (#2401)

* Support write resource with stream property and Support query stream property

* For stream property, we should use ODataStreamPropertyInfo

* Address the comments

* Address the comments

* resolve the comments

* Resolve the comments

* fix the failing public api
  • Loading branch information
xuzhg authored Feb 17, 2021
1 parent f04cd27 commit 1d6ddf9
Show file tree
Hide file tree
Showing 17 changed files with 507 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

namespace Microsoft.AspNet.OData.Formatter
{
/// <summary>
/// Media type mapping that associates requests with stream property.
/// </summary>
public partial class ODataStreamMediaTypeMapping
{
/// <summary>
/// Initializes a new instance of the <see cref="ODataStreamMediaTypeMapping"/> class.
/// </summary>
public ODataStreamMediaTypeMapping()
: base("application/octet-stream")
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.OData.Common;
using Microsoft.AspNet.OData.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OData;
using Microsoft.OData.Edm;
Expand Down Expand Up @@ -120,8 +121,9 @@ internal ODataSerializer GetODataPayloadSerializerImpl(Type type, Func<IEdmModel
{
bool isCountRequest = path != null && path.Segments.LastOrDefault() is CountSegment;
bool isRawValueRequest = path != null && path.Segments.LastOrDefault() is ValueSegment;
bool isStreamRequest = path.IsStreamPropertyPath();

if (((edmType.IsPrimitive() || edmType.IsEnum()) && isRawValueRequest) || isCountRequest)
if (((edmType.IsPrimitive() || edmType.IsEnum()) && isRawValueRequest) || isCountRequest || isStreamRequest)
{
return _rootContainer.GetRequiredService<ODataRawValueSerializer>();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ private void WriteResource(object graph, ODataWriter writer, ODataSerializerCont
else
{
writer.WriteStart(resource);
WriteStreamProperties(selectExpandNode, resourceContext, writer);
WriteComplexProperties(selectExpandNode, resourceContext, writer);
WriteDynamicComplexProperties(resourceContext, writer);
WriteNavigationLinks(selectExpandNode, resourceContext, writer);
Expand Down Expand Up @@ -633,6 +634,7 @@ await writer.WriteEntityReferenceLinkAsync(new ODataEntityReferenceLink
else
{
await writer.WriteStartAsync(resource);
await WriteStreamPropertiesAsync(selectExpandNode, resourceContext, writer);
await WriteComplexPropertiesAsync(selectExpandNode, resourceContext, writer);
await WriteDynamicComplexPropertiesAsync(resourceContext, writer);
await WriteNavigationLinksAsync(selectExpandNode, resourceContext, writer);
Expand Down Expand Up @@ -1269,6 +1271,58 @@ private async Task WriteComplexPropertiesAsync(SelectExpandNode selectExpandNode
}
}

private void WriteStreamProperties(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
{
Contract.Assert(selectExpandNode != null);
Contract.Assert(resourceContext != null);
Contract.Assert(writer != null);

if (selectExpandNode.SelectedStructuralProperties != null)
{
IEnumerable<IEdmStructuralProperty> structuralProperties = selectExpandNode.SelectedStructuralProperties;

foreach (IEdmStructuralProperty structuralProperty in structuralProperties)
{
if (structuralProperty.Type != null && structuralProperty.Type.IsStream())
{
ODataStreamPropertyInfo property = CreateStreamProperty(structuralProperty, resourceContext);

if (property != null)
{
writer.WriteStart(property);
writer.WriteEnd();
}
}
}
}
}

private async Task WriteStreamPropertiesAsync(SelectExpandNode selectExpandNode, ResourceContext resourceContext, ODataWriter writer)
{
Contract.Assert(selectExpandNode != null);
Contract.Assert(resourceContext != null);
Contract.Assert(writer != null);

if (selectExpandNode.SelectedStructuralProperties != null)
{
IEnumerable<IEdmStructuralProperty> structuralProperties = selectExpandNode.SelectedStructuralProperties;

foreach (IEdmStructuralProperty structuralProperty in structuralProperties)
{
if (structuralProperty.Type != null && structuralProperty.Type.IsStream())
{
ODataStreamPropertyInfo property = CreateStreamProperty(structuralProperty, resourceContext);

if (property != null)
{
await writer.WriteStartAsync(property);
await writer.WriteEndAsync();
}
}
}
}
}

private IEnumerable<KeyValuePair<IEdmStructuralProperty, PathSelectItem>> GetPropertiesToWrite(SelectExpandNode selectExpandNode, ResourceContext resourceContext)
{
IDictionary<IEdmStructuralProperty, PathSelectItem> complexProperties = selectExpandNode.SelectedComplexTypeProperties;
Expand Down Expand Up @@ -1561,6 +1615,12 @@ private IEnumerable<ODataProperty> CreateStructuralPropertyBag(SelectExpandNode

foreach (IEdmStructuralProperty structuralProperty in structuralProperties)
{
if (structuralProperty.Type != null && structuralProperty.Type.IsStream())
{
// skip the stream property, the stream property is written in its own logic
continue;
}

ODataProperty property = CreateStructuralProperty(structuralProperty, resourceContext);
if (property != null)
{
Expand All @@ -1572,6 +1632,54 @@ private IEnumerable<ODataProperty> CreateStructuralPropertyBag(SelectExpandNode
return properties;
}

/// <summary>
/// Creates the <see cref="ODataStreamPropertyInfo"/> to be written for the given stream property.
/// </summary>
/// <param name="structuralProperty">The EDM structural property being written.</param>
/// <param name="resourceContext">The context for the entity instance being written.</param>
/// <returns>The <see cref="ODataStreamPropertyInfo"/> to write.</returns>
internal virtual ODataStreamPropertyInfo CreateStreamProperty(IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
{
if (structuralProperty == null)
{
throw Error.ArgumentNull("structuralProperty");
}

if (resourceContext == null)
{
throw Error.ArgumentNull("resourceContext");
}

if (structuralProperty.Type == null || !structuralProperty.Type.IsStream())
{
return null;
}

if (resourceContext.SerializerContext.MetadataLevel != ODataMetadataLevel.FullMetadata)
{
return null;
}

// TODO: we need to return ODataStreamReferenceValue if
// 1) If we have the EditLink link builder
// 2) If we have the ReadLink link builder
// 3) If we have the Core.AcceptableMediaTypes annotation associated with the Stream property

// We need a way for the user to specify a mediatype for an instance of a stream property.
// If specified, we should explicitly write the streamreferencevalue and not let ODL fill it in.

// Although the mediatype is represented as an instance annotation in JSON, it's really control information.
// So we shouldn't use instance annotations to tell us the media type, but have a separate way to specify the media type.
// Perhaps we define an interface (and stream wrapper class that derives from stream and implements the interface) that exposes a MediaType property.
// If the stream property implements this interface, and it specifies a media-type other than application/octet-stream, we explicitly create and write a StreamReferenceValue with that media type.
// We could also use this type to expose properties for things like ReadLink and WriteLink(and even ETag)
// that the user could specify to something other than the default convention
// if they wanted to provide custom routes for reading/writing the stream values or custom ETag values for the stream.

// So far, let's return null and let OData.lib to calculate the ODataStreamReferenceValue by conventions.
return null;
}

/// <summary>
/// Creates the <see cref="ODataProperty"/> to be written for the given entity and the structural property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Formatter\IETagHandler.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataBinaryValueMediaTypeMapping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataCountMediaTypeMapping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataStreamMediaTypeMapping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataEnumValueMediaTypeMapping.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataInputFormatterHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Formatter\ODataModelBinderConverter.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,41 @@ namespace Microsoft.AspNet.OData.Routing
/// </summary>
internal static class ODataPathSegmentExtensions
{
/// <summary>
/// Gets a value indicating whether the given path is a stream property path.
/// </summary>
/// <param name="path">The given odata path.</param>
/// <returns>true/false</returns>
public static bool IsStreamPropertyPath(this ODataPath path)
{
if (path == null)
{
return false;
}

PropertySegment propertySegment = path.Segments.LastOrDefault() as PropertySegment;
if (propertySegment == null)
{
return false;
}

IEdmTypeReference propertyType = propertySegment.Property.Type;
if (propertyType == null)
{
return false;
}

// Edm.Stream, or a type definition whose underlying type is Edm.Stream,
// cannot be used in collections or for non-binding parameters to functions or actions.
// So, we don't need to test it but leave the codes here for awareness.
//if (propertyType.IsCollection())
//{
// propertyType = propertyType.AsCollection().ElementType();
//}

return propertyType.IsStream();
}

public static string TranslatePathTemplateSegment(this PathTemplateSegment pathTemplatesegment, out string value)
{
if (pathTemplatesegment == null)
Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.AspNet.OData/Formatter/ODataMediaTypeFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,15 @@ public override Task WriteToStreamAsync(Type type, object value, Stream writeStr

try
{
if (typeof(Stream).IsAssignableFrom(type))
{
// Ideally, it should go into the "ODataRawValueSerializer",
// However, OData lib doesn't provide the method to overwrite/copyto stream
// So, Here's the workaround
Stream objStream = value as Stream;
return CopyStreamAsync(objStream, writeStream);
}

HttpConfiguration configuration = Request.GetConfiguration();
if (configuration == null)
{
Expand Down Expand Up @@ -335,6 +344,16 @@ public override Task WriteToStreamAsync(Type type, object value, Stream writeStr
}
}

private static async Task CopyStreamAsync(Stream source, Stream destination)
{
if (source != null)
{
await source.CopyToAsync(destination);
}

await destination.FlushAsync();
}

// To factor out request, just pass in a function to get base address. We'd get rid of
// BaseAddressFactory and request.
private Uri GetBaseAddressInternal(HttpRequestMessage request)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ private static ODataMediaTypeFormatter CreateRawValue()
formatter.MediaTypeMappings.Add(new ODataEnumValueMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataBinaryValueMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataCountMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataStreamMediaTypeMapping());
return formatter;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Net.Http;
using System.Net.Http.Formatting;
using Microsoft.AspNet.OData.Common;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Routing;

namespace Microsoft.AspNet.OData.Formatter
{
/// <summary>
/// Media type mapping that associates requests with stream property.
/// </summary>
public partial class ODataStreamMediaTypeMapping : MediaTypeMapping
{
/// <inheritdoc/>
public override double TryMatchMediaType(HttpRequestMessage request)
{
if (request == null)
{
throw Error.ArgumentNull("request");
}

return request.ODataProperties().Path.IsStreamPropertyPath() ? 1 : 0;
}
}
}
1 change: 1 addition & 0 deletions src/Microsoft.AspNet.OData/Microsoft.AspNet.OData.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
<Compile Include="Batch\ODataBatchHttpRequestMessageExtensions.cs" />
<Compile Include="ETagMessageHandler.cs" />
<Compile Include="Formatter\ODataCountMediaTypeMapping.cs" />
<Compile Include="Formatter\ODataStreamMediaTypeMapping.cs" />
<Compile Include="Formatter\Serialization\DefaultODataSerializerProvider.cs" />
<Compile Include="Formatter\Serialization\ODataSerializerContext.cs" />
<Compile Include="Formatter\Serialization\ODataSerializerProvider.cs" />
Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.AspNetCore.OData/Formatter/ODataOutputFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context,
}

HttpResponse response = context.HttpContext.Response;
if (typeof(Stream).IsAssignableFrom(type))
{
// Ideally, it should go into the "ODataRawValueSerializer",
// However, OData lib doesn't provide the method to overwrite/copyto stream
// So, Here's the workaround
Stream objStream = context.Object as Stream;
return CopyStreamAsync(objStream, response);
}

Uri baseAddress = GetBaseAddressInternal(request);
MediaTypeHeaderValue contentType = GetContentType(response.Headers[HeaderNames.ContentType].FirstOrDefault());

Expand Down Expand Up @@ -249,6 +258,16 @@ public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context,
getODataSerializerContext);
}

private static async Task CopyStreamAsync(Stream source, HttpResponse response)
{
if (source != null)
{
await source.CopyToAsync(response.Body);
}

await response.Body.FlushAsync();
}

/// <summary>
/// Internal method used for selecting the base address to be used with OData uris.
/// If the consumer has provided a delegate for overriding our default implementation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ private static ODataOutputFormatter CreateRawValue()
formatter.MediaTypeMappings.Add(new ODataPrimitiveValueMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataEnumValueMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataCountMediaTypeMapping());
formatter.MediaTypeMappings.Add(new ODataStreamMediaTypeMapping());
return formatter;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using Microsoft.AspNet.OData.Common;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNetCore.Http;

namespace Microsoft.AspNet.OData.Formatter
{
/// <summary>
/// Media type mapping that associates requests with stream property.
/// </summary>
/// <remarks>This class derives from a platform-specific class.</remarks>
public partial class ODataStreamMediaTypeMapping : MediaTypeMapping
{
/// <inheritdoc/>
public override double TryMatchMediaType(HttpRequest request)
{
if (request == null)
{
throw Error.ArgumentNull("request");
}

return request.ODataFeature().Path.IsStreamPropertyPath() ? 1 : 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ODataUriResolverExtensionTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)OpenComplexTypeTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)OpenEntityTypeTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ODataStreamPropertyTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PageResultOfTTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PerRequestActionValueBinderTest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PerRequestContentNegotiatorTest.cs" />
Expand Down
Loading

0 comments on commit 1d6ddf9

Please sign in to comment.