First pass at new documentation for 2.0.0-beta.2 onwards
This is definitely a bare minimum, and we'd want more samples etc - but it's probably enough to release 2.0.0-beta.2 and build up from there. Fixes #98. Fixes #74. Fixes #55. Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
parent
cb0d1aa5f2
commit
71003be892
324
README.md
324
README.md
|
@ -3,321 +3,49 @@
|
|||
## Status
|
||||
|
||||
This SDK current supports the following versions of CloudEvents:
|
||||
|
||||
- v1.0
|
||||
|
||||
# sdk-csharp
|
||||
|
||||
.NET Standard 2.0 (C#) SDK for CloudEvents
|
||||
|
||||
The `CloudNative.CloudEvents` package provides utility methods and classes for creating, encoding,
|
||||
decoding, sending, and receiving [CNCF CloudEvents](https://github.com/cloudevents/spec).
|
||||
The `CloudNative.CloudEvents` package provides support for creating, encoding,
|
||||
decoding, sending, and receiving [CNCF
|
||||
CloudEvents](https://github.com/cloudevents/spec). Most applications
|
||||
will want to add dependencies on other `CloudNative.CloudEvents.*`
|
||||
packages for specific event format and protocol binding support. See
|
||||
the [user guide](docs/guide.md) for details of the packages available.
|
||||
|
||||
## A few gotchas highlighted for the impatient who don't usually read docs
|
||||
|
||||
1. The [CloudEvent](src/CloudNative.CloudEvents/CloudEvent.cs) class is not meant to be used with
|
||||
object serializers like JSON.NET and does not have a default constructor to underline this. If you need to serialize or deserialize a CloudEvent directly, always use an [ICloudEventFormatter](src/CloudNative.CloudEvents/ICloudEventFormatter.cs) like the [JsonEventFormatter](src/CloudNative.CloudEvents/JsonEventFormatter.cs).
|
||||
2. The transport integration is provided in the form of extensions and the objective of those extensions
|
||||
is to map the CloudEvent to and from the respective protocol message, like an [HTTP request](src/CloudNative.CloudEvents/CloudEventContent.cs) or [response](src/CloudNative.CloudEvents/HttpClientExtension.cs#L249)
|
||||
object, but the application is otherwise fully in control of the client. Therefore, the extensions do not
|
||||
object serializers like JSON.NET and does not have a default constructor to underline this.
|
||||
If you need to serialize or deserialize a CloudEvent directly, always use a
|
||||
[CloudEventFormatter](src/CloudNative.CloudEvents/CloudEventFormatter.cs)
|
||||
such as [JsonEventFormatter](src/CloudNative.CloudEvents.JsonNet/JsonEventFormatter.cs).
|
||||
2. Protocol binding integration is provided in the form of extensions and the objective of those extensions
|
||||
is to map the CloudEvent to and from the respective protocol message, such as an HTTP request or response.
|
||||
The application is otherwise fully in control of the client. Therefore, the extensions do not
|
||||
add security headers or credentials or any other headers or properties that may be required to interact
|
||||
with a particular product or service. Adding this information is up to the application.
|
||||
|
||||
## CloudEvent
|
||||
## User guide and other documentation
|
||||
|
||||
The `CloudEvent` class reflects the event envelope defined by the
|
||||
[CNCF CloudEvents 1.0 specification](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
|
||||
It supports version 1.0 of CloudEvents by default. It can also handle the pre-release versions
|
||||
0.1, 0.2, and 0.3 of the CloudEvents specification.
|
||||
The [docs/](docs) directory contains more documentation, including
|
||||
the [user guide](docs/guide.md). Feedback on what else to include in
|
||||
the documentation is particularly welcome.
|
||||
|
||||
The strongly typed API reflects the 1.0 standard.
|
||||
## Changes since 1.x
|
||||
|
||||
If required for compatibility with code that leans on one of the prerelease specifications, you can
|
||||
override the specification version explicitly: `new CloudEvent(CloudEventsSpecVersion.V0_1)`.
|
||||
The `SpecVersion` property also allows the version to be switched, meaning you can receive a 0.1
|
||||
event, switch the version number, and forward it as a 1.0 event, with all required mappings done
|
||||
for you.
|
||||
From version 2.0.0-beta.2, there are a number of breaking changes
|
||||
compared with the 1.x series of releases. New code is
|
||||
strongly encouraged to adopt the latest version rather than relying
|
||||
on the 1.3.80 stable release. We are hoping to provide a stable
|
||||
2.0.0 release within the summer of 2021 (May/June/July).
|
||||
|
||||
| **1.0** | Property name | CLR type |
|
||||
| ------------------- | ------------------------ | ----------------------------- |
|
||||
| **id** | `CloudEvent.Id` | `System.String` |
|
||||
| **type** | `CloudEvent.Type` | `System.String` |
|
||||
| **specversion** | `CloudEvent.SpecVersion` | `System.String` |
|
||||
| **time** | `CloudEvent.Time` | `System.DateTime` |
|
||||
| **source** | `CloudEvent.Source` | `System.Uri` |
|
||||
| **subject** | `CloudEvent.Subject` | `System.String` |
|
||||
| **dataschema** | `CloudEvent.DataSchema` | `System.Uri` |
|
||||
| **datacontenttype** | `CloudEvent.ContentType` | `System.Net.Mime.ContentType` |
|
||||
| **data** | `CloudEvent.Data` | `System.Object` |
|
||||
|
||||
The `CloudEvent.Data` property is `object` typed, and may hold any valid serializable
|
||||
CLR type. The following types have special handling:
|
||||
|
||||
- `System.String`: In binary content mode, strings are copied into the transport
|
||||
message payload body using UTF-8 encoding.
|
||||
- `System.Byte[]`: In binary content mode, byte array content is copied into the
|
||||
message paylaod body without further transformation.
|
||||
- `System.Stream`: In binary content mode, stream content is copied into the
|
||||
message paylaod body without further transformation.
|
||||
|
||||
Any other data type is transformed using the given event formatter for the operation
|
||||
or the JSON formatter by default before being added to the transport payload body.
|
||||
|
||||
All extension attributes can be reached via the `CloudEvent.GetAttributes()` method,
|
||||
which returns the internal attribute collection. The internal collection performs
|
||||
all required validations.
|
||||
|
||||
## Extensions
|
||||
|
||||
CloudEvent extensions are represented by implementations of the `ICloudEventExtension`
|
||||
interface. The SDK includes strongly-typed implementations for all offical CloudEvents
|
||||
extensions:
|
||||
|
||||
- `DistributedTracingExtension` for [distributed tracing](https://github.com/cloudevents/spec/blob/master/extensions/distributed-tracing.md)
|
||||
- `SampledRateExtension` for [sampled rate](https://github.com/cloudevents/spec/blob/master/extensions/sampled-rate.md)
|
||||
- `SequenceExtension` for [sequence](https://github.com/cloudevents/spec/blob/master/extensions/sequence.md)
|
||||
|
||||
Extension classes provide type-safe access to the extension attributes as well as implement the
|
||||
required validations and type mappings. An extension object is always created as an
|
||||
independent entity and is then attached to a `CloudEvent` instance. Once attached, the
|
||||
extension object's attributes are merged into the `CloudEvent` instance.
|
||||
|
||||
This snippet shows how to create a `CloudEvent` with an extension:
|
||||
|
||||
```C#
|
||||
var cloudEvent = new CloudEvent(
|
||||
"com.github.pull.create",
|
||||
new Uri("https://github.com/cloudevents/spec/pull/123"),
|
||||
new DistributedTracingExtension()
|
||||
{
|
||||
TraceParent = "value",
|
||||
TraceState = "value"
|
||||
})
|
||||
{
|
||||
ContentType = new ContentType("application/json"),
|
||||
Data = "[]"
|
||||
};
|
||||
```
|
||||
|
||||
The extension can later be accessed via the `Extension<T>()` method:
|
||||
|
||||
```
|
||||
var s = cloudEvent.Extension<DistributedTracingExtension>().TraceParent
|
||||
```
|
||||
|
||||
All APIs where a `CloudEvent` is constructed from an incoming event (or request or
|
||||
response) allow for extension instances to be added via their respective methods, and
|
||||
the extensions are invoked in the mapping process (for instance, to extract information
|
||||
from headers that deviate from the CloudEvents default mapping).
|
||||
|
||||
For example, the server-side mapping for `HttpRequestMessage` allows adding
|
||||
extensions like this:
|
||||
|
||||
```C#
|
||||
public async Task<HttpResponseMessage> Run( HttpRequestMessage req, ILogger log)
|
||||
{
|
||||
var cloudEvent = req.ToCloudEvent(new DistributedTracingExtension());
|
||||
}
|
||||
```
|
||||
|
||||
## Transport Bindings
|
||||
|
||||
This SDK helps with mapping CloudEvents to and from messages or transport frames of
|
||||
popular .NET clients in such a way as to be agnostic of your application's choices of
|
||||
how you want to send an event (be it via HTTP PUT or POST) or how you want to handle
|
||||
settlement of transfers in AMQP or MQTT. The transport binding classes and extensions
|
||||
therefore don't wrap the send and receive operations; you still use the native
|
||||
API of the respective library.
|
||||
|
||||
### HTTP - System.Net.Http.HttpClient
|
||||
|
||||
The .NET [`HttpClient`](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) uses
|
||||
the [`HttpContent`](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent)
|
||||
abstraction to wrap payloads for sending requests that carry entity bodies.
|
||||
|
||||
This SDK provides a [`CloudEventContent`] class derived from `HttpContent` that can be
|
||||
created from a `CloudEvent` instance, the desired `ContentMode`, and an event formatter.
|
||||
|
||||
```C#
|
||||
|
||||
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
|
||||
{
|
||||
ContentType = new ContentType(MediaTypeNames.Application.Json),
|
||||
Data = JsonConvert.SerializeObject("hey there!")
|
||||
};
|
||||
|
||||
var content = new CloudEventContent( cloudEvent,
|
||||
ContentMode.Structured,
|
||||
new JsonEventFormatter());
|
||||
|
||||
var httpClient = new HttpClient();
|
||||
var result = (await httpClient.PostAsync(this.Url, content));
|
||||
```
|
||||
|
||||
For responses, `HttpClient` puts all custom headers onto the `HttpResponseMessage` rather
|
||||
than on the carried `HttpContent` instance. Therefore, if an event is retrieved with
|
||||
`HttpClient` (for instance, from a queue-like structure) the `CloudEvent` is created from
|
||||
the response message object rather than the content object using the `ToCloudEvent()`
|
||||
extension method on `HttpResponseMessage`:
|
||||
|
||||
```C#
|
||||
var httpClient = new HttpClient();
|
||||
// delete and receive message from top of the queue
|
||||
var result = await httpClient.DeleteAsync(new Uri("https://example.com/queue/messages/top"));
|
||||
if (HttpStatusCode.OK == result.StatusCode) {
|
||||
var receivedCloudEvent = await result.ToCloudEvent();
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP - System.Net.HttpWebRequest
|
||||
|
||||
If your application uses the `HttpWebRequest` client, you can copy a CloudEvent into
|
||||
the request payload in structured or binary mode:
|
||||
|
||||
```C#
|
||||
|
||||
HttpWebRequest httpWebRequest = WebRequest.CreateHttp("https://example.com/target");
|
||||
httpWebRequest.Method = "POST";
|
||||
await httpWebRequest.CopyFromAsync(cloudEvent, ContentMode.Structured, new JsonEventFormatter());
|
||||
```
|
||||
|
||||
Bear in mind that the `Method` property must be set to an HTTP method that allows an entity body
|
||||
to be sent, otherwise the copy operation will fail.
|
||||
|
||||
### HTTP - System.Net.HttpListener (HttpRequestMessage)
|
||||
|
||||
On the server-side, you can extract a CloudEvent from the server-side `HttpRequestMessage`
|
||||
with the `ToCloudEventAsync()` extension. If your code handles `HttpRequestContext`,
|
||||
you will use the `Request` property:
|
||||
|
||||
```C#
|
||||
var cloudEvent = await context.Request.ToCloudEventAsync();
|
||||
```
|
||||
|
||||
If you use a functions framework that lets you handle `HttpResponseMessage` and return
|
||||
`HttpResponseMessage`, you will call the extension on the request object directly:
|
||||
|
||||
```C#
|
||||
public async Task<HttpResponseMessage> Run( HttpRequestMessage req, ILogger log)
|
||||
{
|
||||
var cloudEvent = await req.ToCloudEventAsync();
|
||||
}
|
||||
```
|
||||
|
||||
The extension implementation will read the `ContentType` header of the incoming request and
|
||||
automatically select the correct built-in event format decoder. Your code can always pass an
|
||||
overriding format decoder instance as the first argument if needed.
|
||||
|
||||
If your HTTP handler needs to return a CloudEvent, you copy the `CloudEvent` into the
|
||||
response with the `CopyFromAsync()` extension method:
|
||||
|
||||
```C#
|
||||
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
|
||||
{
|
||||
ContentType = new ContentType(MediaTypeNames.Application.Json),
|
||||
Data = JsonConvert.SerializeObject("hey there!")
|
||||
};
|
||||
|
||||
await context.Response.CopyFromAsync(cloudEvent,
|
||||
ContentMode.Structured,
|
||||
new JsonEventFormatter());
|
||||
context.Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
```
|
||||
|
||||
### HTTP - Microsoft.AspNetCore.Http.HttpRequest
|
||||
|
||||
On the server-side, you can extract a CloudEvent from the server-side `HttpRequest`
|
||||
with the `ReadCloudEventAsync()` extension.
|
||||
|
||||
```C#
|
||||
var cloudEvent = await HttpContext.Request.ReadCloudEventAsync();
|
||||
```
|
||||
|
||||
### HTTP - ASP.NET Core MVC
|
||||
|
||||
If you would like to deserialize CloudEvents in actions directly, you can register the
|
||||
`CloudEventJsonInputFormatter` in the MVC options:
|
||||
|
||||
```C#
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMvc(opts =>
|
||||
{
|
||||
opts.InputFormatters.Insert(0, new CloudEventJsonInputFormatter());
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
This formatter will only intercept parameters where CloudEvent is the expected type.
|
||||
|
||||
You can then receive CloudEvent objects in controller actions:
|
||||
|
||||
```C#
|
||||
[HttpPost("resource")]
|
||||
public IActionResult ReceiveCloudEvent([FromBody] CloudEvent cloudEvent)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
```
|
||||
|
||||
### AMQP
|
||||
|
||||
The SDK provides extensions for the [AMQPNetLite](https://github.com/Azure/amqpnetlite) package.
|
||||
|
||||
For AMQP support, you must reference the `CloudNative.CloudEvents.Amqp` assembly and
|
||||
reference the namespace in your code with `using CloudNative.CloudEvents.Amqp`.
|
||||
|
||||
The `AmqpCloudEventMessage` extends the `AMQPNetLite.Message` class. The constructor
|
||||
allows creating a new AMQP message that holds a CloudEvent in either structured or binary
|
||||
content mode.
|
||||
|
||||
```C#
|
||||
|
||||
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
|
||||
{
|
||||
ContentType = new ContentType(MediaTypeNames.Application.Json),
|
||||
Data = JsonConvert.SerializeObject("hey there!")
|
||||
};
|
||||
|
||||
var message = new AmqpCloudEventMessage( cloudEvent,
|
||||
ContentMode.Structured,
|
||||
new JsonEventFormatter());
|
||||
|
||||
```
|
||||
|
||||
For mapping a received `Message` to a CloudEvent, you can use the `ToCloudEvent()` method:
|
||||
|
||||
```C#
|
||||
var receivedCloudEvent = await message.ToCloudEvent();
|
||||
```
|
||||
|
||||
## MQTT
|
||||
|
||||
The SDK provides extensions for the [MQTTnet](https://github.com/chkr1011/MQTTnet) package.
|
||||
For MQTT support, you must reference the `CloudNative.CloudEvents.Mqtt` assembly and
|
||||
reference the namespace in your code with `using CloudNative.CloudEvents.Mqtt`.
|
||||
|
||||
The `MqttCloudEventMessage` extends the `MqttApplicationMessage` class. The constructor
|
||||
allows creating a new MQTT message that holds a CloudEvent in structured content mode.
|
||||
|
||||
```C#
|
||||
|
||||
var cloudEvent = new CloudEvent("com.example.myevent", new Uri("urn:example-com:mysource"))
|
||||
{
|
||||
ContentType = new ContentType(MediaTypeNames.Application.Json),
|
||||
Data = JsonConvert.SerializeObject("hey there!")
|
||||
};
|
||||
|
||||
var message = new MqttCloudEventMessage( cloudEvent,
|
||||
new JsonEventFormatter());
|
||||
|
||||
```
|
||||
|
||||
For mapping a received `MqttApplicationMessage` to a CloudEvent, you can use the
|
||||
`ToCloudEvent()` method:
|
||||
|
||||
```C#
|
||||
var receivedCloudEvent = await message.ToCloudEvent();
|
||||
```
|
||||
A [more details list of changes](docs/changes-since-1x.md) is
|
||||
provided within the documentation.
|
||||
|
||||
## Community
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
# SDK documentation
|
||||
|
||||
**Note: all of this documentation is specific to versions 2.0-beta.2 and onwards**
|
||||
|
||||
This directory contains documentation on:
|
||||
|
||||
- Using the SDK as a consumer
|
||||
- Implementing new event formats and protocol bindings
|
||||
- [Usage guide](guide.md) (this is the most appropriate starting point for most
|
||||
developers if they simply plan on *using* the CloudEvents SDK)
|
||||
- [Changes since version 1.x of the CloudNative.CloudEvents packages](changes-since-1x.md)
|
||||
- Implementing new [event formats](formatters.md) and [protocol bindings](bindings.md)
|
||||
|
||||
## Implementation utility classes
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# Changes since version 1.x
|
||||
|
||||
Many aspects of the SDK have changed since the 1.x versions. Users
|
||||
adopting 2.x should expect to rewrite some code and retest
|
||||
thoroughly when migrating from 1.x.
|
||||
|
||||
The following sections are not exhaustive, but describe the most
|
||||
important changes.
|
||||
|
||||
## Core package
|
||||
|
||||
The `CloudEvent` type constructor now only accepts the spec version
|
||||
and initial extension attributes (with no values). Everything else
|
||||
(type, ID, timestamp etc) must be set via properties or indexers.
|
||||
(In particular, the timestamp and ID are no longer populated
|
||||
automatically.) The spec version for an event is immutable once
|
||||
constructed; everything else can be modified after construction.
|
||||
|
||||
The types used to specify attribute values must match the
|
||||
corresponding attribute type exactly; there is no implicit
|
||||
conversion available. For example, `cloudEvent["source"] =
|
||||
"https://cloudevents.io";` will fail because the `source` attribute
|
||||
is expected to be a URI.
|
||||
|
||||
The following are now fully-abstracted concepts, rather than
|
||||
implicitly using more primitive types:
|
||||
|
||||
- Spec version (`CloudEventSpecVersion`)
|
||||
- Attributes (`CloudEventAttribute`)
|
||||
- Attribute types (`CloudEventAttributeType`)
|
||||
|
||||
The 1.x `CloudEventAttributes` class has now been removed, however -
|
||||
the attributes are contained directly in a map within `CloudEvent`.
|
||||
|
||||
Timestamp attributes are now represented by `DateTimeOffset` instead
|
||||
of `DateTime`, as this provides a more specific "timestamp" concept
|
||||
with fewer ambiguities.
|
||||
|
||||
Extension attributes are no longer expected to implement an
|
||||
interface (the old `ICloudEventExtension`). Instead,
|
||||
`CloudEventAttribute` is used to represent all kinds of attribute,
|
||||
and extensions are encouraged to be provided using C# extension
|
||||
methods and static properties. See the [user
|
||||
guide](guide.md#extension-attributes) for more details.
|
||||
|
||||
## Event formatters
|
||||
|
||||
`CloudEventFormatter` is now an abstract base class (compared with
|
||||
the 1.x interface `ICloudEventFormatter`). Attribute encoding is no
|
||||
longer part of the responsibility of a `CloudEventFormatter`, but
|
||||
binary data encoding (and batch encoding where supported) *are* part
|
||||
of the event formatter.
|
||||
|
||||
The core package no longer contains any event formatters; the
|
||||
Json.NET-based event formatter is now in a separate package
|
||||
(`CloudNative.CloudEvents.NewtonsoftJson`) to avoid an unnecessary
|
||||
dependency. An alternative implementation based on System.Text.Json
|
||||
is now available in the `CloudNative.CloudEvents.SystemTextJson`
|
||||
package.
|
||||
|
||||
Event formatters no longer supports streams for data, but are
|
||||
expected to handle strings and byte arrays, as well as supporting
|
||||
any formatter-specific types (e.g. JSON objects for JSON
|
||||
formatters). While each event formatter is still able to determine
|
||||
its own approach to serialization (meaning that formatters aren't
|
||||
really interchangable), the data responsiblities are more clearly
|
||||
documented, and each formatter should provide details of its
|
||||
serialization and deserialization algorithm.
|
||||
|
||||
## Protocol bindings
|
||||
|
||||
Protocol bindings now typically require a `CloudEventFormatter` for
|
||||
all serialization and deserialization operations, as there's no
|
||||
built-in formatter to use by default. (This sounds inconvenient, but
|
||||
does make the dependency on a specific event format explicit.)
|
||||
|
||||
The method names have been made consistent as far as possible. See
|
||||
[the protocol bindings implementation guide](bindings.md) for
|
||||
details.
|
|
@ -0,0 +1,355 @@
|
|||
# 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.System.Text.Json](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/v1.0.1/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
|
||||
[bindings.md](protocol bindings implementation guide) 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, HttpContentExtensions, HttpListenerExtensions, HttpWebExtensions|
|
||||
|HTTP (ASP.NET Core)|CloudNative.CloudEvents.AspNetCore|HttpRequestExtensions, CloudEventJsonInputFormatter|
|
||||
|AMQP|CloudNative.CloudEvents.Amqp|AmqpClientExtensions|
|
||||
|Kafka|CloudNative.CloudEvents.Kafka|KafkaClientExtensions|
|
||||
|MQTT|CloudNative.CloudEvents.Mqtt|MqttClientExtensions|
|
||||
|
||||
### 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.
|
||||
|
||||
## 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.
|
||||
|
||||
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("https://cloudevents.io/"),
|
||||
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. The 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 -->
|
||||
|
||||
```csharp
|
||||
CloudEventFormatter formatter = new JsonEventFormatter();
|
||||
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
||||
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.
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache 2.0 license.
|
||||
// See LICENSE file in the project root for full license information.
|
||||
|
||||
using CloudNative.CloudEvents.AspNetCore;
|
||||
using CloudNative.CloudEvents.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
|
|
@ -9,7 +9,7 @@ using System;
|
|||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudNative.CloudEvents
|
||||
namespace CloudNative.CloudEvents.AspNetCore
|
||||
{
|
||||
// FIXME: This doesn't get called for binary CloudEvents without content, or with a different data content type.
|
||||
// FIXME: This shouldn't really be tied to JSON. We need to work out how we actually want this to be used.
|
||||
|
|
|
@ -11,7 +11,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CloudNative.CloudEvents
|
||||
namespace CloudNative.CloudEvents.AspNetCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods to convert between HTTP requests and CloudEvents.
|
||||
|
@ -58,7 +58,7 @@ namespace CloudNative.CloudEvents
|
|||
{
|
||||
var headers = httpRequest.Headers;
|
||||
headers.TryGetValue(HttpUtilities.SpecVersionHttpHeader, out var versionId);
|
||||
var version = CloudEventsSpecVersion.FromVersionId(versionId.First())
|
||||
var version = CloudEventsSpecVersion.FromVersionId(versionId.FirstOrDefault())
|
||||
?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", nameof(httpRequest));
|
||||
|
||||
if (version is null)
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2021 Cloud Native Foundation.
|
||||
// Licensed under the Apache 2.0 license.
|
||||
// See LICENSE file in the project root for full license information.
|
||||
|
||||
using CloudNative.CloudEvents.AspNetCore;
|
||||
using CloudNative.CloudEvents.Http;
|
||||
using CloudNative.CloudEvents.NewtonsoftJson;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Internal;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace CloudNative.CloudEvents.UnitTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Tests for the code in the docs/ directory.
|
||||
/// The code itself is currently copy/pasted from this file, but at least we have some confidence
|
||||
/// that it's working code. In the future we can write tooling to extract the code automatically.
|
||||
/// </summary>
|
||||
public class DocumentationSamples
|
||||
{
|
||||
[Fact]
|
||||
public async Task HttpRequestMessageRoundtrip()
|
||||
{
|
||||
var requestMessage = CreateHttpRequestMessage();
|
||||
var request = await ConvertHttpRequestMessage(requestMessage);
|
||||
|
||||
var cloudEvent = await ParseHttpRequestAsync(request);
|
||||
Assert.Equal("event-id", cloudEvent.Id);
|
||||
Assert.Equal("This is CloudEvent data", cloudEvent.Data);
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateHttpRequestMessage()
|
||||
{
|
||||
// Sample: guide.md#PopulateHttpRequestMessage
|
||||
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)
|
||||
};
|
||||
// End sample
|
||||
return request;
|
||||
}
|
||||
|
||||
private static async Task<CloudEvent> ParseHttpRequestAsync(HttpRequest request)
|
||||
{
|
||||
// Sample: guide.md#ParseHttpRequestMessage
|
||||
CloudEventFormatter formatter = new JsonEventFormatter();
|
||||
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
||||
// End sample
|
||||
return cloudEvent;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GameResultRoundtrip()
|
||||
{
|
||||
var requestMessage = SerializeGameResult();
|
||||
var request = await ConvertHttpRequestMessage(requestMessage);
|
||||
var result = await DeserializeGameResult(request);
|
||||
Assert.Equal("player1", result.PlayerId);
|
||||
Assert.Equal("game1", result.GameId);
|
||||
Assert.Equal(200, result.Score);
|
||||
}
|
||||
|
||||
// Sample: guide.md#GameResult
|
||||
public class GameResult
|
||||
{
|
||||
[JsonProperty("playerId")]
|
||||
public string PlayerId { get; set; }
|
||||
|
||||
[JsonProperty("gameId")]
|
||||
public string GameId { get; set; }
|
||||
|
||||
[JsonProperty("score")]
|
||||
public int Score { get; set; }
|
||||
}
|
||||
// End sample
|
||||
|
||||
private static HttpRequestMessage SerializeGameResult()
|
||||
{
|
||||
// Sample: guide.md#SerializeGameResult
|
||||
var result = new GameResult
|
||||
{
|
||||
PlayerId = "player1",
|
||||
GameId = "game1",
|
||||
Score = 200
|
||||
};
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Id = "result-1",
|
||||
Type = "game.played.v1",
|
||||
Source = new Uri("https://cloudevents.io/"),
|
||||
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)
|
||||
};
|
||||
// End sample
|
||||
return request;
|
||||
}
|
||||
|
||||
private static async Task<GameResult> DeserializeGameResult(HttpRequest request)
|
||||
{
|
||||
// Sample: guide.md#DeserializeGameResult
|
||||
CloudEventFormatter formatter = new JsonEventFormatter();
|
||||
CloudEvent cloudEvent = await request.ToCloudEventAsync(formatter);
|
||||
JObject dataAsJObject = (JObject) cloudEvent.Data;
|
||||
GameResult result = dataAsJObject.ToObject<GameResult>();
|
||||
// End sample
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<HttpRequest> ConvertHttpRequestMessage(HttpRequestMessage message)
|
||||
{
|
||||
var request = new DefaultHttpRequest(new DefaultHttpContext());
|
||||
foreach (var header in message.Headers)
|
||||
{
|
||||
request.Headers[header.Key] = header.Value.Single();
|
||||
}
|
||||
foreach (var header in message.Content.Headers)
|
||||
{
|
||||
request.Headers[header.Key] = header.Value.Single();
|
||||
}
|
||||
|
||||
var contentBytes = await message.Content.ReadAsByteArrayAsync();
|
||||
request.Body = new MemoryStream(contentBytes);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue