Additional documentation for more CloudEventFormatter

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2021-05-11 09:35:14 +01:00 committed by Jon Skeet
parent 380fbfbfa7
commit dd97fde565
3 changed files with 112 additions and 19 deletions

View File

@ -20,8 +20,8 @@ batch content mode is not currently implemented in the SDK.)
## Data serialization and deserialization
When serializing data in binary mode messages, all formatters should
handle data provided as a `byte[]`, serializing it without any
When serializing data in binary mode messages, general purpose formatters
should handle data provided as a `byte[]`, serializing it without any
modification. Formatters are also encouraged to support serializing
strings in the obvious way (obeying any character encoding indicated
in the `datacontenttype` attribute).
@ -63,6 +63,31 @@ doesn't allow `CloudEventFormatter` instances to be used
interchangably, it at least provides consumers with some certainty
around what they can expect for a specific formatter.
### General purpose vs single-type event formatters
The above description of data handling is designed as guidance for
general purpose event formatters, which should be able to handle any
kind of CloudEvent data with some reasonable (and well-documented)
behavior.
CloudEvent formatters can also be designed to be "single-type",
explicitly only handling a single type of CloudEvent data, known
as the *target type* of the formatter. These are typically generic
types, where the target type is expressed as the type argument. For
example, both of the built-in JSON formatters have a general purpose
formatter (`JsonEventFormatter`) and a single-type formatter
(`JsonEventFormatter<T>`).
Single-type formatters should still support CloudEvents *without*
any data (omitting any data when serializing, and deserializing to a
CloudEvent with a null `Data` property) but may expect that any data
that *is* provided is expected to be of their target type, and
expressed in an appropriate format, without taking note of the data
content type. For example, `JsonEventFormatter<PubSubMessage>` would
throw an `IllegalCastException` if it is asked to serialize a
CloudEvent with a `Data` property referring to an instance of
`StorageEvent`.
## Validation
Formatter implementations should validate references documented as
@ -79,4 +104,4 @@ method, so that an appropriate `ArgumentException` is thrown.
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.
which should perform validation itself later.

View File

@ -215,6 +215,14 @@ CloudNative.CloudEvents package to avoid unnecessary dependencies.
We would recommend using a single JSON implementation across an
application where possible, for simplicity and consistency.
Each JSON implementation provides a general-purpose event formatter
(`JsonEventFormatter`) and a single-type event formatter
(`JsonEventFormatter<T>`). The single-type event formatter will
automatically deserialize to the type argument for `T`, using the
underlying JSON API. These single-type event formatters are only
suitable where the data is expected to be represented via JSON as
well as the "envelope" of the structured mode message.
## Sample code for protocol bindings and event formatters
Sample code for creating a CloudEvent and using it to populate an
@ -270,7 +278,9 @@ you need to consider the representation you want the CloudEvent data
to take when "on the wire". Likewise when you parse a CloudEvent
from a transport message, you need to be aware of the limitations of
the protocol binding and event formatter you're using, in terms of
how data is deserialized.
how data is deserialized. Every event formatter should carefully
document how it handles data, both for serialization and
deserialization purposes.
As a concrete example, suppose you have a class `GameResult`
representing the result of a single game, and you wish to create a
@ -328,15 +338,15 @@ The `GameResult` object is automatically serialized as JSON in the
HTTP request.
When the CloudEvent is deserialized at the receiving side, however,
it's a little more complex. The event formatter can use the content
type of "application/json" to detect that this is JSON, but it
it's a little more complex. A general purpose event formatter can use the
content type of "application/json" to detect that this is JSON, but it
doesn't know to deserialize it as a `GameResult`. Instead, it
deserializes it as a `JToken` (in this case a `JObject`, as the
content represents a JSON object). The calling code then has to use
normal Json.NET deserialization to convert the `JObject` stored in
`CloudEvent.Data` into a `GameResult`:
<!-- Sample: DeserializeGameResult -->
<!-- Sample: DeserializeGameResult1 -->
```csharp
CloudEventFormatter formatter = new JsonEventFormatter();
@ -345,11 +355,48 @@ JObject dataAsJObject = (JObject) cloudEvent.Data;
GameResult result = dataAsJObject.ToObject<GameResult>();
```
A future CloudEvent formatter could be written to know what type of
data to expect and deserialize it directly; that formatter could
even be a generic class derived from the existing
`JsonEventFormatter`. The `JObject` behavior is particular to
`JsonEventFormatter` - but the important point is that you need to
be aware of what the event formatter you're using is capable of.
Every event formatter should carefully document how it handles data,
both for serialization and deserialization purposes.
An alternative is to use a *single-type* event formatter, which has
a built-in expectation of the data type to deserialize to. For
example, instead of using the non-generic `JsonEventFormatter`
above, we could use the generic equivalent:
<!-- Sample: DeserializeGameResult2 -->
```csharp
CloudEventFormatter formatter = new JsonEventFormatter<GameResult>();
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
GameResult result = (GameResult) cloudEvent.Data;
```
### CloudEventFormatterAttribute
The `CloudEventFormatterAttribute` attribute (which can be abbreviated to
`CloudEventFormatter` when specifying it on a type) can be used to
suggest a suitable `CloudEventFormatter` type to use for a particular
type. This attribute is expected to be used by frameworks which
parse CloudEvents and pass them on to user-provided handlers.
Typically the formatter type specified in the attribute is a
single-type formatter, using the type on which the attribute is
placed as the type argument for a generic formatter type. For
example, the `GameResult` class above could be modified to include
the attribute:
```csharp
[CloudEventFormatter(typeof(JsonEventFormatter<GameResult>))]
public class GameResult
{
...
}
```
That would allow the class to be used in frameworks that use
`CloudEventFormatterAttribute`, without the consumer needing to know
the details of the `CloudEventFormatter` themselves. (The consumer
is typically just interested in the CloudEvent, not how it's being
serialized.)
The use of `CloudEventFormatterAttribute` is by no means mandatory,
and it's entirely reasonable to ignore it even when it's present.
It's an option to consider when writing classes representing the
data within CloudEvents, if you're confident of the format in which
the CloudEvent will typically be delivered.

View File

@ -69,11 +69,22 @@ namespace CloudNative.CloudEvents.UnitTests
}
[Fact]
public async Task GameResultRoundtrip()
public async Task GameResultRoundtrip1()
{
var requestMessage = SerializeGameResult();
var request = await ConvertHttpRequestMessage(requestMessage);
var result = await DeserializeGameResult(request);
var result = await DeserializeGameResult1(request);
Assert.Equal("player1", result.PlayerId);
Assert.Equal("game1", result.GameId);
Assert.Equal(200, result.Score);
}
[Fact]
public async Task GameResultRoundtrip2()
{
var requestMessage = SerializeGameResult();
var request = await ConvertHttpRequestMessage(requestMessage);
var result = await DeserializeGameResult2(request);
Assert.Equal("player1", result.PlayerId);
Assert.Equal("game1", result.GameId);
Assert.Equal(200, result.Score);
@ -121,9 +132,9 @@ namespace CloudNative.CloudEvents.UnitTests
return request;
}
private static async Task<GameResult> DeserializeGameResult(HttpRequest request)
private static async Task<GameResult> DeserializeGameResult1(HttpRequest request)
{
// Sample: guide.md#DeserializeGameResult
// Sample: guide.md#DeserializeGameResult1
CloudEventFormatter formatter = new JsonEventFormatter();
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
JObject dataAsJObject = (JObject) cloudEvent.Data;
@ -132,6 +143,16 @@ namespace CloudNative.CloudEvents.UnitTests
return result;
}
private static async Task<GameResult> DeserializeGameResult2(HttpRequest request)
{
// Sample: guide.md#DeserializeGameResult2
CloudEventFormatter formatter = new JsonEventFormatter<GameResult>();
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
GameResult result = (GameResult) cloudEvent.Data;
// End sample
return result;
}
private static async Task<HttpRequest> ConvertHttpRequestMessage(HttpRequestMessage message)
{
var request = new DefaultHttpRequest(new DefaultHttpContext());