Skip to content

Commit b1f29cf

Browse files
committed
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 <[email protected]>
1 parent 8d3da7d commit b1f29cf

File tree

24 files changed

+315
-42
lines changed

24 files changed

+315
-42
lines changed

docs/bindings.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,11 @@ The conversion should follow the following steps of pseudo-code:
169169
- For binary mode encoding:
170170
- Call `formatter.EncodeBinaryModeEventData` to encode
171171
the data within the CloudEvent
172+
- Call `formatter.GetOrInferDataContentType` to obtain the
173+
appropriate content type, which may be inferred from the
174+
data if the CloudEvent itself does not specify the data content type.
172175
- Populate metadata in the message from the attributes in the
173-
CloudEvent.
176+
CloudEvent and the content type.
174177
- For `To...` methods, return the resulting protocol message.
175178
This must not be null. (`CopyTo...` messages do not return
176179
anything.)

docs/formatters.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,18 @@ The formatter should *not* perform validation on the `CloudEvent`
105105
accepted in `DecodeBinaryModeEventData`, beyond asserting that the
106106
argument is not null. This is typically called by a protocol binding
107107
which should perform validation itself later.
108+
109+
## Data content type inference
110+
111+
Some event formats (e.g. JSON) infer the data content type from the
112+
actual data provided. In the C# SDK, this is implemented via the
113+
`CloudEventFormatter` methods `GetOrInferDataContentType` and
114+
`InferDataContentType`. The first of these is primarily a
115+
convenience method to be called by bindings; the second may be
116+
overridden by any formatter implementation that wishes to infer
117+
a data content type when one is not specified. Implementations *can*
118+
override `GetOrInferDataContentType` if they have unusual
119+
requirements, but the default implementation is usually sufficient.
120+
121+
The base implementation of `InferDataContentType` always returns
122+
null; this means that no content type is inferred by default.

src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ public static Message ToAmqpMessage(this CloudEvent cloudEvent, ContentMode cont
168168
break;
169169
case ContentMode.Binary:
170170
bodySection = new Data { Binary = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent)) };
171-
properties = new Properties { ContentType = cloudEvent.DataContentType };
171+
properties = new Properties { ContentType = formatter.GetOrInferDataContentType(cloudEvent) };
172172
break;
173173
default:
174174
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");

src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public static async Task CopyToHttpResponseAsync(this CloudEvent cloudEvent, Htt
4141
break;
4242
case ContentMode.Binary:
4343
content = formatter.EncodeBinaryModeEventData(cloudEvent);
44-
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
44+
contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
4545
break;
4646
default:
4747
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");

src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ namespace CloudNative.CloudEvents
3030
/// Avro record, so the value will have the natural Avro deserialization type for that data (which may
3131
/// not be exactly the same as the type that was serialized).
3232
/// </para>
33+
/// <para>
34+
/// This event formatter does not infer any data content type.
35+
/// </para>
3336
/// </remarks>
3437
public class AvroEventFormatter : CloudEventFormatter
3538
{
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2022 Cloud Native Foundation.
2+
// Licensed under the Apache 2.0 license.
3+
// See LICENSE file in the project root for full license information.
4+
5+
using System.Runtime.CompilerServices;
6+
7+
[assembly: InternalsVisibleTo("CloudNative.CloudEvents.UnitTests,PublicKey="
8+
+ "0024000004800000940000000602000000240000525341310004000001000100e945e99352d0b8"
9+
+ "90ddb645995bc05ef5a22497d97e78196b9f6148ea33b0c1b219f0c28df523878d1d8c9d042a02"
10+
+ "f005777461dffe455b348f82b39fcbc64985ef091295c0ad2dcb265c23589e9ce8e48dbe84c8e1"
11+
+ "7fc37555938b2669aea7575cee288809065aa9dc04dff67ce1dfc5a3167770323c1a2c632f0eb2"
12+
+ "f8c64acf")]

src/CloudNative.CloudEvents.Kafka/KafkaExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public static class KafkaExtensions
2020
{
2121
private const string KafkaHeaderPrefix = "ce_";
2222

23-
private const string KafkaContentTypeAttributeName = "content-type";
23+
// Visible for testing
24+
internal const string KafkaContentTypeAttributeName = "content-type";
2425
private const string SpecVersionKafkaHeader = KafkaHeaderPrefix + "specversion";
2526

2627
/// <summary>
@@ -155,7 +156,7 @@ private static void InitPartitioningKey(Message<string?, byte[]> message, CloudE
155156
break;
156157
case ContentMode.Binary:
157158
value = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent));
158-
contentTypeHeaderValue = cloudEvent.DataContentType;
159+
contentTypeHeaderValue = formatter.GetOrInferDataContentType(cloudEvent);
159160
break;
160161
default:
161162
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");

src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -446,23 +446,33 @@ private void WriteCloudEventForBatchOrStructuredMode(JsonWriter writer, CloudEve
446446

447447
if (cloudEvent.Data is object)
448448
{
449-
if (cloudEvent.DataContentType is null)
449+
if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
450450
{
451+
cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
451452
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
452-
writer.WriteValue(JsonMediaType);
453+
writer.WriteValue(inferredDataContentType);
453454
}
454455
EncodeStructuredModeData(cloudEvent, writer);
455456
}
456457
writer.WriteEndObject();
457458
}
458459

460+
/// <summary>
461+
/// Infers the data content type of a CloudEvent based on its data. This implementation
462+
/// infers a data content type of "application/json" for any non-binary data, and performs
463+
/// no inference for binary data.
464+
/// </summary>
465+
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
466+
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
467+
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
468+
459469
/// <summary>
460470
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="JsonWriter"/>.
461471
/// </summary>
462472
/// <remarks>
463473
/// <para>
464474
/// This implementation follows the rules listed in the class remarks. Override this method
465-
/// to provide more specialized behavior, writing only <see cref="DataPropertyName"/> or
475+
/// to provide more specialized behavior, usually writing only <see cref="DataPropertyName"/> or
466476
/// <see cref="DataBase64PropertyName"/> properties.
467477
/// </para>
468478
/// </remarks>
@@ -480,7 +490,7 @@ protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWrite
480490
}
481491
else
482492
{
483-
ContentType dataContentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
493+
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent));
484494
if (IsJsonMediaType(dataContentType.MediaType))
485495
{
486496
writer.WritePropertyName(DataPropertyName);

src/CloudNative.CloudEvents.Protobuf/ProtobufEventFormatter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ namespace CloudNative.CloudEvents.Protobuf
5959
/// a string, otherwise it is left as a byte array. Derived classes can specialize this behavior by overriding
6060
/// <see cref="DecodeBinaryModeEventData(ReadOnlyMemory{byte}, CloudEvent)"/>.
6161
/// </para>
62+
/// <para>
63+
/// This event formatter does not infer any data content type.
64+
/// </para>
6265
/// </remarks>
6366
public class ProtobufEventFormatter : CloudEventFormatter
6467
{

src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,16 +457,26 @@ private void WriteCloudEventForBatchOrStructuredMode(Utf8JsonWriter writer, Clou
457457

458458
if (cloudEvent.Data is object)
459459
{
460-
if (cloudEvent.DataContentType is null)
460+
if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
461461
{
462+
cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
462463
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
463-
writer.WriteStringValue(JsonMediaType);
464+
writer.WriteStringValue(inferredDataContentType);
464465
}
465466
EncodeStructuredModeData(cloudEvent, writer);
466467
}
467468
writer.WriteEndObject();
468469
}
469470

471+
/// <summary>
472+
/// Infers the data content type of a CloudEvent based on its data. This implementation
473+
/// infers a data content type of "application/json" for any non-binary data, and performs
474+
/// no inference for binary data.
475+
/// </summary>
476+
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
477+
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
478+
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
479+
470480
/// <summary>
471481
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="Utf8JsonWriter"/>.
472482
/// </summary>

0 commit comments

Comments
 (0)