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
|
## Status
|
||||||
|
|
||||||
This SDK current supports the following versions of CloudEvents:
|
This SDK current supports the following versions of CloudEvents:
|
||||||
|
|
||||||
- v1.0
|
- v1.0
|
||||||
|
|
||||||
# sdk-csharp
|
# sdk-csharp
|
||||||
|
|
||||||
.NET Standard 2.0 (C#) SDK for CloudEvents
|
.NET Standard 2.0 (C#) SDK for CloudEvents
|
||||||
|
|
||||||
The `CloudNative.CloudEvents` package provides utility methods and classes for creating, encoding,
|
The `CloudNative.CloudEvents` package provides support for creating, encoding,
|
||||||
decoding, sending, and receiving [CNCF CloudEvents](https://github.com/cloudevents/spec).
|
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
|
## 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
|
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).
|
object serializers like JSON.NET and does not have a default constructor to underline this.
|
||||||
2. The transport integration is provided in the form of extensions and the objective of those extensions
|
If you need to serialize or deserialize a CloudEvent directly, always use a
|
||||||
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)
|
[CloudEventFormatter](src/CloudNative.CloudEvents/CloudEventFormatter.cs)
|
||||||
object, but the application is otherwise fully in control of the client. Therefore, the extensions do not
|
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
|
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.
|
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
|
The [docs/](docs) directory contains more documentation, including
|
||||||
[CNCF CloudEvents 1.0 specification](https://github.com/cloudevents/spec/blob/v1.0/spec.md).
|
the [user guide](docs/guide.md). Feedback on what else to include in
|
||||||
It supports version 1.0 of CloudEvents by default. It can also handle the pre-release versions
|
the documentation is particularly welcome.
|
||||||
0.1, 0.2, and 0.3 of the CloudEvents specification.
|
|
||||||
|
|
||||||
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
|
From version 2.0.0-beta.2, there are a number of breaking changes
|
||||||
override the specification version explicitly: `new CloudEvent(CloudEventsSpecVersion.V0_1)`.
|
compared with the 1.x series of releases. New code is
|
||||||
The `SpecVersion` property also allows the version to be switched, meaning you can receive a 0.1
|
strongly encouraged to adopt the latest version rather than relying
|
||||||
event, switch the version number, and forward it as a 1.0 event, with all required mappings done
|
on the 1.3.80 stable release. We are hoping to provide a stable
|
||||||
for you.
|
2.0.0 release within the summer of 2021 (May/June/July).
|
||||||
|
|
||||||
| **1.0** | Property name | CLR type |
|
A [more details list of changes](docs/changes-since-1x.md) is
|
||||||
| ------------------- | ------------------------ | ----------------------------- |
|
provided within the documentation.
|
||||||
| **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();
|
|
||||||
```
|
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
# SDK documentation
|
# SDK documentation
|
||||||
|
|
||||||
|
**Note: all of this documentation is specific to versions 2.0-beta.2 and onwards**
|
||||||
|
|
||||||
This directory contains documentation on:
|
This directory contains documentation on:
|
||||||
|
|
||||||
- Using the SDK as a consumer
|
- [Usage guide](guide.md) (this is the most appropriate starting point for most
|
||||||
- Implementing new event formats and protocol bindings
|
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
|
## 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.
|
// Licensed under the Apache 2.0 license.
|
||||||
// See LICENSE file in the project root for full license information.
|
// See LICENSE file in the project root for full license information.
|
||||||
|
|
||||||
|
using CloudNative.CloudEvents.AspNetCore;
|
||||||
using CloudNative.CloudEvents.NewtonsoftJson;
|
using CloudNative.CloudEvents.NewtonsoftJson;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
|
|
@ -9,7 +9,7 @@ using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
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 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.
|
// 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.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CloudNative.CloudEvents
|
namespace CloudNative.CloudEvents.AspNetCore
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Extension methods to convert between HTTP requests and CloudEvents.
|
/// Extension methods to convert between HTTP requests and CloudEvents.
|
||||||
|
@ -58,7 +58,7 @@ namespace CloudNative.CloudEvents
|
||||||
{
|
{
|
||||||
var headers = httpRequest.Headers;
|
var headers = httpRequest.Headers;
|
||||||
headers.TryGetValue(HttpUtilities.SpecVersionHttpHeader, out var versionId);
|
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));
|
?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", nameof(httpRequest));
|
||||||
|
|
||||||
if (version is null)
|
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