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

View File

@ -13,7 +13,7 @@ jobs:
steps:
- name: Check out our repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: true
@ -21,7 +21,7 @@ jobs:
- name: Setup .NET 6.0
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Build
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
EndProjectSection
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
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}
{D8055631-E6BB-4CD2-8162-F674D6D30E76} = {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}
{730D4C5E-DC5B-498C-ADFB-05CB81ECCEC8} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
{1566A665-9FFF-4D87-9C7B-CC06C72C9BFF} = {5AD5E051-9A8E-46D9-B0C5-8933718C6D1F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
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 -->
<IsPackable>False</IsPackable>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
</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 System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
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();
return MimeUtilities.IsCloudEventsContentType(contentType);
@ -249,4 +250,23 @@ namespace CloudNative.CloudEvents.Amqp
return applicationProperties;
}
}
}
}
#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">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>AMQP extensions for CloudNative.CloudEvents</Description>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;amqp</PackageTags>
</PropertyGroup>

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;aspnetcore;aspnet</PackageTags>
</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.
using var sr = new StreamReader(typeof(AvroEventFormatter)
.Assembly
.GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json"));
.GetManifestResourceStream("CloudNative.CloudEvents.Avro.AvroSchema.json")!);
return (RecordSchema) Schema.Parse(sr.ReadToEnd());
}

View File

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

View File

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

View File

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

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<PackageTags>cncf;cloudnative;cloudevents;events;json;newtonsoft</PackageTags>
</PropertyGroup>

View File

@ -345,7 +345,9 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
{
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>
@ -524,7 +526,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
}
else
{
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent));
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)!);
if (IsJsonMediaType(dataContentType.MediaType))
{
writer.WritePropertyName(DataPropertyName);
@ -696,7 +698,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
/// <inheritdoc />
protected override void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWriter writer)
{
T data = (T)cloudEvent.Data;
var data = (T?)cloudEvent.Data;
writer.WritePropertyName(DataPropertyName);
Serializer.Serialize(writer, data);
}

View File

@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<PackageTags>cncf;cloudnative;cloudevents;events;protobuf</PackageTags>
<LangVersion>10.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<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>
<LangVersion>8.0</LangVersion>
<PackageTags>cncf;cloudnative;cloudevents;events;json;systemtextjson</PackageTags>
<Nullable>enable</Nullable>
</PropertyGroup>

View File

@ -5,10 +5,12 @@
using CloudNative.CloudEvents.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Xml.Linq;
@ -94,6 +96,13 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// </summary>
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>
/// The options to use when serializing objects to JSON.
/// </summary>
@ -108,22 +117,54 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
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>
/// Creates a JsonEventFormatter that uses the specified <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <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>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter(JsonSerializerOptions? serializerOptions, JsonDocumentOptions documentOptions)
{
SerializerOptions = serializerOptions;
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 />
public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
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
/// its <see cref="CloudEvent.Data"/> property.
/// <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)
{
// 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))
{
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/"))
{
@ -564,6 +614,10 @@ namespace CloudNative.CloudEvents.SystemTextJson
}
/// <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)
{
Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
@ -584,10 +638,18 @@ namespace CloudNative.CloudEvents.SystemTextJson
var encoding = MimeUtilities.GetEncoding(contentType);
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);
}
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));
}
}
@ -654,22 +716,57 @@ namespace CloudNative.CloudEvents.SystemTextJson
/// Creates a JsonEventFormatter that uses the default <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
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>
/// Creates a JsonEventFormatter that uses the serializer <see cref="JsonSerializerOptions"/>
/// and <see cref="JsonDocumentOptions"/> for serializing and parsing.
/// </summary>
/// <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>
#if NET7_0_OR_GREATER
[RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")]
#endif
public JsonEventFormatter(JsonSerializerOptions serializerOptions, JsonDocumentOptions 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 />
#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)
{
Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
@ -679,10 +776,18 @@ namespace CloudNative.CloudEvents.SystemTextJson
return Array.Empty<byte>();
}
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);
}
/// <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)
{
Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
@ -692,22 +797,45 @@ namespace CloudNative.CloudEvents.SystemTextJson
cloudEvent.Data = null;
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 />
#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)
{
T data = (T)cloudEvent.Data;
var data = (T?)cloudEvent.Data;
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 />
protected override void DecodeStructuredModeDataProperty(JsonElement dataElement, CloudEvent cloudEvent) =>
// Note: this is an inefficient way of doing this.
// See https://github.com/dotnet/runtime/issues/31274 - when that's implemented, we can use the new method here.
cloudEvent.Data = JsonSerializer.Deserialize<T>(dataElement.GetRawText(), SerializerOptions);
#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 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.
/// <inheritdoc />

View File

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

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net7.0;net8.0</TargetFrameworks>
<Description>CNCF CloudEvents SDK</Description>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<PackageTags>cloudnative;cloudevents;events</PackageTags>
</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
// we've created using the parameterless constructor.
var buffer = memory.GetBuffer();
return new ReadOnlyMemory<byte>(buffer, 0, (int) memory.Position);
return new ReadOnlyMemory<byte>(buffer, 0, (int)memory.Position);
}
/// <summary>
@ -65,7 +65,7 @@ namespace CloudNative.CloudEvents.Core
public static MemoryStream AsStream(ReadOnlyMemory<byte> 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>
@ -79,7 +79,7 @@ namespace CloudNative.CloudEvents.Core
// TODO: If we introduce an additional netstandard2.1 target, we can use encoding.GetString(memory.Span)
var segment = GetArraySegment(memory);
return encoding.GetString(segment.Array, segment.Offset, segment.Count);
return encoding.GetString(segment.Array!, segment.Offset, segment.Count);
}
/// <summary>
@ -92,7 +92,7 @@ namespace CloudNative.CloudEvents.Core
{
Validation.CheckNotNull(destination, nameof(destination));
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>
@ -108,7 +108,7 @@ namespace CloudNative.CloudEvents.Core
var segment = GetArraySegment(memory);
// 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.
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
: memory.ToArray();
}

View File

@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
@ -57,7 +58,7 @@ namespace CloudNative.CloudEvents.Core
var header = new MediaTypeHeaderValue(contentType.MediaType);
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;
}
@ -76,7 +77,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary>
/// <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>
public static bool IsCloudEventsContentType(string? contentType) =>
public static bool IsCloudEventsContentType([NotNullWhen(true)] string? contentType) =>
contentType is string &&
contentType.StartsWith(MediaType, StringComparison.InvariantCultureIgnoreCase) &&
!contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
@ -86,7 +87,7 @@ namespace CloudNative.CloudEvents.Core
/// </summary>
/// <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>
public static bool IsCloudEventsBatchContentType(string? contentType) =>
public static bool IsCloudEventsBatchContentType([NotNullWhen(true)] string? contentType) =>
contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase);
}
}

View File

@ -5,6 +5,7 @@
using CloudNative.CloudEvents.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
@ -60,6 +61,7 @@ namespace CloudNative.CloudEvents.Http
Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage));
return HasCloudEventsBatchContentType(httpRequestMessage.Content);
}
/// <summary>
/// Indicates whether this <see cref="HttpResponseMessage"/> holds a batch of CloudEvents.
@ -83,7 +85,7 @@ namespace CloudNative.CloudEvents.Http
this HttpResponseMessage httpResponseMessage,
CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes);
ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary>
/// Converts this HTTP response message into a CloudEvent object
@ -112,7 +114,7 @@ namespace CloudNative.CloudEvents.Http
this HttpRequestMessage httpRequestMessage,
CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes);
ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary>
/// 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));
}
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)
{
Validation.CheckNotNull(formatter, nameof(formatter));
@ -142,7 +144,7 @@ namespace CloudNative.CloudEvents.Http
}
else
{
string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content.Headers);
string? versionId = MaybeGetVersionId(headers) ?? MaybeGetVersionId(content?.Headers);
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));
@ -151,7 +153,7 @@ namespace CloudNative.CloudEvents.Http
?? throw new ArgumentException($"Unknown CloudEvents spec version '{versionId}'", paramName);
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);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
@ -231,7 +233,7 @@ namespace CloudNative.CloudEvents.Http
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)
{
Validation.CheckNotNull(formatter, nameof(formatter));
@ -332,15 +334,15 @@ namespace CloudNative.CloudEvents.Http
private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory<byte> content) =>
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?
: new ByteArrayContent(content.ToArray());
// 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);
private static bool HasCloudEventsBatchContentType(HttpContent content) =>
private static bool HasCloudEventsBatchContentType([NotNullWhen(true)] HttpContent? content) =>
MimeUtilities.IsCloudEventsBatchContentType(content?.Headers?.ContentType?.MediaType);
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>
public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEvent(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes);
ToCloudEvent(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
/// <summary>
/// Converts this listener request into a CloudEvent object, with the given extension attributes.
@ -179,7 +179,7 @@ namespace CloudNative.CloudEvents.Http
}
else
{
string versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader];
string? versionId = httpListenerRequest.Headers[HttpUtilities.SpecVersionHttpHeader];
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));
@ -191,12 +191,12 @@ namespace CloudNative.CloudEvents.Http
var headers = httpListenerRequest.Headers;
foreach (var key in headers.AllKeys)
{
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key);
string? attributeName = HttpUtilities.GetAttributeNameFromHeaderName(key!);
if (attributeName is null || attributeName == CloudEventsSpecVersion.SpecVersionAttribute.Name)
{
continue;
}
string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]);
string attributeValue = HttpUtilities.DecodeHeaderValue(headers[key]!);
cloudEvent.SetAttributeFromString(attributeName, attributeValue);
}
@ -223,7 +223,7 @@ namespace CloudNative.CloudEvents.Http
this HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter,
params CloudEventAttribute[]? extensionAttributes) =>
ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?)extensionAttributes);
ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable<CloudEventAttribute>?) extensionAttributes);
/// <summary>
/// Converts this HTTP request message into a CloudEvent batch.
@ -263,6 +263,7 @@ namespace CloudNative.CloudEvents.Http
CloudEventFormatter formatter,
IEnumerable<CloudEventAttribute>? extensionAttributes) =>
ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: false).GetAwaiter().GetResult();
private async static Task<IReadOnlyList<CloudEvent>> ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute>? extensionAttributes, bool async)

View File

@ -27,6 +27,10 @@
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://cloudevents.io</PackageProjectUrl>
<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>
<!-- Package the icon specified in the PackageIcon property -->

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using CloudNative.CloudEvents.UnitTests;
using CloudNative.CloudEvents.UnitTests.ConformanceTestData;
using Microsoft.Extensions.ObjectPool;
using System;
using System.Collections.Generic;
using System.Linq;
@ -21,7 +22,8 @@ public class ConformanceTest
private static IEnumerable<object[]> SelectTestIds(ConformanceTestType type) =>
allTests
.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[]> InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent);
@ -29,20 +31,21 @@ public class ConformanceTest
public static IEnumerable<object[]> InvalidBatchTestIds => SelectTestIds(ConformanceTestType.InvalidBatch);
[Theory, MemberData(nameof(ValidEventTestIds))]
public void ValidEvent(string testId)
public void ValidEvent(string testId, bool useContext)
{
var test = GetTestById(testId);
CloudEvent expected = SampleEvents.FromId(test.SampleId);
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);
}
[Theory, MemberData(nameof(InvalidEventTestIds))]
public void InvalidEvent(string testId)
public void InvalidEvent(string testId, bool useContext)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// 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.
@ -50,7 +53,7 @@ public class ConformanceTest
}
[Theory, MemberData(nameof(ValidBatchTestIds))]
public void ValidBatch(string testId)
public void ValidBatch(string testId, bool useContext)
{
var test = GetTestById(testId);
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.
var json = test.Batch.ToString();
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);
}
[Theory, MemberData(nameof(InvalidBatchTestIds))]
public void InvalidBatch(string testId)
public void InvalidBatch(string testId, bool useContext)
{
var test = GetTestById(testId);
var formatter = new JsonEventFormatter();
var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter();
var extensions = test.SampleExtensionAttributes ? SampleEvents.SampleExtensionAttributes : null;
// We don't have a convenience method for batches, so serialize the array back to JSON.
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 -->
<IsPackable>False</IsPackable>
<LangVersion>12.0</LangVersion>
</PropertyGroup>
</Project>