diff --git a/samples/HttpSend/Program.cs b/samples/HttpSend/Program.cs index 9f02f34..fa07f41 100644 --- a/samples/HttpSend/Program.cs +++ b/samples/HttpSend/Program.cs @@ -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 diff --git a/src/CloudNative.CloudEvents/Http/CloudEventHttpContent.cs b/src/CloudNative.CloudEvents/Http/CloudEventHttpContent.cs deleted file mode 100644 index 467a4d0..0000000 --- a/src/CloudNative.CloudEvents/Http/CloudEventHttpContent.cs +++ /dev/null @@ -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? - - /// - /// This class is for use with `HttpClient` and constructs content and headers for - /// a HTTP request from a CloudEvent. - /// - public class CloudEventHttpContent : HttpContent - { - private readonly InnerByteArrayContent inner; - - /// - /// Constructor - /// - /// CloudEvent - /// Content mode. Structured or binary. - /// Event formatter - 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); - } - } - } - - /// - /// This inner class is required to get around the 'protected'-ness of the - /// override functions of HttpContent for enabling containment/delegation - /// - 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); - } - } - } -} \ No newline at end of file diff --git a/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs new file mode 100644 index 0000000..8b33ca5 --- /dev/null +++ b/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs @@ -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 +{ + /// + /// CloudEvent extension methods related to . + /// + public static class HttpContentExtensions + { + /// + /// Converts a CloudEvent to . + /// + /// CloudEvent + /// Content mode. Structured or binary. + /// Event formatter + 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; + } + } +} diff --git a/test/CloudNative.CloudEvents.IntegrationTests/AspNetCore/CloudEventControllerTests.cs b/test/CloudNative.CloudEvents.IntegrationTests/AspNetCore/CloudEventControllerTests.cs index 7e8336d..ca8df59 100644 --- a/test/CloudNative.CloudEvents.IntegrationTests/AspNetCore/CloudEventControllerTests.cs +++ b/test/CloudNative.CloudEvents.IntegrationTests/AspNetCore/CloudEventControllerTests.cs @@ -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); diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs index 7e77aca..b841b9a 100644 --- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs @@ -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 => diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/CloudEventHttpContentTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpContentExtensionsTest.cs similarity index 63% rename from test/CloudNative.CloudEvents.UnitTests/Http/CloudEventHttpContentTest.cs rename to test/CloudNative.CloudEvents.UnitTests/Http/HttpContentExtensionsTest.cs index 9e4b8b9..8ca606c 100644 --- a/test/CloudNative.CloudEvents.UnitTests/Http/CloudEventHttpContentTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpContentExtensionsTest.cs @@ -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(() => new CloudEventHttpContent(cloudEvent, ContentMode.Binary, new JsonEventFormatter())); + var exception = Assert.Throws(() => 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" - }; } }