Additional documentation for more CloudEventFormatter
Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
parent
380fbfbfa7
commit
dd97fde565
|
@ -20,8 +20,8 @@ batch content mode is not currently implemented in the SDK.)
|
||||||
|
|
||||||
## Data serialization and deserialization
|
## Data serialization and deserialization
|
||||||
|
|
||||||
When serializing data in binary mode messages, all formatters should
|
When serializing data in binary mode messages, general purpose formatters
|
||||||
handle data provided as a `byte[]`, serializing it without any
|
should handle data provided as a `byte[]`, serializing it without any
|
||||||
modification. Formatters are also encouraged to support serializing
|
modification. Formatters are also encouraged to support serializing
|
||||||
strings in the obvious way (obeying any character encoding indicated
|
strings in the obvious way (obeying any character encoding indicated
|
||||||
in the `datacontenttype` attribute).
|
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
|
interchangably, it at least provides consumers with some certainty
|
||||||
around what they can expect for a specific formatter.
|
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
|
## Validation
|
||||||
|
|
||||||
Formatter implementations should validate references documented as
|
Formatter implementations should validate references documented as
|
||||||
|
|
|
@ -215,6 +215,14 @@ CloudNative.CloudEvents package to avoid unnecessary dependencies.
|
||||||
We would recommend using a single JSON implementation across an
|
We would recommend using a single JSON implementation across an
|
||||||
application where possible, for simplicity and consistency.
|
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 protocol bindings and event formatters
|
||||||
|
|
||||||
Sample code for creating a CloudEvent and using it to populate an
|
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
|
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
|
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
|
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`
|
As a concrete example, suppose you have a class `GameResult`
|
||||||
representing the result of a single game, and you wish to create a
|
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.
|
HTTP request.
|
||||||
|
|
||||||
When the CloudEvent is deserialized at the receiving side, however,
|
When the CloudEvent is deserialized at the receiving side, however,
|
||||||
it's a little more complex. The event formatter can use the content
|
it's a little more complex. A general purpose event formatter can use the
|
||||||
type of "application/json" to detect that this is JSON, but it
|
content type of "application/json" to detect that this is JSON, but it
|
||||||
doesn't know to deserialize it as a `GameResult`. Instead, it
|
doesn't know to deserialize it as a `GameResult`. Instead, it
|
||||||
deserializes it as a `JToken` (in this case a `JObject`, as the
|
deserializes it as a `JToken` (in this case a `JObject`, as the
|
||||||
content represents a JSON object). The calling code then has to use
|
content represents a JSON object). The calling code then has to use
|
||||||
normal Json.NET deserialization to convert the `JObject` stored in
|
normal Json.NET deserialization to convert the `JObject` stored in
|
||||||
`CloudEvent.Data` into a `GameResult`:
|
`CloudEvent.Data` into a `GameResult`:
|
||||||
|
|
||||||
<!-- Sample: DeserializeGameResult -->
|
<!-- Sample: DeserializeGameResult1 -->
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
CloudEventFormatter formatter = new JsonEventFormatter();
|
CloudEventFormatter formatter = new JsonEventFormatter();
|
||||||
|
@ -345,11 +355,48 @@ JObject dataAsJObject = (JObject) cloudEvent.Data;
|
||||||
GameResult result = dataAsJObject.ToObject<GameResult>();
|
GameResult result = dataAsJObject.ToObject<GameResult>();
|
||||||
```
|
```
|
||||||
|
|
||||||
A future CloudEvent formatter could be written to know what type of
|
An alternative is to use a *single-type* event formatter, which has
|
||||||
data to expect and deserialize it directly; that formatter could
|
a built-in expectation of the data type to deserialize to. For
|
||||||
even be a generic class derived from the existing
|
example, instead of using the non-generic `JsonEventFormatter`
|
||||||
`JsonEventFormatter`. The `JObject` behavior is particular to
|
above, we could use the generic equivalent:
|
||||||
`JsonEventFormatter` - but the important point is that you need to
|
|
||||||
be aware of what the event formatter you're using is capable of.
|
<!-- Sample: DeserializeGameResult2 -->
|
||||||
Every event formatter should carefully document how it handles data,
|
|
||||||
both for serialization and deserialization purposes.
|
```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.
|
||||||
|
|
|
@ -69,11 +69,22 @@ namespace CloudNative.CloudEvents.UnitTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GameResultRoundtrip()
|
public async Task GameResultRoundtrip1()
|
||||||
{
|
{
|
||||||
var requestMessage = SerializeGameResult();
|
var requestMessage = SerializeGameResult();
|
||||||
var request = await ConvertHttpRequestMessage(requestMessage);
|
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("player1", result.PlayerId);
|
||||||
Assert.Equal("game1", result.GameId);
|
Assert.Equal("game1", result.GameId);
|
||||||
Assert.Equal(200, result.Score);
|
Assert.Equal(200, result.Score);
|
||||||
|
@ -121,9 +132,9 @@ namespace CloudNative.CloudEvents.UnitTests
|
||||||
return request;
|
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();
|
CloudEventFormatter formatter = new JsonEventFormatter();
|
||||||
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
||||||
JObject dataAsJObject = (JObject) cloudEvent.Data;
|
JObject dataAsJObject = (JObject) cloudEvent.Data;
|
||||||
|
@ -132,6 +143,16 @@ namespace CloudNative.CloudEvents.UnitTests
|
||||||
return result;
|
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)
|
private static async Task<HttpRequest> ConvertHttpRequestMessage(HttpRequestMessage message)
|
||||||
{
|
{
|
||||||
var request = new DefaultHttpRequest(new DefaultHttpContext());
|
var request = new DefaultHttpRequest(new DefaultHttpContext());
|
||||||
|
|
Loading…
Reference in New Issue