├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── nuget.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── CloudEvents.sln ├── CloudEvents.sln.DotSettings ├── CloudEvents.v3.ncrunchsolution ├── CloudEventsSdk.snk ├── Directory.Packages.props ├── LICENSE ├── MAINTAINERS.md ├── README.md ├── RELEASING.md ├── docs ├── README.md ├── bindings.md ├── changes-since-1x.md ├── formatters.md ├── guide.md └── history.md ├── generate_protos.sh ├── global.json ├── nuget-icon.png ├── samples ├── CloudNative.CloudEvents.AspNetCoreSample │ ├── CloudEventJsonInputFormatter.cs │ ├── CloudNative.CloudEvents.AspNetCoreSample.csproj │ ├── CloudNative.CloudEvents.AspNetCoreSample.http │ ├── Controllers │ │ └── CloudEventController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── Directory.Build.props ├── Directory.Build.targets ├── HttpSend │ ├── HttpSend.csproj │ └── Program.cs └── README.md ├── src ├── CloudNative.CloudEvents.Amqp │ ├── AmqpExtensions.cs │ └── CloudNative.CloudEvents.Amqp.csproj ├── CloudNative.CloudEvents.AspNetCore │ ├── CloudNative.CloudEvents.AspNetCore.csproj │ ├── HttpRequestExtensions.cs │ └── HttpResponseExtensions.cs ├── CloudNative.CloudEvents.Avro │ ├── AvroEventFormatter.cs │ ├── AvroSchema.json │ ├── BasicGenericRecordSerializer.cs │ ├── CloudNative.CloudEvents.Avro.csproj │ ├── Interfaces │ │ └── IGenericRecordSerializer.cs │ └── ObsoleteFormatter.cs ├── CloudNative.CloudEvents.Kafka │ ├── AssemblyInfo.cs │ ├── CloudNative.CloudEvents.Kafka.csproj │ └── KafkaExtensions.cs ├── CloudNative.CloudEvents.Mqtt │ ├── CloudNative.CloudEvents.Mqtt.csproj │ └── MqttExtensions.cs ├── CloudNative.CloudEvents.NewtonsoftJson │ ├── CloudNative.CloudEvents.NewtonsoftJson.csproj │ └── JsonEventFormatter.cs ├── CloudNative.CloudEvents.Protobuf │ ├── CloudNative.CloudEvents.Protobuf.csproj │ ├── Cloudevents.g.cs │ ├── ProtoSchemaReflection.cs │ ├── ProtobufEventFormatter.cs │ └── README.md ├── CloudNative.CloudEvents.SystemTextJson │ ├── CloudNative.CloudEvents.SystemTextJson.csproj │ └── JsonEventFormatter.cs ├── CloudNative.CloudEvents │ ├── AssemblyInfo.cs │ ├── CloudEvent.cs │ ├── CloudEventAttribute.cs │ ├── CloudEventAttributeType.cs │ ├── CloudEventFormatter.cs │ ├── CloudEventFormatterAttribute.cs │ ├── CloudEventsSpecVersion.cs │ ├── CloudNative.CloudEvents.csproj │ ├── CollectionExtensions.cs │ ├── ContentMode.cs │ ├── Core │ │ ├── BinaryDataUtilities.cs │ │ ├── CloudEventAttributeTypeOrdinal.cs │ │ ├── CloudEventAttributeTypes.cs │ │ ├── MimeUtilities.cs │ │ └── Validation.cs │ ├── Extensions │ │ ├── Partitioning.cs │ │ ├── Sampling.cs │ │ └── Sequence.cs │ ├── Http │ │ ├── HttpClientExtensions.cs │ │ ├── HttpListenerExtensions.cs │ │ ├── HttpUtilities.cs │ │ └── HttpWebExtensions.cs │ ├── Strings.Designer.cs │ ├── Strings.resx │ └── Timestamps.cs ├── Directory.Build.props └── Directory.Build.targets └── test ├── CloudNative.CloudEvents.IntegrationTests ├── AspNetCore │ └── CloudEventControllerTests.cs ├── CloudNative.CloudEvents.IntegrationTests.csproj └── Properties │ └── launchSettings.json ├── CloudNative.CloudEvents.UnitTests ├── Amqp │ └── AmqpTest.cs ├── AspNetCore │ ├── HttpRequestExtensionsTest.cs │ └── HttpResponseExtensionsTest.cs ├── Avro │ ├── AvroEventFormatterTest.cs │ └── Helpers │ │ └── FakeGenericRecordSerializer.cs ├── CloudEventAttributeTest.cs ├── CloudEventAttributeTypeTest.cs ├── CloudEventFormatterAttributeTest.cs ├── CloudEventFormatterExtensions.cs ├── CloudEventFormatterTest.cs ├── CloudEventTest.cs ├── CloudEventsSpecVersionTest.cs ├── CloudNative.CloudEvents.UnitTests.csproj ├── ConformanceTestData │ ├── SampleBatches.cs │ ├── SampleEvents.cs │ └── TestDataProvider.cs ├── Core │ ├── BinaryDataUtilitiesTest.cs │ ├── CloudEventAttributeTypesTest.cs │ └── MimeUtilitiesTest.cs ├── DocumentationSamples.cs ├── Extensions │ ├── PartitioningTest.cs │ ├── SamplingTest.cs │ └── SequenceTest.cs ├── Http │ ├── HttpClientExtensionsTest.cs │ ├── HttpListenerExtensionsTest.cs │ ├── HttpTestBase.cs │ ├── HttpUtilitiesTest.cs │ └── HttpWebExtensionsTest.cs ├── Kafka │ └── KafkaTest.cs ├── Mqtt │ └── MqttTest.cs ├── NewtonsoftJson │ ├── AttributedModel.cs │ ├── ConformanceTest.cs │ ├── ConformanceTestFile.cs │ ├── GenericJsonEventFormatterTest.cs │ ├── JTokenAsserter.cs │ ├── JsonEventFormatterTest.cs │ ├── SpecializedFormatterTest.cs │ └── SpecializedJsonReaderTest.cs ├── Protobuf │ ├── CompatibilityTest.cs │ ├── Conformance.cs │ ├── ConformanceTestFile.cs │ ├── ConformanceTests.g.cs │ ├── ProtobufEventFormatterTest.cs │ ├── TestMessages.g.cs │ └── test_messages.proto ├── SystemTextJson │ ├── AttributedModel.cs │ ├── ConformanceTest.cs │ ├── ConformanceTestFile.cs │ ├── GenericJsonEventFormatterTest.cs │ ├── JsonElementAsserter.cs │ ├── JsonEventFormatterTest.cs │ └── SpecializedFormatterTest.cs ├── TestHelpers.cs └── TimestampsTest.cs ├── Directory.Build.props └── Directory.Build.targets /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-22.04 13 | env: 14 | DOTNET_NOLOGO: true 15 | 16 | steps: 17 | - name: Check out our repo 18 | uses: actions/checkout@v3 19 | with: 20 | submodules: true 21 | 22 | # Build with .NET 8.0 SDK 23 | # Test with .NET 6.0 and 8.0 24 | - name: Setup .NET 6.0 and 8.0 25 | uses: actions/setup-dotnet@v3 26 | with: 27 | dotnet-version: | 28 | 8.0.x 29 | 6.0.x 30 | 31 | - name: Build 32 | run: | 33 | dotnet build 34 | dotnet test 35 | # Pack production packages to validate compatibility 36 | dotnet pack -p:ContinuousIntegrationBuild=true 37 | -------------------------------------------------------------------------------- /.github/workflows/nuget.yml: -------------------------------------------------------------------------------- 1 | name: Push packages to NuGet 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | 9 | release: 10 | runs-on: ubuntu-22.04 11 | env: 12 | DOTNET_NOLOGO: true 13 | 14 | steps: 15 | - name: Check out our repo 16 | uses: actions/checkout@v3 17 | with: 18 | submodules: true 19 | 20 | # Build with .NET 8.0 SDK 21 | # Test with .NET 6.0 and 8.0 22 | - name: Setup .NET 6.0 and 8.0 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: | 26 | 8.0.x 27 | 6.0.x 28 | 29 | - name: Build 30 | run: | 31 | dotnet build -c Release -p:ContinuousIntegrationBuild=true 32 | dotnet test -c Release 33 | mkdir nuget 34 | 35 | - name: Push to NuGet 36 | run: | 37 | dotnet pack -c Release -p:ContinuousIntegrationBuild=true -o $PWD/nuget 38 | for file in nuget/*.nupkg; do dotnet nuget push -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} $file; done 39 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "conformance"] 2 | path = conformance 3 | url = https://github.com/cloudevents/conformance.git 4 | branch = format-tests 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/test/CloudNative.CloudEvents.UnitTests/bin/Debug/netcoreapp3.1/CloudNative.CloudEvents.UnitTests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/test/CloudNative.CloudEvents.UnitTests", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The CloudEvents C# SDK is committed to the [CNCF Code of 4 | Conduct](https://github.com/cncf/foundation/blob/main/code-of-conduct.md). 5 | Please refer to that document for details of the standards, reporting processes and 6 | enforcement. 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CloudEvents C# SDK 2 | 3 | We welcome contributions from the community! Please take some time to become 4 | acquainted with the process before submitting a pull request. There are just 5 | a few things to keep in mind. 6 | 7 | # Issues 8 | 9 | If you have found a bug, want to add an improvement, or suggest an 10 | API change, please create an issue before proceeding with a pull 11 | request. If you are intending to create a pull request yourself, 12 | please indicate this in the issue to avoid duplication of work. 13 | 14 | Please be aware that while the maintainers are typically familiar 15 | with the core aspects of CloudEvents, along with the HTTP transport 16 | options, and JSON/XML/Protobuf event formats, they are less 17 | experienced with some other transports and formats (such as AMQP and 18 | Kafka). If you have a feature request or issue around these, you may 19 | need to provide more details or even implement the improvement 20 | yourself - with support from the maintainers, of course. 21 | 22 | # Pull requests 23 | 24 | Typically, a pull request should relate to an existing issue. For 25 | very minor changes such as typos in the documentation this isn't 26 | really necessary. 27 | 28 | ## Getting started 29 | 30 | When creating a pull request, first fork this repository and clone it to your 31 | local development environment. Then add this repository as the upstream. 32 | 33 | ```console 34 | git clone https://github.com/mygithuborg/sdk-csharp.git 35 | cd sdk-csharp 36 | git remote add upstream https://github.com/cloudevents/sdk-csharp.git 37 | ``` 38 | 39 | ## Branches 40 | 41 | The first thing you'll need to do is create a branch for your work. 42 | If you are submitting a pull request that fixes or relates to an existing 43 | GitHub issue, you can use the issue number in your branch name to keep things 44 | organized. 45 | 46 | ```console 47 | git fetch upstream 48 | git reset --hard upstream/main 49 | git checkout main 50 | git checkout -b fix-some-issue 51 | ``` 52 | 53 | ## Commit Messages 54 | 55 | All commit message lines should be kept to fewer than 80 characters if possible. 56 | 57 | Commit messages following [Conventional Commits] 58 | (https://www.conventionalcommits.org/en/v1.0.0/#summary) are 59 | welcome, but not currently required. 60 | 61 | Where the commit addresses an issue, please refer to that 62 | numerically. For example: 63 | 64 | ```log 65 | fix: Make HTTP header handling case-insensitive 66 | 67 | Fixes #12345 68 | ``` 69 | 70 | ### Signing your commits 71 | 72 | Each commit must be signed. Use the `--signoff` flag for your commits. 73 | 74 | ```console 75 | git commit --signoff 76 | ``` 77 | 78 | This will add a line to every git commit message: 79 | 80 | Signed-off-by: Joe Smith 81 | 82 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 83 | 84 | The sign-off is a signature line at the end of your commit message. Your 85 | signature certifies that you wrote the patch or otherwise have the right to pass 86 | it on as open-source code. See [developercertificate.org](http://developercertificate.org/) 87 | for the full text of the certification. 88 | 89 | Be sure to have your `user.name` and `user.email` set in your git config. 90 | If your git config information is set properly then viewing the `git log` 91 | information for your commit will look something like this: 92 | 93 | ``` 94 | Author: Joe Smith 95 | Date: Thu Feb 2 11:41:15 2018 -0800 96 | 97 | Update README 98 | 99 | Signed-off-by: Joe Smith 100 | ``` 101 | 102 | Notice the `Author` and `Signed-off-by` lines match. If they don't your PR will 103 | be rejected by the automated DCO check. 104 | 105 | ## Staying Current with `main` 106 | 107 | As you are working on your branch, changes may happen on `main`. Before 108 | submitting your pull request, be sure that your branch has been updated 109 | with the latest commits. 110 | 111 | ```console 112 | git fetch upstream 113 | git rebase upstream/main 114 | ``` 115 | 116 | This may cause conflicts if the files you are changing on your branch are 117 | also changed on main. Error messages from `git` will indicate if conflicts 118 | exist and what files need attention. Resolve the conflicts in each file, then 119 | continue with the rebase with `git rebase --continue`. 120 | 121 | 122 | If you've already pushed some changes to your `origin` fork, you'll 123 | need to force push these changes. 124 | 125 | ```console 126 | git push -f origin fix-some-issue 127 | ``` 128 | 129 | ## Submitting and updating your pull request 130 | 131 | Before submitting a pull request, you should make sure that all of the tests 132 | successfully pass. 133 | 134 | Once you have sent your pull request, `main` may continue to evolve 135 | before your pull request has landed. If there are any commits on `main` 136 | that conflict with your changes, you may need to update your branch with 137 | these changes before the pull request can land. Resolve conflicts the same 138 | way as before. 139 | 140 | ```console 141 | git fetch upstream 142 | git rebase upstream/main 143 | # fix any potential conflicts 144 | git push -f origin fix-some-issue 145 | ``` 146 | 147 | This will cause the pull request to be updated with your changes, and 148 | CI will rerun. 149 | 150 | A maintainer may ask you to make changes to your pull request. Sometimes these 151 | changes are minor and shouldn't appear in the commit log. For example, you may 152 | have a typo in one of your code comments that should be fixed before merge. 153 | You can prevent this from adding noise to the commit log with an interactive 154 | rebase. See the [git documentation](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History) 155 | for details. 156 | 157 | ```console 158 | git commit -m "fixup: fix typo" 159 | git rebase -i upstream/main # follow git instructions 160 | ``` 161 | 162 | Once you have rebased your commits, you can force push to your fork as before. 163 | -------------------------------------------------------------------------------- /CloudEvents.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True -------------------------------------------------------------------------------- /CloudEvents.v3.ncrunchsolution: -------------------------------------------------------------------------------- 1 |  2 | 3 | False 4 | True 5 | 6 | -------------------------------------------------------------------------------- /CloudEventsSdk.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudevents/sdk-csharp/5e042beb97e44d4a6b58fe3248e340424ba0306a/CloudEventsSdk.snk -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | Current active maintainers of this SDK: 4 | 5 | - [Jon Skeet](https://github.com/jskeet) 6 | - [Josh Love](https://github.com/JoshLove-msft) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/cloudevents/sdk-csharp/actions/workflows/build.yml/badge.svg) 2 | 3 | ## Status 4 | 5 | This SDK current supports the following versions of CloudEvents: 6 | 7 | - v1.0 8 | 9 | # sdk-csharp 10 | 11 | .NET Standard 2.0 (C#) SDK for CloudEvents 12 | 13 | The `CloudNative.CloudEvents` package provides support for creating, encoding, 14 | decoding, sending, and receiving [CNCF 15 | CloudEvents](https://github.com/cloudevents/spec). Most applications 16 | will want to add dependencies on other `CloudNative.CloudEvents.*` 17 | packages for specific event format and protocol binding support. See 18 | the [user guide](docs/guide.md) for details of the packages available. 19 | 20 | ## A few gotchas highlighted for the impatient who don't usually read docs 21 | 22 | 1. The [CloudEvent](src/CloudNative.CloudEvents/CloudEvent.cs) class is not meant to be used with 23 | object serializers like JSON.NET. If you need to serialize or deserialize a CloudEvent directly, always use a 24 | [CloudEventFormatter](src/CloudNative.CloudEvents/CloudEventFormatter.cs) 25 | such as [JsonEventFormatter](src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs). 26 | 2. Protocol binding integration is provided in the form of extensions and the objective of those extensions 27 | is to map the CloudEvent to and from the respective protocol message, such as an HTTP request or response. 28 | The application is otherwise fully in control of the client. Therefore, the extensions do not 29 | add security headers or credentials or any other headers or properties that may be required to interact 30 | with a particular product or service. Adding this information is up to the application. 31 | 32 | ## User guide and other documentation 33 | 34 | The [docs/](docs) directory contains more documentation, including 35 | the [user guide](docs/guide.md). Feedback on what else to include in 36 | the documentation is particularly welcome. 37 | 38 | ## Changes since 1.x 39 | 40 | From version 2.0.0-beta.2, there are a number of breaking changes 41 | compared with the 1.x series of releases. New code is 42 | strongly encouraged to adopt the latest version rather than relying 43 | on the 1.3.80 stable release. 44 | 45 | The stable 2.0.0 version was released on June 15th 2021, and all 46 | users are encouraged to use this (or later) versions. 47 | 48 | A [more details list of changes](docs/changes-since-1x.md) is 49 | provided within the documentation. 50 | 51 | ## Community 52 | 53 | - There are bi-weekly calls immediately following the [Serverless/CloudEvents 54 | call](https://github.com/cloudevents/spec#meeting-time) at 55 | 9am PT (US Pacific). Which means they will typically start at 10am PT, but 56 | if the other call ends early then the SDK call will start early as well. 57 | See the [CloudEvents meeting minutes](https://docs.google.com/document/d/1OVF68rpuPK5shIHILK9JOqlZBbfe91RNzQ7u_P7YCDE/edit#) 58 | to determine which week will have the call. 59 | - Slack: #cloudeventssdk channel under 60 | [CNCF's Slack workspace](https://slack.cncf.io/). 61 | - Email: https://lists.cncf.io/g/cncf-cloudevents-sdk 62 | - Contact for additional information: Clemens Vasters (`@Clemens Vasters` 63 | on slack). 64 | 65 | The C# SDK welcomes community contributions; see the [contributing 66 | document](CONTRIBUTING.md) for more details. 67 | 68 | Each SDK may have its own unique processes, tooling and guidelines. Common 69 | governance related material can be found in the 70 | [CloudEvents `docs`](https://github.com/cloudevents/spec/tree/main/docs) 71 | directory. In particular, in there you will find information 72 | concerning how SDK projects are 73 | [managed](https://github.com/cloudevents/spec/blob/main/docs/SDK-GOVERNANCE.md), 74 | [guidelines](https://github.com/cloudevents/spec/blob/main/docs/SDK-maintainer-guidelines.md) 75 | for how PR reviews and approval, and our 76 | [Code of Conduct](https://github.com/cloudevents/spec/blob/main/docs/GOVERNANCE.md#additional-information) 77 | information. 78 | 79 | If there is a security concern with one of the CloudEvents specifications, or 80 | with one of the project's SDKs, please send an email to 81 | [cncf-cloudevents-security@lists.cncf.io](mailto:cncf-cloudevents-security@lists.cncf.io). 82 | 83 | ## Additional SDK Resources 84 | 85 | - [List of current active maintainers](MAINTAINERS.md) 86 | - [How to contribute to the project](CONTRIBUTING.md) 87 | - [SDK's License](LICENSE) 88 | - [SDK's Release process](RELEASING.md) 89 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Release processes 2 | 3 | (This file aims to document the release process from 2.0 releases onwards.) 4 | 5 | ## General 6 | 7 | - Packages are released via GitHub actions, with a securely configured NuGet API key. 8 | - Packages are pushed to https://nuget.org; there are no other NuGet package repositories involved. 9 | - Other than "while a release is pending", the version in the repository is the "most recently released" 10 | version of the code. 11 | - Packages are only created and pushed based on a GitHub release 12 | (so there should never be a package pushed that we can't later find the right source code). 13 | - The commit that is tagged for each release should only contain changes to the version number and 14 | documentation (e.g. the version history). Code changes should appear in previous commits. 15 | - A pull request may contain commits that affect the code and also a commit for a release; the release 16 | commit must be the final commit within the pull request. 17 | 18 | The normal steps are expected to be: 19 | 20 | - All contributors make code changes and get them approved and merged as normal 21 | - The maintainers agree on the need for a new release (either on GitHub or externally) 22 | - A PR is created and merged by a maintainer (with normal approval) that contains documentation changes 23 | (e.g. [version history](docs/history.md)) and the version number change. 24 | - The maintainer who creates and merges this change is also (by default) responsible for manually creating 25 | the GitHub release and (automatically) a corresponding tag. See below for the format of these. 26 | - NuGet packages are automatically created and pushed when the release is created. 27 | - After a minor or major release, the `PackageValidationBaselineVersion` is updated 28 | to the new version number as the baseline for a future release to be compatible with. 29 | 30 | ## Stable package versioning 31 | 32 | It's helpful to use project references between the "satellite" 33 | packages (e.g. CloudNative.CloudEvents.AspNetCore) and the central 34 | SDK package (CloudNative.CloudEvents). This requires that all 35 | packages are released together, to avoid (for example) a satellite 36 | package being released with a dependency on an unreleased feature in 37 | the SDK package. While this may mean some packages are re-released 38 | without any actual changes other than their dependency on the core 39 | package, it makes versioning problems much less likely, and also 40 | acts as encouragement to use the latest version of the core package. 41 | 42 | Within this repository, this is achieved by the following mechanisms: 43 | 44 | - Most individual csproj files do not specify a version 45 | - The [Directory.Build.props](src/Directory.Build.props) file has a `` element 46 | specifying the version of all packages which don't need a separate 47 | major version 48 | 49 | A single GitHub release (and tag) will be created for each release, 50 | to cover all packages. 51 | 52 | - Example tag name: "CloudNative.CloudEvents.All-2.0.0" 53 | - Example release title: "All packages version 2.0.0" 54 | 55 | ### Exception: packages with different major versions 56 | 57 | For some "satellite" packages, we need a different major version 58 | number, typically to adopt a new major version of a dependency. In 59 | this case, the satellite package will have its own major version, 60 | but keep the minor and patch version of everything else. 61 | 62 | For example, in order to take a new major version of the `MQTTnet` 63 | dependency, `CloudNative.CloudEvents.Mqtt` 3.8.0 was released with 64 | version 2.8.0 of all other packages. 65 | 66 | ## New / unstable package versioning 67 | 68 | New packages are introduced with alpha and beta versions as would 69 | normally be expected. To avoid "chasing" the current stable release 70 | version, these will be labeled using 1.0.0 as the notional stable 71 | version, before synchronizing with the current "umbrella" stable 72 | version when it becomes stable. (This requires a new release for all 73 | packages, for the same reasons given above. This is not expected to 74 | be a frequent occurence, however.) 75 | 76 | For example, a new package might have the following version sequence: 77 | 78 | - 1.0.0-alpha.1 79 | - 1.0.0-beta.1 80 | - 1.0.0-beta.2 81 | - 2.1.0 82 | 83 | While a package is in pre-release, it should specify the Version 84 | element in its project file, which will effectively override the 85 | "default" stable version. 86 | 87 | 92 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # SDK documentation 2 | 3 | **Note: all of this documentation is specific to versions 2.0-beta.2 and onwards** 4 | 5 | This directory contains documentation on: 6 | 7 | - [Usage guide](guide.md) (this is the most appropriate starting point for most 8 | developers if they simply plan on *using* the CloudEvents SDK) 9 | - [Version history](history.md) from 2.0 onwards 10 | - [Changes since version 1.x of the CloudNative.CloudEvents packages](changes-since-1x.md) 11 | - Implementing new [event formats](formatters.md) and [protocol bindings](bindings.md) 12 | 13 | ## Implementation utility classes 14 | 15 | The `CloudNative.CloudEvents.Core` namespace contains utility 16 | classes which are generally helpful when implementing [protocol 17 | bindings](bindings.md) or [event formatters](formatters.md) but are 18 | not expected to be used by code which only creates or consumes 19 | CloudEvents. 20 | 21 | The classes in this namespace are static classes, but the methods 22 | are deliberately not extension methods. This avoids the methods from 23 | being suggested to non-implementation code. 24 | -------------------------------------------------------------------------------- /docs/changes-since-1x.md: -------------------------------------------------------------------------------- 1 | # Changes since version 1.x 2 | 3 | Many aspects of the SDK have changed since the 1.x versions. Users 4 | adopting 2.x should expect to rewrite some code and retest 5 | thoroughly when migrating from 1.x. 6 | 7 | The following sections are not exhaustive, but describe the most 8 | important changes. 9 | 10 | ## Core package 11 | 12 | The `CloudEvent` type constructor now only accepts the spec version 13 | and initial extension attributes (with no values). Everything else 14 | (type, ID, timestamp etc) must be set via properties or indexers. 15 | (In particular, the timestamp and ID are no longer populated 16 | automatically.) The spec version for an event is immutable once 17 | constructed; everything else can be modified after construction. 18 | 19 | The types used to specify attribute values must match the 20 | corresponding attribute type exactly; there is no implicit 21 | conversion available. For example, `cloudEvent["source"] = 22 | "https://cloudevents.io";` will fail because the `source` attribute 23 | is expected to be a URI. 24 | 25 | The following are now fully-abstracted concepts, rather than 26 | implicitly using more primitive types: 27 | 28 | - Spec version (`CloudEventSpecVersion`) 29 | - Attributes (`CloudEventAttribute`) 30 | - Attribute types (`CloudEventAttributeType`) 31 | 32 | The 1.x `CloudEventAttributes` class has now been removed, however - 33 | the attributes are contained directly in a map within `CloudEvent`. 34 | 35 | Timestamp attributes are now represented by `DateTimeOffset` instead 36 | of `DateTime`, as this provides a more specific "timestamp" concept 37 | with fewer ambiguities. 38 | 39 | Extension attributes are no longer expected to implement an 40 | interface (the old `ICloudEventExtension`). Instead, 41 | `CloudEventAttribute` is used to represent all kinds of attribute, 42 | and extensions are encouraged to be provided using C# extension 43 | methods and static properties. See the [user 44 | guide](guide.md#extension-attributes) for more details. 45 | 46 | The distributed tracing extension attributes have been removed for 47 | now, while their long-term future is discussed in the broader 48 | CloudEvent ecosystem. 49 | 50 | ## Event formatters 51 | 52 | `CloudEventFormatter` is now an abstract base class (compared with 53 | the 1.x interface `ICloudEventFormatter`). Attribute encoding is no 54 | longer part of the responsibility of a `CloudEventFormatter`, but 55 | binary data encoding (and batch encoding where supported) *are* part 56 | of the event formatter. 57 | 58 | The core package no longer contains any event formatters; the 59 | Json.NET-based event formatter is now in a separate package 60 | (`CloudNative.CloudEvents.NewtonsoftJson`) to avoid an unnecessary 61 | dependency. An alternative implementation based on System.Text.Json 62 | is now available in the `CloudNative.CloudEvents.SystemTextJson` 63 | package. 64 | 65 | Event formatters no longer supports streams for data, but are 66 | expected to handle strings and byte arrays, as well as supporting 67 | any formatter-specific types (e.g. JSON objects for JSON 68 | formatters). While each event formatter is still able to determine 69 | its own approach to serialization (meaning that formatters aren't 70 | really interchangable), the data responsiblities are more clearly 71 | documented, and each formatter should provide details of its 72 | serialization and deserialization algorithm. 73 | 74 | ## Protocol bindings 75 | 76 | Protocol bindings now typically require a `CloudEventFormatter` for 77 | all serialization and deserialization operations, as there's no 78 | built-in formatter to use by default. (This sounds inconvenient, but 79 | does make the dependency on a specific event format explicit.) 80 | 81 | The method names have been made consistent as far as possible. See 82 | [the protocol bindings implementation guide](bindings.md) for 83 | details. 84 | -------------------------------------------------------------------------------- /docs/formatters.md: -------------------------------------------------------------------------------- 1 | # Implementing an event formatter 2 | 3 | The `CloudEventFormatter` abstract type in the C# SDK is an 4 | augmentation of the [Event 5 | Format](https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md#event-format) 6 | concept in the specification. 7 | 8 | Strictly speaking, CloudEvent data is simply a sequence of bytes. In 9 | practical terms, it's useful to be able to store any object 10 | reference in the `CloudEvent.Data` property, leaving a 11 | `CloudEventFormatter` to perform serialization and deserialization 12 | when requested. 13 | 14 | This means that `CloudEventFormatter` implementations need to be 15 | aware of all content modes (binary, structured and batch) and 16 | document how they handle data of various types. A 17 | `CloudEventFormatter` implementation *may* implement only a subset 18 | of content modes, but should document this very clearly. (Note: 19 | batch content mode is not currently implemented in the SDK.) 20 | 21 | ## Data serialization and deserialization 22 | 23 | When serializing data in binary mode messages, general purpose formatters 24 | should handle data provided as a `byte[]`, serializing it without any 25 | modification. Formatters are also encouraged to support serializing 26 | strings in the obvious way (obeying any character encoding indicated 27 | in the `datacontenttype` attribute). 28 | 29 | When deserializing data in binary mode messages, event formatters 30 | may use the content type to determine the in-memory object type to 31 | deserialize to. For example, a JSON formatter may decode data in a 32 | message with a content type of "application/json" to a JSON API type 33 | (such as `Newtonsoft.Json.Linq.JToken`). Formatters are encouraged 34 | to deserialize data with a content type beginning `text/` (and which 35 | don't otherwise have a special meaning to the formatter) as 36 | strings, obeying any character encoding indicated in the content 37 | type. If the content type is unknown to the formatter, the data 38 | should be populated in the `CloudEvent` as a simple byte array. 39 | 40 | When serializing and deserializing data in a structured mode 41 | message, an event formatter should follow the rules of the event 42 | format it is implementing. The event formatter should be as 43 | consistent as is reasonably possible in terms of its handling of 44 | binary mode data and structured mode data, however. In particular, a 45 | well-designed event format should usually not be restricted to any 46 | specific data type, so any data that can be serialized in a binary 47 | mode message should be serializable in a structured mode message 48 | too. 49 | 50 | Inconsistencies may still arise, when the structured message 51 | contains more information about the original data than the 52 | corresponding binary message. For example, an event format may use a 53 | different serialization format for text and binary data, allowing 54 | string and byte arrays to be serialized and then deserialized 55 | seamlessly even if the content type is unknown to the formatter. 56 | However, a binary mode messages serialized from the same data string may 57 | lose that distinction, resulting in a `Data` property with a byte 58 | array reference rather than a string, if nothing within the content 59 | type indicates that the data is text. 60 | 61 | Event formatters should document their behavior clearly. While this 62 | doesn't allow `CloudEventFormatter` instances to be used 63 | interchangably, it at least provides consumers with some certainty 64 | around what they can expect for a specific formatter. 65 | 66 | ### General purpose vs single-type event formatters 67 | 68 | The above description of data handling is designed as guidance for 69 | general purpose event formatters, which should be able to handle any 70 | kind of CloudEvent data with some reasonable (and well-documented) 71 | behavior. 72 | 73 | CloudEvent formatters can also be designed to be "single-type", 74 | explicitly only handling a single type of CloudEvent data, known 75 | as the *target type* of the formatter. These are typically generic 76 | types, where the target type is expressed as the type argument. For 77 | example, both of the built-in JSON formatters have a general purpose 78 | formatter (`JsonEventFormatter`) and a single-type formatter 79 | (`JsonEventFormatter`). 80 | 81 | Single-type formatters should still support CloudEvents *without* 82 | any data (omitting any data when serializing, and deserializing to a 83 | CloudEvent with a null `Data` property) but may expect that any data 84 | that *is* provided is expected to be of their target type, and 85 | expressed in an appropriate format, without taking note of the data 86 | content type. For example, `JsonEventFormatter` would 87 | throw an `IllegalCastException` if it is asked to serialize a 88 | CloudEvent with a `Data` property referring to an instance of 89 | `StorageEvent`. 90 | 91 | ## Validation 92 | 93 | Formatter implementations should validate references documented as 94 | being non-null, and additionally perform CloudEvent validation on: 95 | 96 | - Any `CloudEvent`s returned by the formatter from 97 | `DecodeStructuredModeMessage` or `DecodeStructuredModeMessageAsync` 98 | - The `CloudEvent` accepted in `EncodeBinaryModeEventData` or 99 | `EncodeStructuredModeMessage` 100 | 101 | Validation should be performed using the `Validation.CheckCloudEventArgument` 102 | method, so that an appropriate `ArgumentException` is thrown. 103 | 104 | The formatter should *not* perform validation on the `CloudEvent` 105 | accepted in `DecodeBinaryModeEventData`, beyond asserting that the 106 | argument is not null. This is typically called by a protocol binding 107 | which should perform validation itself later. 108 | 109 | ## Data content type inference 110 | 111 | Some event formats (e.g. JSON) infer the data content type from the 112 | actual data provided. In the C# SDK, this is implemented via the 113 | `CloudEventFormatter` methods `GetOrInferDataContentType` and 114 | `InferDataContentType`. The first of these is primarily a 115 | convenience method to be called by bindings; the second may be 116 | overridden by any formatter implementation that wishes to infer 117 | a data content type when one is not specified. Implementations *can* 118 | override `GetOrInferDataContentType` if they have unusual 119 | requirements, but the default implementation is usually sufficient. 120 | 121 | The base implementation of `InferDataContentType` always returns 122 | null; this means that no content type is inferred by default. 123 | -------------------------------------------------------------------------------- /generate_protos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2021 Cloud Native Foundation. 3 | # Licensed under the Apache 2.0 license. 4 | # See LICENSE file in the project root for full license information. 5 | 6 | set -e 7 | PROTOBUF_VERSION=22.0 8 | 9 | # Generates the classes for the protobuf event format 10 | 11 | case "$OSTYPE" in 12 | linux*) 13 | PROTOBUF_PLATFORM=linux-x86_64 14 | PROTOC=tmp/bin/protoc 15 | ;; 16 | win* | msys* | cygwin*) 17 | PROTOBUF_PLATFORM=win64 18 | PROTOC=tmp/bin/protoc.exe 19 | ;; 20 | darwin*) 21 | PROTOBUF_PLATFORM=osx-x86_64 22 | PROTOC=tmp/bin/protoc 23 | ;; 24 | *) 25 | echo "Unknown OSTYPE: $OSTYPE" 26 | exit 1 27 | esac 28 | 29 | # Clean up previous generation results 30 | rm -f src/CloudNative.CloudEvents.Protobuf/*.g.cs 31 | rm -f test/CloudNative.CloudEvents.UnitTests/Protobuf/*.g.cs 32 | 33 | rm -rf tmp 34 | mkdir tmp 35 | cd tmp 36 | 37 | echo "- Downloading protobuf@$PROTOBUF_VERSION" 38 | curl -sSL \ 39 | https://github.com/protocolbuffers/protobuf/releases/download/v$PROTOBUF_VERSION/protoc-$PROTOBUF_VERSION-$PROTOBUF_PLATFORM.zip \ 40 | --output protobuf.zip 41 | unzip -q protobuf.zip 42 | 43 | echo "- Downloading schema" 44 | # TODO: Use the 1.0.2 branch when it exists. 45 | mkdir cloudevents 46 | curl -sSL https://raw.githubusercontent.com/cloudevents/spec/main/cloudevents/formats/cloudevents.proto -o cloudevents/cloudevents.proto 47 | 48 | cd .. 49 | 50 | # Schema proto 51 | $PROTOC \ 52 | -I tmp/include \ 53 | -I tmp/cloudevents \ 54 | --csharp_out=src/CloudNative.CloudEvents.Protobuf \ 55 | --csharp_opt=file_extension=.g.cs \ 56 | tmp/cloudevents/cloudevents.proto 57 | 58 | # Test protos 59 | $PROTOC \ 60 | -I tmp/include \ 61 | -I test/CloudNative.CloudEvents.UnitTests/Protobuf \ 62 | --csharp_out=test/CloudNative.CloudEvents.UnitTests/Protobuf \ 63 | --csharp_opt=file_extension=.g.cs \ 64 | test/CloudNative.CloudEvents.UnitTests/Protobuf/*.proto 65 | 66 | # Conformance test protos 67 | $PROTOC \ 68 | -I tmp/include \ 69 | -I tmp/cloudevents \ 70 | -I conformance/format/protobuf \ 71 | --csharp_out=test/CloudNative.CloudEvents.UnitTests/Protobuf \ 72 | --csharp_opt=file_extension=.g.cs \ 73 | conformance/format/protobuf/*.proto 74 | 75 | echo "Generated code." 76 | rm -rf tmp 77 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.400", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMinor" 6 | } 7 | } -------------------------------------------------------------------------------- /nuget-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudevents/sdk-csharp/5e042beb97e44d4a6b58fe3248e340424ba0306a/nuget-icon.png -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/CloudEventJsonInputFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.AspNetCore; 6 | using CloudNative.CloudEvents.Core; 7 | using Microsoft.AspNetCore.Mvc.Formatters; 8 | using Microsoft.Net.Http.Headers; 9 | using System; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace CloudNative.CloudEvents.AspNetCoreSample 14 | { 15 | // FIXME: This doesn't get called for binary CloudEvents without content, or with a different data content type. 16 | // FIXME: This shouldn't really be tied to JSON. We need to work out how we actually want this to be used. 17 | // See 18 | 19 | /// 20 | /// A that parses HTTP requests into CloudEvents. 21 | /// 22 | public class CloudEventJsonInputFormatter : TextInputFormatter 23 | { 24 | private readonly CloudEventFormatter _formatter; 25 | 26 | /// 27 | /// Constructs a new instance that uses the given formatter for deserialization. 28 | /// 29 | /// 30 | public CloudEventJsonInputFormatter(CloudEventFormatter formatter) 31 | { 32 | _formatter = Validation.CheckNotNull(formatter, nameof(formatter)); 33 | SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/json")); 34 | SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/cloudevents+json")); 35 | 36 | SupportedEncodings.Add(Encoding.UTF8); 37 | SupportedEncodings.Add(Encoding.Unicode); 38 | } 39 | 40 | /// 41 | public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) 42 | { 43 | Validation.CheckNotNull(context, nameof(context)); 44 | Validation.CheckNotNull(encoding, nameof(encoding)); 45 | 46 | var request = context.HttpContext.Request; 47 | 48 | try 49 | { 50 | var cloudEvent = await request.ToCloudEventAsync(_formatter); 51 | return await InputFormatterResult.SuccessAsync(cloudEvent); 52 | } 53 | catch (Exception) 54 | { 55 | return await InputFormatterResult.FailureAsync(); 56 | } 57 | } 58 | 59 | /// 60 | protected override bool CanReadType(Type type) 61 | => type == typeof(CloudEvent) && base.CanReadType(type); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/CloudNative.CloudEvents.AspNetCoreSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/CloudNative.CloudEvents.AspNetCoreSample.http: -------------------------------------------------------------------------------- 1 | @HostAddress = https://localhost:5001 2 | 3 | POST {{HostAddress}}/api/events/receive 4 | Content-Type: application/json 5 | CE-SpecVersion: 1.0 6 | CE-Type: "com.example.myevent" 7 | CE-Source: "urn:example-com:mysource:abc" 8 | CE-Id: "c457b7c5-c038-4be9-98b9-938cb64a4fbf" 9 | 10 | { 11 | "message": "Hello world!" 12 | } 13 | 14 | ### 15 | 16 | GET {{HostAddress}}/api/events/generate 17 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/Controllers/CloudEventController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.NewtonsoftJson; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Newtonsoft.Json.Linq; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Text; 11 | 12 | namespace CloudNative.CloudEvents.AspNetCoreSample.Controllers 13 | { 14 | [Route("api/events")] 15 | [ApiController] 16 | public class CloudEventController : ControllerBase 17 | { 18 | private static readonly CloudEventFormatter formatter = new JsonEventFormatter(); 19 | 20 | [HttpPost("receive")] 21 | public ActionResult> ReceiveCloudEvent([FromBody] CloudEvent cloudEvent) 22 | { 23 | var attributeMap = new JObject(); 24 | foreach (var (attribute, value) in cloudEvent.GetPopulatedAttributes()) 25 | { 26 | attributeMap[attribute.Name] = attribute.Format(value); 27 | } 28 | return Ok($"Received event with ID {cloudEvent.Id}, attributes: {attributeMap}"); 29 | } 30 | 31 | /// 32 | /// Generates a CloudEvent in "structured mode", where all CloudEvent information is 33 | /// included within the body of the response. 34 | /// 35 | [HttpGet("generate")] 36 | public ActionResult GenerateCloudEvent() 37 | { 38 | var evt = new CloudEvent 39 | { 40 | Type = "CloudNative.CloudEvents.AspNetCoreSample", 41 | Source = new Uri("https://github.com/cloudevents/sdk-csharp"), 42 | Time = DateTimeOffset.Now, 43 | DataContentType = "application/json", 44 | Id = Guid.NewGuid().ToString(), 45 | Data = new 46 | { 47 | Language = "C#", 48 | EnvironmentVersion = Environment.Version.ToString() 49 | } 50 | }; 51 | // Format the event as the body of the response. This is UTF-8 JSON because of 52 | // the CloudEventFormatter we're using, but EncodeStructuredModeMessage always 53 | // returns binary data. We could return the data directly, but for debugging 54 | // purposes it's useful to have the JSON string. 55 | var bytes = formatter.EncodeStructuredModeMessage(evt, out var contentType); 56 | string json = Encoding.UTF8.GetString(bytes.Span); 57 | var result = Ok(json); 58 | 59 | // Specify the content type of the response: this is what makes it a CloudEvent. 60 | // (In "binary mode", the content type is the content type of the data, and headers 61 | // indicate that it's a CloudEvent.) 62 | result.ContentTypes.Add(contentType.MediaType); 63 | return result; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.AspNetCoreSample; 6 | using Microsoft.AspNetCore.Builder; 7 | using CloudNative.CloudEvents.NewtonsoftJson; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | builder.Services.AddControllers(opts => 13 | opts.InputFormatters.Insert(0, new CloudEventJsonInputFormatter(new JsonEventFormatter()))); 14 | 15 | var app = builder.Build(); 16 | 17 | app.MapControllers(); 18 | 19 | app.Run(); 20 | 21 | // Generated `Program` class when using top-level statements 22 | // is internal by default. Make this `public` here for tests. 23 | public partial class Program { } 24 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "WebApplication": { 5 | "commandName": "Project", 6 | "launchBrowser": true, 7 | "launchUrl": "api/events/generate", 8 | "applicationUrl": "https://localhost:5001", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/CloudNative.CloudEvents.AspNetCoreSample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) 5 | 6 | 7 | False 8 | 9 | 10 | $(RepoRoot)/CloudEventsSdk.snk 11 | True 12 | True 13 | True 14 | true 15 | 16 | 17 | False 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/HttpSend/HttpSend.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/HttpSend/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents; 6 | using CloudNative.CloudEvents.Http; 7 | using CloudNative.CloudEvents.NewtonsoftJson; 8 | using McMaster.Extensions.CommandLineUtils; 9 | using Newtonsoft.Json; 10 | using System; 11 | using System.ComponentModel.DataAnnotations; 12 | using System.Net.Http; 13 | using System.Net.Mime; 14 | using System.Threading.Tasks; 15 | 16 | namespace HttpSend 17 | { 18 | // This application uses the McMaster.Extensions.CommandLineUtils library for parsing the command 19 | // line and calling the application code. The [Option] attributes designate the parameters. 20 | internal class Program 21 | { 22 | [Option(Description = "CloudEvents 'source' (default: urn:example-com:mysource:abc)", LongName = "source", ShortName = "s")] 23 | private string Source { get; } = "urn:example-com:mysource:abc"; 24 | 25 | [Option(Description = "CloudEvents 'type' (default: com.example.myevent)", LongName = "type", ShortName = "t")] 26 | private string Type { get; } = "com.example.myevent"; 27 | 28 | [Required, Option(Description = "HTTP(S) address to send the event to", LongName = "url", ShortName = "u"),] 29 | private Uri Url { get; } 30 | 31 | public static int Main(string[] args) => CommandLineApplication.Execute(args); 32 | 33 | private async Task OnExecuteAsync() 34 | { 35 | var cloudEvent = new CloudEvent 36 | { 37 | Id = Guid.NewGuid().ToString(), 38 | Type = Type, 39 | Source = new Uri(Source), 40 | DataContentType = MediaTypeNames.Application.Json, 41 | Data = JsonConvert.SerializeObject("hey there!") 42 | }; 43 | 44 | var content = cloudEvent.ToHttpContent(ContentMode.Structured, new JsonEventFormatter()); 45 | 46 | var httpClient = new HttpClient(); 47 | // Your application remains in charge of adding any further headers or 48 | // other information required to authenticate/authorize or otherwise 49 | // dispatch the call at the server. 50 | var result = await httpClient.PostAsync(Url, content); 51 | 52 | Console.WriteLine(result.StatusCode); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Samples 2 | 3 | This directory contains a sample ASP.NET Core application that exposes two endpoints for: 4 | 5 | - Receiving a CloudEvent (`/api/events/receive`) 6 | - Generating a CloudEvent (`/api/events/generate`) 7 | 8 | ## Run the sample 9 | 10 | To run the sample, execute the `dotnet run` command in the `CloudNative.CloudEvents.AspNetCoreSample` directory. 11 | 12 | ```shell 13 | dotnet run --framework net6.0 14 | ``` 15 | 16 | After running the web service using the command above, there are three strategies for sending requests to the web service. 17 | 18 | ### Using the `HttpSend` tool 19 | 20 | The `HttpSend` project provides a CLI tool for sending requests to the `/api/events/receive` endpoint exposed by the service. To use the tool, navigate to the `HttpSend` directory and execute the following command: 21 | 22 | ```shell 23 | dotnet run --framework net6.0 --url https://localhost:5001/api/events/receive 24 | ``` 25 | 26 | ### Using the `.http` file 27 | 28 | The [CloudNative.CloudEvents.AspNetCore.http file](./CloudNative.CloudEvents.AspNetCoreSample/CloudNative.CloudEvents.AspNetCoreSample.http) can be used to send requests to the web service. Native support for executing requests in `.http` file is provided in JetBrains Rider and Visual Studio. Support for sending requests in VS Code is provided via the [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client). 29 | 30 | ### Using the `curl` command 31 | 32 | Requests to the web service can also be run using the `curl` command line tool. 33 | 34 | ```shell 35 | curl --request POST \ 36 | --url https://localhost:5001/api/events/receive \ 37 | --header 'ce-id: "c457b7c5-c038-4be9-98b9-938cb64a4fbf"' \ 38 | --header 'ce-source: "urn:example-com:mysource:abc"' \ 39 | --header 'ce-specversion: 1.0' \ 40 | --header 'ce-type: "com.example.myevent"' \ 41 | --header 'content-type: application/json' \ 42 | --header 'user-agent: vscode-restclient' \ 43 | --data '{"message": "Hello world!"}' 44 | ``` 45 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Amqp/CloudNative.CloudEvents.Amqp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | AMQP extensions for CloudNative.CloudEvents 6 | 8.0 7 | enable 8 | cncf;cloudnative;cloudevents;events;amqp 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.AspNetCore/CloudNative.CloudEvents.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | ASP.Net Core extensions for CloudNative.CloudEvents 6 | 8.0 7 | enable 8 | cncf;cloudnative;cloudevents;events;aspnetcore;aspnet 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.AspNetCore/HttpResponseExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using CloudNative.CloudEvents.Http; 7 | using Microsoft.AspNetCore.Http; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Net.Mime; 11 | using System.Threading.Tasks; 12 | 13 | namespace CloudNative.CloudEvents.AspNetCore 14 | { 15 | /// 16 | /// Extension methods to convert between HTTP responses and CloudEvents. 17 | /// 18 | public static class HttpResponseExtensions 19 | { 20 | /// 21 | /// Copies a into an . 22 | /// 23 | /// The CloudEvent to copy. Must not be null, and must be a valid CloudEvent. 24 | /// The response to copy the CloudEvent to. Must not be null. 25 | /// Content mode (structured or binary) 26 | /// The formatter to use within the conversion. Must not be null. 27 | /// A task representing the asynchronous operation. 28 | public static async Task CopyToHttpResponseAsync(this CloudEvent cloudEvent, HttpResponse destination, 29 | ContentMode contentMode, CloudEventFormatter formatter) 30 | { 31 | Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); 32 | Validation.CheckNotNull(destination, nameof(destination)); 33 | Validation.CheckNotNull(formatter, nameof(formatter)); 34 | 35 | ReadOnlyMemory content; 36 | ContentType? contentType; 37 | switch (contentMode) 38 | { 39 | case ContentMode.Structured: 40 | content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); 41 | break; 42 | case ContentMode.Binary: 43 | content = formatter.EncodeBinaryModeEventData(cloudEvent); 44 | contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent)); 45 | break; 46 | default: 47 | throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); 48 | } 49 | if (contentType is object) 50 | { 51 | destination.ContentType = contentType.ToString(); 52 | } 53 | else if (content.Length != 0) 54 | { 55 | throw new ArgumentException("The 'datacontenttype' attribute value must be specified", nameof(cloudEvent)); 56 | } 57 | 58 | // Map headers in either mode. 59 | // Including the headers in structured mode is optional in the spec (as they're already within the body) but 60 | // can be useful. 61 | destination.Headers.Append(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId)); 62 | foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes()) 63 | { 64 | var attribute = attributeAndValue.Key; 65 | var value = attributeAndValue.Value; 66 | // The content type is already handled based on the content mode. 67 | if (attribute != cloudEvent.SpecVersion.DataContentTypeAttribute) 68 | { 69 | string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value)); 70 | destination.Headers.Append(HttpUtilities.HttpHeaderPrefix + attribute.Name, headerValue); 71 | } 72 | } 73 | 74 | destination.ContentLength = content.Length; 75 | await BinaryDataUtilities.CopyToStreamAsync(content, destination.Body).ConfigureAwait(false); 76 | } 77 | 78 | /// 79 | /// Copies a batch into an . 80 | /// 81 | /// The CloudEvent batch to copy. Must not be null, and must be a valid CloudEvent. 82 | /// The response to copy the CloudEvent to. Must not be null. 83 | /// The formatter to use within the conversion. Must not be null. 84 | /// A task representing the asynchronous operation. 85 | public static async Task CopyToHttpResponseAsync(this IReadOnlyList cloudEvents, 86 | HttpResponse destination, CloudEventFormatter formatter) 87 | { 88 | Validation.CheckCloudEventBatchArgument(cloudEvents, nameof(cloudEvents)); 89 | Validation.CheckNotNull(destination, nameof(destination)); 90 | Validation.CheckNotNull(formatter, nameof(formatter)); 91 | 92 | // TODO: Validate that all events in the batch have the same version? 93 | // See https://github.com/cloudevents/spec/issues/807 94 | 95 | ReadOnlyMemory content = formatter.EncodeBatchModeMessage(cloudEvents, out var contentType); 96 | destination.ContentType = contentType.ToString(); 97 | destination.ContentLength = content.Length; 98 | await BinaryDataUtilities.CopyToStreamAsync(content, destination.Body).ConfigureAwait(false); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Avro/AvroSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "namespace": "io.cloudevents", 3 | "type": "record", 4 | "name": "CloudEvent", 5 | "version": "1.0", 6 | "doc": "Avro Event Format for CloudEvents", 7 | "fields": [ 8 | { 9 | "name": "attribute", 10 | "type": { 11 | "type": "map", 12 | "values": ["null", "boolean", "int", "string", "bytes"] 13 | } 14 | }, 15 | { 16 | "name": "data", 17 | "type": [ 18 | "bytes", 19 | "null", 20 | "boolean", 21 | { 22 | "type": "map", 23 | "values": [ 24 | "null", 25 | "boolean", 26 | { 27 | "type": "record", 28 | "name": "CloudEventData", 29 | "doc": "Representation of a JSON Value", 30 | "fields": [ 31 | { 32 | "name": "value", 33 | "type": { 34 | "type": "map", 35 | "values": [ 36 | "null", 37 | "boolean", 38 | { "type": "map", "values": "CloudEventData" }, 39 | { "type": "array", "items": "CloudEventData" }, 40 | "double", 41 | "string" 42 | ] 43 | } 44 | } 45 | ] 46 | }, 47 | "double", 48 | "string" 49 | ] 50 | }, 51 | { "type": "array", "items": "CloudEventData" }, 52 | "double", 53 | "string" 54 | ] 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Avro/BasicGenericRecordSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Avro.Generic; 6 | using Avro.IO; 7 | using CloudNative.CloudEvents.Avro.Interfaces; 8 | using System; 9 | using System.IO; 10 | 11 | namespace CloudNative.CloudEvents.Avro; 12 | 13 | /// 14 | /// The default implementation of the . 15 | /// 16 | /// 17 | /// Makes use of the Avro and 18 | /// together with the embedded Avro schema. 19 | /// 20 | internal sealed class BasicGenericRecordSerializer : IGenericRecordSerializer 21 | { 22 | private readonly DefaultReader avroReader; 23 | private readonly DefaultWriter avroWriter; 24 | 25 | public BasicGenericRecordSerializer() 26 | { 27 | avroReader = new DefaultReader(AvroEventFormatter.AvroSchema, AvroEventFormatter.AvroSchema); 28 | avroWriter = new DefaultWriter(AvroEventFormatter.AvroSchema); 29 | } 30 | 31 | /// 32 | public ReadOnlyMemory Serialize(GenericRecord record) 33 | { 34 | var memStream = new MemoryStream(); 35 | var encoder = new BinaryEncoder(memStream); 36 | avroWriter.Write(record, encoder); 37 | return memStream.ToArray(); 38 | } 39 | 40 | /// 41 | public GenericRecord Deserialize(Stream rawMessagebody) 42 | { 43 | var decoder = new BinaryDecoder(rawMessagebody); 44 | // The reuse parameter *is* allowed to be null... 45 | return avroReader.Read(reuse: null!, decoder); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Avro/CloudNative.CloudEvents.Avro.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | Avro extensions for CloudNative.CloudEvents 6 | cncf;cloudnative;cloudevents;events;avro 7 | 10.0 8 | enable 9 | 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Avro/Interfaces/IGenericRecordSerializer.cs: -------------------------------------------------------------------------------- 1 | using Avro.Generic; 2 | using System; 3 | using System.IO; 4 | 5 | namespace CloudNative.CloudEvents.Avro.Interfaces; 6 | 7 | /// 8 | /// Used to serialize and deserialize an Avro 9 | /// matching the 10 | /// CloudEvent Avro schema. 11 | /// 12 | /// 13 | /// 14 | /// An implementation of this interface can optionally be supplied to the in cases 15 | /// where a custom Avro serialiser is required for integration with pre-existing tools/infrastructure. 16 | /// 17 | /// 18 | /// It is recommended to use the default serializer before defining your own wherever possible. 19 | /// 20 | /// 21 | public interface IGenericRecordSerializer 22 | { 23 | /// 24 | /// Serialize an Avro . 25 | /// 26 | /// 27 | /// The record is guaranteed to match the 28 | /// 29 | /// CloudEvent Avro schema. 30 | /// 31 | ReadOnlyMemory Serialize(GenericRecord value); 32 | 33 | /// 34 | /// Deserialize a matching the 35 | /// 36 | /// CloudEvent Avro schema, represented as a stream. 37 | /// 38 | GenericRecord Deserialize(Stream messageBody); 39 | } 40 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Avro/ObsoleteFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace CloudNative.CloudEvents; 8 | 9 | /// 10 | /// Formatter that implements the Avro Event Format. 11 | /// 12 | /// 13 | /// This class is the wrong namespace, and is only present for backward compatibility reasons. 14 | /// Please use CloudNative.CloudEvents.Avro.AvroEventFormatter instead 15 | /// (which this class derives from for convenience). 16 | /// 17 | [Obsolete("This class is the wrong namespace, and is only present for backward compatibility reasons. Please use CloudNative.CloudEvents.Avro.AvroEventFormatter.")] 18 | public class AvroEventFormatter : Avro.AvroEventFormatter 19 | { 20 | } 21 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Kafka/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("CloudNative.CloudEvents.UnitTests,PublicKey=" 8 | + "0024000004800000940000000602000000240000525341310004000001000100e945e99352d0b8" 9 | + "90ddb645995bc05ef5a22497d97e78196b9f6148ea33b0c1b219f0c28df523878d1d8c9d042a02" 10 | + "f005777461dffe455b348f82b39fcbc64985ef091295c0ad2dcb265c23589e9ce8e48dbe84c8e1" 11 | + "7fc37555938b2669aea7575cee288809065aa9dc04dff67ce1dfc5a3167770323c1a2c632f0eb2" 12 | + "f8c64acf")] -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Kafka/CloudNative.CloudEvents.Kafka.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | Kafka extensions for CloudNative.CloudEvents 6 | cncf;cloudnative;cloudevents;events;kafka 7 | 8.0 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Mqtt/CloudNative.CloudEvents.Mqtt.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | MQTT extensions for CloudNative.CloudEvents 6 | cncf;cloudnative;cloudevents;events;mqtt 7 | 8.0 8 | 3.$(MinorVersion).$(PatchVersion) 9 | 3.$(PackageValidationMinor).0 10 | enable 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Mqtt/MqttExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using MQTTnet; 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace CloudNative.CloudEvents.Mqtt 11 | { 12 | /// 13 | /// Extension methods to convert between CloudEvents and MQTT messages. 14 | /// 15 | public static class MqttExtensions 16 | { 17 | /// 18 | /// Converts this MQTT message into a CloudEvent object. 19 | /// 20 | /// The MQTT message to convert. Must not be null. 21 | /// The event formatter to use to parse the CloudEvent. Must not be null. 22 | /// The extension attributes to use when parsing the CloudEvent. May be null. 23 | /// A reference to a validated CloudEvent instance. 24 | public static CloudEvent ToCloudEvent(this MqttApplicationMessage message, 25 | CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => 26 | ToCloudEvent(message, formatter, (IEnumerable?) extensionAttributes); 27 | 28 | /// 29 | /// Converts this MQTT message into a CloudEvent object. 30 | /// 31 | /// The MQTT message to convert. Must not be null. 32 | /// The event formatter to use to parse the CloudEvent. Must not be null. 33 | /// The extension attributes to use when parsing the CloudEvent. May be null. 34 | /// A reference to a validated CloudEvent instance. 35 | public static CloudEvent ToCloudEvent(this MqttApplicationMessage message, 36 | CloudEventFormatter formatter, IEnumerable? extensionAttributes) 37 | { 38 | Validation.CheckNotNull(formatter, nameof(formatter)); 39 | Validation.CheckNotNull(message, nameof(message)); 40 | 41 | // TODO: Determine if there's a sensible content type we should apply. 42 | return formatter.DecodeStructuredModeMessage(message.PayloadSegment, contentType: null, extensionAttributes); 43 | } 44 | 45 | // TODO: Support both binary and structured mode. 46 | /// 47 | /// Converts a CloudEvent to . 48 | /// 49 | /// The CloudEvent to convert. Must not be null, and must be a valid CloudEvent. 50 | /// Content mode. Currently only structured mode is supported. 51 | /// The formatter to use within the conversion. Must not be null. 52 | /// The MQTT topic for the message. May be null. 53 | public static MqttApplicationMessage ToMqttApplicationMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter, string? topic) 54 | { 55 | Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); 56 | Validation.CheckNotNull(formatter, nameof(formatter)); 57 | 58 | switch (contentMode) 59 | { 60 | case ContentMode.Structured: 61 | return new MqttApplicationMessage 62 | { 63 | Topic = topic, 64 | PayloadSegment = BinaryDataUtilities.GetArraySegment(formatter.EncodeStructuredModeMessage(cloudEvent, out _)) 65 | }; 66 | default: 67 | throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.NewtonsoftJson/CloudNative.CloudEvents.NewtonsoftJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | JSON support for the CNCF CloudEvents SDK, based on Newtonsoft.Json. 6 | 8.0 7 | enable 8 | cncf;cloudnative;cloudevents;events;json;newtonsoft 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Protobuf/CloudNative.CloudEvents.Protobuf.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | Support for the Protobuf event format in for CloudNative.CloudEvents 6 | cncf;cloudnative;cloudevents;events;protobuf 7 | 10.0 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Protobuf/ProtoSchemaReflection.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Google.Protobuf.Reflection; 6 | using System; 7 | 8 | namespace CloudNative.CloudEvents.V1; 9 | 10 | /// 11 | /// Access to reflection information for the CloudEvents protobuf schema. 12 | /// 13 | /// 14 | /// This class exists for backward-compatibility, when the protobuf messages 15 | /// were generated with a file named ProtoSchema.proto instead of cloudevents.proto. 16 | /// 17 | [Obsolete($"Use {nameof(CloudeventsReflection)} instead.")] 18 | public class ProtoSchemaReflection 19 | { 20 | /// File descriptor for cloudevents.proto 21 | public static FileDescriptor Descriptor => CloudeventsReflection.Descriptor; 22 | } 23 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.Protobuf/README.md: -------------------------------------------------------------------------------- 1 | # CloudNative.CloudEvents.Protobuf 2 | 3 | This implements the Protobuf Event Format for CloudEvents. 4 | 5 | The [../../generate_protos.sh]() script is used to generate the C# code from the 6 | spec, as well as any messages used for testing. 7 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents.SystemTextJson/CloudNative.CloudEvents.SystemTextJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | JSON support for the CNCF CloudEvents SDK, based on System.Text.Json. 6 | 8.0 7 | cncf;cloudnative;cloudevents;events;json;systemtextjson 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Runtime.CompilerServices; 6 | 7 | [assembly: InternalsVisibleTo("CloudNative.CloudEvents.UnitTests,PublicKey=" 8 | + "0024000004800000940000000602000000240000525341310004000001000100e945e99352d0b8" 9 | + "90ddb645995bc05ef5a22497d97e78196b9f6148ea33b0c1b219f0c28df523878d1d8c9d042a02" 10 | + "f005777461dffe455b348f82b39fcbc64985ef091295c0ad2dcb265c23589e9ce8e48dbe84c8e1" 11 | + "7fc37555938b2669aea7575cee288809065aa9dc04dff67ce1dfc5a3167770323c1a2c632f0eb2" 12 | + "f8c64acf")] -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/CloudEventFormatterAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using System; 7 | using System.Reflection; 8 | 9 | namespace CloudNative.CloudEvents 10 | { 11 | /// 12 | /// Indicates the type for the "target" type on which this attribute is placed. 13 | /// The formatter type is expected to be a concrete type derived from , 14 | /// and must have a public parameterless constructor. It should ensure that any decoded CloudEvents 15 | /// populate the property with an instance of the target type (or leave it 16 | /// as null if the CloudEvent has no data). 17 | /// 18 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, Inherited = true, AllowMultiple = false)] 19 | public sealed class CloudEventFormatterAttribute : Attribute 20 | { 21 | /// 22 | /// The type to use for CloudEvent formatting. Must not be null. 23 | /// 24 | public Type FormatterType { get; } 25 | 26 | /// 27 | /// Constructs an instance of the attribute for the specified formatter type. 28 | /// 29 | /// The type performing the data conversions. 30 | public CloudEventFormatterAttribute(Type formatterType) => 31 | FormatterType = formatterType; 32 | 33 | /// 34 | /// Creates a based on if 35 | /// the specified target type (or an ancestor) has the attribute applied to it. This method does not 36 | /// perform any caching; callers may wish to cache the results themselves. 37 | /// 38 | /// The type for which to create a formatter if possible. Must not be null 39 | /// The target type is decorated with this attribute, but the 40 | /// type cannot be instantiated or does not derive from . 41 | /// A new instance of the specified formatter, or null if the type is not decorated with this attribute. 42 | public static CloudEventFormatter? CreateFormatter(Type targetType) 43 | { 44 | Validation.CheckNotNull(targetType, nameof(targetType)); 45 | var attribute = targetType.GetCustomAttribute(inherit: true); 46 | if (attribute is null) 47 | { 48 | return null; 49 | } 50 | 51 | // It's fine for the converter creation to fail: we'll try it on every attempt, 52 | // and always end up with an exception. 53 | var formatterType = attribute.FormatterType; 54 | if (formatterType is null) 55 | { 56 | throw new ArgumentException($"The {nameof(CloudEventFormatterAttribute)} on type {targetType} has no converter type specified.", nameof(targetType)); 57 | } 58 | 59 | object? instance; 60 | try 61 | { 62 | instance = Activator.CreateInstance(formatterType); 63 | } 64 | catch (Exception e) 65 | { 66 | throw new ArgumentException($"Unable to create CloudEvent formatter for target type {targetType}", nameof(targetType), e); 67 | } 68 | 69 | var formatter = instance as CloudEventFormatter; 70 | if (formatter is null) 71 | { 72 | throw new ArgumentException($"CloudEventFormatter type {formatterType} does not derive from {nameof(CloudEventFormatter)}.", nameof(targetType)); 73 | } 74 | 75 | return formatter; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1;net8.0 5 | CNCF CloudEvents SDK 6 | latest 7 | enable 8 | cloudnative;cloudevents;events 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | True 20 | True 21 | Strings.resx 22 | 23 | 24 | 25 | 26 | 27 | ResXFileCodeGenerator 28 | Strings.Designer.cs 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace CloudNative.CloudEvents 8 | { 9 | /// 10 | /// Extension methods on collections. Some of these already exist in newer 11 | /// framework versions. 12 | /// 13 | internal static class CollectionExtensions 14 | { 15 | // Note: this is a bit more specialized than the versoin in the framework, to make defaulting simpler to handle. 16 | internal static TValue? GetValueOrDefault(this IReadOnlyDictionary dictionary, TKey key) 17 | where TValue : class => 18 | dictionary.TryGetValue(key, out var value) ? value : default(TValue?); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/ContentMode.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | namespace CloudNative.CloudEvents 6 | { 7 | /// 8 | /// ContentMode enumeration for protocol bindings. 9 | /// 10 | public enum ContentMode 11 | { 12 | /// 13 | /// Structured mode. The complete CloudEvent is contained in the transport body 14 | /// 15 | Structured, 16 | /// 17 | /// Binary mode. The CloudEvent is projected onto the transport frame 18 | /// 19 | Binary 20 | } 21 | } -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Core/CloudEventAttributeTypeOrdinal.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | namespace CloudNative.CloudEvents.Core 6 | { 7 | /// 8 | /// Enum of attribute types, to allow efficient switching over . 9 | /// Each attribute type has a unique value, returned by . 10 | /// 11 | /// 12 | /// This type is in the "Core" namespace and exposed via CloudEventAttributeTypes as relatively few consumers will need to use it. 13 | /// 14 | public enum CloudEventAttributeTypeOrdinal 15 | { 16 | // Note: changing the values here is a breaking change. 17 | 18 | /// 19 | /// Ordinal for 20 | /// 21 | Binary = 0, 22 | /// 23 | /// Ordinal for 24 | /// 25 | Boolean = 1, 26 | /// 27 | /// Ordinal for 28 | /// 29 | Integer = 2, 30 | /// 31 | /// Ordinal for 32 | /// 33 | String = 3, 34 | /// 35 | /// Ordinal for 36 | /// 37 | Uri = 4, 38 | /// 39 | /// Ordinal for 40 | /// 41 | UriReference = 5, 42 | /// 43 | /// Ordinal for 44 | /// 45 | Timestamp = 6 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Core/CloudEventAttributeTypes.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | namespace CloudNative.CloudEvents.Core 6 | { 7 | /// 8 | /// Utility methods for working with , in contexts 9 | /// where the functionality is required by formatter/protocol binding implementations, 10 | /// but we want to obscure it from other users. 11 | /// 12 | public static class CloudEventAttributeTypes 13 | { 14 | /// 15 | /// Returns the associated with , 16 | /// for convenient switching over attribute types. 17 | /// 18 | /// The attribute type. Must not be null. 19 | /// The ordinal enum value associated with the attribute type. 20 | public static CloudEventAttributeTypeOrdinal GetOrdinal(CloudEventAttributeType type) 21 | { 22 | Validation.CheckNotNull(type, nameof(type)); 23 | return type.Ordinal; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Core/MimeUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Net.Http.Headers; 8 | using System.Net.Mime; 9 | using System.Text; 10 | 11 | namespace CloudNative.CloudEvents.Core 12 | { 13 | /// 14 | /// Utility methods around MIME. 15 | /// 16 | public static class MimeUtilities 17 | { 18 | /// 19 | /// The media type (also known as MIME type) for CloudEvents. Related media types 20 | /// (e.g. for a batch of CloudEvents, or with a specific format) usually begin with this string. 21 | /// 22 | public static string MediaType { get; } = "application/cloudevents"; 23 | 24 | /// 25 | /// The media type to use for batch mode. This is usually suffixed with a format-specific 26 | /// type, e.g. "+json". 27 | /// 28 | public static string BatchMediaType { get; } = MediaType + "-batch"; 29 | 30 | /// 31 | /// Returns an encoding from a content type, defaulting to UTF-8. 32 | /// 33 | /// The content type, or null if no content type is known. 34 | /// An encoding suitable for the charset specified in , 35 | /// or UTF-8 if no charset has been specified. 36 | public static Encoding GetEncoding(ContentType? contentType) => 37 | contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8; 38 | 39 | /// 40 | /// Converts a into a . 41 | /// 42 | /// The header value to convert. May be null. 43 | /// The converted content type, or null if is null. 44 | public static ContentType? ToContentType(MediaTypeHeaderValue? headerValue) => 45 | headerValue is null ? null : new ContentType(headerValue.ToString()); 46 | 47 | /// 48 | /// Converts a into a . 49 | /// 50 | /// The content type to convert. May be null. 51 | /// The converted media type header value, or null if is null. 52 | public static MediaTypeHeaderValue? ToMediaTypeHeaderValue(ContentType? contentType) 53 | { 54 | if (contentType is null) 55 | { 56 | return null; 57 | } 58 | var header = new MediaTypeHeaderValue(contentType.MediaType); 59 | foreach (string parameterName in contentType.Parameters.Keys) 60 | { 61 | header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName])); 62 | } 63 | return header; 64 | } 65 | 66 | /// 67 | /// Creates a from the given value, or returns null 68 | /// if the input is null. 69 | /// 70 | /// The content type textual value. May be null. 71 | /// The converted content type, or null if is null. 72 | public static ContentType? CreateContentTypeOrNull(string? contentType) => 73 | contentType is null ? null : new ContentType(contentType); 74 | 75 | /// 76 | /// Determines whether the given content type denotes a (non-batch) CloudEvent. 77 | /// 78 | /// The content type to check. May be null, in which case the result is false. 79 | /// true if the given content type denotes a (non-batch) CloudEvent; false otherwise 80 | public static bool IsCloudEventsContentType([NotNullWhen(true)] string? contentType) => 81 | contentType is string && 82 | contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) && 83 | !contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); 84 | 85 | /// 86 | /// Determines whether the given content type denotes a CloudEvent batch. 87 | /// 88 | /// The content type to check. May be null, in which case the result is false. 89 | /// true if the given content type represents a CloudEvent batch; false otherwise 90 | public static bool IsCloudEventsBatchContentType([NotNullWhen(true)] string? contentType) => 91 | contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Extensions/Partitioning.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace CloudNative.CloudEvents.Extensions 10 | { 11 | /// 12 | /// Support for the partitioning 13 | /// CloudEvent extension. 14 | /// 15 | public static class Partitioning 16 | { 17 | /// 18 | /// representing the 'partitionkey' extension attribute. 19 | /// 20 | public static CloudEventAttribute PartitionKeyAttribute { get; } = 21 | CloudEventAttribute.CreateExtension("partitionkey", CloudEventAttributeType.String); 22 | 23 | /// 24 | /// A read-only sequence of all attributes related to the partitioning extension. 25 | /// 26 | public static IEnumerable AllAttributes { get; } = 27 | new[] { PartitionKeyAttribute }.ToList().AsReadOnly(); 28 | 29 | /// 30 | /// Sets the on the given . 31 | /// 32 | /// The CloudEvent on which to set the attribute. Must not be null. 33 | /// The partition key to set. May be null, in which case the attribute is 34 | /// removed from . 35 | /// , for convenient method chaining. 36 | public static CloudEvent SetPartitionKey(this CloudEvent cloudEvent, string? partitionKey) 37 | { 38 | Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); 39 | cloudEvent[PartitionKeyAttribute] = partitionKey; 40 | return cloudEvent; 41 | } 42 | 43 | /// 44 | /// Retrieves the from the given . 45 | /// 46 | /// The CloudEvent from which to retrieve the attribute. Must not be null. 47 | /// The partition key, or null if does not have a partition key set. 48 | public static string? GetPartitionKey(this CloudEvent cloudEvent) => 49 | (string?) Validation.CheckNotNull(cloudEvent, nameof(cloudEvent))[PartitionKeyAttribute]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Extensions/Sampling.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace CloudNative.CloudEvents.Extensions 11 | { 12 | /// 13 | /// Support for the sampling 14 | /// CloudEvent extension. 15 | /// 16 | public static class Sampling 17 | { 18 | /// 19 | /// representing the 'sampledrate' extension attribute. 20 | /// 21 | public static CloudEventAttribute SampledRateAttribute { get; } = 22 | CloudEventAttribute.CreateExtension("sampledrate", CloudEventAttributeType.Integer, PositiveInteger); 23 | 24 | /// 25 | /// A read-only sequence of all attributes related to the sampling extension. 26 | /// 27 | public static IEnumerable AllAttributes { get; } = 28 | new[] { SampledRateAttribute }.ToList().AsReadOnly(); 29 | 30 | /// 31 | /// Sets the on the given . 32 | /// 33 | /// The CloudEvent on which to set the attribute. Must not be null. 34 | /// The sampled rate to set. May be null, in which case the attribute is 35 | /// removed from . If this value is non-null, it must be positive. 36 | /// , for convenient method chaining. 37 | public static CloudEvent SetSampledRate(this CloudEvent cloudEvent, int? sampledRate) 38 | { 39 | Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); 40 | cloudEvent[SampledRateAttribute] = sampledRate; 41 | return cloudEvent; 42 | } 43 | 44 | /// 45 | /// Retrieves the from the given . 46 | /// 47 | /// The CloudEvent from which to retrieve the attribute. Must not be null. 48 | /// The sampled rate, or null if does not have a sampled rate set. 49 | public static int? GetSampledRate(this CloudEvent cloudEvent) => 50 | (int?) Validation.CheckNotNull(cloudEvent, nameof(cloudEvent))[SampledRateAttribute]; 51 | 52 | private static void PositiveInteger(object value) 53 | { 54 | if ((int) value <= 0) 55 | { 56 | throw new ArgumentOutOfRangeException("Sampled rate must be positive."); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Extensions/Sequence.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace CloudNative.CloudEvents.Extensions 11 | { 12 | /// 13 | /// Support for the sequence 14 | /// CloudEvent extension. 15 | /// 16 | public static class Sequence 17 | { 18 | // TODO: Potentially make it public. But public constants make me nervous. 19 | private const string IntegerType = "Integer"; 20 | 21 | /// 22 | /// representing the 'sequence' extension attribute. 23 | /// 24 | public static CloudEventAttribute SequenceAttribute { get; } = 25 | CloudEventAttribute.CreateExtension("sequence", CloudEventAttributeType.String); 26 | 27 | /// 28 | /// representing the 'sequencetype' extension attribute. 29 | /// 30 | public static CloudEventAttribute SequenceTypeAttribute { get; } = 31 | CloudEventAttribute.CreateExtension("sequencetype", CloudEventAttributeType.String); 32 | 33 | private static CloudEventAttribute SurrogateIntegerAttribute { get; } = 34 | CloudEventAttribute.CreateExtension("int", CloudEventAttributeType.Integer); 35 | 36 | /// 37 | /// A read-only sequence of all attributes related to the sequence extension. 38 | /// 39 | public static IEnumerable AllAttributes { get; } = 40 | new[] { SequenceAttribute, SequenceTypeAttribute }.ToList().AsReadOnly(); 41 | 42 | /// 43 | /// Sets both and attributes based on the specified value. 44 | /// 45 | /// The CloudEvent on which to set the attributes. Must not be null. 46 | /// The sequence value to set. May be null, in which case both attributes are removed from 47 | /// . 48 | /// is a non-null value for an unsupported sequence type. 49 | /// , for convenient method chaining. 50 | public static CloudEvent SetSequence(this CloudEvent cloudEvent, object? value) 51 | { 52 | Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); 53 | if (value is null) 54 | { 55 | cloudEvent[SequenceAttribute] = null; 56 | cloudEvent[SequenceTypeAttribute] = null; 57 | } 58 | else if (value is int) 59 | { 60 | // TODO: Validation? Would be nice to get a sort of "surrogate" attribute here... 61 | cloudEvent[SequenceAttribute] = SurrogateIntegerAttribute.Format(value); 62 | cloudEvent[SequenceTypeAttribute] = IntegerType; 63 | } 64 | else 65 | { 66 | throw new ArgumentException($"No sequence type known for type {value.GetType()}"); 67 | } 68 | return cloudEvent; 69 | } 70 | 71 | // TODO: Naming of these extension methods 72 | 73 | /// 74 | /// Retrieves the value from the event, without any 75 | /// further transformation. 76 | /// 77 | /// The CloudEvent from which to retrieve the attribute. Must not be null. 78 | /// The value, as a string, or null if the attribute is not set. 79 | public static string? GetSequenceString(this CloudEvent cloudEvent) => 80 | (string?) Validation.CheckNotNull(cloudEvent, nameof(cloudEvent))[SequenceAttribute]; 81 | 82 | /// 83 | /// Retrieves the value from the event, without any 84 | /// further transformation. 85 | /// 86 | /// The CloudEvent from which to retrieve the attribute. Must not be null. 87 | /// The value, as a string, or null if the attribute is not set. 88 | public static string? GetSequenceType(this CloudEvent cloudEvent) => 89 | (string?) Validation.CheckNotNull(cloudEvent, nameof(cloudEvent))[SequenceTypeAttribute]; 90 | 91 | /// 92 | /// Retrieves the value from the event, 93 | /// parsing it according to the value of . 94 | /// If no type is present in the event, the string value is 95 | /// returned without further transformation. 96 | /// 97 | /// 98 | /// The value of from , transformed 99 | /// based on the value of , or null if the attribute is not set. 100 | /// The is present, but unknown to this library. 101 | public static object? GetSequenceValue(this CloudEvent cloudEvent) 102 | { 103 | Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); 104 | var sequence = GetSequenceString(cloudEvent); 105 | if (sequence is null) 106 | { 107 | return null; 108 | } 109 | var type = GetSequenceType(cloudEvent); 110 | if (type == null) 111 | { 112 | return sequence; 113 | } 114 | if (type == IntegerType) 115 | { 116 | return SurrogateIntegerAttribute.Parse(sequence); 117 | } 118 | throw new InvalidOperationException($"Unknown sequence type '{type}'"); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Net; 9 | using System.Net.Mime; 10 | using System.Threading.Tasks; 11 | 12 | namespace CloudNative.CloudEvents.Http 13 | { 14 | /// 15 | /// Extension methods for and related types. 16 | /// 17 | public static class HttpWebExtensions 18 | { 19 | // TODO: HttpWebResponse as well? 20 | 21 | /// 22 | /// Copies a into the specified . 23 | /// 24 | /// CloudEvent to copy. Must not be null, and must be a valid CloudEvent. 25 | /// The request to populate. Must not be null. 26 | /// Content mode (structured or binary) 27 | /// The formatter to use within the conversion. Must not be null. 28 | /// A task representing the asynchronous operation. 29 | public static async Task CopyToHttpWebRequestAsync(this CloudEvent cloudEvent, HttpWebRequest destination, 30 | ContentMode contentMode, CloudEventFormatter formatter) 31 | { 32 | Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); 33 | Validation.CheckNotNull(destination, nameof(destination)); 34 | Validation.CheckNotNull(formatter, nameof(formatter)); 35 | 36 | ReadOnlyMemory content; 37 | // The content type to include in the ContentType header - may be the data content type, or the formatter's content type. 38 | ContentType? contentType; 39 | switch (contentMode) 40 | { 41 | case ContentMode.Structured: 42 | content = formatter.EncodeStructuredModeMessage(cloudEvent, out contentType); 43 | break; 44 | case ContentMode.Binary: 45 | content = formatter.EncodeBinaryModeEventData(cloudEvent); 46 | contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent)); 47 | break; 48 | default: 49 | throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}"); 50 | } 51 | 52 | if (contentType is object) 53 | { 54 | destination.ContentType = contentType.ToString(); 55 | } 56 | else if (content.Length != 0) 57 | { 58 | throw new ArgumentException(Strings.ErrorContentTypeUnspecified, nameof(cloudEvent)); 59 | } 60 | 61 | // Map headers in either mode. 62 | // Including the headers in structured mode is optional in the spec (as they're already within the body) but 63 | // can be useful. 64 | destination.Headers.Add(HttpUtilities.SpecVersionHttpHeader, HttpUtilities.EncodeHeaderValue(cloudEvent.SpecVersion.VersionId)); 65 | foreach (var attributeAndValue in cloudEvent.GetPopulatedAttributes()) 66 | { 67 | var attribute = attributeAndValue.Key; 68 | var value = attributeAndValue.Value; 69 | if (attribute != cloudEvent.SpecVersion.DataContentTypeAttribute) 70 | { 71 | string headerValue = HttpUtilities.EncodeHeaderValue(attribute.Format(value)); 72 | destination.Headers.Add(HttpUtilities.HttpHeaderPrefix + attribute.Name, headerValue); 73 | } 74 | } 75 | await BinaryDataUtilities.CopyToStreamAsync(content, destination.GetRequestStream()).ConfigureAwait(false); 76 | } 77 | 78 | /// 79 | /// Copies a batch into the specified . 80 | /// 81 | /// CloudEvent batch to copy. Must not be null, and must be a valid CloudEvent. 82 | /// The request to populate. Must not be null. 83 | /// The formatter to use within the conversion. Must not be null. 84 | /// A task representing the asynchronous operation. 85 | public static async Task CopyToHttpWebRequestAsync(this IReadOnlyList cloudEvents, 86 | HttpWebRequest destination, 87 | CloudEventFormatter formatter) 88 | { 89 | Validation.CheckCloudEventBatchArgument(cloudEvents, nameof(cloudEvents)); 90 | Validation.CheckNotNull(destination, nameof(destination)); 91 | Validation.CheckNotNull(formatter, nameof(formatter)); 92 | 93 | ReadOnlyMemory content = formatter.EncodeBatchModeMessage(cloudEvents, out var contentType); 94 | destination.ContentType = contentType.ToString(); 95 | await BinaryDataUtilities.CopyToStreamAsync(content, destination.GetRequestStream()).ConfigureAwait(false); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/CloudNative.CloudEvents/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace CloudNative.CloudEvents { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("CloudNative.CloudEvents.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The 'specversion' attribute cannot be used as an indexer key for CloudEvent. 65 | /// 66 | internal static string ErrorCannotIndexBySpecVersionAttribute { 67 | get { 68 | return ResourceManager.GetString("ErrorCannotIndexBySpecVersionAttribute", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to The 'datacontenttype' attribute value must be a content-type expression compliant with RFC2046. 74 | /// 75 | internal static string ErrorContentTypeIsNotRFC2046 { 76 | get { 77 | return ResourceManager.GetString("ErrorContentTypeIsNotRFC2046", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to The 'datacontenttype' attribute value must be specified. 83 | /// 84 | internal static string ErrorContentTypeUnspecified { 85 | get { 86 | return ResourceManager.GetString("ErrorContentTypeUnspecified", resourceCulture); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 2 9 | 8 10 | 0 11 | 8 12 | $(MajorVersion).$(MinorVersion).$(PatchVersion) 13 | 19 | 2.$(PackageValidationMinor).0 20 | 21 | 22 | $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) 23 | 24 | 25 | $(RepoRoot)/CloudEventsSdk.snk 26 | True 27 | True 28 | True 29 | true 30 | true 31 | true 32 | true 33 | 34 | 35 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 36 | true 37 | true 38 | nuget-icon.png 39 | 40 | git 41 | https://github.com/cloudevents/sdk-csharp 42 | Apache-2.0 43 | https://cloudevents.io 44 | Copyright Cloud Native Foundation 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.IntegrationTests/AspNetCore/CloudEventControllerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.AspNetCoreSample; 6 | using CloudNative.CloudEvents.Http; 7 | using CloudNative.CloudEvents.NewtonsoftJson; 8 | using Microsoft.AspNetCore.Mvc.Testing; 9 | using System; 10 | using System.Net; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | 14 | namespace CloudNative.CloudEvents.IntegrationTests.AspNetCore 15 | { 16 | public class CloudEventControllerTests : IClassFixture> 17 | { 18 | private readonly WebApplicationFactory _factory; 19 | 20 | public CloudEventControllerTests(WebApplicationFactory factory) 21 | { 22 | _factory = factory; 23 | } 24 | 25 | [Theory] 26 | [InlineData(ContentMode.Structured)] 27 | [InlineData(ContentMode.Binary)] 28 | public async Task Controller_WithValidCloudEvent_NoContent_DeserializesUsingPipeline(ContentMode contentMode) 29 | { 30 | // Arrange 31 | var expectedExtensionKey = "comexampleextension1"; 32 | var expectedExtensionValue = Guid.NewGuid().ToString(); 33 | var cloudEvent = new CloudEvent 34 | { 35 | Type = "test-type-æøå", 36 | Source = new Uri("urn:integration-tests"), 37 | Id = Guid.NewGuid().ToString(), 38 | DataContentType = "application/json", 39 | Data = new { key1 = "value1" }, 40 | [expectedExtensionKey] = expectedExtensionValue 41 | }; 42 | 43 | var httpContent = cloudEvent.ToHttpContent(contentMode, new JsonEventFormatter()); 44 | 45 | // Act 46 | var result = await _factory.CreateClient().PostAsync("/api/events/receive", httpContent); 47 | 48 | // Assert 49 | string resultContent = await result.Content.ReadAsStringAsync(); 50 | Assert.Equal(HttpStatusCode.OK, result.StatusCode); 51 | Assert.Contains(cloudEvent.Id, resultContent); 52 | Assert.Contains(cloudEvent.Type, resultContent); 53 | Assert.Contains($"\"{expectedExtensionKey}\": \"{expectedExtensionValue}\"", resultContent); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.IntegrationTests/CloudNative.CloudEvents.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.IntegrationTests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:65094/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "CloudNative.CloudEvents.IntegrationTests": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:65095/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Amqp/AmqpTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Amqp; 6 | using Amqp.Framing; 7 | using CloudNative.CloudEvents.NewtonsoftJson; 8 | using System; 9 | using System.Net.Mime; 10 | using System.Text; 11 | using Xunit; 12 | using static CloudNative.CloudEvents.UnitTests.TestHelpers; 13 | 14 | namespace CloudNative.CloudEvents.Amqp.UnitTests 15 | { 16 | public class AmqpTest 17 | { 18 | [Fact] 19 | public void AmqpStructuredMessageTest() 20 | { 21 | // The AMQPNetLite library is factored such that we don't need to do a wire test here. 22 | var cloudEvent = CreateSampleCloudEvent(); 23 | var message = cloudEvent.ToAmqpMessage(ContentMode.Structured, new JsonEventFormatter()); 24 | Assert.True(message.IsCloudEvent()); 25 | AssertDecodeThenEqual(cloudEvent, message); 26 | } 27 | 28 | [Fact] 29 | public void AmqpBinaryMessageTest() 30 | { 31 | // The AMQPNetLite library is factored such that we don't need to do a wire test here. 32 | var cloudEvent = CreateSampleCloudEvent(); 33 | var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter()); 34 | Assert.True(message.IsCloudEvent()); 35 | var encodedAmqpMessage = message.Encode(); 36 | 37 | var message1 = Message.Decode(encodedAmqpMessage); 38 | Assert.True(message1.IsCloudEvent()); 39 | var receivedCloudEvent = message1.ToCloudEvent(new JsonEventFormatter()); 40 | 41 | AssertCloudEventsEqual(cloudEvent, receivedCloudEvent); 42 | } 43 | 44 | [Fact] 45 | public void BinaryMode_ContentTypeCanBeInferredByFormatter() 46 | { 47 | var cloudEvent = new CloudEvent 48 | { 49 | Data = "plain text" 50 | }.PopulateRequiredAttributes(); 51 | 52 | var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter()); 53 | Assert.Equal("application/json", message.Properties.ContentType); 54 | } 55 | 56 | [Fact] 57 | public void AmqpNormalizesTimestampsToUtc() 58 | { 59 | var cloudEvent = new CloudEvent 60 | { 61 | Type = "com.github.pull.create", 62 | Source = new Uri("https://github.com/cloudevents/spec/pull/123"), 63 | Id = "A234-1234-1234", 64 | // 2018-04-05T18:31:00+01:00 => 2018-04-05T17:31:00Z 65 | Time = new DateTimeOffset(2018, 4, 5, 18, 31, 0, TimeSpan.FromHours(1)) 66 | }; 67 | 68 | var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter()); 69 | var encodedAmqpMessage = message.Encode(); 70 | 71 | var message1 = Message.Decode(encodedAmqpMessage); 72 | var receivedCloudEvent = message1.ToCloudEvent(new JsonEventFormatter()); 73 | 74 | AssertTimestampsEqual("2018-04-05T17:31:00Z", receivedCloudEvent.Time!.Value); 75 | } 76 | 77 | [Fact] 78 | public void EncodeTextDataInBinaryMode_PopulatesDataProperty() 79 | { 80 | var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); 81 | cloudEvent.DataContentType = "text/plain"; 82 | cloudEvent.Data = "some text"; 83 | 84 | var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter()); 85 | var body = Assert.IsType(message.BodySection); 86 | var text = Encoding.UTF8.GetString(body.Binary); 87 | Assert.Equal("some text", text); 88 | } 89 | 90 | [Fact] 91 | public void DefaultPrefix() 92 | { 93 | var cloudEvent = CreateSampleCloudEvent(); 94 | 95 | var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter()); 96 | Assert.Equal(cloudEvent.Id, message.ApplicationProperties["cloudEvents:id"]); 97 | AssertDecodeThenEqual(cloudEvent, message); 98 | } 99 | 100 | [Fact] 101 | public void UnderscorePrefix() 102 | { 103 | var cloudEvent = CreateSampleCloudEvent(); 104 | var message = cloudEvent.ToAmqpMessageWithUnderscorePrefix(ContentMode.Binary, new JsonEventFormatter()); 105 | Assert.Equal(cloudEvent.Id, message.ApplicationProperties["cloudEvents_id"]); 106 | AssertDecodeThenEqual(cloudEvent, message); 107 | } 108 | 109 | [Fact] 110 | public void ColonPrefix() 111 | { 112 | var cloudEvent = CreateSampleCloudEvent(); 113 | var message = cloudEvent.ToAmqpMessageWithColonPrefix(ContentMode.Binary, new JsonEventFormatter()); 114 | Assert.Equal(cloudEvent.Id, message.ApplicationProperties["cloudEvents:id"]); 115 | AssertDecodeThenEqual(cloudEvent, message); 116 | } 117 | 118 | private void AssertDecodeThenEqual(CloudEvent cloudEvent, Message message) 119 | { 120 | var encodedAmqpMessage = message.Encode(); 121 | 122 | var message1 = Message.Decode(encodedAmqpMessage); 123 | var receivedCloudEvent = message1.ToCloudEvent(new JsonEventFormatter()); 124 | AssertCloudEventsEqual(cloudEvent, receivedCloudEvent); 125 | } 126 | 127 | /// 128 | /// Returns a CloudEvent with XML data and an extension. 129 | /// 130 | private static CloudEvent CreateSampleCloudEvent() => new CloudEvent 131 | { 132 | Type = "com.github.pull.create", 133 | Source = new Uri("https://github.com/cloudevents/spec/pull"), 134 | Subject = "123", 135 | Id = "A234-1234-1234", 136 | Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero), 137 | DataContentType = MediaTypeNames.Text.Xml, 138 | Data = "", 139 | ["comexampleextension1"] = "value" 140 | }; 141 | } 142 | } -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/AspNetCore/HttpRequestExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using CloudNative.CloudEvents.NewtonsoftJson; 7 | using Microsoft.AspNetCore.Http; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Net.Mime; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | using static CloudNative.CloudEvents.UnitTests.TestHelpers; 14 | 15 | namespace CloudNative.CloudEvents.AspNetCore.UnitTests 16 | { 17 | public class HttpRequestExtensionsTest 18 | { 19 | public static TheoryData?> SingleCloudEventMessages = new TheoryData?> 20 | { 21 | { 22 | "Binary", 23 | "text/plain", 24 | new Dictionary 25 | { 26 | { "ce-specversion", "1.0" }, 27 | { "ce-type", "test-type" }, 28 | { "ce-id", "test-id" }, 29 | { "ce-source", "//test" } 30 | } 31 | }, 32 | { 33 | "Structured", 34 | "application/cloudevents+json", 35 | null 36 | } 37 | }; 38 | 39 | public static TheoryData?> BatchMessages = new TheoryData?> 40 | { 41 | { 42 | "Batch", 43 | "application/cloudevents-batch+json", 44 | null 45 | } 46 | }; 47 | 48 | public static TheoryData?> NonCloudEventMessages = new TheoryData?> 49 | { 50 | { 51 | "Plain text", 52 | "text/plain", 53 | null 54 | } 55 | }; 56 | 57 | [Theory] 58 | [MemberData(nameof(SingleCloudEventMessages))] 59 | public void IsCloudEvent_True(string description, string contentType, IDictionary? headers) 60 | { 61 | // Really only present for display purposes. 62 | Assert.NotNull(description); 63 | 64 | var request = CreateRequest(new byte[0], new ContentType(contentType)); 65 | CopyHeaders(headers, request); 66 | Assert.True(request.IsCloudEvent()); 67 | } 68 | 69 | [Theory] 70 | [MemberData(nameof(BatchMessages))] 71 | [MemberData(nameof(NonCloudEventMessages))] 72 | public void IsCloudEvent_False(string description, string contentType, IDictionary? headers) 73 | { 74 | // Really only present for display purposes. 75 | Assert.NotNull(description); 76 | 77 | var request = CreateRequest(new byte[0], new ContentType(contentType)); 78 | CopyHeaders(headers, request); 79 | Assert.False(request.IsCloudEvent()); 80 | } 81 | 82 | [Theory] 83 | [MemberData(nameof(BatchMessages))] 84 | public void IsCloudEventBatch_True(string description, string contentType, IDictionary? headers) 85 | { 86 | // Really only present for display purposes. 87 | Assert.NotNull(description); 88 | 89 | var request = CreateRequest(new byte[0], new ContentType(contentType)); 90 | CopyHeaders(headers, request); 91 | Assert.True(request.IsCloudEventBatch()); 92 | } 93 | 94 | [Theory] 95 | [MemberData(nameof(SingleCloudEventMessages))] 96 | [MemberData(nameof(NonCloudEventMessages))] 97 | public void IsCloudEventBatch_False(string description, string contentType, IDictionary? headers) 98 | { 99 | // Really only present for display purposes. 100 | Assert.NotNull(description); 101 | 102 | var request = CreateRequest(new byte[0], new ContentType(contentType)); 103 | CopyHeaders(headers, request); 104 | Assert.False(request.IsCloudEventBatch()); 105 | } 106 | 107 | // TODO: Non-batch conversion tests 108 | 109 | [Fact] 110 | public async Task ToCloudEventBatchAsync_Valid() 111 | { 112 | var batch = CreateSampleBatch(); 113 | 114 | var formatter = new JsonEventFormatter(); 115 | var contentBytes = formatter.EncodeBatchModeMessage(batch, out var contentType); 116 | 117 | AssertBatchesEqual(batch, await CreateRequest(contentBytes, contentType).ToCloudEventBatchAsync(formatter, EmptyExtensionArray)); 118 | AssertBatchesEqual(batch, await CreateRequest(contentBytes, contentType).ToCloudEventBatchAsync(formatter, EmptyExtensionSequence)); 119 | } 120 | 121 | [Fact] 122 | public async Task ToCloudEventBatchAsync_Invalid() 123 | { 124 | // Most likely accident: calling ToCloudEventBatchAsync with a single event in structured mode. 125 | var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); 126 | var formatter = new JsonEventFormatter(); 127 | var contentBytes = formatter.EncodeStructuredModeMessage(cloudEvent, out var contentType); 128 | await Assert.ThrowsAsync(() => CreateRequest(contentBytes, contentType).ToCloudEventBatchAsync(formatter, EmptyExtensionArray)); 129 | await Assert.ThrowsAsync(() => CreateRequest(contentBytes, contentType).ToCloudEventBatchAsync(formatter, EmptyExtensionSequence)); 130 | } 131 | 132 | private static HttpRequest CreateRequest(ReadOnlyMemory content, ContentType contentType) 133 | { 134 | var request = new DefaultHttpContext().Request; 135 | request.ContentType = contentType.ToString(); 136 | request.Body = BinaryDataUtilities.AsStream(content); 137 | return request; 138 | } 139 | 140 | private static void CopyHeaders(IDictionary? source, HttpRequest target) 141 | { 142 | if (source is null) 143 | { 144 | return; 145 | } 146 | foreach (var header in source) 147 | { 148 | target.Headers.Append(header.Key, header.Value); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Avro/Helpers/FakeGenericRecordSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Avro.Generic; 6 | using CloudNative.CloudEvents.Avro.Interfaces; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.IO; 10 | 11 | namespace CloudNative.CloudEvents.UnitTests.Avro.Helpers; 12 | 13 | internal class FakeGenericRecordSerializer : IGenericRecordSerializer 14 | { 15 | public byte[]? SerializeResponse { get; private set; } 16 | public GenericRecord DeserializeResponse { get; private set; } 17 | public int DeserializeCalls { get; private set; } = 0; 18 | public int SerializeCalls { get; private set; } = 0; 19 | 20 | public FakeGenericRecordSerializer() 21 | { 22 | DeserializeResponse = new GenericRecord(CloudEvents.Avro.AvroEventFormatter.AvroSchema); 23 | } 24 | 25 | public GenericRecord Deserialize(Stream messageBody) 26 | { 27 | DeserializeCalls++; 28 | return DeserializeResponse; 29 | } 30 | 31 | public ReadOnlyMemory Serialize(GenericRecord value) 32 | { 33 | SerializeCalls++; 34 | return SerializeResponse; 35 | } 36 | 37 | public void SetSerializeResponse(byte[] response) => SerializeResponse = response; 38 | 39 | public void SetDeserializeResponseAttributes(string id, string type, string source) => 40 | DeserializeResponse.Add("attribute", new Dictionary() 41 | { 42 | { CloudEventsSpecVersion.SpecVersionAttribute.Name, CloudEventsSpecVersion.Default.VersionId }, 43 | { CloudEventsSpecVersion.Default.IdAttribute.Name, id}, 44 | { CloudEventsSpecVersion.Default.TypeAttribute.Name, type}, 45 | { CloudEventsSpecVersion.Default.SourceAttribute.Name, source} 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterAttributeTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.Mime; 8 | using Xunit; 9 | 10 | namespace CloudNative.CloudEvents.UnitTests 11 | { 12 | public class CloudEventFormatterAttributeTest 13 | { 14 | [Fact] 15 | public void CreateFormatter_NoAttribute() => 16 | Assert.Null(CloudEventFormatterAttribute.CreateFormatter(typeof(NoAttribute))); 17 | 18 | [Fact] 19 | public void CreateFormatter_Valid() => 20 | Assert.IsType(CloudEventFormatterAttribute.CreateFormatter(typeof(ValidAttribute))); 21 | 22 | [Theory] 23 | [InlineData(typeof(NonInstantiableAttribute))] 24 | [InlineData(typeof(NonEventFormatterAttribute))] 25 | [InlineData(typeof(NullFormatterAttribute))] 26 | public void CreateFormatter_Invalid(Type targetType) => 27 | Assert.Throws(() => CloudEventFormatterAttribute.CreateFormatter(targetType)); 28 | 29 | public class NoAttribute 30 | { 31 | } 32 | 33 | [CloudEventFormatter(typeof(AbstractCloudEventFormatter))] 34 | public class NonInstantiableAttribute 35 | { 36 | } 37 | 38 | [CloudEventFormatter(null!)] 39 | public class NullFormatterAttribute 40 | { 41 | } 42 | 43 | [CloudEventFormatter(typeof(object))] 44 | public class NonEventFormatterAttribute 45 | { 46 | } 47 | 48 | [CloudEventFormatter(typeof(SampleCloudEventFormatter))] 49 | public class ValidAttribute 50 | { 51 | } 52 | 53 | public abstract class AbstractCloudEventFormatter : CloudEventFormatter 54 | { 55 | } 56 | 57 | public class SampleCloudEventFormatter : CloudEventFormatter 58 | { 59 | public override IReadOnlyList DecodeBatchModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) => 60 | throw new NotImplementedException(); 61 | 62 | public override void DecodeBinaryModeEventData(ReadOnlyMemory body, CloudEvent cloudEvent) => 63 | throw new NotImplementedException(); 64 | 65 | public override CloudEvent DecodeStructuredModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) => 66 | throw new NotImplementedException(); 67 | 68 | public override ReadOnlyMemory EncodeBatchModeMessage(IEnumerable cloudEvents, out ContentType contentType) => 69 | throw new NotImplementedException(); 70 | 71 | public override ReadOnlyMemory EncodeBinaryModeEventData(CloudEvent cloudEvent) => 72 | throw new NotImplementedException(); 73 | 74 | public override ReadOnlyMemory EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) => 75 | throw new NotImplementedException(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Text; 7 | 8 | namespace CloudNative.CloudEvents.UnitTests 9 | { 10 | /// 11 | /// Extension methods for CloudEventFormatters to simplify testing. 12 | /// Often in tests we have structured mode data as strings, and usually the content type isn't important, 13 | /// so it's useful to be able to just decode that string directly. 14 | /// 15 | internal static class CloudEventFormatterExtensions 16 | { 17 | internal static CloudEvent DecodeStructuredModeText(this CloudEventFormatter eventFormatter, string text) => 18 | eventFormatter.DecodeStructuredModeMessage(Encoding.UTF8.GetBytes(text), contentType: null, extensionAttributes: null); 19 | 20 | internal static CloudEvent DecodeStructuredModeText(this CloudEventFormatter eventFormatter, string text, IEnumerable extensionAttributes) => 21 | eventFormatter.DecodeStructuredModeMessage(Encoding.UTF8.GetBytes(text), contentType: null, extensionAttributes); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/CloudEventFormatterTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net.Mime; 8 | using Xunit; 9 | 10 | namespace CloudNative.CloudEvents.UnitTests 11 | { 12 | public class CloudEventFormatterTest 13 | { 14 | [Fact] 15 | public void GetOrInferDataContentType_NullCloudEvent() 16 | { 17 | var formatter = new ContentTypeInferringFormatter(); 18 | Assert.Throws(() => formatter.GetOrInferDataContentType(null!)); 19 | } 20 | 21 | [Fact] 22 | public void GetOrInferDataContentType_NoDataOrDataContentType() 23 | { 24 | var formatter = new ContentTypeInferringFormatter(); 25 | var cloudEvent = new CloudEvent(); 26 | Assert.Null(formatter.GetOrInferDataContentType(cloudEvent)); 27 | } 28 | 29 | [Fact] 30 | public void GetOrInferDataContentType_HasDataContentType() 31 | { 32 | var formatter = new ContentTypeInferringFormatter(); 33 | var cloudEvent = new CloudEvent { DataContentType = "test/pass" }; 34 | Assert.Equal(cloudEvent.DataContentType, formatter.GetOrInferDataContentType(cloudEvent)); 35 | } 36 | 37 | [Fact] 38 | public void GetOrInferDataContentType_HasDataButNoContentType_OverriddenInferDataContentType() 39 | { 40 | var formatter = new ContentTypeInferringFormatter(); 41 | var cloudEvent = new CloudEvent { Data = "some-data" }; 42 | Assert.Equal("test/some-data", formatter.GetOrInferDataContentType(cloudEvent)); 43 | } 44 | 45 | [Fact] 46 | public void GetOrInferDataContentType_DataButNoContentType_DefaultInferDataContentType() 47 | { 48 | var formatter = new ThrowingEventFormatter(); 49 | var cloudEvent = new CloudEvent { Data = "some-data" }; 50 | Assert.Null(formatter.GetOrInferDataContentType(cloudEvent)); 51 | } 52 | 53 | private class ContentTypeInferringFormatter : ThrowingEventFormatter 54 | { 55 | protected override string? InferDataContentType(object data) => $"test/{data}"; 56 | } 57 | 58 | /// 59 | /// Event formatter that overrides every abstract method to throw NotImplementedException. 60 | /// This can be derived from (and further overridden) to easily test concrete methods 61 | /// in CloudEventFormatter itself. 62 | /// 63 | private class ThrowingEventFormatter : CloudEventFormatter 64 | { 65 | public override IReadOnlyList DecodeBatchModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) => 66 | throw new NotImplementedException(); 67 | 68 | public override void DecodeBinaryModeEventData(ReadOnlyMemory body, CloudEvent cloudEvent) => 69 | throw new NotImplementedException(); 70 | 71 | public override CloudEvent DecodeStructuredModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) => 72 | throw new NotImplementedException(); 73 | 74 | public override ReadOnlyMemory EncodeBatchModeMessage(IEnumerable cloudEvents, out ContentType contentType) => 75 | throw new NotImplementedException(); 76 | 77 | public override ReadOnlyMemory EncodeBinaryModeEventData(CloudEvent cloudEvent) => 78 | throw new NotImplementedException(); 79 | 80 | public override ReadOnlyMemory EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) => 81 | throw new NotImplementedException(); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/CloudEventsSpecVersionTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using Xunit; 7 | 8 | namespace CloudNative.CloudEvents.UnitTests 9 | { 10 | public class CloudEventsSpecVersionTest 11 | { 12 | [Theory] 13 | [InlineData(null)] 14 | [InlineData("bogus")] 15 | [InlineData("1")] 16 | public void FromVersionId_Unknown(string? versionId) => 17 | Assert.Null(CloudEventsSpecVersion.FromVersionId(versionId)); 18 | 19 | [Theory] 20 | [InlineData("1.0")] 21 | public void FromVersionId_Known(string versionId) 22 | { 23 | var version = CloudEventsSpecVersion.FromVersionId(versionId); 24 | Assert.NotNull(version); 25 | Assert.Equal(versionId, version!.VersionId); 26 | } 27 | 28 | [Fact] 29 | public void V1Source_MustBeNonEmpty() 30 | { 31 | var attribute = CloudEventsSpecVersion.V1_0.SourceAttribute; 32 | var uri = new Uri("", UriKind.RelativeOrAbsolute); 33 | Assert.Throws(() => attribute.Validate(uri)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0;net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/ConformanceTestData/SampleBatches.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData; 11 | 12 | public static class SampleBatches 13 | { 14 | private static readonly ConcurrentDictionary> batchesById = new ConcurrentDictionary>(); 15 | 16 | private static readonly IReadOnlyList empty = Register("empty"); 17 | private static readonly IReadOnlyList minimal = Register("minimal", SampleEvents.Minimal); 18 | private static readonly IReadOnlyList minimal2 = Register("minimal2", SampleEvents.Minimal, SampleEvents.Minimal); 19 | private static readonly IReadOnlyList minimalAndAllCore = Register("minimalAndAllCore", SampleEvents.Minimal, SampleEvents.AllCore); 20 | private static readonly IReadOnlyList minimalAndAllExtensionTypes = 21 | Register("minimalAndAllExtensionTypes", SampleEvents.Minimal, SampleEvents.AllExtensionTypes); 22 | 23 | internal static IReadOnlyList Empty => Clone(empty); 24 | internal static IReadOnlyList Minimal => Clone(minimal); 25 | internal static IReadOnlyList Minimal2 => Clone(minimal2); 26 | internal static IReadOnlyList MinimalAndAllCore => Clone(minimalAndAllCore); 27 | internal static IReadOnlyList MinimalAndAllExtensionTypes => Clone(minimalAndAllExtensionTypes); 28 | 29 | internal static IReadOnlyList FromId(string id) => batchesById.TryGetValue(id, out var batch) 30 | ? Clone(batch) 31 | : throw new ArgumentException($"No such sample batch: '{id}'"); 32 | 33 | private static IReadOnlyList Clone(IReadOnlyList cloudEvents) => 34 | cloudEvents.Select(SampleEvents.Clone).ToList().AsReadOnly(); 35 | 36 | private static IReadOnlyList Register(string id, params CloudEvent[] cloudEvents) 37 | { 38 | var list = new List(cloudEvents).AsReadOnly(); 39 | batchesById[id] = list; 40 | return list; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/ConformanceTestData/SampleEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Collections.Generic; 8 | 9 | namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData; 10 | 11 | internal static class SampleEvents 12 | { 13 | private static readonly ConcurrentDictionary eventsById = new ConcurrentDictionary(); 14 | 15 | private static readonly IReadOnlyList allExtensionAttributes = new List() 16 | { 17 | CloudEventAttribute.CreateExtension("extinteger", CloudEventAttributeType.Integer), 18 | CloudEventAttribute.CreateExtension("extboolean", CloudEventAttributeType.Boolean), 19 | CloudEventAttribute.CreateExtension("extstring", CloudEventAttributeType.String), 20 | CloudEventAttribute.CreateExtension("exttimestamp", CloudEventAttributeType.Timestamp), 21 | CloudEventAttribute.CreateExtension("exturi", CloudEventAttributeType.Uri), 22 | CloudEventAttribute.CreateExtension("exturiref", CloudEventAttributeType.UriReference), 23 | CloudEventAttribute.CreateExtension("extbinary", CloudEventAttributeType.Binary), 24 | }.AsReadOnly(); 25 | 26 | private static readonly CloudEvent minimal = new CloudEvent 27 | { 28 | Id = "minimal", 29 | Type = "io.cloudevents.test", 30 | Source = new Uri("https://cloudevents.io") 31 | }.Register(); 32 | 33 | private static readonly CloudEvent allCore = minimal.With(evt => 34 | { 35 | evt.Id = "allCore"; 36 | evt.DataContentType = "text/plain"; 37 | evt.DataSchema = new Uri("https://cloudevents.io/dataschema"); 38 | evt.Subject = "tests"; 39 | evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero); 40 | }).Register(); 41 | 42 | private static readonly CloudEvent minimalWithTime = minimal.With(evt => 43 | { 44 | evt.Id = "minimalWithTime"; 45 | evt.Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero); 46 | }).Register(); 47 | 48 | private static readonly CloudEvent minimalWithRelativeSource = minimal.With(evt => 49 | { 50 | evt.Id = "minimalWithRelativeSource"; 51 | evt.Source = new Uri("#fragment", UriKind.RelativeOrAbsolute); 52 | }).Register(); 53 | 54 | private static readonly CloudEvent simpleTextData = minimal.With(evt => 55 | { 56 | evt.Id = "simpleTextData"; 57 | evt.Data = "Simple text"; 58 | evt.DataContentType = "text/plain"; 59 | }).Register(); 60 | 61 | private static readonly CloudEvent allExtensionTypes = minimal.WithSampleExtensionAttributes().With(evt => 62 | { 63 | evt.Id = "allExtensionTypes"; 64 | 65 | evt["extinteger"] = 10; 66 | evt["extboolean"] = true; 67 | evt["extstring"] = "text"; 68 | evt["extbinary"] = new byte[] { 77, 97 }; 69 | evt["exttimestamp"] = new DateTimeOffset(2023, 3, 31, 15, 12, 0, TimeSpan.Zero); 70 | evt["exturi"] = new Uri("https://cloudevents.io"); 71 | evt["exturiref"] = new Uri("//authority/path", UriKind.RelativeOrAbsolute); 72 | }).Register(); 73 | 74 | internal static CloudEvent Minimal => Clone(minimal); 75 | internal static CloudEvent AllCore => Clone(allCore); 76 | internal static CloudEvent MinimalWithTime => Clone(minimalWithTime); 77 | internal static CloudEvent MinimalWithRelativeSource => Clone(minimalWithRelativeSource); 78 | internal static CloudEvent SimpleTextData => Clone(simpleTextData); 79 | internal static CloudEvent AllExtensionTypes => Clone(allExtensionTypes); 80 | internal static IReadOnlyList SampleExtensionAttributes => allExtensionAttributes; 81 | 82 | internal static CloudEvent FromId(string id) => eventsById.TryGetValue(id, out var evt) 83 | ? Clone(evt) 84 | : throw new ArgumentException($"No such sample event: '{id}'"); 85 | 86 | // TODO: Make this available somewhere else? 87 | internal static CloudEvent Clone(CloudEvent evt) 88 | { 89 | var clone = new CloudEvent(evt.SpecVersion, evt.ExtensionAttributes); 90 | foreach (var attr in evt.GetPopulatedAttributes()) 91 | { 92 | clone[attr.Key] = attr.Value; 93 | } 94 | // TODO: Deep copy where appropriate? 95 | clone.Data = evt.Data; 96 | return clone; 97 | } 98 | 99 | private static CloudEvent With(this CloudEvent evt, Action action) 100 | { 101 | var clone = Clone(evt); 102 | action(clone); 103 | return clone; 104 | } 105 | 106 | /// 107 | /// Returns a clone of the given CloudEvent, with all attributes in 108 | /// registered but without values. 109 | /// 110 | private static CloudEvent WithSampleExtensionAttributes(this CloudEvent evt) => evt.With(clone => 111 | { 112 | foreach (var attribute in allExtensionAttributes) 113 | { 114 | clone[attribute] = null; 115 | } 116 | }); 117 | 118 | private static CloudEvent Register(this CloudEvent evt) 119 | { 120 | eventsById[evt.Id ?? throw new InvalidOperationException("No ID in sample event")] = evt; 121 | return evt; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/ConformanceTestData/TestDataProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | 10 | namespace CloudNative.CloudEvents.UnitTests.ConformanceTestData; 11 | 12 | internal class TestDataProvider 13 | { 14 | private static readonly string ConformanceTestDataRoot = Path.Combine(FindRepoRoot(), "conformance", "format"); 15 | 16 | public static TestDataProvider Json { get; } = new TestDataProvider("json", "*.json"); 17 | public static TestDataProvider Protobuf { get; } = new TestDataProvider("protobuf", "*.json"); 18 | public static TestDataProvider Xml { get; } = new TestDataProvider("xml", "*.xml"); 19 | 20 | 21 | private readonly string testDataDirectory; 22 | private readonly string searchPattern; 23 | 24 | private TestDataProvider(string relativeDirectory, string searchPattern) 25 | { 26 | testDataDirectory = Path.Combine(ConformanceTestDataRoot, relativeDirectory); 27 | this.searchPattern = searchPattern; 28 | } 29 | 30 | public IEnumerable ListTestFiles() => Directory.EnumerateFiles(testDataDirectory, searchPattern); 31 | 32 | /// 33 | /// Loads all tests, assuming multiple tests per file, to be loaded based on textual file content. 34 | /// 35 | /// The deserialized test file type. 36 | /// The deserialized test type. 37 | /// A function to parse the content of the file (provided as a string) to a test file. 38 | /// A function to extract all the tests within the given test file. 39 | public IReadOnlyList LoadTests(Func fileParser, Func> testExtractor) => 40 | ListTestFiles() 41 | .Select(file => fileParser(File.ReadAllText(file))) 42 | .SelectMany(testExtractor) 43 | .ToList() 44 | .AsReadOnly(); 45 | 46 | private static string FindRepoRoot() 47 | { 48 | var currentDirectory = Path.GetFullPath("."); 49 | var directory = new DirectoryInfo(currentDirectory); 50 | while (directory != null && 51 | (!File.Exists(Path.Combine(directory.FullName, "LICENSE")) 52 | || !File.Exists(Path.Combine(directory.FullName, "CloudEvents.sln")))) 53 | { 54 | directory = directory.Parent; 55 | } 56 | if (directory == null) 57 | { 58 | throw new Exception("Unable to determine root directory. Please run within the sdk-csharp repository."); 59 | } 60 | return directory.FullName; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Core/BinaryDataUtilitiesTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Linq; 7 | using Xunit; 8 | 9 | namespace CloudNative.CloudEvents.Core.UnitTests 10 | { 11 | public class BinaryDataUtilitiesTest 12 | { 13 | [Fact] 14 | public void AsArray_ReturningOriginal() 15 | { 16 | byte[] original = { 1, 2, 3, 4, 5 }; 17 | var segment = new ArraySegment(original); 18 | Assert.Same(original, BinaryDataUtilities.AsArray(segment)); 19 | } 20 | 21 | [Theory] 22 | [InlineData(0)] 23 | [InlineData(1)] 24 | public void AsArray_FromPartialSegment(int offset) 25 | { 26 | byte[] original = { 1, 2, 3, 4, 5 }; 27 | var segment = new ArraySegment(original, offset, 4); 28 | var actual = BinaryDataUtilities.AsArray(segment); 29 | Assert.True(actual.SequenceEqual(segment)); 30 | Assert.NotSame(original, actual); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Core/CloudEventAttributeTypesTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using Xunit; 7 | 8 | namespace CloudNative.CloudEvents.Core.UnitTests 9 | { 10 | public class CloudEventAttributeTypesTest 11 | { 12 | public static readonly TheoryData AllTypes = new TheoryData 13 | { 14 | CloudEventAttributeType.Binary, 15 | CloudEventAttributeType.Boolean, 16 | CloudEventAttributeType.Integer, 17 | CloudEventAttributeType.String, 18 | CloudEventAttributeType.Timestamp, 19 | CloudEventAttributeType.Uri, 20 | CloudEventAttributeType.UriReference 21 | }; 22 | 23 | [Fact] 24 | public void GetOrdinal_NullInput() => 25 | Assert.Throws(() => CloudEventAttributeTypes.GetOrdinal(null!)); 26 | 27 | [Theory] 28 | [MemberData(nameof(AllTypes))] 29 | public void GetOrdinal_NonNullInput(CloudEventAttributeType type) => 30 | Assert.Equal(type.Ordinal, CloudEventAttributeTypes.GetOrdinal(type)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Core/MimeUtilitiesTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Linq; 6 | using System.Net.Http.Headers; 7 | using System.Net.Mime; 8 | using System.Text; 9 | using Xunit; 10 | 11 | namespace CloudNative.CloudEvents.Core.UnitTests 12 | { 13 | public class MimeUtilitiesTest 14 | { 15 | [Theory] 16 | [InlineData("application/json")] 17 | [InlineData("application/json; charset=iso-8859-1")] 18 | [InlineData("application/json; charset=iso-8859-1; name=some-name")] 19 | [InlineData("application/json; charset=iso-8859-1; name=some-name; x=y; a=b")] 20 | [InlineData("application/json; charset=iso-8859-1; name=some-name; boundary=xyzzy; x=y")] 21 | public void ContentTypeConversions(string text) 22 | { 23 | var originalContentType = new ContentType(text); 24 | var header = MimeUtilities.ToMediaTypeHeaderValue(originalContentType); 25 | AssertEqualParts(text, header!.ToString()); 26 | var convertedContentType = MimeUtilities.ToContentType(header); 27 | AssertEqualParts(originalContentType.ToString(), convertedContentType!.ToString()); 28 | 29 | // Conversions can end up reordering the parameters. In reality we're only 30 | // likely to end up with a media type and charset, but our tests use more parameters. 31 | // This just makes them deterministic. 32 | void AssertEqualParts(string expected, string actual) 33 | { 34 | expected = string.Join(";", expected.Split(";").OrderBy(x => x)); 35 | actual = string.Join(";", actual.Split(";").OrderBy(x => x)); 36 | Assert.Equal(expected, actual); 37 | } 38 | } 39 | 40 | [Fact] 41 | public void ContentTypeConversions_Null() 42 | { 43 | Assert.Null(MimeUtilities.ToMediaTypeHeaderValue(default(ContentType))); 44 | Assert.Null(MimeUtilities.ToContentType(default(MediaTypeHeaderValue))); 45 | } 46 | 47 | [Theory] 48 | [InlineData("iso-8859-1")] 49 | [InlineData("utf-8")] 50 | public void ContentTypeGetEncoding(string charSet) 51 | { 52 | var contentType = new ContentType($"text/plain; charset={charSet}"); 53 | Encoding encoding = MimeUtilities.GetEncoding(contentType); 54 | Assert.Equal(charSet, encoding.WebName); 55 | } 56 | 57 | [Fact] 58 | public void ContentTypeGetEncoding_NoContentType() 59 | { 60 | ContentType? contentType = null; 61 | Encoding encoding = MimeUtilities.GetEncoding(contentType); 62 | Assert.Equal(Encoding.UTF8, encoding); 63 | } 64 | 65 | [Fact] 66 | public void ContentTypeGetEncoding_NoCharSet() 67 | { 68 | ContentType contentType = new ContentType("text/plain"); 69 | Encoding encoding = MimeUtilities.GetEncoding(contentType); 70 | Assert.Equal(Encoding.UTF8, encoding); 71 | } 72 | 73 | [Theory] 74 | [InlineData(null)] 75 | [InlineData("text/plain")] 76 | public void CreateContentTypeOrNull_WithContentType(string? text) 77 | { 78 | ContentType? ct = MimeUtilities.CreateContentTypeOrNull(text); 79 | Assert.Equal(text, ct?.ToString()); 80 | } 81 | 82 | [Theory] 83 | [InlineData("text/plain", false)] 84 | [InlineData(null, false)] 85 | [InlineData("application/cloudevents", true)] 86 | [InlineData("application/cloudevents+json", true)] 87 | // It's not entirely clear that this *should* be true... 88 | [InlineData("application/cloudeventstrailing", true)] 89 | [InlineData("application/cloudevents-batch", false)] 90 | [InlineData("application/cloudevents-batch+json", false)] 91 | public void IsCloudEventsContentType(string? contentType, bool expectedResult) => 92 | Assert.Equal(expectedResult, MimeUtilities.IsCloudEventsContentType(contentType)); 93 | 94 | [Theory] 95 | [InlineData("text/plain", false)] 96 | [InlineData(null, false)] 97 | [InlineData("application/cloudevents", false)] 98 | [InlineData("application/cloudevents+json", false)] 99 | [InlineData("application/cloudeventstrailing", false)] 100 | [InlineData("application/cloudevents-batch", true)] 101 | // It's not entirely clear that this *should* be true... 102 | [InlineData("application/cloudevents-batchtrailing", true)] 103 | [InlineData("application/cloudevents-batch+json", true)] 104 | public void IsCloudEventsBatchContentType(string? contentType, bool expectedResult) => 105 | Assert.Equal(expectedResult, MimeUtilities.IsCloudEventsBatchContentType(contentType)); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Extensions/PartitioningTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.NewtonsoftJson; 6 | using Xunit; 7 | using static CloudNative.CloudEvents.UnitTests.CloudEventFormatterExtensions; 8 | 9 | namespace CloudNative.CloudEvents.Extensions.UnitTests 10 | { 11 | public class PartitioningTest 12 | { 13 | private static readonly string sampleJson = @" 14 | { 15 | 'specversion' : '1.0', 16 | 'type' : 'com.github.pull.create', 17 | 'id' : 'A234-1234-1234', 18 | 'source' : '//event-source', 19 | 'partitionkey' : 'abc', 20 | }".Replace('\'', '"'); 21 | 22 | 23 | [Fact] 24 | public void ParseJson() 25 | { 26 | var jsonFormatter = new JsonEventFormatter(); 27 | var cloudEvent = jsonFormatter.DecodeStructuredModeText(sampleJson); 28 | Assert.Equal("abc", cloudEvent["partitionkey"]); 29 | Assert.Equal("abc", cloudEvent[Partitioning.PartitionKeyAttribute]); 30 | Assert.Equal("abc", cloudEvent.GetPartitionKey()); 31 | } 32 | 33 | [Fact] 34 | public void Transcode() 35 | { 36 | var jsonFormatter = new JsonEventFormatter(); 37 | var cloudEvent1 = jsonFormatter.DecodeStructuredModeText(sampleJson); 38 | var jsonData = jsonFormatter.EncodeStructuredModeMessage(cloudEvent1, out var contentType); 39 | var cloudEvent = jsonFormatter.DecodeStructuredModeMessage(jsonData, contentType, null); 40 | 41 | Assert.Equal("abc", cloudEvent["partitionkey"]); 42 | Assert.Equal("abc", cloudEvent[Partitioning.PartitionKeyAttribute]); 43 | Assert.Equal("abc", cloudEvent.GetPartitionKey()); 44 | } 45 | 46 | [Fact] 47 | public void SetPartitionKey() 48 | { 49 | var cloudEvent = new CloudEvent(); 50 | cloudEvent.SetPartitionKey("xyz"); 51 | Assert.Equal("xyz", cloudEvent["partitionkey"]); 52 | Assert.Equal("xyz", cloudEvent[Partitioning.PartitionKeyAttribute]); 53 | 54 | cloudEvent.SetPartitionKey(null); 55 | Assert.Null(cloudEvent["partitionkey"]); 56 | Assert.Null(cloudEvent[Partitioning.PartitionKeyAttribute]); 57 | } 58 | 59 | [Fact] 60 | public void GetPartitionKey() 61 | { 62 | var cloudEvent = new CloudEvent 63 | { 64 | ["partitionkey"] = "xyz" 65 | }; 66 | Assert.Equal("xyz", cloudEvent.GetPartitionKey()); 67 | 68 | cloudEvent["partitionkey"] = null; 69 | Assert.Null(cloudEvent.GetPartitionKey()); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Extensions/SamplingTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.NewtonsoftJson; 6 | using System; 7 | using Xunit; 8 | using static CloudNative.CloudEvents.UnitTests.CloudEventFormatterExtensions; 9 | 10 | namespace CloudNative.CloudEvents.Extensions.UnitTests 11 | { 12 | public class SamplingTest 13 | { 14 | private static readonly string sampleJson = @" 15 | { 16 | 'specversion' : '1.0', 17 | 'type' : 'com.github.pull.create', 18 | 'id' : 'A234-1234-1234', 19 | 'source' : '//event-source', 20 | 'sampledrate' : 1, 21 | }".Replace('\'', '"'); 22 | 23 | [Fact] 24 | public void SamplingParse() 25 | { 26 | var jsonFormatter = new JsonEventFormatter(); 27 | var cloudEvent = jsonFormatter.DecodeStructuredModeText(sampleJson, Sampling.AllAttributes); 28 | 29 | Assert.Equal(1, cloudEvent["sampledrate"]); 30 | Assert.Equal(1, cloudEvent.GetSampledRate()); 31 | } 32 | 33 | [Fact] 34 | public void SamplingJsonTranscode() 35 | { 36 | var jsonFormatter = new JsonEventFormatter(); 37 | var cloudEvent1 = jsonFormatter.DecodeStructuredModeText(sampleJson); 38 | // Note that the value is just a string here, as we don't know the attribute type. 39 | Assert.Equal("1", cloudEvent1["sampledrate"]); 40 | 41 | var jsonData = jsonFormatter.EncodeStructuredModeMessage(cloudEvent1, out var contentType); 42 | var cloudEvent = jsonFormatter.DecodeStructuredModeMessage(jsonData, contentType, Sampling.AllAttributes); 43 | 44 | // When parsing with the attributes in place, the value is propagated as an integer. 45 | Assert.Equal(1, cloudEvent["sampledrate"]); 46 | Assert.Equal(1, cloudEvent.GetSampledRate()); 47 | } 48 | 49 | [Fact] 50 | public void SetAttributeValue_Invalid() 51 | { 52 | var cloudEvent = new CloudEvent(Sampling.AllAttributes); 53 | Assert.Throws(() => cloudEvent["sampledrate"] = 0); 54 | } 55 | 56 | [Fact] 57 | public void SetSampledRate() 58 | { 59 | var cloudEvent = new CloudEvent(); 60 | cloudEvent.SetSampledRate(5); 61 | Assert.Equal(5, cloudEvent["sampledrate"]); 62 | 63 | cloudEvent.SetSampledRate(null); 64 | Assert.Null(cloudEvent["sampledrate"]); 65 | } 66 | 67 | [Fact] 68 | public void SetSampleRate_Invalid() 69 | { 70 | var cloudEvent = new CloudEvent(); 71 | Assert.Throws(() => cloudEvent.SetSampledRate(0)); 72 | } 73 | 74 | [Fact] 75 | public void GetSampledRate_NotSet() 76 | { 77 | var cloudEvent = new CloudEvent(); 78 | Assert.Null(cloudEvent.GetSampledRate()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Extensions/SequenceTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.NewtonsoftJson; 6 | using System; 7 | using Xunit; 8 | using static CloudNative.CloudEvents.UnitTests.CloudEventFormatterExtensions; 9 | 10 | namespace CloudNative.CloudEvents.Extensions.UnitTests 11 | { 12 | public class SequenceTest 13 | { 14 | private static readonly string sampleJson = @" 15 | { 16 | 'specversion' : '1.0', 17 | 'type' : 'com.github.pull.create', 18 | 'id' : 'A234-1234-1234', 19 | 'source' : '//event-source', 20 | 'sequencetype' : 'Integer', 21 | 'sequence' : '25' 22 | }".Replace('\'', '"'); 23 | 24 | [Fact] 25 | public void Parse() 26 | { 27 | var jsonFormatter = new JsonEventFormatter(); 28 | var cloudEvent = jsonFormatter.DecodeStructuredModeText(sampleJson, Sequence.AllAttributes); 29 | 30 | Assert.Equal("Integer", cloudEvent[Sequence.SequenceTypeAttribute]); 31 | Assert.Equal("25", cloudEvent[Sequence.SequenceAttribute]); 32 | } 33 | 34 | [Fact] 35 | public void Transcode() 36 | { 37 | var jsonFormatter = new JsonEventFormatter(); 38 | var cloudEvent1 = jsonFormatter.DecodeStructuredModeText(sampleJson); 39 | var jsonData = jsonFormatter.EncodeStructuredModeMessage(cloudEvent1, out var contentType); 40 | var cloudEvent = jsonFormatter.DecodeStructuredModeMessage(jsonData, contentType, Sequence.AllAttributes); 41 | 42 | Assert.Equal("Integer", cloudEvent[Sequence.SequenceTypeAttribute]); 43 | Assert.Equal("25", cloudEvent[Sequence.SequenceAttribute]); 44 | } 45 | 46 | [Fact] 47 | public void GetSequenceExtensionMethods_Integer() 48 | { 49 | var cloudEvent = new CloudEvent 50 | { 51 | ["sequencetype"] = "Integer", 52 | ["sequence"] = "25" 53 | }; 54 | 55 | Assert.Equal(25, cloudEvent.GetSequenceValue()); 56 | Assert.Equal("25", cloudEvent.GetSequenceString()); 57 | Assert.Equal("Integer", cloudEvent.GetSequenceType()); 58 | } 59 | 60 | [Fact] 61 | public void GetSequenceExtensionMethods_Null() 62 | { 63 | var cloudEvent = new CloudEvent(); 64 | 65 | Assert.Null(cloudEvent.GetSequenceValue()); 66 | Assert.Null(cloudEvent.GetSequenceString()); 67 | Assert.Null(cloudEvent.GetSequenceType()); 68 | } 69 | 70 | [Fact] 71 | public void GetSequenceExtensionMethods_UnknownType() 72 | { 73 | var cloudEvent = new CloudEvent 74 | { 75 | ["sequencetype"] = "Mystery", 76 | ["sequence"] = "xyz" 77 | }; 78 | 79 | Assert.Equal("Mystery", cloudEvent.GetSequenceType()); 80 | Assert.Equal("xyz", cloudEvent.GetSequenceString()); 81 | Assert.Throws(() => cloudEvent.GetSequenceValue()); 82 | } 83 | 84 | [Fact] 85 | public void SetSequence_Null() 86 | { 87 | var cloudEvent = new CloudEvent 88 | { 89 | ["sequence"] = "xyz", 90 | ["sequencetype"] = "new sequence type" 91 | }; 92 | cloudEvent.SetSequence(null); 93 | 94 | Assert.Null(cloudEvent["sequence"]); 95 | Assert.Null(cloudEvent["sequencetype"]); 96 | } 97 | 98 | 99 | [Fact] 100 | public void SetSequence_Integer() 101 | { 102 | var cloudEvent = new CloudEvent().SetSequence(15); 103 | 104 | Assert.Equal("15", cloudEvent["sequence"]); 105 | Assert.Equal("Integer", cloudEvent["sequencetype"]); 106 | } 107 | 108 | [Fact] 109 | public void SetSequence_UnknownType() 110 | { 111 | var cloudEvent = new CloudEvent(); 112 | var uri = new Uri("https://oddsequencetype"); 113 | Assert.Throws(() => cloudEvent.SetSequence(uri)); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Http/HttpTestBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Net; 8 | using System.Net.Sockets; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | 12 | namespace CloudNative.CloudEvents.Http.UnitTests 13 | { 14 | /// 15 | /// Base class for HTTP tests, which sets up an HttpListener. 16 | /// 17 | public abstract class HttpTestBase : IDisposable 18 | { 19 | internal static readonly DateTimeOffset SampleTimestamp = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero); 20 | internal string ListenerAddress { get; } 21 | internal const string TestContextHeader = "testcontext"; 22 | private readonly HttpListener listener; 23 | private readonly Task processingTask; 24 | private volatile bool disposed; 25 | 26 | internal ConcurrentDictionary> PendingRequests { get; } = 27 | new ConcurrentDictionary>(); 28 | 29 | public HttpTestBase() 30 | { 31 | var port = GetRandomUnusedPort(); 32 | ListenerAddress = $"http://localhost:{port}/"; 33 | listener = new HttpListener() 34 | { 35 | AuthenticationSchemes = AuthenticationSchemes.Anonymous, 36 | Prefixes = { ListenerAddress } 37 | }; 38 | listener.Start(); 39 | processingTask = ProcessRequestsAsync(); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | // Note: we don't protected against multiple disposal, but that's not 45 | // expected to be a problem. (We're not disposing of this manually.) 46 | disposed = true; 47 | listener.Stop(); 48 | if (!processingTask.Wait(1000)) 49 | { 50 | throw new InvalidOperationException("Processing task did not complete"); 51 | } 52 | } 53 | 54 | private async Task ProcessRequestsAsync() 55 | { 56 | while (!disposed) 57 | { 58 | HttpListenerContext context; 59 | try 60 | { 61 | context = await listener.GetContextAsync().ConfigureAwait(false); 62 | } 63 | // The listener throws when it's stopped. 64 | // We want to handle that gracefully, but allow any other error to bubble up. 65 | catch (Exception e) when (disposed && (e is ObjectDisposedException || e is HttpListenerException)) 66 | { 67 | return; 68 | } 69 | try 70 | { 71 | await HandleContext(context).ConfigureAwait(false); 72 | } 73 | catch (Exception e) 74 | { 75 | var response = context.Response; 76 | var responseContent = Encoding.UTF8.GetBytes($"Error processing request: {e}"); 77 | response.ContentLength64 = responseContent.Length; 78 | response.StatusCode = 500; 79 | response.OutputStream.Write(responseContent); 80 | } 81 | context.Response.Close(); 82 | } 83 | } 84 | 85 | private async Task HandleContext(HttpListenerContext requestContext) 86 | { 87 | var ctxHeaderValue = requestContext.Request.Headers[TestContextHeader] 88 | ?? throw new InvalidOperationException("Test context header was missing"); 89 | 90 | if (PendingRequests.TryRemove(ctxHeaderValue, out var pending)) 91 | { 92 | await pending(requestContext); 93 | } 94 | else 95 | { 96 | throw new Exception($"Request with context header '{ctxHeaderValue}' was not handled"); 97 | } 98 | } 99 | 100 | private static int GetRandomUnusedPort() 101 | { 102 | var listener = new TcpListener(IPAddress.Loopback, 0); 103 | try 104 | { 105 | listener.Start(); 106 | return ((IPEndPoint) listener.LocalEndpoint).Port; 107 | } 108 | finally 109 | { 110 | listener.Stop(); 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Http/HttpUtilitiesTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using Xunit; 7 | 8 | namespace CloudNative.CloudEvents.Http.UnitTests 9 | { 10 | public class HttpUtilitiesTest 11 | { 12 | [Theory] 13 | [InlineData("simple", "simple")] 14 | [InlineData("Euro \u20AC \U0001F600", "Euro%20%E2%82%AC%20%F0%9F%98%80")] 15 | [InlineData("space encoded", "space%20encoded")] 16 | [InlineData("percent%encoded", "percent%25encoded")] 17 | [InlineData("quote\"encoded", "quote%22encoded")] 18 | [InlineData("caf\u00e9", "caf%C3%A9")] 19 | // This wouldn't be a valid attribute value in CloudEvents 1.0, but we encode ASCII control characters 20 | // for good measure, so let's test it. 21 | [InlineData("line1\r\nline2", "line1%0D%0Aline2")] 22 | public void RoundTripHeaderValue(string original, string encoded) 23 | { 24 | var actualEncoded = HttpUtilities.EncodeHeaderValue(original); 25 | Assert.Equal(encoded, actualEncoded); 26 | 27 | var actualDecoded = HttpUtilities.DecodeHeaderValue(encoded); 28 | Assert.Equal(original, actualDecoded); 29 | } 30 | 31 | // This is for values which would be encoded a different way, but we need to 32 | // test the decode path 33 | [Theory] 34 | [InlineData("lenient decoding %30%31", "lenient decoding 01")] 35 | [InlineData(@""" quoted text ""unquoted", " quoted text unquoted")] 36 | [InlineData(@"""escaped quote\""end""", @"escaped quote""end")] 37 | [InlineData(@"""escaped backslash\\end""", @"escaped backslash\end")] 38 | [InlineData(@"non-escaping backslash\end", @"non-escaping backslash\end")] 39 | // Mixed case for percent encoding 40 | [InlineData("Euro%20%e2%82%ac%20%f0%9F%98%80", "Euro \u20AC \U0001F600")] 41 | public void DecodeHeaderValue_NonRoundTrip(string headerValue, string expectedResult) 42 | { 43 | var actualResult = HttpUtilities.DecodeHeaderValue(headerValue); 44 | Assert.Equal(expectedResult, actualResult); 45 | } 46 | 47 | [Theory] 48 | [InlineData("unterminated percent %")] 49 | [InlineData("unterminated percent %0")] 50 | [InlineData("non hex percent %g0")] 51 | [InlineData("non hex percent %0g")] 52 | [InlineData("non hex percent %0$")] 53 | [InlineData("low surrogate %ED%B0%80")] 54 | [InlineData("high surrogate %ED%A0%80")] 55 | [InlineData("surrogate pair via two UTF-16 %ED%A0%80%ED%B0%80")] 56 | [InlineData("overlong UTF-8 %C0%A0")] 57 | [InlineData("incomplete end UTF-8 %E2")] 58 | [InlineData("incomplete end UTF-8 %E2%")] 59 | [InlineData("incomplete non-percent UTF-8 %E2x")] 60 | [InlineData("invalid UTF-8 first byte %82")] 61 | [InlineData("invalid UTF-8 second byte %E2%E2")] 62 | [InlineData(@"""unterminated quote")] 63 | [InlineData(@"""unterminated escape \")] 64 | public void DecodeHeaderValue_Invalid(string headerValue) => 65 | Assert.Throws(() => HttpUtilities.DecodeHeaderValue(headerValue)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Mqtt/MqttTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.NewtonsoftJson; 6 | using MQTTnet; 7 | using MQTTnet.Client; 8 | using MQTTnet.Server; 9 | using System; 10 | using System.Net.Mime; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | using static CloudNative.CloudEvents.UnitTests.TestHelpers; 14 | 15 | namespace CloudNative.CloudEvents.Mqtt.UnitTests 16 | { 17 | public class MqttTest : IDisposable 18 | { 19 | private readonly MqttServer mqttServer; 20 | 21 | public MqttTest() 22 | { 23 | var optionsBuilder = new MqttServerOptionsBuilder() 24 | .WithConnectionBacklog(100) 25 | .WithDefaultEndpoint() 26 | .WithDefaultEndpointPort(52355); 27 | 28 | this.mqttServer = new MqttFactory().CreateMqttServer(optionsBuilder.Build()); 29 | mqttServer.StartAsync().GetAwaiter().GetResult(); 30 | } 31 | 32 | public void Dispose() 33 | { 34 | mqttServer.StopAsync().GetAwaiter().GetResult(); 35 | } 36 | 37 | [Fact] 38 | public async Task MqttSendTest() 39 | { 40 | 41 | var jsonEventFormatter = new JsonEventFormatter(); 42 | var cloudEvent = new CloudEvent 43 | { 44 | Type = "com.github.pull.create", 45 | Source = new Uri("https://github.com/cloudevents/spec/pull/123"), 46 | Id = "A234-1234-1234", 47 | Time = new DateTimeOffset(2018, 4, 5, 17, 31, 0, TimeSpan.Zero), 48 | DataContentType = MediaTypeNames.Text.Xml, 49 | Data = "", 50 | ["comexampleextension1"] = "value" 51 | }; 52 | 53 | var client = new MqttFactory().CreateMqttClient(); 54 | 55 | var options = new MqttClientOptionsBuilder() 56 | .WithClientId("Client1") 57 | .WithTcpServer("127.0.0.1", 52355) 58 | .WithCleanSession() 59 | .Build(); 60 | 61 | TaskCompletionSource tcs = new TaskCompletionSource(); 62 | await client.ConnectAsync(options); 63 | client.ApplicationMessageReceivedAsync += args => 64 | { 65 | tcs.SetResult(args.ApplicationMessage.ToCloudEvent(jsonEventFormatter)); 66 | return Task.CompletedTask; 67 | }; 68 | 69 | var result = await client.SubscribeAsync("abc"); 70 | await client.PublishAsync(cloudEvent.ToMqttApplicationMessage(ContentMode.Structured, new JsonEventFormatter(), topic: "abc")); 71 | var receivedCloudEvent = await tcs.Task; 72 | 73 | Assert.Equal(CloudEventsSpecVersion.Default, receivedCloudEvent.SpecVersion); 74 | Assert.Equal("com.github.pull.create", receivedCloudEvent.Type); 75 | Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source); 76 | Assert.Equal("A234-1234-1234", receivedCloudEvent.Id); 77 | AssertTimestampsEqual("2018-04-05T17:31:00Z", receivedCloudEvent.Time!.Value); 78 | Assert.Equal(MediaTypeNames.Text.Xml, receivedCloudEvent.DataContentType); 79 | Assert.Equal("", receivedCloudEvent.Data); 80 | 81 | Assert.Equal("value", (string?) receivedCloudEvent["comexampleextension1"]); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/AttributedModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Newtonsoft.Json; 6 | 7 | namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests 8 | { 9 | [CloudEventFormatter(typeof(JsonEventFormatter))] 10 | internal class AttributedModel 11 | { 12 | public const string JsonPropertyName = "customattribute"; 13 | 14 | [JsonProperty(JsonPropertyName)] 15 | public string? AttributedProperty { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/ConformanceTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.UnitTests; 6 | using CloudNative.CloudEvents.UnitTests.ConformanceTestData; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using Xunit; 12 | 13 | namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests; 14 | 15 | public class ConformanceTest 16 | { 17 | private static readonly IReadOnlyList allTests = 18 | TestDataProvider.Json.LoadTests(ConformanceTestFile.FromJson, file => file.Tests); 19 | 20 | private static JsonConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id); 21 | private static IEnumerable SelectTestIds(ConformanceTestType type) => 22 | allTests 23 | .Where(test => test.TestType == type) 24 | .Select(test => new object[] { test.Id }); 25 | 26 | public static IEnumerable ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent); 27 | public static IEnumerable InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent); 28 | public static IEnumerable ValidBatchTestIds => SelectTestIds(ConformanceTestType.ValidBatch); 29 | public static IEnumerable InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch); 30 | 31 | [Theory, MemberData(nameof(ValidEventTestIds))] 32 | public void ValidEvent(string testId) 33 | { 34 | var test = GetTestById(testId); 35 | CloudEvent expected = SampleEvents.FromId(test.SampleId); 36 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 37 | CloudEvent actual = new JsonEventFormatter().ConvertFromJObject(test.Event, extensions); 38 | TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 39 | } 40 | 41 | [Theory, MemberData(nameof(InvalidEventTestIds))] 42 | public void InvalidEvent(string testId) 43 | { 44 | var test = GetTestById(testId); 45 | var formatter = new JsonEventFormatter(); 46 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 47 | Assert.Throws(() => formatter.ConvertFromJObject(test.Event, extensions)); 48 | } 49 | 50 | [Theory, MemberData(nameof(ValidBatchTestIds))] 51 | public void ValidBatch(string testId) 52 | { 53 | var test = GetTestById(testId); 54 | IReadOnlyList expected = SampleBatches.FromId(test.SampleId); 55 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 56 | // We don't have a convenience method for batches, so serialize the array back to JSON. 57 | var json = test.Batch.ToString(); 58 | var body = Encoding.UTF8.GetBytes(json); 59 | IReadOnlyList actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions); 60 | TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 61 | } 62 | 63 | [Theory, MemberData(nameof(InvalidBatchTestIds))] 64 | public void InvalidBatch(string testId) 65 | { 66 | var test = GetTestById(testId); 67 | var formatter = new JsonEventFormatter(); 68 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 69 | // We don't have a convenience method for batches, so serialize the array back to JSON. 70 | var json = test.Batch.ToString(); 71 | var body = Encoding.UTF8.GetBytes(json); 72 | Assert.Throws(() => formatter.DecodeBatchModeMessage(body, contentType: null, extensions)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/ConformanceTestFile.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests; 11 | 12 | #nullable disable 13 | 14 | public class ConformanceTestFile 15 | { 16 | private static readonly JsonSerializerSettings serializerSeettings = new() { DateParseHandling = DateParseHandling.None }; 17 | 18 | public ConformanceTestType? TestType { get; set; } 19 | public List Tests { get; } = new List(); 20 | 21 | public static ConformanceTestFile FromJson(string json) 22 | { 23 | var testFile = JsonConvert.DeserializeObject(json, serializerSeettings) ?? throw new InvalidOperationException(); 24 | foreach (var test in testFile.Tests) 25 | { 26 | test.TestType ??= testFile.TestType; 27 | } 28 | return testFile; 29 | } 30 | } 31 | 32 | public class JsonConformanceTest 33 | { 34 | public string Id { get; set; } 35 | public string Description { get; set; } 36 | public ConformanceTestType? TestType { get; set; } 37 | public string SampleId { get; set; } 38 | public JObject Event { get; set; } 39 | public JArray Batch { get; set; } 40 | public bool RoundTrip { get; set; } 41 | public bool SampleExtensionAttributes { get; set; } 42 | public bool ExtensionConstraints { get; set; } 43 | } 44 | 45 | public enum ConformanceTestType 46 | { 47 | ValidSingleEvent, 48 | ValidBatch, 49 | InvalidSingleEvent, 50 | InvalidBatch 51 | } -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/JTokenAsserter.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Newtonsoft.Json.Linq; 6 | using System; 7 | using System.Collections; 8 | using System.Collections.Generic; 9 | using Xunit; 10 | 11 | namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests 12 | { 13 | 14 | internal class JTokenAsserter : IEnumerable 15 | { 16 | private readonly List<(string name, JTokenType type, object? value)> expectations = new List<(string, JTokenType, object?)>(); 17 | 18 | // Just for collection initializers 19 | public IEnumerator GetEnumerator() => throw new NotImplementedException(); 20 | 21 | public void Add(string name, JTokenType type, T value) => 22 | expectations.Add((name, type, value)); 23 | 24 | public void AssertProperties(JObject? obj, bool assertCount) 25 | { 26 | Assert.NotNull(obj); 27 | foreach (var expectation in expectations) 28 | { 29 | Assert.True( 30 | obj!.TryGetValue(expectation.name, out var token), 31 | $"Expected property '{expectation.name}' to be present"); 32 | Assert.Equal(expectation.type, token!.Type); 33 | // No need to check null values, as they'll have a null token type. 34 | if (expectation.value is object) 35 | { 36 | Assert.Equal(expectation.value, token.ToObject(expectation.value.GetType())); 37 | } 38 | } 39 | if (assertCount) 40 | { 41 | Assert.Equal(expectations.Count, obj!.Count); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/NewtonsoftJson/SpecializedJsonReaderTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.Core; 6 | using CloudNative.CloudEvents.UnitTests; 7 | using Newtonsoft.Json; 8 | using Newtonsoft.Json.Linq; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using Xunit; 13 | 14 | namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests 15 | { 16 | /// 17 | /// Tests for the specialization of 18 | /// 19 | public class SpecializedJsonReaderTest 20 | { 21 | [Fact] 22 | public void DefaultImplementation_ReturnsJsonTextReader() 23 | { 24 | var formatter = new CreateJsonReaderExposingFormatter(); 25 | var reader = formatter.CreateJsonReaderPublic(CreateJsonStream(), null); 26 | Assert.IsType(reader); 27 | } 28 | 29 | [Fact] 30 | public void DefaultImplementation_NoPropertyNameTable() 31 | { 32 | var formatter = new JsonEventFormatter(); 33 | var event1 = formatter.DecodeStructuredModeMessage(CreateJsonStream(), null, null); 34 | var event2 = formatter.DecodeStructuredModeMessage(CreateJsonStream(), null, null); 35 | 36 | JObject data1 = (JObject) event1.Data!; 37 | JObject data2 = (JObject) event2.Data!; 38 | 39 | var property1 = data1.Properties().Single(); 40 | var property2 = data2.Properties().Single(); 41 | Assert.Equal(property1.Name, property2.Name); 42 | Assert.NotSame(property1.Name, property2.Name); 43 | } 44 | 45 | [Fact] 46 | public void Specialization_WithPropertyNameTable() 47 | { 48 | var formatter = new PropertyNameTableFormatter(); 49 | var event1 = formatter.DecodeStructuredModeMessage(CreateJsonStream(), null, null); 50 | var event2 = formatter.DecodeStructuredModeMessage(CreateJsonStream(), null, null); 51 | 52 | JObject data1 = (JObject) event1.Data!; 53 | JObject data2 = (JObject) event2.Data!; 54 | 55 | var property1 = data1.Properties().Single(); 56 | var property2 = data2.Properties().Single(); 57 | Assert.Equal(property1.Name, property2.Name); 58 | Assert.Same(property1.Name, property2.Name); 59 | } 60 | 61 | private Stream CreateJsonStream() 62 | { 63 | var cloudEvent = new CloudEvent 64 | { 65 | Data = new { DataName = "DataValue" } 66 | }.PopulateRequiredAttributes(); 67 | var bytes = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out _); 68 | return BinaryDataUtilities.AsStream(bytes); 69 | } 70 | 71 | private class CreateJsonReaderExposingFormatter : JsonEventFormatter 72 | { 73 | public JsonReader CreateJsonReaderPublic(Stream stream, Encoding? encoding) => 74 | base.CreateJsonReader(stream, encoding); 75 | } 76 | 77 | private class PropertyNameTableFormatter : JsonEventFormatter 78 | { 79 | private readonly DefaultJsonNameTable table; 80 | 81 | public PropertyNameTableFormatter() 82 | { 83 | // Names aren't automatically cached by JsonTextReader, so we need to prepopulate the table. 84 | table = new DefaultJsonNameTable(); 85 | table.Add("DataName"); 86 | } 87 | 88 | protected override JsonReader CreateJsonReader(Stream stream, Encoding? encoding) 89 | { 90 | var reader = (JsonTextReader) base.CreateJsonReader(stream, encoding); 91 | reader.PropertyNameTable = table; 92 | return reader; 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Protobuf/CompatibilityTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.V1; 6 | using Xunit; 7 | 8 | namespace CloudNative.CloudEvents.UnitTests.Protobuf; 9 | 10 | public class CompatibilityTest 11 | { 12 | [Fact] 13 | public void ProtoSchemaReflectionEquivalence() 14 | { 15 | #pragma warning disable CS0618 // Type or member is obsolete 16 | Assert.Same(CloudeventsReflection.Descriptor, ProtoSchemaReflection.Descriptor); 17 | #pragma warning restore CS0618 // Type or member is obsolete 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Protobuf/Conformance.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.UnitTests; 6 | using CloudNative.CloudEvents.UnitTests.ConformanceTestData; 7 | using Google.Protobuf; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using Xunit; 12 | 13 | namespace CloudNative.CloudEvents.Protobuf.UnitTests; 14 | 15 | public class Conformance 16 | { 17 | private static readonly IReadOnlyList allTests = 18 | TestDataProvider.Protobuf.LoadTests(ConformanceTestFile.FromJson, file => file.Tests); 19 | 20 | private static ConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id); 21 | private static IEnumerable SelectTestIds(ConformanceTest.EventOneofCase eventCase) => 22 | allTests 23 | .Where(test => test.EventCase == eventCase) 24 | .Select(test => new object[] { test.Id }); 25 | 26 | public static IEnumerable ValidEventTestIds => SelectTestIds(ConformanceTest.EventOneofCase.ValidSingle); 27 | public static IEnumerable InvalidEventTestIds => SelectTestIds(ConformanceTest.EventOneofCase.InvalidSingle); 28 | public static IEnumerable ValidBatchTestIds => SelectTestIds(ConformanceTest.EventOneofCase.ValidBatch); 29 | public static IEnumerable InvalidBatchTestIds => SelectTestIds(ConformanceTest.EventOneofCase.InvalidBatch); 30 | 31 | [Theory, MemberData(nameof(ValidEventTestIds))] 32 | public void ValidEvent(string testId) 33 | { 34 | var test = GetTestById(testId); 35 | CloudEvent expected = SampleEvents.FromId(test.SampleId); 36 | CloudEvent actual = new ProtobufEventFormatter().ConvertFromProto(test.ValidSingle, null); 37 | TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 38 | } 39 | 40 | [Theory, MemberData(nameof(InvalidEventTestIds))] 41 | public void InvalidEvent(string testId) 42 | { 43 | var test = GetTestById(testId); 44 | var formatter = new ProtobufEventFormatter(); 45 | Assert.Throws(() => formatter.ConvertFromProto(test.InvalidSingle, null)); 46 | } 47 | 48 | [Theory, MemberData(nameof(ValidBatchTestIds))] 49 | public void ValidBatch(string testId) 50 | { 51 | var test = GetTestById(testId); 52 | IReadOnlyList expected = SampleBatches.FromId(test.SampleId); 53 | 54 | // We don't have a convenience method for batches, so serialize batch back to binary. 55 | var body = test.ValidBatch.ToByteArray(); 56 | IReadOnlyList actual = new ProtobufEventFormatter().DecodeBatchModeMessage(body, null, null); 57 | TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 58 | } 59 | 60 | [Theory, MemberData(nameof(InvalidBatchTestIds))] 61 | public void InvalidBatch(string testId) 62 | { 63 | var test = GetTestById(testId); 64 | var formatter = new ProtobufEventFormatter(); 65 | // We don't have a convenience method for batches, so serialize batch back to binary. 66 | var body = test.InvalidBatch.ToByteArray(); 67 | Assert.Throws(() => formatter.DecodeBatchModeMessage(body, null, null)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Protobuf/ConformanceTestFile.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using Google.Protobuf; 6 | using Google.Protobuf.Reflection; 7 | 8 | namespace CloudNative.CloudEvents.Protobuf.UnitTests; 9 | 10 | public partial class ConformanceTestFile 11 | { 12 | private static readonly JsonParser jsonParser = 13 | new(JsonParser.Settings.Default.WithTypeRegistry(TypeRegistry.FromFiles(ConformanceTestsReflection.Descriptor))); 14 | 15 | internal static ConformanceTestFile FromJson(string json) => 16 | jsonParser.Parse(json); 17 | } 18 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/Protobuf/test_messages.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package io.cloudevents.v1.tests; 4 | 5 | option csharp_namespace = "CloudNative.CloudEvents.Protobuf.UnitTests"; 6 | 7 | message PayloadData1 { 8 | string name = 1; 9 | } 10 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/SystemTextJson/AttributedModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System.Text.Json.Serialization; 6 | 7 | namespace CloudNative.CloudEvents.SystemTextJson.UnitTests 8 | { 9 | [CloudEventFormatter(typeof(JsonEventFormatter))] 10 | internal class AttributedModel 11 | { 12 | public const string JsonPropertyName = "customattribute"; 13 | 14 | [JsonPropertyName(JsonPropertyName)] 15 | public string? AttributedProperty { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTest.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using CloudNative.CloudEvents.UnitTests; 6 | using CloudNative.CloudEvents.UnitTests.ConformanceTestData; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Text; 11 | using Xunit; 12 | 13 | namespace CloudNative.CloudEvents.SystemTextJson.UnitTests; 14 | 15 | public class ConformanceTest 16 | { 17 | private static readonly IReadOnlyList allTests = 18 | TestDataProvider.Json.LoadTests(ConformanceTestFile.FromJson, file => file.Tests); 19 | 20 | private static JsonConformanceTest GetTestById(string id) => allTests.Single(test => test.Id == id); 21 | private static IEnumerable SelectTestIds(ConformanceTestType type) => 22 | allTests 23 | .Where(test => test.TestType == type) 24 | .Select(test => new object[] { test.Id }); 25 | 26 | public static IEnumerable ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent); 27 | public static IEnumerable InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent); 28 | public static IEnumerable ValidBatchTestIds => SelectTestIds(ConformanceTestType.ValidBatch); 29 | public static IEnumerable InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch); 30 | 31 | [Theory, MemberData(nameof(ValidEventTestIds))] 32 | public void ValidEvent(string testId) 33 | { 34 | var test = GetTestById(testId); 35 | CloudEvent expected = SampleEvents.FromId(test.SampleId); 36 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 37 | CloudEvent actual = new JsonEventFormatter().ConvertFromJsonElement(test.Event, extensions); 38 | TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 39 | } 40 | 41 | [Theory, MemberData(nameof(InvalidEventTestIds))] 42 | public void InvalidEvent(string testId) 43 | { 44 | var test = GetTestById(testId); 45 | var formatter = new JsonEventFormatter(); 46 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 47 | // Hmm... we throw FormatException in some cases, when ArgumentException would be better. 48 | // Changing that would be "somewhat breaking"... it's unclear how much we should worry. 49 | Assert.ThrowsAny(() => formatter.ConvertFromJsonElement(test.Event, extensions)); 50 | } 51 | 52 | [Theory, MemberData(nameof(ValidBatchTestIds))] 53 | public void ValidBatch(string testId) 54 | { 55 | var test = GetTestById(testId); 56 | IReadOnlyList expected = SampleBatches.FromId(test.SampleId); 57 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 58 | // We don't have a convenience method for batches, so serialize the array back to JSON. 59 | var json = test.Batch.ToString(); 60 | var body = Encoding.UTF8.GetBytes(json); 61 | IReadOnlyList actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions); 62 | TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); 63 | } 64 | 65 | [Theory, MemberData(nameof(InvalidBatchTestIds))] 66 | public void InvalidBatch(string testId) 67 | { 68 | var test = GetTestById(testId); 69 | var formatter = new JsonEventFormatter(); 70 | var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; 71 | // We don't have a convenience method for batches, so serialize the array back to JSON. 72 | var json = test.Batch.ToString(); 73 | var body = Encoding.UTF8.GetBytes(json); 74 | // Hmm... we throw FormatException in some cases, when ArgumentException would be better. 75 | // Changing that would be "somewhat breaking"... it's unclear how much we should worry. 76 | Assert.ThrowsAny(() => formatter.DecodeBatchModeMessage(body, contentType: null, extensions)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTestFile.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text.Json; 8 | using System.Text.Json.Nodes; 9 | using System.Text.Json.Serialization; 10 | 11 | namespace CloudNative.CloudEvents.SystemTextJson.UnitTests; 12 | 13 | #nullable disable 14 | 15 | public class ConformanceTestFile 16 | { 17 | private static readonly JsonSerializerOptions serializerOptions = new() { Converters = { new JsonStringEnumConverter() } }; 18 | 19 | [JsonPropertyName("testType")] 20 | public ConformanceTestType? TestType { get; set; } 21 | 22 | // Note: we need a setter here; System.Text.Json doesn't support adding to an existing collection. 23 | // See https://github.com/dotnet/runtime/issues/30258 24 | [JsonPropertyName("tests")] 25 | public List Tests { get; set; } = new List(); 26 | 27 | public static ConformanceTestFile FromJson(string json) 28 | { 29 | var testFile = JsonSerializer.Deserialize(json, serializerOptions) ?? throw new InvalidOperationException(); 30 | foreach (var test in testFile.Tests) 31 | { 32 | test.TestType ??= testFile.TestType; 33 | } 34 | return testFile; 35 | } 36 | } 37 | 38 | public class JsonConformanceTest 39 | { 40 | [JsonPropertyName("id")] 41 | public string Id { get; set; } 42 | [JsonPropertyName("description")] 43 | public string Description { get; set; } 44 | [JsonPropertyName("testType")] 45 | public ConformanceTestType? TestType { get; set; } 46 | [JsonPropertyName("sampleId")] 47 | public string SampleId { get; set; } 48 | [JsonPropertyName("event")] 49 | public JsonElement Event { get; set; } 50 | [JsonPropertyName("batch")] 51 | public JsonArray Batch { get; set; } 52 | [JsonPropertyName("sampleExtensionAttributes")] 53 | public bool SampleExtensionAttributes { get; set; } 54 | [JsonPropertyName("extensionConstraints")] 55 | public bool ExtensionConstraints { get; set; } 56 | } 57 | 58 | public enum ConformanceTestType 59 | { 60 | ValidSingleEvent, 61 | ValidBatch, 62 | InvalidSingleEvent, 63 | InvalidBatch 64 | } -------------------------------------------------------------------------------- /test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonElementAsserter.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Cloud Native Foundation. 2 | // Licensed under the Apache 2.0 license. 3 | // See LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Text.Json; 10 | using Xunit; 11 | 12 | namespace CloudNative.CloudEvents.SystemTextJson.UnitTests 13 | { 14 | internal class JsonElementAsserter : IEnumerable 15 | { 16 | private readonly List<(string name, JsonValueKind type, object? value)> expectations = new List<(string, JsonValueKind, object?)>(); 17 | 18 | // Just for collection initializers 19 | public IEnumerator GetEnumerator() => throw new NotImplementedException(); 20 | 21 | public void Add(string name, JsonValueKind type, T value) => 22 | expectations.Add((name, type, value)); 23 | 24 | public void AssertProperties(JsonElement obj, bool assertCount) 25 | { 26 | foreach (var expectation in expectations) 27 | { 28 | Assert.True( 29 | obj.TryGetProperty(expectation.name, out var property), 30 | $"Expected property '{expectation.name}' to be present"); 31 | Assert.Equal(expectation.type, property.ValueKind); 32 | // No need to check null values, as they'll have a null token type. 33 | if (expectation.value is object) 34 | { 35 | var value = property.ValueKind switch 36 | { 37 | JsonValueKind.True => true, 38 | JsonValueKind.False => false, 39 | JsonValueKind.String => property.GetString(), 40 | JsonValueKind.Number => property.GetInt32(), 41 | JsonValueKind.Null => (object?) null, 42 | JsonValueKind.Object => JsonSerializer.Deserialize(property.GetRawText(), expectation.value.GetType()), 43 | _ => throw new Exception($"Unhandled value kind: {property.ValueKind}") 44 | }; 45 | 46 | Assert.Equal(expectation.value, value); 47 | } 48 | } 49 | if (assertCount) 50 | { 51 | Assert.Equal(expectations.Count, obj.EnumerateObject().Count()); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $([System.IO.Path]::GetDirectoryName($([MSBuild]::GetPathOfFileAbove('.gitignore', '$(MSBuildThisFileDirectory)')))) 5 | 6 | 7 | False 8 | 9 | 10 | $(RepoRoot)/CloudEventsSdk.snk 11 | True 12 | True 13 | True 14 | true 15 | 16 | 17 | False 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $([System.IO.Path]::Combine('$(IntermediateOutputPath)','$(TargetFrameworkMoniker).AssemblyAttributes$(DefaultLanguageSourceExtension)')) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | --------------------------------------------------------------------------------