Remove unnecessary inheritance for HttpContent
Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
parent
72a5dc31f4
commit
8e56fe32b9
|
@ -41,7 +41,7 @@ namespace HttpSend
|
|||
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();
|
||||
// your application remains in charge of adding any further headers or
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ namespace CloudNative.CloudEvents.IntegrationTests.AspNetCore
|
|||
[expectedExtensionKey] = expectedExtensionValue
|
||||
};
|
||||
|
||||
var httpContent = new CloudEventHttpContent(cloudEvent, contentMode, new JsonEventFormatter());
|
||||
var httpContent = cloudEvent.ToHttpContent(contentMode, new JsonEventFormatter());
|
||||
|
||||
// Act
|
||||
var result = await _factory.CreateClient().PostAsync("/api/events/receive", httpContent);
|
||||
|
|
|
@ -95,7 +95,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
PendingRequests.TryAdd(ctx, context =>
|
||||
|
@ -215,7 +215,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
};
|
||||
|
||||
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);
|
||||
|
||||
PendingRequests.TryAdd(ctx, context =>
|
||||
|
|
|
@ -6,17 +6,18 @@ using CloudNative.CloudEvents.NewtonsoftJson;
|
|||
using System;
|
||||
using System.Net.Http.Headers;
|
||||
using Xunit;
|
||||
using static CloudNative.CloudEvents.UnitTests.TestHelpers;
|
||||
|
||||
namespace CloudNative.CloudEvents.Http.UnitTests
|
||||
{
|
||||
public class CloudEventHttpContentTest
|
||||
public class HttpContentExtensionsTest
|
||||
{
|
||||
[Fact]
|
||||
public void ContentType_FromCloudEvent_BinaryMode()
|
||||
{
|
||||
var cloudEvent = CreateEmptyCloudEvent();
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
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");
|
||||
Assert.Equal(expectedContentType, content.Headers.ContentType);
|
||||
}
|
||||
|
@ -27,26 +28,18 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
[Fact]
|
||||
public void NoContentType_NoContent()
|
||||
{
|
||||
var cloudEvent = CreateEmptyCloudEvent();
|
||||
var content = new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter());
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
|
||||
Assert.Null(content.Headers.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoContentType_WithContent()
|
||||
{
|
||||
var cloudEvent = CreateEmptyCloudEvent();
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
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);
|
||||
}
|
||||
|
||||
private static CloudEvent CreateEmptyCloudEvent() =>
|
||||
new CloudEvent
|
||||
{
|
||||
Type = "type",
|
||||
Source = new Uri("https://source"),
|
||||
Id = "id"
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue