diff --git a/docs/bindings.md b/docs/bindings.md
index c110f50..59af2ce 100644
--- a/docs/bindings.md
+++ b/docs/bindings.md
@@ -169,8 +169,11 @@ The conversion should follow the following steps of pseudo-code:
- For binary mode encoding:
- Call `formatter.EncodeBinaryModeEventData` to encode
the data within the CloudEvent
+ - Call `formatter.GetOrInferDataContentType` to obtain the
+ appropriate content type, which may be inferred from the
+ data if the CloudEvent itself does not specify the data content type.
- Populate metadata in the message from the attributes in the
- CloudEvent.
+ CloudEvent and the content type.
- For `To...` methods, return the resulting protocol message.
This must not be null. (`CopyTo...` messages do not return
anything.)
diff --git a/docs/formatters.md b/docs/formatters.md
index 47346bd..416af4d 100644
--- a/docs/formatters.md
+++ b/docs/formatters.md
@@ -105,3 +105,18 @@ The formatter should *not* perform validation on the `CloudEvent`
accepted in `DecodeBinaryModeEventData`, beyond asserting that the
argument is not null. This is typically called by a protocol binding
which should perform validation itself later.
+
+## Data content type inference
+
+Some event formats (e.g. JSON) infer the data content type from the
+actual data provided. In the C# SDK, this is implemented via the
+`CloudEventFormatter` methods `GetOrInferDataContentType` and
+`InferDataContentType`. The first of these is primarily a
+convenience method to be called by bindings; the second may be
+overridden by any formatter implementation that wishes to infer
+a data content type when one is not specified. Implementations *can*
+override `GetOrInferDataContentType` if they have unusual
+requirements, but the default implementation is usually sufficient.
+
+The base implementation of `InferDataContentType` always returns
+null; this means that no content type is inferred by default.
diff --git a/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs b/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs
index d947e0d..2e8ebdd 100644
--- a/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs
+++ b/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs
@@ -168,7 +168,7 @@ namespace CloudNative.CloudEvents.Amqp
break;
case ContentMode.Binary:
bodySection = new Data { Binary = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent)) };
- properties = new Properties { ContentType = cloudEvent.DataContentType };
+ properties = new Properties { ContentType = formatter.GetOrInferDataContentType(cloudEvent) };
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs b/src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs
index b8f45d6..102a871 100644
--- a/src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs
+++ b/src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs
@@ -41,7 +41,7 @@ namespace CloudNative.CloudEvents.AspNetCore
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
- contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
+ contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs
index e8f472b..1099f46 100644
--- a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs
+++ b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs
@@ -30,6 +30,9 @@ namespace CloudNative.CloudEvents
/// Avro record, so the value will have the natural Avro deserialization type for that data (which may
/// not be exactly the same as the type that was serialized).
///
+ ///
+ /// This event formatter does not infer any data content type.
+ ///
///
public class AvroEventFormatter : CloudEventFormatter
{
diff --git a/src/CloudNative.CloudEvents.Kafka/AssemblyInfo.cs b/src/CloudNative.CloudEvents.Kafka/AssemblyInfo.cs
new file mode 100644
index 0000000..3a2e4ff
--- /dev/null
+++ b/src/CloudNative.CloudEvents.Kafka/AssemblyInfo.cs
@@ -0,0 +1,12 @@
+// Copyright 2022 Cloud Native Foundation.
+// Licensed under the Apache 2.0 license.
+// See LICENSE file in the project root for full license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("CloudNative.CloudEvents.UnitTests,PublicKey="
+ + "0024000004800000940000000602000000240000525341310004000001000100e945e99352d0b8"
+ + "90ddb645995bc05ef5a22497d97e78196b9f6148ea33b0c1b219f0c28df523878d1d8c9d042a02"
+ + "f005777461dffe455b348f82b39fcbc64985ef091295c0ad2dcb265c23589e9ce8e48dbe84c8e1"
+ + "7fc37555938b2669aea7575cee288809065aa9dc04dff67ce1dfc5a3167770323c1a2c632f0eb2"
+ + "f8c64acf")]
\ No newline at end of file
diff --git a/src/CloudNative.CloudEvents.Kafka/KafkaExtensions.cs b/src/CloudNative.CloudEvents.Kafka/KafkaExtensions.cs
index e3d0118..f016092 100644
--- a/src/CloudNative.CloudEvents.Kafka/KafkaExtensions.cs
+++ b/src/CloudNative.CloudEvents.Kafka/KafkaExtensions.cs
@@ -20,7 +20,8 @@ namespace CloudNative.CloudEvents.Kafka
{
private const string KafkaHeaderPrefix = "ce_";
- private const string KafkaContentTypeAttributeName = "content-type";
+ // Visible for testing
+ internal const string KafkaContentTypeAttributeName = "content-type";
private const string SpecVersionKafkaHeader = KafkaHeaderPrefix + "specversion";
///
@@ -155,7 +156,7 @@ namespace CloudNative.CloudEvents.Kafka
break;
case ContentMode.Binary:
value = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent));
- contentTypeHeaderValue = cloudEvent.DataContentType;
+ contentTypeHeaderValue = formatter.GetOrInferDataContentType(cloudEvent);
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs
index 46ebea7..476c3a2 100644
--- a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs
+++ b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs
@@ -446,23 +446,33 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
if (cloudEvent.Data is object)
{
- if (cloudEvent.DataContentType is null)
+ if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
{
+ cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
- writer.WriteValue(JsonMediaType);
+ writer.WriteValue(inferredDataContentType);
}
EncodeStructuredModeData(cloudEvent, writer);
}
writer.WriteEndObject();
}
+ ///
+ /// Infers the data content type of a CloudEvent based on its data. This implementation
+ /// infers a data content type of "application/json" for any non-binary data, and performs
+ /// no inference for binary data.
+ ///
+ /// The CloudEvent to infer the data content from. Must not be null.
+ /// The inferred data content type, or null if no inference is performed.
+ protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
+
///
/// Encodes structured mode data within a CloudEvent, writing it to the specified .
///
///
///
/// This implementation follows the rules listed in the class remarks. Override this method
- /// to provide more specialized behavior, writing only or
+ /// to provide more specialized behavior, usually writing only or
/// properties.
///
///
@@ -480,7 +490,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
}
else
{
- ContentType dataContentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
+ ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent));
if (IsJsonMediaType(dataContentType.MediaType))
{
writer.WritePropertyName(DataPropertyName);
diff --git a/src/CloudNative.CloudEvents.Protobuf/ProtobufEventFormatter.cs b/src/CloudNative.CloudEvents.Protobuf/ProtobufEventFormatter.cs
index 9d041cc..1266fc4 100644
--- a/src/CloudNative.CloudEvents.Protobuf/ProtobufEventFormatter.cs
+++ b/src/CloudNative.CloudEvents.Protobuf/ProtobufEventFormatter.cs
@@ -59,6 +59,9 @@ namespace CloudNative.CloudEvents.Protobuf
/// a string, otherwise it is left as a byte array. Derived classes can specialize this behavior by overriding
/// .
///
+ ///
+ /// This event formatter does not infer any data content type.
+ ///
///
public class ProtobufEventFormatter : CloudEventFormatter
{
diff --git a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs
index 41420b9..3aad7ed 100644
--- a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs
+++ b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs
@@ -457,16 +457,26 @@ namespace CloudNative.CloudEvents.SystemTextJson
if (cloudEvent.Data is object)
{
- if (cloudEvent.DataContentType is null)
+ if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
{
+ cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
- writer.WriteStringValue(JsonMediaType);
+ writer.WriteStringValue(inferredDataContentType);
}
EncodeStructuredModeData(cloudEvent, writer);
}
writer.WriteEndObject();
}
+ ///
+ /// Infers the data content type of a CloudEvent based on its data. This implementation
+ /// infers a data content type of "application/json" for any non-binary data, and performs
+ /// no inference for binary data.
+ ///
+ /// The CloudEvent to infer the data content from. Must not be null.
+ /// The inferred data content type, or null if no inference is performed.
+ protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
+
///
/// Encodes structured mode data within a CloudEvent, writing it to the specified .
///
diff --git a/src/CloudNative.CloudEvents/CloudEventFormatter.cs b/src/CloudNative.CloudEvents/CloudEventFormatter.cs
index 4f4bf23..0cb3e96 100644
--- a/src/CloudNative.CloudEvents/CloudEventFormatter.cs
+++ b/src/CloudNative.CloudEvents/CloudEventFormatter.cs
@@ -164,5 +164,41 @@ namespace CloudNative.CloudEvents
/// Must not be null (on return).
/// The batch representation of the CloudEvent.
public abstract ReadOnlyMemory EncodeBatchModeMessage(IEnumerable cloudEvents, out ContentType contentType);
+
+ ///
+ /// Determines the effective data content type of the given CloudEvent.
+ ///
+ ///
+ ///
+ /// This implementation validates that is not null,
+ /// returns the existing if that's not null,
+ /// and otherwise returns null if is null or
+ /// delegates to to infer the data content type
+ /// from the actual data.
+ ///
+ ///
+ /// Derived classes may override this if additional information is needed from the CloudEvent
+ /// in order to determine the effective data content type, but most cases can be handled by
+ /// simply overriding .
+ ///
+ ///
+ /// The CloudEvent to get or infer the data content type from. Must not be null.
+ /// The data content type of the CloudEvent, or null for no data content type.
+ public virtual string? GetOrInferDataContentType(CloudEvent cloudEvent)
+ {
+ Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
+ return cloudEvent.DataContentType is string dataContentType ? dataContentType
+ : cloudEvent.Data is not object data ? null
+ : InferDataContentType(data);
+ }
+
+ ///
+ /// Infers the effective data content type based on the actual data. This base implementation
+ /// always returns null, but derived classes may override this method to effectively provide
+ /// a default data content type based on the in-memory data type.
+ ///
+ /// The data within a CloudEvent. Should not be null.
+ /// The inferred content type, or null if no content type is inferred.
+ protected virtual string? InferDataContentType(object data) => null;
}
}
\ No newline at end of file
diff --git a/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj b/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj
index d574eb5..9339a50 100644
--- a/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj
+++ b/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj
@@ -3,7 +3,7 @@
netstandard2.0;netstandard2.1
CNCF CloudEvents SDK
- 8.0
+ latest
enable
cloudnative;cloudevents;events
diff --git a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs
index acd975b..374bd96 100644
--- a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs
+++ b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs
@@ -273,7 +273,7 @@ namespace CloudNative.CloudEvents.Http
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
- contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
+ contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs
index 61ad2c8..7e83936 100644
--- a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs
+++ b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs
@@ -41,7 +41,7 @@ namespace CloudNative.CloudEvents.Http
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
- contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
+ contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs
index 7b3a0cc..5c2d3f3 100644
--- a/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs
+++ b/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs
@@ -43,7 +43,7 @@ namespace CloudNative.CloudEvents.Http
break;
case ContentMode.Binary:
content = formatter.EncodeBinaryModeEventData(cloudEvent);
- contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
+ contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
break;
default:
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
diff --git a/test/CloudNative.CloudEvents.UnitTests/Amqp/AmqpTest.cs b/test/CloudNative.CloudEvents.UnitTests/Amqp/AmqpTest.cs
index d94d270..db14bd4 100644
--- a/test/CloudNative.CloudEvents.UnitTests/Amqp/AmqpTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/Amqp/AmqpTest.cs
@@ -5,6 +5,7 @@
using Amqp;
using Amqp.Framing;
using CloudNative.CloudEvents.NewtonsoftJson;
+using Newtonsoft.Json.Linq;
using System;
using System.Net.Mime;
using System.Text;
@@ -18,8 +19,7 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
[Fact]
public void AmqpStructuredMessageTest()
{
- // the AMQPNetLite library is factored such
- // that we don't need to do a wire test here
+ // The AMQPNetLite library is factored such that we don't need to do a wire test here.
var cloudEvent = new CloudEvent
{
Type = "com.github.pull.create",
@@ -55,9 +55,7 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
[Fact]
public void AmqpBinaryMessageTest()
{
- // the AMQPNetLite library is factored such
- // that we don't need to do a wire test here
-
+ // The AMQPNetLite library is factored such that we don't need to do a wire test here.
var cloudEvent = new CloudEvent
{
Type = "com.github.pull.create",
@@ -89,6 +87,18 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
Assert.Equal("value", (string?)receivedCloudEvent["comexampleextension1"]);
}
+ [Fact]
+ public void BinaryMode_ContentTypeCanBeInferredByFormatter()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = "plain text"
+ }.PopulateRequiredAttributes();
+
+ var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter());
+ Assert.Equal("application/json", message.Properties.ContentType);
+ }
+
[Fact]
public void AmqpNormalizesTimestampsToUtc()
{
diff --git a/test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpResponseExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpResponseExtensionsTest.cs
index d99b68b..b511a22 100644
--- a/test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpResponseExtensionsTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpResponseExtensionsTest.cs
@@ -40,9 +40,22 @@ namespace CloudNative.CloudEvents.AspNetCore.UnitTests
// There's no data content type header; the content type itself is used for that.
Assert.False(response.Headers.ContainsKey("ce-datacontenttype"));
}
-
+
[Fact]
- public async Task CopyToHttpResponseAsync_ContentButNoContentType()
+ public async Task CopyToHttpResponseAsync_BinaryDataButNoDataContentType()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = new byte[10],
+ }.PopulateRequiredAttributes();
+ var formatter = new JsonEventFormatter();
+ var response = CreateResponse();
+ // The formatter doesn't infer the data content type for binary data.
+ await Assert.ThrowsAsync(() => cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter));
+ }
+
+ [Fact]
+ public async Task CopyToHttpResponseAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
{
var cloudEvent = new CloudEvent
{
@@ -50,7 +63,11 @@ namespace CloudNative.CloudEvents.AspNetCore.UnitTests
}.PopulateRequiredAttributes();
var formatter = new JsonEventFormatter();
var response = CreateResponse();
- await Assert.ThrowsAsync(() => cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter));
+ await cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter);
+ var content = GetContent(response);
+ // The formatter infers that it should encode the string as a JSON value (so it includes the double quotes)
+ Assert.Equal("application/json", response.ContentType);
+ Assert.Equal("\"plain text\"", Encoding.UTF8.GetString(content.Span));
}
[Fact]
diff --git a/test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterTest.cs
new file mode 100644
index 0000000..c05900c
--- /dev/null
+++ b/test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterTest.cs
@@ -0,0 +1,87 @@
+// Copyright 2022 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.Collections.Generic;
+using System.IO;
+using System.Net.Mime;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace CloudNative.CloudEvents.UnitTests
+{
+ public class CloudEventFormatterTest
+ {
+ [Fact]
+ public void GetOrInferDataContentType_NullCloudEvent()
+ {
+ var formatter = new ContentTypeInferringFormatter();
+ Assert.Throws(() => formatter.GetOrInferDataContentType(null!));
+ }
+
+ [Fact]
+ public void GetOrInferDataContentType_NoDataOrDataContentType()
+ {
+ var formatter = new ContentTypeInferringFormatter();
+ var cloudEvent = new CloudEvent();
+ Assert.Null(formatter.GetOrInferDataContentType(cloudEvent));
+ }
+
+ [Fact]
+ public void GetOrInferDataContentType_HasDataContentType()
+ {
+ var formatter = new ContentTypeInferringFormatter();
+ var cloudEvent = new CloudEvent { DataContentType = "test/pass" };
+ Assert.Equal(cloudEvent.DataContentType, formatter.GetOrInferDataContentType(cloudEvent));
+ }
+
+ [Fact]
+ public void GetOrInferDataContentType_HasDataButNoContentType_OverriddenInferDataContentType()
+ {
+ var formatter = new ContentTypeInferringFormatter();
+ var cloudEvent = new CloudEvent { Data = "some-data" };
+ Assert.Equal("test/some-data", formatter.GetOrInferDataContentType(cloudEvent));
+ }
+
+ [Fact]
+ public void GetOrInferDataContentType_DataButNoContentType_DefaultInferDataContentType()
+ {
+ var formatter = new ThrowingEventFormatter();
+ var cloudEvent = new CloudEvent { Data = "some-data" };
+ Assert.Null(formatter.GetOrInferDataContentType(cloudEvent));
+ }
+
+ private class ContentTypeInferringFormatter : ThrowingEventFormatter
+ {
+ protected override string? InferDataContentType(object data) => $"test/{data}";
+ }
+
+ ///
+ /// Event formatter that overrides every abstract method to throw NotImplementedException.
+ /// This can be derived from (and further overridden) to easily test concrete methods
+ /// in CloudEventFormatter itself.
+ ///
+ private class ThrowingEventFormatter : CloudEventFormatter
+ {
+ public override IReadOnlyList DecodeBatchModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) =>
+ throw new NotImplementedException();
+
+ public override void DecodeBinaryModeEventData(ReadOnlyMemory body, CloudEvent cloudEvent) =>
+ throw new NotImplementedException();
+
+ public override CloudEvent DecodeStructuredModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) =>
+ throw new NotImplementedException();
+
+ public override ReadOnlyMemory EncodeBatchModeMessage(IEnumerable cloudEvents, out ContentType contentType) =>
+ throw new NotImplementedException();
+
+ public override ReadOnlyMemory EncodeBinaryModeEventData(CloudEvent cloudEvent) =>
+ throw new NotImplementedException();
+
+ public override ReadOnlyMemory EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) =>
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs
index fb512e8..3707306 100644
--- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpClientExtensionsTest.cs
@@ -397,7 +397,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
// It should be okay to not set a DataContentType if there's no data...
// but what if there's a data value which is an empty string, empty byte array or empty stream?
[Fact]
- public void NoContentType_NoContent()
+ public void NoDataContentType_NoData()
{
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
@@ -405,10 +405,22 @@ namespace CloudNative.CloudEvents.Http.UnitTests
}
[Fact]
- public void NoContentType_WithContent()
+ public void NoDataContentType_ContentTypeInferredFromFormatter()
{
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
- cloudEvent.Data = "Some text";
+ // The JSON event format infers application/json for non-binary data
+ cloudEvent.Data = new { Name = "xyz" };
+ var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
+ var expectedContentType = new MediaTypeHeaderValue("application/json");
+ Assert.Equal(expectedContentType, content.Headers.ContentType);
+ }
+
+ [Fact]
+ public void NoDataContentType_NoContentTypeInferredFromFormatter()
+ {
+ var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
+ // The JSON event format does not infer a data content type for binary data
+ cloudEvent.Data = new byte[10];
var exception = Assert.Throws(() => cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter()));
Assert.StartsWith(Strings.ErrorContentTypeUnspecified, exception.Message);
}
diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs
index bfb8654..0e3b91f 100644
--- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpListenerExtensionsTest.cs
@@ -207,15 +207,30 @@ namespace CloudNative.CloudEvents.Http.UnitTests
}
[Fact]
- public async Task CopyToListenerResponseAsync_ContentButNoContentType()
+ public async Task CopyToListenerResponseAsync_BinaryDataButNoDataContentType()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = new byte[10],
+ }.PopulateRequiredAttributes();
+ var formatter = new JsonEventFormatter();
+ await GetResponseAsync(
+ async context => await Assert.ThrowsAsync(() => cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter)));
+ }
+
+ [Fact]
+ public async Task CopyToListenerResponseAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
{
var cloudEvent = new CloudEvent
{
Data = "plain text",
}.PopulateRequiredAttributes();
var formatter = new JsonEventFormatter();
- await GetResponseAsync(
- async context => await Assert.ThrowsAsync(() => cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter)));
+ var response = await GetResponseAsync(
+ async context => await cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter));
+ var content = response.Content;
+ Assert.Equal("application/json", content.Headers.ContentType.MediaType);
+ Assert.Equal("\"plain text\"", await content.ReadAsStringAsync());
}
[Fact]
diff --git a/test/CloudNative.CloudEvents.UnitTests/Http/HttpWebExtensionsTest.cs b/test/CloudNative.CloudEvents.UnitTests/Http/HttpWebExtensionsTest.cs
index 9496c78..e6ab1ea 100644
--- a/test/CloudNative.CloudEvents.UnitTests/Http/HttpWebExtensionsTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/Http/HttpWebExtensionsTest.cs
@@ -108,6 +108,32 @@ namespace CloudNative.CloudEvents.Http.UnitTests
Assert.True(result.StatusCode == HttpStatusCode.NoContent, content);
}
+ [Fact]
+ public async Task CopyToHttpWebRequestAsync_BinaryDataButNoDataContentType()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = new byte[10],
+ }.PopulateRequiredAttributes();
+ HttpWebRequest httpWebRequest = WebRequest.CreateHttp(ListenerAddress + "ep");
+ httpWebRequest.Method = "POST";
+ await Assert.ThrowsAsync(
+ async () => await cloudEvent.CopyToHttpWebRequestAsync(httpWebRequest, ContentMode.Binary, new JsonEventFormatter()));
+ }
+
+ [Fact]
+ public async Task CopyToHttpWebRequestAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = "plain text",
+ }.PopulateRequiredAttributes();
+ HttpWebRequest httpWebRequest = WebRequest.CreateHttp(ListenerAddress + "ep");
+ httpWebRequest.Method = "POST";
+ await cloudEvent.CopyToHttpWebRequestAsync(httpWebRequest, ContentMode.Binary, new JsonEventFormatter());
+ Assert.Equal("application/json", httpWebRequest.ContentType);
+ }
+
[Fact]
public async Task CopyToHttpWebRequestAsync_Batch()
{
diff --git a/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs b/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs
index 77c704d..e50aff5 100644
--- a/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/Kafka/KafkaTest.cs
@@ -8,6 +8,7 @@ using Confluent.Kafka;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
+using System.Linq;
using System.Net.Mime;
using System.Text;
using Xunit;
@@ -61,8 +62,8 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
Assert.True(message.IsCloudEvent());
- // using serialization to create fully independent copy thus simulating message transport
- // real transport will work in a similar way
+ // Using serialization to create fully independent copy thus simulating message transport.
+ // The real transport will work in a similar way.
var serialized = JsonConvert.SerializeObject(message, new HeaderConverter());
var messageCopy = JsonConvert.DeserializeObject>(serialized, new HeadersConverter(), new HeaderConverter())!;
@@ -104,8 +105,8 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
var message = cloudEvent.ToKafkaMessage(ContentMode.Binary, new JsonEventFormatter());
Assert.True(message.IsCloudEvent());
- // using serialization to create fully independent copy thus simulating message transport
- // real transport will work in a similar way
+ // Using serialization to create fully independent copy thus simulating message transport.
+ // The real transport will work in a similar way.
var serialized = JsonConvert.SerializeObject(message, new HeaderConverter());
var settings = new JsonSerializerSettings
{
@@ -128,6 +129,20 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
Assert.Equal("value", (string?)receivedCloudEvent["comexampleextension1"]);
}
+ [Fact]
+ public void ContentTypeCanBeInferredByFormatter()
+ {
+ var cloudEvent = new CloudEvent
+ {
+ Data = "plain text"
+ }.PopulateRequiredAttributes();
+
+ var message = cloudEvent.ToKafkaMessage(ContentMode.Binary, new JsonEventFormatter());
+ var contentTypeHeader = message.Headers.Single(h => h.Key == KafkaExtensions.KafkaContentTypeAttributeName);
+ var contentTypeValue = Encoding.UTF8.GetString(contentTypeHeader.GetValueBytes());
+ Assert.Equal("application/json", contentTypeValue);
+ }
+
private class HeadersConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
diff --git a/test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/JsonEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/JsonEventFormatterTest.cs
index 223e890..1901a90 100644
--- a/test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/JsonEventFormatterTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/JsonEventFormatterTest.cs
@@ -1036,23 +1036,22 @@ namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests
}
[Fact]
- public void EncodeStructured_BinaryData_DefaultContentTypeToApplicationJson()
+ public void EncodeStructured_BinaryData_DefaultContentTypeIsNotImplied()
{
var cloudEvent = new CloudEvent
{
Data = SampleBinaryData
}.PopulateRequiredAttributes();
- // While it's odd for a CloudEvent to have binary data but no data content type,
- // the spec says the data should be placed in data_base64, and the content type should
- // default to application/json. (Checking in https://github.com/cloudevents/spec/issues/933)
+ // If a CloudEvent to have binary data but no data content type,
+ // the spec says the data should be placed in data_base64, but the content type
+ // should *not* be defaulted to application/json, as clarified in https://github.com/cloudevents/spec/issues/933
var encoded = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType);
Assert.Equal("application/cloudevents+json; charset=utf-8", contentType.ToString());
JObject obj = ParseJson(encoded);
var asserter = new JTokenAsserter
{
{ "data_base64", JTokenType.String, SampleBinaryDataBase64 },
- { "datacontenttype", JTokenType.String, "application/json" },
{ "id", JTokenType.String, "test-id" },
{ "source", JTokenType.String, "//test" },
{ "specversion", JTokenType.String, "1.0" },
diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs
index 7ab00b2..b417ead 100644
--- a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs
+++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs
@@ -1043,23 +1043,22 @@ namespace CloudNative.CloudEvents.SystemTextJson.UnitTests
}
[Fact]
- public void EncodeStructured_BinaryData_DefaultContentTypeToApplicationJson()
+ public void EncodeStructured_BinaryData_DefaultContentTypeIsNotImplied()
{
var cloudEvent = new CloudEvent
{
Data = SampleBinaryData
}.PopulateRequiredAttributes();
- // While it's odd for a CloudEvent to have binary data but no data content type,
- // the spec says the data should be placed in data_base64, and the content type should
- // default to application/json. (Checking in https://github.com/cloudevents/spec/issues/933)
+ // If a CloudEvent to have binary data but no data content type,
+ // the spec says the data should be placed in data_base64, but the content type
+ // should *not* be defaulted to application/json, as clarified in https://github.com/cloudevents/spec/issues/933
var encoded = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType);
Assert.Equal("application/cloudevents+json; charset=utf-8", contentType.ToString());
JsonElement obj = ParseJson(encoded);
var asserter = new JsonElementAsserter
{
{ "data_base64", JsonValueKind.String, SampleBinaryDataBase64 },
- { "datacontenttype", JsonValueKind.String, "application/json" },
{ "id", JsonValueKind.String, "test-id" },
{ "source", JsonValueKind.String, "//test" },
{ "specversion", JsonValueKind.String, "1.0" },