Add .NET 7, 8, trimming and AOT support

Also fixed several nullability warnings.

Signed-off-by: Giovanni Bassi <giggio@giggio.net>
This commit is contained in:
Giovanni Bassi 2023-11-13 15:08:03 -03:00
parent 6f484339e8
commit 6b24309fc9
No known key found for this signature in database
GPG Key ID: 1237AB122E6F4761
36 changed files with 822 additions and 264 deletions

View File

@ -1,6 +1,7 @@
name: Build name: Build
on: on:
workflow_dispatch:
push: push:
branches: branches:
- main - main
@ -15,15 +16,15 @@ jobs:
steps: steps:
- name: Check out our repo - name: Check out our repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
# Build with .NET 6.0 SDK # Build with .NET 6.0 SDK
- name: Setup .NET 6.0 - name: Setup .NET 8.0
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 6.0.x dotnet-version: 8.0.x
- name: Build - name: Build
run: | run: |

View File

@ -13,7 +13,7 @@ jobs:
steps: steps:
- name: Check out our repo - name: Check out our repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
submodules: true submodules: true
@ -21,7 +21,7 @@ jobs:
- name: Setup .NET 6.0 - name: Setup .NET 6.0
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 6.0.x dotnet-version: 8.0.x
- name: Build - name: Build
run: | run: |

View File

@ -72,6 +72,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "xml", "xml", "{4012C753-68D
conformance\format\xml\valid-events.xml = conformance\format\xml\valid-events.xml conformance\format\xml\valid-events.xml = conformance\format\xml\valid-events.xml
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpSendJson", "samples\HttpSendJson\HttpSendJson.csproj", "{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.MinApiSample", "samples\CloudNative.CloudEvents.MinApiSample\CloudNative.CloudEvents.MinApiSample.csproj", "{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -238,15 +244,43 @@ Global
{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x64.Build.0 = Release|Any CPU {9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x64.Build.0 = Release|Any CPU
{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x86.ActiveCfg = Release|Any CPU {9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x86.ActiveCfg = Release|Any CPU
{9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x86.Build.0 = Release|Any CPU {9D82AC2B-0075-4161-AE0E-4A6629C9FF2A}.Release|x86.Build.0 = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|x64.ActiveCfg = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|x64.Build.0 = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|x86.ActiveCfg = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Debug|x86.Build.0 = Debug|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|Any CPU.Build.0 = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|x64.ActiveCfg = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|x64.Build.0 = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|x86.ActiveCfg = Release|Any CPU
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8}.Release|x86.Build.0 = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|x64.ActiveCfg = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|x64.Build.0 = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|x86.ActiveCfg = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Debug|x86.Build.0 = Debug|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|Any CPU.Build.0 = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|x64.ActiveCfg = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|x64.Build.0 = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|x86.ActiveCfg = Release|Any CPU
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{F1B9B769-DB6B-481F-905C-24FE3B12E00E} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
{9760D744-D1BF-40E3-BD6F-7F639BFB9188} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
{A5906FBA-D73A-4A09-8539-CB10D7B586AE} = {8CCC98B3-1776-49FF-96D6-947A9E5DFB0A} {A5906FBA-D73A-4A09-8539-CB10D7B586AE} = {8CCC98B3-1776-49FF-96D6-947A9E5DFB0A}
{D8055631-E6BB-4CD2-8162-F674D6D30E76} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE} {D8055631-E6BB-4CD2-8162-F674D6D30E76} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
{119AD438-878B-4383-BC9F-779F1605E711} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE} {119AD438-878B-4383-BC9F-779F1605E711} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
{4012C753-68DE-4737-936F-F5DBC485C51B} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE} {4012C753-68DE-4737-936F-F5DBC485C51B} = {A5906FBA-D73A-4A09-8539-CB10D7B586AE}
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F77A454C-CC17-4AD6-823A-64E1A94FDA0A} SolutionGuid = {F77A454C-CC17-4AD6-823A-64E1A94FDA0A}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\CloudNative.CloudEvents.AspNetCore\CloudNative.CloudEvents.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" />
<ProjectReference Include="..\..\src\CloudNative.CloudEvents.SystemTextJson\CloudNative.CloudEvents.SystemTextJson.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<SelfContained>true</SelfContained>
<PublishAot>true</PublishAot>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,85 @@
// Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents;
using CloudNative.CloudEvents.Http;
using CloudNative.CloudEvents.SystemTextJson;
using CloudNative.CloudEvents.AspNetCore;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var formatter = new JsonEventFormatter<Message>(MyJsonContext.Default);
app.MapPost("/api/events/receive/", async (HttpRequest request) =>
{
var cloudEvent = await request.ToCloudEventAsync(formatter);
using var ms = new MemoryStream();
using var writer = new Utf8JsonWriter(ms, new() { Indented = true });
writer.WriteStartObject();
foreach (var (attribute, value) in cloudEvent.GetPopulatedAttributes())
writer.WriteString(attribute.Name, attribute.Format(value));
writer.WriteEndObject();
await writer.FlushAsync();
var attributeMap = Encoding.UTF8.GetString(ms.ToArray());
return Results.Text($"Received event with ID {cloudEvent.Id}, attributes: {attributeMap}");
});
app.MapPost("/api/events/receive2/", (Event e) => Results.Json(e.CloudEvent.Data, MyJsonContext.Default));
app.MapPost("/api/events/receive3/", (Message message) => Results.Json(message, MyJsonContext.Default));
app.MapGet("/api/events/generate/", () =>
{
var evt = new CloudEvent
{
Type = "CloudNative.CloudEvents.MinApiSample",
Source = new Uri("https://github.com/cloudevents/sdk-csharp"),
Time = DateTimeOffset.Now,
DataContentType = "application/json",
Id = Guid.NewGuid().ToString(),
Data = new Message("C#", Environment.Version.ToString())
};
// Format the event as the body of the response. This is UTF-8 JSON because of
// the CloudEventFormatter we're using, but EncodeStructuredModeMessage always
// returns binary data. We could return the data directly, but for debugging
// purposes it's useful to have the JSON string.
var bytes = formatter.EncodeStructuredModeMessage(evt, out var contentType);
string json = Encoding.UTF8.GetString(bytes.Span);
// Specify the content type of the response: this is what makes it a CloudEvent.
// (In "binary mode", the content type is the content type of the data, and headers
// indicate that it's a CloudEvent.)
return Results.Content(json, contentType.MediaType, Encoding.UTF8);
});
app.Run();
[JsonSerializable(typeof(Message))]
internal partial class MyJsonContext : JsonSerializerContext { }
public class Event
{
private readonly static JsonEventFormatter formatter = new JsonEventFormatter<Message>(MyJsonContext.Default);
// required for receive2
public static async ValueTask<Event?> BindAsync(HttpContext context)
{
var cloudEvent = await context.Request.ToCloudEventAsync(formatter);
return new Event { CloudEvent = cloudEvent };
}
public required CloudEvent CloudEvent { get; init; }
}
record class Message(string Language, string EnvironmentVersion)
{
private readonly static JsonEventFormatter formatter = new JsonEventFormatter<Message>(MyJsonContext.Default);
// required for receive3
public static async ValueTask<Message?> BindAsync(HttpContext context)
{
var cloudEvent = await context.Request.ToCloudEventAsync(formatter);
return cloudEvent.Data is Message message ? message : null;
}
}

View File

@ -0,0 +1,14 @@
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "api/events/generate",
"applicationUrl": "http://localhost:5002",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -14,5 +14,6 @@
<!-- Never pack any sample projects --> <!-- Never pack any sample projects -->
<IsPackable>False</IsPackable> <IsPackable>False</IsPackable>
<LangVersion>12.0</LangVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="docopt.net" Version="0.8.1" />
<ProjectReference Include="..\..\src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" />
<ProjectReference Include="..\..\src\CloudNative.CloudEvents.SystemTextJson\CloudNative.CloudEvents.SystemTextJson.csproj" />
</ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<SelfContained>true</SelfContained>
<PublishAot>true</PublishAot>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,70 @@
// Copyright (c) Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents;
using CloudNative.CloudEvents.Http;
using CloudNative.CloudEvents.SystemTextJson;
using DocoptNet;
using System.Net.Mime;
using static System.Console;
// This application uses the docopt.net library for parsing the command
// line and calling the application code.
ProgramArguments programArguments = new();
var result = await ProgramArguments.CreateParserWithVersion()
.Parse(args)
.Match(RunAsync,
result => { WriteLine(result.Help); return Task.FromResult(1); },
result => { WriteLine(result.Version); return Task.FromResult(0); },
result => { Error.WriteLine(result.Usage); return Task.FromResult(1); });
return result;
static async Task<int> RunAsync(ProgramArguments args)
{
var cloudEvent = new CloudEvent
{
Id = Guid.NewGuid().ToString(),
Type = args.OptType,
Source = new Uri(args.OptSource),
DataContentType = MediaTypeNames.Application.Json,
Data = System.Text.Json.JsonSerializer.Serialize("hey there!", GeneratedJsonContext.Default.String)
};
var content = cloudEvent.ToHttpContent(ContentMode.Structured, new JsonEventFormatter(GeneratedJsonContext.Default));
var httpClient = new HttpClient();
// Your application remains in charge of adding any further headers or
// other information required to authenticate/authorize or otherwise
// dispatch the call at the server.
var result = await httpClient.PostAsync(args.OptUrl, content);
WriteLine(result.StatusCode);
return 0;
}
[System.Text.Json.Serialization.JsonSerializable(typeof(string))]
internal partial class GeneratedJsonContext : System.Text.Json.Serialization.JsonSerializerContext
{
}
[DocoptArguments]
partial class ProgramArguments
{
const string Help = @"HttpSendJson.
Usage:
HttpSendJson --url=URL [--type=TYPE] [--source=SOURCE]
HttpSendJson (-h | --help)
HttpSendJson --version
Options:
--url=URL HTTP(S) address to send the event to.
--type=TYPE CloudEvents 'type' [default: com.example.myevent].
--source=SOURCE CloudEvents 'source' [default: urn:example-com:mysource:abc].
-h --help Show this screen.
--version Show version.
";
public static string Version => $"producer {typeof(ProgramArguments).Assembly.GetName().Version}";
public static IParser<ProgramArguments> CreateParserWithVersion() => CreateParser().WithVersion(Version);
}

View File

@ -8,6 +8,7 @@ using Amqp.Types;
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Mime; using System.Net.Mime;
@ -145,7 +146,7 @@ namespace CloudNative.CloudEvents.Amqp
} }
} }
private static bool HasCloudEventsContentType(Message message, out string? contentType) private static bool HasCloudEventsContentType(Message message, [NotNullWhen(true)] out string? contentType)
{ {
contentType = message.Properties.ContentType?.ToString(); contentType = message.Properties.ContentType?.ToString();
return MimeUtilities.IsCloudEventsContentType(contentType); return MimeUtilities.IsCloudEventsContentType(contentType);
@ -250,3 +251,22 @@ namespace CloudNative.CloudEvents.Amqp
} }
} }
} }
#if NETSTANDARD2_0
namespace System.Diagnostics.CodeAnalysis
{
/// <summary>Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.</summary>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>Initializes the attribute with the specified return value condition.</summary>
/// <param name="returnValue">
/// The return value condition. If the method returns this value, the associated parameter will not be null.
/// </param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
/// <summary>Gets the return value condition.</summary>
public bool ReturnValue { get; }
}
}
#endif

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>AMQP extensions for CloudNative.CloudEvents</Description> <Description>AMQP extensions for CloudNative.CloudEvents</Description>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;amqp</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;amqp</PackageTags>
</PropertyGroup> </PropertyGroup>

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>ASP.Net Core extensions for CloudNative.CloudEvents</Description> <Description>ASP.Net Core extensions for CloudNative.CloudEvents</Description>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;aspnetcore;aspnet</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;aspnetcore;aspnet</PackageTags>
</PropertyGroup> </PropertyGroup>

View File

@ -185,7 +185,7 @@ namespace CloudNative.CloudEvents.Avro
// will fail and that's okay since the type is useless without the proper schema. // will fail and that's okay since the type is useless without the proper schema.
using var sr = new StreamReader(typeof(AvroEventFormatter) using var sr = new StreamReader(typeof(AvroEventFormatter)
.Assembly .Assembly
.GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json")); .GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json")!);
return (RecordSchema) Schema.Parse(sr.ReadToEnd()); return (RecordSchema) Schema.Parse(sr.ReadToEnd());
} }

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>Avro extensions for CloudNative.CloudEvents</Description> <Description>Avro extensions for CloudNative.CloudEvents</Description>
<PackageTags>cncf;cloudnative;cloudevents;events;avro</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;avro</PackageTags>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>Kafka extensions for CloudNative.CloudEvents</Description> <Description>Kafka extensions for CloudNative.CloudEvents</Description>
<PackageTags>cncf;cloudnative;cloudevents;events;kafka</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;kafka</PackageTags>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>MQTT extensions for CloudNative.CloudEvents</Description> <Description>MQTT extensions for CloudNative.CloudEvents</Description>
<PackageTags>cncf;cloudnative;cloudevents;events;mqtt</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;mqtt</PackageTags>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>JSON support for the CNCF CloudEvents SDK, based on Newtonsoft.Json.</Description> <Description>JSON support for the CNCF CloudEvents SDK, based on Newtonsoft.Json.</Description>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;json;newtonsoft</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;json;newtonsoft</PackageTags>
</PropertyGroup> </PropertyGroup>

View File

@ -345,7 +345,9 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
{ {
throw new ArgumentException($"Structured mode property '{DataBase64PropertyName}' must be a string, when present."); throw new ArgumentException($"Structured mode property '{DataBase64PropertyName}' must be a string, when present.");
} }
cloudEvent.Data = Convert.FromBase64String((string?)dataBase64Token); var tokenString = (string?)dataBase64Token;
if (tokenString != null)
cloudEvent.Data = Convert.FromBase64String(tokenString);
} }
/// <summary> /// <summary>
@ -524,7 +526,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
} }
else else
{ {
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)); ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)!);
if (IsJsonMediaType(dataContentType.MediaType)) if (IsJsonMediaType(dataContentType.MediaType))
{ {
writer.WritePropertyName(DataPropertyName); writer.WritePropertyName(DataPropertyName);
@ -696,7 +698,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
/// <inheritdoc /> /// <inheritdoc />
protected override void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWriter writer) protected override void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWriter writer)
{ {
T data = (T)cloudEvent.Data; var data = (T?)cloudEvent.Data;
writer.WritePropertyName(DataPropertyName); writer.WritePropertyName(DataPropertyName);
Serializer.Serialize(writer, data); Serializer.Serialize(writer, data);
} }

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>Support for the Protobuf event format in for CloudNative.CloudEvents</Description> <Description>Support for the Protobuf event format in for CloudNative.CloudEvents</Description>
<PackageTags>cncf;cloudnative;cloudevents;events;protobuf</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;protobuf</PackageTags>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>JSON support for the CNCF CloudEvents SDK, based on System.Text.Json.</Description> <Description>JSON support for the CNCF CloudEvents SDK, based on System.Text.Json.</Description>
<LangVersion>8.0</LangVersion>
<PackageTags>cncf;cloudnative;cloudevents;events;json;systemtextjson</PackageTags> <PackageTags>cncf;cloudnative;cloudevents;events;json;systemtextjson</PackageTags>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -5,10 +5,12 @@
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml.Linq; using System.Xml.Linq;
@ -94,6 +96,13 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// </summary> /// </summary>
protected const string DataPropertyName = "data"; protected const string DataPropertyName = "data";
#if NET7_0_OR_GREATER
/// <summary>
/// Json serialization context used to serialize and enable trimming and AOT.
/// </summary>
protected readonly JsonSerializerContext? JsonSerializerContext;
#endif
/// <summary> /// <summary>
/// The options to use when serializing objects to JSON. /// The options to use when serializing objects to JSON.
/// </summary> /// </summary>
@ -108,22 +117,54 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/> /// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing. /// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary> /// </summary>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter() : this(null, default) public JsonEventFormatter() : this(null, default)
{ {
} }
#if NET7_0_OR_GREATER
/// <summary>
/// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <param name="jsonSerializerContext">The json context used for serializing objects to JSON.</param>
public JsonEventFormatter(JsonSerializerContext jsonSerializerContext) : this(default, jsonSerializerContext)
{
}
#endif
/// <summary> /// <summary>
/// Creates a JsonEventFormatter that uses the specified <see cref="JsonSerializerOptions"/> /// Creates a JsonEventFormatter that uses the specified <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing. /// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary> /// </summary>
/// <param name="serializerOptions">The options to use when serializing objects to JSON. May be null.</param> /// <param name="serializerOptions">The options to use when serializing objects to JSON. May be null.</param>
/// <param name="documentOptions">The options to use when parsing JSON documents.</param> /// <param name="documentOptions">The options to use when parsing JSON documents.</param>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter(JsonSerializerOptions? serializerOptions, JsonDocumentOptions documentOptions) public JsonEventFormatter(JsonSerializerOptions? serializerOptions, JsonDocumentOptions documentOptions)
{ {
SerializerOptions = serializerOptions; SerializerOptions = serializerOptions;
DocumentOptions = documentOptions; DocumentOptions = documentOptions;
} }
#if NET7_0_OR_GREATER
/// <summary>
/// Creates a JsonEventFormatter that uses the specified <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <param name="documentOptions">The options to use when parsing JSON documents.</param>
/// <param name="jsonSerializerContext">The json context used for serializing objects to JSON.</param>
public JsonEventFormatter(JsonDocumentOptions documentOptions, JsonSerializerContext jsonSerializerContext)
{
Validation.CheckNotNull(jsonSerializerContext, nameof(jsonSerializerContext));
DocumentOptions = documentOptions;
JsonSerializerContext = jsonSerializerContext;
}
#endif
/// <inheritdoc /> /// <inheritdoc />
public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) => public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
await DecodeStructuredModeMessageImpl(body, contentType, extensionAttributes, true).ConfigureAwait(false); await DecodeStructuredModeMessageImpl(body, contentType, extensionAttributes, true).ConfigureAwait(false);
@ -533,6 +574,10 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// <param name="cloudEvent">The CloudEvent being encoded, which will have a non-null value for /// <param name="cloudEvent">The CloudEvent being encoded, which will have a non-null value for
/// its <see cref="CloudEvent.Data"/> property. /// its <see cref="CloudEvent.Data"/> property.
/// <param name="writer"/>The writer to serialize the data to. Will not be null.</param> /// <param name="writer"/>The writer to serialize the data to. Will not be null.</param>
#if NET7_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
#endif
protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonWriter writer) protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonWriter writer)
{ {
// Binary data is encoded using the data_base64 property, regardless of content type. // Binary data is encoded using the data_base64 property, regardless of content type.
@ -548,7 +593,12 @@ namespace CloudNative.CloudEvents.SystemTextJson
if (IsJsonMediaType(dataContentType.MediaType)) if (IsJsonMediaType(dataContentType.MediaType))
{ {
writer.WritePropertyName(DataPropertyName); writer.WritePropertyName(DataPropertyName);
JsonSerializer.Serialize(writer, cloudEvent.Data, SerializerOptions); #if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
JsonSerializer.Serialize(writer, cloudEvent.Data, cloudEvent.Data!.GetType(), JsonSerializerContext);
else
#endif
JsonSerializer.Serialize(writer, cloudEvent.Data, SerializerOptions);
} }
else if (cloudEvent.Data is string text && dataContentType.MediaType.StartsWith("text/")) else if (cloudEvent.Data is string text && dataContentType.MediaType.StartsWith("text/"))
{ {
@ -564,6 +614,10 @@ namespace CloudNative.CloudEvents.SystemTextJson
} }
/// <inheritdoc /> /// <inheritdoc />
#if NET7_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
#endif
public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent) public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent)
{ {
Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
@ -584,10 +638,18 @@ namespace CloudNative.CloudEvents.SystemTextJson
var encoding = MimeUtilities.GetEncoding(contentType); var encoding = MimeUtilities.GetEncoding(contentType);
if (encoding is UTF8Encoding) if (encoding is UTF8Encoding)
{ {
#if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, cloudEvent.Data.GetType(), JsonSerializerContext);
#endif
return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, SerializerOptions); return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, SerializerOptions);
} }
else else
{ {
#if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
return MimeUtilities.GetEncoding(contentType).GetBytes(JsonSerializer.Serialize(cloudEvent.Data, cloudEvent.Data.GetType(), JsonSerializerContext));
#endif
return MimeUtilities.GetEncoding(contentType).GetBytes(JsonSerializer.Serialize(cloudEvent.Data, SerializerOptions)); return MimeUtilities.GetEncoding(contentType).GetBytes(JsonSerializer.Serialize(cloudEvent.Data, SerializerOptions));
} }
} }
@ -654,22 +716,57 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/> /// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing. /// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary> /// </summary>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter() public JsonEventFormatter()
{ {
} }
#if NET7_0_OR_GREATER
/// <summary>
/// Creates a JsonEventFormatter that uses the serializer <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <param name="jsonSerializerContext">The json context used for serializing objects to JSON.</param>
public JsonEventFormatter(JsonSerializerContext jsonSerializerContext)
: base(default, jsonSerializerContext)
{
}
#endif
/// <summary> /// <summary>
/// Creates a JsonEventFormatter that uses the serializer <see cref="JsonSerializerOptions"/> /// Creates a JsonEventFormatter that uses the serializer <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing. /// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary> /// </summary>
/// <param name="serializerOptions">The options to use when serializing and parsing. May be null.</param> /// <param name="serializerOptions">The options to use when serializing and parsing. May be null.</param>
/// <param name="documentOptions">The options to use when parsing JSON documents.</param> /// <param name="documentOptions">The options to use when parsing JSON documents.</param>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter(JsonSerializerOptions serializerOptions, JsonDocumentOptions documentOptions) public JsonEventFormatter(JsonSerializerOptions serializerOptions, JsonDocumentOptions documentOptions)
: base(serializerOptions, documentOptions) : base(serializerOptions, documentOptions)
{ {
} }
#if NET7_0_OR_GREATER
/// <summary>
/// Creates a JsonEventFormatter that uses the serializer <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <param name="documentOptions">The options to use when parsing JSON documents.</param>
/// <param name="jsonSerializerContext">The json context used for serializing objects to JSON.</param>
public JsonEventFormatter(JsonDocumentOptions documentOptions, JsonSerializerContext jsonSerializerContext)
: base(documentOptions, jsonSerializerContext)
{
}
#endif
/// <inheritdoc /> /// <inheritdoc />
#if NET7_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
#endif
public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent) public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent)
{ {
Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
@ -679,10 +776,18 @@ namespace CloudNative.CloudEvents.SystemTextJson
return Array.Empty<byte>(); return Array.Empty<byte>();
} }
T data = (T)cloudEvent.Data; T data = (T)cloudEvent.Data;
#if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
return JsonSerializer.SerializeToUtf8Bytes(data, data.GetType(), JsonSerializerContext);
#endif
return JsonSerializer.SerializeToUtf8Bytes(data, SerializerOptions); return JsonSerializer.SerializeToUtf8Bytes(data, SerializerOptions);
} }
/// <inheritdoc /> /// <inheritdoc />
#if NET7_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
#endif
public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudEvent cloudEvent) public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudEvent cloudEvent)
{ {
Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
@ -692,22 +797,45 @@ namespace CloudNative.CloudEvents.SystemTextJson
cloudEvent.Data = null; cloudEvent.Data = null;
return; return;
} }
cloudEvent.Data = JsonSerializer.Deserialize<T>(body.Span, SerializerOptions); #if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
cloudEvent.Data = JsonSerializer.Deserialize(body.Span, typeof(T), JsonSerializerContext);
else
#endif
cloudEvent.Data = JsonSerializer.Deserialize<T>(body.Span, SerializerOptions);
} }
/// <inheritdoc /> /// <inheritdoc />
#if NET7_0_OR_GREATER
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
[UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
#endif
protected override void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonWriter writer) protected override void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonWriter writer)
{ {
T data = (T)cloudEvent.Data; var data = (T?)cloudEvent.Data;
writer.WritePropertyName(DataPropertyName); writer.WritePropertyName(DataPropertyName);
JsonSerializer.Serialize(writer, data, SerializerOptions); #if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
JsonSerializer.Serialize(writer, data, data!.GetType(), JsonSerializerContext);
else
#endif
JsonSerializer.Serialize(writer, data, SerializerOptions);
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void DecodeStructuredModeDataProperty(JsonElement dataElement, CloudEvent cloudEvent) => #if NET7_0_OR_GREATER
// Note: this is an inefficient way of doing this. [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Constructor already annotated.")]
// See https://github.com/dotnet/runtime/issues/31274 - when that's implemented, we can use the new method here. [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "Constructor already annotated.")]
cloudEvent.Data = JsonSerializer.Deserialize<T>(dataElement.GetRawText(), SerializerOptions); #endif
protected override void DecodeStructuredModeDataProperty(JsonElement dataElement, CloudEvent cloudEvent)
{
#if NET7_0_OR_GREATER
if (JsonSerializerContext != null)
cloudEvent.Data = JsonSerializer.Deserialize(dataElement, typeof(T), JsonSerializerContext);
else
#endif
cloudEvent.Data = JsonSerializer.Deserialize<T>(dataElement.GetRawText(), SerializerOptions);
}
// TODO: Consider decoding the base64 data as a byte array, then using DecodeBinaryModeData. // TODO: Consider decoding the base64 data as a byte array, then using DecodeBinaryModeData.
/// <inheritdoc /> /// <inheritdoc />

View File

@ -4,6 +4,7 @@
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection; using System.Reflection;
namespace CloudNative.CloudEvents namespace CloudNative.CloudEvents
@ -21,6 +22,9 @@ namespace CloudNative.CloudEvents
/// <summary> /// <summary>
/// The type to use for CloudEvent formatting. Must not be null. /// The type to use for CloudEvent formatting. Must not be null.
/// </summary> /// </summary>
#if NET7_0_OR_GREATER
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
#endif
public Type FormatterType { get; } public Type FormatterType { get; }
/// <summary> /// <summary>
@ -56,7 +60,7 @@ namespace CloudNative.CloudEvents
throw new ArgumentException($"The {nameof(CloudEventFormatterAttribute)} on type {targetType} has no converter type specified.", nameof(targetType)); throw new ArgumentException($"The {nameof(CloudEventFormatterAttribute)} on type {targetType} has no converter type specified.", nameof(targetType));
} }
object instance; object? instance;
try try
{ {
instance = Activator.CreateInstance(formatterType); instance = Activator.CreateInstance(formatterType);
@ -73,6 +77,6 @@ namespace CloudNative.CloudEvents
} }
return formatter; return formatter;
} }
} }
} }

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>CNCF CloudEvents SDK</Description> <Description>CNCF CloudEvents SDK</Description>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<PackageTags>cloudnative;cloudevents;events</PackageTags> <PackageTags>cloudnative;cloudevents;events</PackageTags>
</PropertyGroup> </PropertyGroup>

View File

@ -32,7 +32,7 @@ namespace CloudNative.CloudEvents.Core
// It's safe to use memory.GetBuffer() and memory.Position here, as this is a stream // It's safe to use memory.GetBuffer() and memory.Position here, as this is a stream
// we've created using the parameterless constructor. // we've created using the parameterless constructor.
var buffer = memory.GetBuffer(); var buffer = memory.GetBuffer();
return new ReadOnlyMemory<byte>(buffer, 0, (int) memory.Position); return new ReadOnlyMemory<byte>(buffer, 0, (int)memory.Position);
} }
/// <summary> /// <summary>
@ -65,7 +65,7 @@ namespace CloudNative.CloudEvents.Core
public static MemoryStream AsStream(ReadOnlyMemory<byte> memory) public static MemoryStream AsStream(ReadOnlyMemory<byte> memory)
{ {
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
return new MemoryStream(segment.Array, segment.Offset, segment.Count, false); return new MemoryStream(segment.Array!, segment.Offset, segment.Count, false);
} }
/// <summary> /// <summary>
@ -79,7 +79,7 @@ namespace CloudNative.CloudEvents.Core
// TODO: If we introduce an additional netstandard2.1 target, we can use encoding.GetString(memory.Span) // TODO: If we introduce an additional netstandard2.1 target, we can use encoding.GetString(memory.Span)
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
return encoding.GetString(segment.Array, segment.Offset, segment.Count); return encoding.GetString(segment.Array!, segment.Offset, segment.Count);
} }
/// <summary> /// <summary>
@ -92,7 +92,7 @@ namespace CloudNative.CloudEvents.Core
{ {
Validation.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(destination, nameof(destination));
var segment = GetArraySegment(source); var segment = GetArraySegment(source);
await destination.WriteAsync(segment.Array, segment.Offset, segment.Count).ConfigureAwait(false); await destination.WriteAsync(segment.Array!, segment.Offset, segment.Count).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@ -108,7 +108,7 @@ namespace CloudNative.CloudEvents.Core
var segment = GetArraySegment(memory); var segment = GetArraySegment(memory);
// We probably don't actually need to check the offset: if the count is the same as the length, // We probably don't actually need to check the offset: if the count is the same as the length,
// I can't see how the offset can be non-zero. But it doesn't *hurt* as a check. // I can't see how the offset can be non-zero. But it doesn't *hurt* as a check.
return segment.Offset == 0 && segment.Count == segment.Array.Length return segment.Array is not null && segment.Offset == 0 && segment.Count == segment.Array.Length
? segment.Array ? segment.Array
: memory.ToArray(); : memory.ToArray();
} }

View File

@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
@ -57,7 +58,7 @@ namespace CloudNative.CloudEvents.Core
var header = new MediaTypeHeaderValue(contentType.MediaType); var header = new MediaTypeHeaderValue(contentType.MediaType);
foreach (string parameterName in contentType.Parameters.Keys) foreach (string parameterName in contentType.Parameters.Keys)
{ {
header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName].ToString())); header.Parameters.Add(new NameValueHeaderValue(parameterName, contentType.Parameters[parameterName]!.ToString()));
} }
return header; return header;
} }
@ -76,7 +77,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary> /// </summary>
/// <param name="contentType">The content type to check. May be null, in which case the result is false.</param> /// <param name="contentType">The content type to check. May be null, in which case the result is false.</param>
/// <returns>true if the given content type denotes a (non-batch) CloudEvent; false otherwise</returns> /// <returns>true if the given content type denotes a (non-batch) CloudEvent; false otherwise</returns>
public static bool IsCloudEventsContentType(string? contentType) => public static bool IsCloudEventsContentType([NotNullWhen(true)] string? contentType) =>
contentType is string && contentType is string &&
contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) && contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) &&
!contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); !contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
@ -86,7 +87,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary> /// </summary>
/// <param name="contentType">The content type to check. May be null, in which case the result is false.</param> /// <param name="contentType">The content type to check. May be null, in which case the result is false.</param>
/// <returns>true if the given content type represents a CloudEvent batch; false otherwise</returns> /// <returns>true if the given content type represents a CloudEvent batch; false otherwise</returns>
public static bool IsCloudEventsBatchContentType(string? contentType) => public static bool IsCloudEventsBatchContentType([NotNullWhen(true)] string? contentType) =>
contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
} }
} }

View File

@ -5,6 +5,7 @@
using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
@ -61,6 +62,7 @@ namespace CloudNative.CloudEvents.Http
return HasCloudEventsBatchContentType(httpRequestMessage.Content); return HasCloudEventsBatchContentType(httpRequestMessage.Content);
} }
/// <summary> /// <summary>
/// Indicates whether this <see cref="HttpResponseMessage"/> holds a batch of CloudEvents. /// Indicates whether this <see cref="HttpResponseMessage"/> holds a batch of CloudEvents.
/// </summary> /// </summary>
@ -83,7 +85,7 @@ namespace CloudNative.CloudEvents.Http
this HttpResponseMessage httpResponseMessage, this HttpResponseMessage httpResponseMessage,
CloudEventFormatter formatter, CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) => params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes); ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary> /// <summary>
/// Converts this HTTP response message into a CloudEvent object /// Converts this HTTP response message into a CloudEvent object
@ -112,7 +114,7 @@ namespace CloudNative.CloudEvents.Http
this HttpRequestMessage httpRequestMessage, this HttpRequestMessage httpRequestMessage,
CloudEventFormatter formatter, CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) => params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes); ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary> /// <summary>
/// Converts this HTTP request message into a CloudEvent object. /// Converts this HTTP request message into a CloudEvent object.
@ -130,7 +132,7 @@ namespace CloudNative.CloudEvents.Http
return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage));
} }
private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content, private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent? content,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{ {
Validation.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
@ -142,7 +144,7 @@ namespace CloudNative.CloudEvents.Http
} }
else else
{ {
string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content.Headers); string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content?.Headers);
if (versionId is null) if (versionId is null)
{ {
throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(paramName)); throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(paramName));
@ -151,7 +153,7 @@ namespace CloudNative.CloudEvents.Http
?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", paramName); ?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", paramName);
var cloudEvent = new CloudEvent(version, extensionAttributes); var cloudEvent = new CloudEvent(version, extensionAttributes);
foreach (var header in headers.Concat(content.Headers)) foreach (var header in headers.Concat(content!.Headers))
{ {
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(header.Key); string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(header.Key);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
@ -231,7 +233,7 @@ namespace CloudNative.CloudEvents.Http
return ToCloudEventBatchInternalAsync(httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); return ToCloudEventBatchInternalAsync(httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage));
} }
private static async Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpContent content, private static async Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpContent? content,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{ {
Validation.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
@ -332,15 +334,15 @@ namespace CloudNative.CloudEvents.Http
private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory<byte> content) => private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory<byte> content) =>
MemoryMarshal.TryGetArray(content, out var segment) MemoryMarshal.TryGetArray(content, out var segment)
? new ByteArrayContent(segment.Array, segment.Offset, segment.Count) ? new ByteArrayContent(segment.Array!, segment.Offset, segment.Count)
// TODO: Just throw? // TODO: Just throw?
: new ByteArrayContent(content.ToArray()); : new ByteArrayContent(content.ToArray());
// TODO: This would include "application/cloudeventsarerubbish" for example... // TODO: This would include "application/cloudeventsarerubbish" for example...
private static bool HasCloudEventsContentType(HttpContent content) => private static bool HasCloudEventsContentType([NotNullWhen(true)] HttpContent? content) =>
MimeUtilities.IsCloudEventsContentType(content?.Headers?.ContentType?.MediaType); MimeUtilities.IsCloudEventsContentType(content?.Headers?.ContentType?.MediaType);
private static bool HasCloudEventsBatchContentType(HttpContent content) => private static bool HasCloudEventsBatchContentType([NotNullWhen(true)] HttpContent? content) =>
MimeUtilities.IsCloudEventsBatchContentType(content?.Headers?.ContentType?.MediaType); MimeUtilities.IsCloudEventsBatchContentType(content?.Headers?.ContentType?.MediaType);
private static string? MaybeGetVersionId(HttpHeaders? headers) => private static string? MaybeGetVersionId(HttpHeaders? headers) =>

View File

@ -151,7 +151,7 @@ namespace CloudNative.CloudEvents.Http
/// <returns>A reference to a validated CloudEvent instance.</returns> /// <returns>A reference to a validated CloudEvent instance.</returns>
public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest, public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEvent(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes); ToCloudEvent(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary> /// <summary>
/// Converts this listener request into a CloudEvent object, with the given extension attributes. /// Converts this listener request into a CloudEvent object, with the given extension attributes.
@ -179,7 +179,7 @@ namespace CloudNative.CloudEvents.Http
} }
else else
{ {
string versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader]; string? versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader];
if (versionId is null) if (versionId is null)
{ {
throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpListenerRequest)); throw new ArgumentException($"Request does not represent a CloudEvent. It has neither a {HttpUtilities.SpecVersionHttpHeader} header, nor a suitable content type.", nameof(httpListenerRequest));
@ -191,12 +191,12 @@ namespace CloudNative.CloudEvents.Http
var headers = httpListenerRequest.Headers; var headers = httpListenerRequest.Headers;
foreach (var key in headers.AllKeys) foreach (var key in headers.AllKeys)
{ {
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key); string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key!);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name) if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
{ {
continue; continue;
} }
string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]); string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]!);
cloudEvent.SetAttributeFromString(attributeName, attributeValue); cloudEvent.SetAttributeFromString(attributeName, attributeValue);
} }
@ -223,7 +223,7 @@ namespace CloudNative.CloudEvents.Http
this HttpListenerRequest httpListenerRequest, this HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) => params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes); ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes);
/// <summary> /// <summary>
/// Converts this HTTP request message into a CloudEvent batch. /// Converts this HTTP request message into a CloudEvent batch.
@ -264,6 +264,7 @@ namespace CloudNative.CloudEvents.Http
IEnumerable<CloudEventAttribute>? extensionAttributes) => IEnumerable<CloudEventAttribute>? extensionAttributes) =>
ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: false).GetAwaiter().GetResult(); ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: false).GetAwaiter().GetResult();
private async static Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest, private async static Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, bool async) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, bool async)
{ {

View File

@ -27,6 +27,10 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://cloudevents.io</PackageProjectUrl> <PackageProjectUrl>https://cloudevents.io</PackageProjectUrl>
<Copyright>Copyright Cloud Native Foundation</Copyright> <Copyright>Copyright Cloud Native Foundation</Copyright>
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
<LangVersion>12.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<!-- Package the icon specified in the PackageIcon property --> <!-- Package the icon specified in the PackageIcon property -->

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View File

@ -4,6 +4,7 @@
using CloudNative.CloudEvents.UnitTests; using CloudNative.CloudEvents.UnitTests;
using CloudNative.CloudEvents.UnitTests.ConformanceTestData; using CloudNative.CloudEvents.UnitTests.ConformanceTestData;
using Microsoft.Extensions.ObjectPool;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -21,7 +22,8 @@ public class ConformanceTest
private static IEnumerable<object[]> SelectTestIds(ConformanceTestType type) => private static IEnumerable<object[]> SelectTestIds(ConformanceTestType type) =>
allTests allTests
.Where(test => test.TestType == type) .Where(test => test.TestType == type)
.Select(test => new object[] { test.Id }); .Select(test => new object[][] { [test.Id, true], [test.Id, false] })
.SelectMany(x => x);
public static IEnumerable<object[]> ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent); public static IEnumerable<object[]> ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent);
public static IEnumerable<object[]> InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent); public static IEnumerable<object[]> InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent);
@ -29,20 +31,21 @@ public class ConformanceTest
public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch); public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch);
[Theory, MemberData(nameof(ValidEventTestIds))] [Theory, MemberData(nameof(ValidEventTestIds))]
public void ValidEvent(string testId) public void ValidEvent(string testId, bool useContext)
{ {
var test = GetTestById(testId); var test = GetTestById(testId);
CloudEvent expected = SampleEvents.FromId(test.SampleId); CloudEvent expected = SampleEvents.FromId(test.SampleId);
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
CloudEvent actual = new JsonEventFormatter().ConvertFromJsonElement(test.Event, extensions); CloudEvent actual = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter())
.ConvertFromJsonElement(test.Event, extensions);
TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); TestHelpers.AssertCloudEventsEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
} }
[Theory, MemberData(nameof(InvalidEventTestIds))] [Theory, MemberData(nameof(InvalidEventTestIds))]
public void InvalidEvent(string testId) public void InvalidEvent(string testId, bool useContext)
{ {
var test = GetTestById(testId); var test = GetTestById(testId);
var formatter = new JsonEventFormatter(); var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// Hmm... we throw FormatException in some cases, when ArgumentException would be better. // Hmm... we throw FormatException in some cases, when ArgumentException would be better.
// Changing that would be "somewhat breaking"... it's unclear how much we should worry. // Changing that would be "somewhat breaking"... it's unclear how much we should worry.
@ -50,7 +53,7 @@ public class ConformanceTest
} }
[Theory, MemberData(nameof(ValidBatchTestIds))] [Theory, MemberData(nameof(ValidBatchTestIds))]
public void ValidBatch(string testId) public void ValidBatch(string testId, bool useContext)
{ {
var test = GetTestById(testId); var test = GetTestById(testId);
IReadOnlyList<CloudEvent> expected = SampleBatches.FromId(test.SampleId); IReadOnlyList<CloudEvent> expected = SampleBatches.FromId(test.SampleId);
@ -58,15 +61,16 @@ public class ConformanceTest
// We don't have a convenience method for batches, so serialize the array back to JSON. // We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString(); var json = test.Batch.ToString();
var body = Encoding.UTF8.GetBytes(json); var body = Encoding.UTF8.GetBytes(json);
IReadOnlyList<CloudEvent> actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions); IReadOnlyList<CloudEvent> actual = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter())
.DecodeBatchModeMessage(body, contentType: null, extensions);
TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer); TestHelpers.AssertBatchesEqual(expected, actual, TestHelpers.InstantOnlyTimestampComparer);
} }
[Theory, MemberData(nameof(InvalidBatchTestIds))] [Theory, MemberData(nameof(InvalidBatchTestIds))]
public void InvalidBatch(string testId) public void InvalidBatch(string testId, bool useContext)
{ {
var test = GetTestById(testId); var test = GetTestById(testId);
var formatter = new JsonEventFormatter(); var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null; var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON. // We don't have a convenience method for batches, so serialize the array back to JSON.
var json = test.Batch.ToString(); var json = test.Batch.ToString();

View File

@ -0,0 +1,12 @@
// Copyright 2023 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
namespace CloudNative.CloudEvents.SystemTextJson.UnitTests;
[System.Text.Json.Serialization.JsonSerializable(typeof(JsonConformanceTest))]
[System.Text.Json.Serialization.JsonSerializable(typeof(AttributedModel))]
[System.Text.Json.Serialization.JsonSerializable(typeof(int))]
internal partial class GeneratedJsonContext : System.Text.Json.Serialization.JsonSerializerContext
{
}

View File

@ -14,5 +14,6 @@
<!-- Never pack any test projects --> <!-- Never pack any test projects -->
<IsPackable>False</IsPackable> <IsPackable>False</IsPackable>
<LangVersion>12.0</LangVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>