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
|
||||
|
||||
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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in New Issue