Remove unnecessary inheritance for HttpContent

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2021-02-26 08:38:47 +00:00 committed by Jon Skeet
parent 72a5dc31f4
commit 8e56fe32b9
6 changed files with 85 additions and 129 deletions

View File

@ -41,7 +41,7 @@ namespace HttpSend
Data = JsonConvert.SerializeObject("hey there!") Data = JsonConvert.SerializeObject("hey there!")
}; };
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Structured, new JsonEventFormatter()); var content = cloudEvent.ToHttpContent(ContentMode.Structured, new JsonEventFormatter());
var httpClient = new HttpClient(); var httpClient = new HttpClient();
// your application remains in charge of adding any further headers or // your application remains in charge of adding any further headers or

View File

@ -1,110 +0,0 @@
// Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Mime;
using System.Threading.Tasks;
namespace CloudNative.CloudEvents.Http
{
// TODO: Do we really need to have a subclass here? How about a static factory method instead?
/// <summary>
/// This class is for use with `HttpClient` and constructs content and headers for
/// a HTTP request from a CloudEvent.
/// </summary>
public class CloudEventHttpContent : HttpContent
{
private readonly InnerByteArrayContent inner;
/// <summary>
/// Constructor
/// </summary>
/// <param name="cloudEvent">CloudEvent</param>
/// <param name="contentMode">Content mode. Structured or binary.</param>
/// <param name="formatter">Event formatter</param>
public CloudEventHttpContent(CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter)
{
byte[] content;
ContentType contentType;
switch (contentMode)
{
case ContentMode.Structured:
content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType);
// This is optional in the specification, but can be useful.
MapHeaders(cloudEvent, includeDataContentType: true);
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
MapHeaders(cloudEvent, includeDataContentType: false);
break;
default:
throw new ArgumentException($"Unsupported content mode: {contentMode}");
}
inner = new InnerByteArrayContent(content);
if (contentType is object)
{
Headers.ContentType = contentType.ToMediaTypeHeaderValue();
}
else if (content.Length != 0)
{
throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent));
}
}
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) =>
inner.InnerSerializeToStreamAsync(stream, context);
protected override bool TryComputeLength(out long length) =>
inner.InnerTryComputeLength(out length);
private void MapHeaders(CloudEvent cloudEvent, bool includeDataContentType)
{
Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId));
foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes())
{
CloudEventAttribute attribute = attributeAndValue.Key;
string headerName = HttpUtilities.HttpHeaderPrefix + attribute.Name;
object value = attributeAndValue.Value;
// Only map the data content type attribute to a header if we've been asked to
if (attribute == cloudEvent.SpecVersion.DataContentTypeAttribute && !includeDataContentType)
{
continue;
}
else
{
string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value));
Headers.Add(headerName, headerValue);
}
}
}
/// <summary>
/// This inner class is required to get around the 'protected'-ness of the
/// override functions of HttpContent for enabling containment/delegation
/// </summary>
class InnerByteArrayContent : ByteArrayContent
{
public InnerByteArrayContent(byte[] content) : base(content)
{
}
public Task InnerSerializeToStreamAsync(Stream stream, TransportContext context)
{
return base.SerializeToStreamAsync(stream, context);
}
public bool InnerTryComputeLength(out long length)
{
return base.TryComputeLength(out length);
}
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright 2021 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
using System.Net.Http;
using System.Net.Mime;
namespace CloudNative.CloudEvents.Http
{
/// <summary>
/// CloudEvent extension methods related to <see cref="HttpContent"/>.
/// </summary>
public static class HttpContentExtensions
{
/// <summary>
/// Converts a CloudEvent to <see cref="HttpContent"/>.
/// </summary>
/// <param name="cloudEvent">CloudEvent</param>
/// <param name="contentMode">Content mode. Structured or binary.</param>
/// <param name="formatter">Event formatter</param>
public static HttpContent ToHttpContent(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter)
{
byte[] content;
// The content type to include in the ContentType header - may be the data content type, or the formatter's content type.
ContentType contentType;
switch (contentMode)
{
case ContentMode.Structured:
content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType);
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
}
var ret = new ByteArrayContent(content);
if (contentType is object)
{
ret.Headers.ContentType = contentType.ToMediaTypeHeaderValue();
}
else if (content.Length != 0)
{
throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent));
}
// Map headers in either mode.
// Including the headers in structured mode is optional in the spec (as they're already within the body) but
// can be useful.
ret.Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId));
foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes())
{
CloudEventAttribute attribute = attributeAndValue.Key;
string headerName = HttpUtilities.HttpHeaderPrefix + attribute.Name;
object value = attributeAndValue.Value;
// Skip the data content type attribute in binary mode, because it's already in the content type header.
if (attribute == cloudEvent.SpecVersion.DataContentTypeAttribute && contentMode == ContentMode.Binary)
{
continue;
}
else
{
string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value));
ret.Headers.Add(headerName, headerValue);
}
}
return ret;
}
}
}

View File

@ -40,7 +40,7 @@ namespace CloudNative.CloudEvents.IntegrationTests.AspNetCore
[expectedExtensionKey] = expectedExtensionValue [expectedExtensionKey] = expectedExtensionValue
}; };
var httpContent = new CloudEventHttpContent(cloudEvent, contentMode, new JsonEventFormatter()); var httpContent = cloudEvent.ToHttpContent(contentMode, new JsonEventFormatter());
// Act // Act
var result = await _factory.CreateClient().PostAsync("/api/events/receive", httpContent); var result = await _factory.CreateClient().PostAsync("/api/events/receive", httpContent);

View File

@ -95,7 +95,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
}; };
string ctx = Guid.NewGuid().ToString(); string ctx = Guid.NewGuid().ToString();
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter()); var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
content.Headers.Add(TestContextHeader, ctx); content.Headers.Add(TestContextHeader, ctx);
PendingRequests.TryAdd(ctx, context => PendingRequests.TryAdd(ctx, context =>
@ -215,7 +215,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
}; };
string ctx = Guid.NewGuid().ToString(); string ctx = Guid.NewGuid().ToString();
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Structured, new JsonEventFormatter()); var content = cloudEvent.ToHttpContent(ContentMode.Structured, new JsonEventFormatter());
content.Headers.Add(TestContextHeader, ctx); content.Headers.Add(TestContextHeader, ctx);
PendingRequests.TryAdd(ctx, context => PendingRequests.TryAdd(ctx, context =>

View File

@ -6,17 +6,18 @@ using CloudNative.CloudEvents.NewtonsoftJson;
using System; using System;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Xunit; using Xunit;
using static CloudNative.CloudEvents.UnitTests.TestHelpers;
namespace CloudNative.CloudEvents.Http.UnitTests namespace CloudNative.CloudEvents.Http.UnitTests
{ {
public class CloudEventHttpContentTest public class HttpContentExtensionsTest
{ {
[Fact] [Fact]
public void ContentType_FromCloudEvent_BinaryMode() public void ContentType_FromCloudEvent_BinaryMode()
{ {
var cloudEvent = CreateEmptyCloudEvent(); var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
cloudEvent.DataContentType = "text/plain"; cloudEvent.DataContentType = "text/plain";
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter()); var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
var expectedContentType = new MediaTypeHeaderValue("text/plain"); var expectedContentType = new MediaTypeHeaderValue("text/plain");
Assert.Equal(expectedContentType, content.Headers.ContentType); Assert.Equal(expectedContentType, content.Headers.ContentType);
} }
@ -27,26 +28,18 @@ namespace CloudNative.CloudEvents.Http.UnitTests
[Fact] [Fact]
public void NoContentType_NoContent() public void NoContentType_NoContent()
{ {
var cloudEvent = CreateEmptyCloudEvent(); var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter()); var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
Assert.Null(content.Headers.ContentType); Assert.Null(content.Headers.ContentType);
} }
[Fact] [Fact]
public void NoContentType_WithContent() public void NoContentType_WithContent()
{ {
var cloudEvent = CreateEmptyCloudEvent(); var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
cloudEvent.Data = "Some text"; cloudEvent.Data = "Some text";
var exception = Assert.Throws<ArgumentException>(() => new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter())); var exception = Assert.Throws<ArgumentException>(() => cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter()));
Assert.StartsWith(Strings.ErrorContentTypeUnspecified, exception.Message); Assert.StartsWith(Strings.ErrorContentTypeUnspecified, exception.Message);
} }
private static CloudEvent CreateEmptyCloudEvent() =>
new CloudEvent
{
Type = "type",
Source = new Uri("https://source"),
Id = "id"
};
} }
} }