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!")
|
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
|
||||||
|
|
|
@ -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
|
[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);
|
||||||
|
|
|
@ -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 =>
|
||||||
|
|
|
@ -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"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue