403 lines
17 KiB
Markdown
403 lines
17 KiB
Markdown
# Usage guide
|
|
|
|
This guide provides a whistle-stop tour of .NET CloudEvents SDK. It
|
|
is not exhaustive by any means; please [file an
|
|
issue](https://github.com/cloudevents/sdk-csharp/issues) if you
|
|
would like to suggest a specific area for further documentation.
|
|
|
|
## NuGet packages
|
|
|
|
The CloudEvents SDK consists of a number of NuGet packages, to avoid
|
|
unnecessary dependencies. These packages are:
|
|
|
|
|NuGet package|Description|
|
|
|-|-|
|
|
|[CloudNative.CloudEvents](https://www.nuget.org/packages/CloudNative.CloudEvents)|Core SDK|
|
|
|[CloudNative.CloudEvents.Amqp](https://www.nuget.org/packages/CloudNative.CloudEvents.Amqp)|AMQP protocol binding using [AMQPNetLite](https://www.nuget.org/packages/AMQPNetLite)|
|
|
|[CloudNative.CloudEvents.AspNetCore](https://www.nuget.org/packages/CloudNative.CloudEvents.AspNetCore)|ASP.NET Core support for CloudEvents|
|
|
|[CloudNative.CloudEvents.Avro](https://www.nuget.org/packages/CloudNative.CloudEvents.Avro)|Avro event formatter using [Apache.Avro](https://www.nuget.org/packages/Apache.Avro)|
|
|
|[CloudNative.CloudEvents.Kafka](https://www.nuget.org/packages/CloudNative.CloudEvents.Kafka)|Kafka protocol binding using [Confluent.Kafka](https://www.nuget.org/packages/Confluent.Kafka)|
|
|
|[CloudNative.CloudEvents.Mqtt](https://www.nuget.org/packages/CloudNative.CloudEvents.Mqtt)|MQTT protocol binding using [MQTTnet](https://www.nuget.org/packages/MQTTnet)|
|
|
|[CloudNative.CloudEvents.NewtonsoftJson](https://www.nuget.org/packages/CloudNative.CloudEvents.NewtonsoftJson)|JSON event formatter using [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json)|
|
|
|[CloudNative.CloudEvents.SystemTextJson](https://www.nuget.org/packages/CloudNative.CloudEvents.SystemTextJson)|JSON event formatter using [System.Text.Json](https://www.nuget.org/packages/System.Text.Json)|
|
|
|
|
Note that protocol bindings for HTTP using `HttpRequestMessage`,
|
|
`HttpResponseMessage`, `HttpContent`, `HttpListenerRequest`,
|
|
`HttpListenerResponse` and `HttpWebRequest` are part of the core SDK.
|
|
|
|
## In-memory CloudEvent representation
|
|
|
|
The most important type in the CloudEvents SDK is the `CloudEvent`
|
|
type. This contains all the information about a CloudEvent,
|
|
including its *attributes* and *data*.
|
|
|
|
Attributes are effectively metadata about the CloudEvent. Each
|
|
attribute is represented by a `CloudEventAttribute` which is aware
|
|
of the attribute name, its kind (see below), its data type (as a
|
|
`CloudEventAttributeType`) and any constraints (such as whether it
|
|
can be present but empty).
|
|
|
|
There are three kinds of attributes:
|
|
|
|
- Required: these attributes are part of the CloudEvents
|
|
specification, and are required on all valid CloudEvents.
|
|
- Optional: these attributes are part of the CloudEvents
|
|
specification, but are not required to be present in order for
|
|
a CloudEvent to be considered valid.
|
|
- Extension: these attributes are not formalized as part of the
|
|
CloudEvents specification. The CloudEvents specification repository
|
|
[includes descriptions of some extension
|
|
attributes](https://github.com/cloudevents/spec/tree/main/cloudevents/extensions)
|
|
that may become standardized over time, but they are not
|
|
considered part of the specification.
|
|
|
|
One attribute is handled differently to all others within the
|
|
.NET CloudEvents SDK: the `specversion` attribute. Once a
|
|
`CloudEvent` object has been created, its `specversion` cannot be
|
|
changed. Currently, only the 1.0 specification is supported anyway;
|
|
when new versions arise, we expect to provide a method to create a
|
|
new `CloudEvent` object from an existing one, but with a new version
|
|
(and with modified properties where appropriate). The specification
|
|
version can be specified explicitly in the CloudEvent constructor,
|
|
but otherwise defaults to 1.0.
|
|
|
|
The optional and required attributes can be accessed in three ways:
|
|
|
|
- Via specific properties, e.g. `cloudEvent.Id` or `cloudEvent.Time`
|
|
- Via the string-based indexer, e.g. `cloudEvent["id"]`
|
|
- Via the CloudEventAttribute-based indexer, e.g.
|
|
`cloudEvent[myAttribute]`
|
|
|
|
Extension attributes do not have specific properties, so can only be
|
|
accessed via one of the indexers.
|
|
|
|
The value returned by the indexer (or accepted when calling the
|
|
setter) depends on the attribute type:
|
|
|
|
|CloudEvent attribute type|.NET type|
|
|
|-|-|
|
|
|String|System.String|
|
|
|Integer|System.Int32|
|
|
|Boolean|System.Boolean|
|
|
|Binary|System.Byte\[\]|
|
|
|URI|System.Uri|
|
|
|URI-Reference|System.Uri|
|
|
|Timestamp|System.DateTimeOffset|
|
|
|
|
When a value is set by the string-based indexer and the CloudEvent
|
|
isn't already aware of the attribute, it is assumed to be a
|
|
string-based extension attribute with no constraints.
|
|
|
|
The `CloudEvent.Data` property deserves special consideration, but
|
|
is best understood after reading about [protocol
|
|
bindings](#protocol-bindings) and [CloudEvent
|
|
formatters](#cloudevent-formatters). If you're already familiar with
|
|
those topics, [jump straight to data
|
|
considerations](#data-considerations).
|
|
|
|
## Extension attributes
|
|
|
|
Extension attributes can be specified without any values when a
|
|
CloudEvent is created. This is typically the case when using a
|
|
protocol binding to parse a transport message: if you're aware of
|
|
any extensions you *might* see in the CloudEvent, and want to use
|
|
them later, pass those extensions into the relevant method and the
|
|
CloudEvent will be created with them. This allows any extension
|
|
attribute values to be validated while the CloudEvent is being
|
|
parsed.
|
|
|
|
The CloudEvents SDK contains some predefined extension attributes in
|
|
the `CloudNative.CloudEvents.Extensions` namespace. The SDK exposes
|
|
these with the following pattern, which you are encouraged to follow
|
|
if you write your own extensions:
|
|
|
|
- Create a static class for all related extension attributes (e.g. the
|
|
`sequence` and `sequencetype` extension attributes are both exposed
|
|
via the `CloudNative.CloudEvents.Extension.Sequence` class)
|
|
- Create a static read-only property of type `CloudEventAttribute`
|
|
for each extension attribute
|
|
- Create a static read-only property of type
|
|
`IEnumerable<CloudEventAttribute>` called `AllAttributes`, typically
|
|
implemented via a `ReadOnlyCollection<T>`. This makes it easy to
|
|
pass "all the related extensions" into the CloudEvent constructor
|
|
or protocol binding methods accepting
|
|
`IEnumerable<CloudEventAttribute>`. It also makes it easy to combine
|
|
multiple extension attributes using the LINQ `Concat` method
|
|
- Create extension methods to interact with CloudEvents, such as the
|
|
`SetSequence(this CloudEvent cloudEvent, object value)` method
|
|
in `Sequence`.
|
|
|
|
When fetching extension attribute values from a CloudEvent, if the
|
|
attribute type is not String, you *may* wish fetch the value by
|
|
attribute name rather than by the attribute. This allows you to
|
|
handle the case where the attribute value has been populated without
|
|
prior knowledge of the attribute, and defaulted to a String type. If
|
|
you know that the CloudEvent will always have been populated using
|
|
the correct extension attribute, this is unnecessary complexity -
|
|
but if you need to work with arbitrary CloudEvent instances, it can
|
|
be more flexible.
|
|
|
|
## Protocol bindings
|
|
|
|
*Protocol bindings* are used to transport CloudEvents on specific
|
|
protocols (e.g. HTTP or Kafka). Each protocol binding has its own
|
|
methods, typically extracting a CloudEvent from an existing
|
|
transport message, or creating/populating a transport message with
|
|
an existing CloudEvent.
|
|
|
|
Protocol bindings work with [CloudEvent formatters](#event-formatters) to
|
|
determine exactly how the CloudEvent is represented within any
|
|
given transport message.
|
|
|
|
Due to differences between protocols, there's no abstract base class
|
|
or interface for protocol bindings. However, protocol bindings are
|
|
encouraged to follow certain conventions to provide a reasonably
|
|
consistent experience across protocols. See the
|
|
[protocol bindings implementation guide](bindings.md) for more
|
|
details of these conventions.
|
|
|
|
The following table summarizes the protocol bindings available:
|
|
|
|
|Protocol binding|Namespace|Types|
|
|
|-|-|-|
|
|
|HTTP (built-in)|CloudNative.CloudEvents.Http|HttpClientExtensions, HttpListenerExtensions, HttpWebExtensions|
|
|
|HTTP (ASP.NET Core)|CloudNative.CloudEvents.AspNetCore|HttpRequestExtensions, CloudEventJsonInputFormatter|
|
|
|AMQP|CloudNative.CloudEvents.Amqp|AmqpExtensions|
|
|
|Kafka|CloudNative.CloudEvents.Kafka|KafkaExtensions|
|
|
|MQTT|CloudNative.CloudEvents.Mqtt|MqttExtensions|
|
|
|
|
### Content modes and batches
|
|
|
|
Most protocol bindings support two *content modes*:
|
|
|
|
- In *structured mode*, all the CloudEvent information is placed in the protocol message body,
|
|
with the exact format governed by the [CloudEvent format](#cloudevent-formatters) in use. The
|
|
content type of the message indicates that the message represents a CloudEvent.
|
|
- In *binary mode*, the CloudEvent data is placed in the protocol message body,
|
|
but the attributes of the CloudEvent are placed in the protocol metadata (e.g. HTTP headers).
|
|
In this case, the content type of the message is the content type of the data of the CloudEvent.
|
|
|
|
Protocol bindings typically expose this option via a parameter of type `ContentMode` when serializing
|
|
a CloudEvent into a protocol message. Deserialization is typically transparent, using the appropriate
|
|
content mode based on the content type of the message being read.
|
|
|
|
Some protocol bindings (e.g. HTTP) also support a *batch mode*. This
|
|
is like structured mode, in that all the CloudEvent information is
|
|
placed in the message body, but the message body can contain any
|
|
number of CloudEvents (including none). Where a protocol binding
|
|
supports batch mode, batch-specific methods are typically provided.
|
|
|
|
## CloudEvent formatters
|
|
|
|
For structured mode (and batch mode) messages, the way in which the
|
|
CloudEvent (or batch of CloudEvents) is represented is determined by
|
|
the *CloudEvent format* being used. In the .NET SDK, a CloudEvent
|
|
format is represented by concrete types derived from the
|
|
`CloudEventFormatter` abstract base class. Two formats are supported:
|
|
|
|
- JSON, via the `JsonEventFormatter` types in the `CloudNative.CloudEvents.SystemTextJson` and
|
|
`CloudNative.CloudEvents.NewtonsoftJson` packages
|
|
- Avro, via the `AvroEventFormatter` type in the `CloudNative.CloudEvents.Avro` package
|
|
|
|
Note that a `CloudEventFormatter` in the .NET SDK has more
|
|
responsibility than a CloudEvent format in the specification, in
|
|
that it is *also* responsible for serializing the data of the event
|
|
in both structured and binary modes. For example, the
|
|
`JsonEventFormatter` implementations will serialize objects as JSON
|
|
objects. See the [Data considerations](#data-considerations) section for more details.
|
|
|
|
There are two different JSON implementations as they use different
|
|
JSON APIs for implementation purposes. This can affect the
|
|
serialized data, as each underlying JSON API has its own set of
|
|
attributes and settings governing the serialization and
|
|
deserialization. Both are provided separately from the core
|
|
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
|
|
`HttpRequestMessage` (typically for sending with `HttpClient`):
|
|
|
|
<!-- Sample: PopulateHttpRequestMessage -->
|
|
```csharp
|
|
CloudEvent cloudEvent = new CloudEvent
|
|
{
|
|
Id = "event-id",
|
|
Type = "event-type",
|
|
Source = new Uri("https://cloudevents.io/"),
|
|
Time = DateTimeOffset.UtcNow,
|
|
DataContentType = "text/plain",
|
|
Data = "This is CloudEvent data"
|
|
};
|
|
|
|
CloudEventFormatter formatter = new JsonEventFormatter();
|
|
HttpRequestMessage request = new HttpRequestMessage
|
|
{
|
|
Method = HttpMethod.Post,
|
|
Content = cloudEvent.ToHttpContent(ContentMode.Structured, formatter)
|
|
};
|
|
```
|
|
|
|
`ToHttpContent` is an extension method requiring a `using` directive of
|
|
|
|
```csharp
|
|
using CloudNative.CloudEvents.Http;
|
|
```
|
|
|
|
Sample code for consuming a CloudEvent within an ASP.NET Core `HttpRequest`:
|
|
|
|
<!-- Sample: ParseHttpRequest -->
|
|
```csharp
|
|
CloudEventFormatter formatter = new JsonEventFormatter();
|
|
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
|
```
|
|
|
|
`ToCloudEventAsync` is an extension method requiring a `using` directive of
|
|
|
|
```csharp
|
|
using CloudNative.CloudEvents.AspNetCore;
|
|
```
|
|
|
|
## Data considerations
|
|
|
|
The `CloudEvent.Data` property is of type `System.Object` and can
|
|
hold any value. However, outside unit testing, CloudEvents are
|
|
almost always serialized using a protocol binding and event
|
|
formatter, and then deserialized later. When creating a CloudEvent
|
|
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. 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
|
|
CloudEvent for this result, using a JSON representation of the data
|
|
in an HTTP request. The class might look like this:
|
|
|
|
<!-- Sample: GameResult -->
|
|
|
|
```csharp
|
|
public class GameResult
|
|
{
|
|
[JsonProperty("playerId")]
|
|
public string PlayerId { get; set; }
|
|
|
|
[JsonProperty("gameId")]
|
|
public string GameId { get; set; }
|
|
|
|
[JsonProperty("score")]
|
|
public int Score { get; set; }
|
|
}
|
|
```
|
|
|
|
Using the `JsonEventFormatter` from the
|
|
`CloudNative.CloudEvents.NewtonsoftJson` package, including an
|
|
instance of `GameResult` as the data of a CloudEvent and then using
|
|
that as the content of an `HttpRequestMessage` is simple:
|
|
|
|
<!-- Sample: SerializeGameResult -->
|
|
|
|
```csharp
|
|
var result = new GameResult
|
|
{
|
|
PlayerId = "player1",
|
|
GameId = "game1",
|
|
Score = 200
|
|
};
|
|
var cloudEvent = new CloudEvent
|
|
{
|
|
Id = "result-1",
|
|
Type = "game.played.v1",
|
|
Source = new Uri("/game", UriKind.Relative),
|
|
Time = DateTimeOffset.UtcNow,
|
|
DataContentType = "application/json",
|
|
Data = result
|
|
};
|
|
var formatter = new JsonEventFormatter();
|
|
var request = new HttpRequestMessage
|
|
{
|
|
Method = HttpMethod.Post,
|
|
Content = cloudEvent.ToHttpContent(ContentMode.Binary, formatter)
|
|
};
|
|
```
|
|
|
|
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. 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: DeserializeGameResult1 -->
|
|
|
|
```csharp
|
|
CloudEventFormatter formatter = new JsonEventFormatter();
|
|
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
|
JObject dataAsJObject = (JObject) cloudEvent.Data;
|
|
GameResult result = dataAsJObject.ToObject<GameResult>();
|
|
```
|
|
|
|
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.
|