Support data content type inference

- New methods in CloudEventFormatter to support inference
- JsonEventFormatter infers a content type of application/json for non-binary data
- All transports use the inferred content type when formatting in binary mode
- Documentation for both formatters and bindings has been updated

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2022-02-11 11:50:03 +00:00 committed by Jon Skeet
parent 8d3da7d1e8
commit b1f29cf25b
24 changed files with 316 additions and 43 deletions

View File

@ -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.)

View File

@ -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.

View File

@ -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}");

View File

@ -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}");

View File

@ -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).
/// </para>
/// <para>
/// This event formatter does not infer any data content type.
/// </para>
/// </remarks>
public class AvroEventFormatter : CloudEventFormatter
{

View File

@ -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")]

View File

@ -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";
/// <summary>
@ -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}");

View File

@ -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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
/// <summary>
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="JsonWriter"/>.
/// </summary>
/// <remarks>
/// <para>
/// This implementation follows the rules listed in the class remarks. Override this method
/// to provide more specialized behavior, writing only <see cref="DataPropertyName"/> or
/// to provide more specialized behavior, usually writing only <see cref="DataPropertyName"/> or
/// <see cref="DataBase64PropertyName"/> properties.
/// </para>
/// </remarks>
@ -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);

View File

@ -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
/// <see cref="DecodeBinaryModeEventData(ReadOnlyMemory{byte}, CloudEvent)"/>.
/// </para>
/// <para>
/// This event formatter does not infer any data content type.
/// </para>
/// </remarks>
public class ProtobufEventFormatter : CloudEventFormatter
{

View File

@ -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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
/// <summary>
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="Utf8JsonWriter"/>.
/// </summary>

View File

@ -164,5 +164,41 @@ namespace CloudNative.CloudEvents
/// Must not be null (on return).</param>
/// <returns>The batch representation of the CloudEvent.</returns>
public abstract ReadOnlyMemory<byte> EncodeBatchModeMessage(IEnumerable<CloudEvent> cloudEvents, out ContentType contentType);
/// <summary>
/// Determines the effective data content type of the given CloudEvent.
/// </summary>
/// <remarks>
/// <para>
/// This implementation validates that <paramref name="cloudEvent"/> is not null,
/// returns the existing <see cref="CloudEvent.DataContentType"/> if that's not null,
/// and otherwise returns null if <see cref="CloudEvent.Data"/> is null or
/// delegates to <see cref="InferDataContentType(object)"/> to infer the data content type
/// from the actual data.
/// </para>
/// <para>
/// 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 <see cref="InferDataContentType(object)"/>.
/// </para>
/// </remarks>
/// <param name="cloudEvent">The CloudEvent to get or infer the data content type from. Must not be null.</param>
/// <returns>The data content type of the CloudEvent, or null for no data content type.</returns>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="data">The data within a CloudEvent. Should not be null.</param>
/// <returns>The inferred content type, or null if no content type is inferred.</returns>
protected virtual string? InferDataContentType(object data) => null;
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<Description>CNCF CloudEvents SDK</Description>
<LangVersion>8.0</LangVersion>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PackageTags>cloudnative;cloudevents;events</PackageTags>
</PropertyGroup>

View File

@ -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}");

View File

@ -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}");

View File

@ -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}");

View File

@ -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()
{

View File

@ -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<ArgumentException>(() => 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<ArgumentException>(() => 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]

View File

@ -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<ArgumentNullException>(() => 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}";
}
/// <summary>
/// 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.
/// </summary>
private class ThrowingEventFormatter : CloudEventFormatter
{
public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
throw new NotImplementedException();
public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudEvent cloudEvent) =>
throw new NotImplementedException();
public override CloudEvent DecodeStructuredModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
throw new NotImplementedException();
public override ReadOnlyMemory<byte> EncodeBatchModeMessage(IEnumerable<CloudEvent> cloudEvents, out ContentType contentType) =>
throw new NotImplementedException();
public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent) =>
throw new NotImplementedException();
public override ReadOnlyMemory<byte> EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) =>
throw new NotImplementedException();
}
}
}

View File

@ -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<ArgumentException>(() => cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter()));
Assert.StartsWith(Strings.ErrorContentTypeUnspecified, exception.Message);
}

View File

@ -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<ArgumentException>(() => 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<ArgumentException>(() => 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]

View File

@ -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<ArgumentException>(
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()
{

View File

@ -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<Message<string?, byte[]>>(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)

View File

@ -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" },

View File

@ -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" },