diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a5afda..c068604 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: | diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index 4ee48a5..be4f97e 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -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: | diff --git a/CloudEvents.sln b/CloudEvents.sln index 530452a..93bd82a 100644 --- a/CloudEvents.sln +++ b/CloudEvents.sln @@ -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} diff --git a/samples/CloudNative.CloudEvents.MinApiSample/CloudNative.CloudEvents.MinApiSample.csproj b/samples/CloudNative.CloudEvents.MinApiSample/CloudNative.CloudEvents.MinApiSample.csproj new file mode 100644 index 0000000..5e20f7b --- /dev/null +++ b/samples/CloudNative.CloudEvents.MinApiSample/CloudNative.CloudEvents.MinApiSample.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + + + + + + + + true + true + None + False + + diff --git a/samples/CloudNative.CloudEvents.MinApiSample/Program.cs b/samples/CloudNative.CloudEvents.MinApiSample/Program.cs new file mode 100644 index 0000000..6f142a6 --- /dev/null +++ b/samples/CloudNative.CloudEvents.MinApiSample/Program.cs @@ -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(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(MyJsonContext.Default); + // required for receive2 + public static async ValueTask 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(MyJsonContext.Default); + // required for receive3 + public static async ValueTask BindAsync(HttpContext context) + { + var cloudEvent = await context.Request.ToCloudEventAsync(formatter); + return cloudEvent.Data is Message message ? message : null; + } +} + diff --git a/samples/CloudNative.CloudEvents.MinApiSample/Properties/launchSettings.json b/samples/CloudNative.CloudEvents.MinApiSample/Properties/launchSettings.json new file mode 100644 index 0000000..c202106 --- /dev/null +++ b/samples/CloudNative.CloudEvents.MinApiSample/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "api/events/generate", + "applicationUrl": "http://localhost:5002", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/CloudNative.CloudEvents.MinApiSample/appsettings.Development.json b/samples/CloudNative.CloudEvents.MinApiSample/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/CloudNative.CloudEvents.MinApiSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/CloudNative.CloudEvents.MinApiSample/appsettings.json b/samples/CloudNative.CloudEvents.MinApiSample/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/CloudNative.CloudEvents.MinApiSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/Directory.Build.props b/samples/Directory.Build.props index b688048..deb85b9 100644 --- a/samples/Directory.Build.props +++ b/samples/Directory.Build.props @@ -14,5 +14,6 @@ False + 12.0 diff --git a/samples/HttpSendJson/HttpSendJson.csproj b/samples/HttpSendJson/HttpSendJson.csproj new file mode 100644 index 0000000..39f79b1 --- /dev/null +++ b/samples/HttpSendJson/HttpSendJson.csproj @@ -0,0 +1,19 @@ + + + Exe + net8.0 + enable + enable + + + + + + + + true + true + None + False + + diff --git a/samples/HttpSendJson/Program.cs b/samples/HttpSendJson/Program.cs new file mode 100644 index 0000000..2715286 --- /dev/null +++ b/samples/HttpSendJson/Program.cs @@ -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 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 CreateParserWithVersion() => CreateParser().WithVersion(Version); +} \ No newline at end of file diff --git a/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs b/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs index 001b832..6515320 100644 --- a/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs +++ b/src/CloudNative.CloudEvents.Amqp/AmqpExtensions.cs @@ -8,6 +8,7 @@ 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 @@ public static CloudEvent ToCloudEvent( } } - 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 @@ private static ApplicationProperties MapHeaders(CloudEvent cloudEvent, string pr return applicationProperties; } } -} \ No newline at end of file +} + +#if NETSTANDARD2_0 +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif \ No newline at end of file diff --git a/src/CloudNative.CloudEvents.Amqp/CloudNative.CloudEvents.Amqp.csproj b/src/CloudNative.CloudEvents.Amqp/CloudNative.CloudEvents.Amqp.csproj index a0506ec..6e499ee 100644 --- a/src/CloudNative.CloudEvents.Amqp/CloudNative.CloudEvents.Amqp.csproj +++ b/src/CloudNative.CloudEvents.Amqp/CloudNative.CloudEvents.Amqp.csproj @@ -1,9 +1,8 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 AMQP extensions for CloudNative.CloudEvents - 8.0 enable cncf;cloudnative;cloudevents;events;amqp diff --git a/src/CloudNative.CloudEvents.AspNetCore/CloudNative.CloudEvents.AspNetCore.csproj b/src/CloudNative.CloudEvents.AspNetCore/CloudNative.CloudEvents.AspNetCore.csproj index b073a81..eb74f91 100644 --- a/src/CloudNative.CloudEvents.AspNetCore/CloudNative.CloudEvents.AspNetCore.csproj +++ b/src/CloudNative.CloudEvents.AspNetCore/CloudNative.CloudEvents.AspNetCore.csproj @@ -1,9 +1,8 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 ASP.Net Core extensions for CloudNative.CloudEvents - 8.0 enable cncf;cloudnative;cloudevents;events;aspnetcore;aspnet diff --git a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs index 47ded29..27f6bf0 100644 --- a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs +++ b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs @@ -185,7 +185,7 @@ private static RecordSchema ParseEmbeddedSchema() // 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()); } diff --git a/src/CloudNative.CloudEvents.Avro/CloudNative.CloudEvents.Avro.csproj b/src/CloudNative.CloudEvents.Avro/CloudNative.CloudEvents.Avro.csproj index 3f0fb6e..bcab240 100644 --- a/src/CloudNative.CloudEvents.Avro/CloudNative.CloudEvents.Avro.csproj +++ b/src/CloudNative.CloudEvents.Avro/CloudNative.CloudEvents.Avro.csproj @@ -1,10 +1,9 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 Avro extensions for CloudNative.CloudEvents cncf;cloudnative;cloudevents;events;avro - 10.0 enable diff --git a/src/CloudNative.CloudEvents.Kafka/CloudNative.CloudEvents.Kafka.csproj b/src/CloudNative.CloudEvents.Kafka/CloudNative.CloudEvents.Kafka.csproj index 9926330..d091a60 100644 --- a/src/CloudNative.CloudEvents.Kafka/CloudNative.CloudEvents.Kafka.csproj +++ b/src/CloudNative.CloudEvents.Kafka/CloudNative.CloudEvents.Kafka.csproj @@ -1,10 +1,9 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 Kafka extensions for CloudNative.CloudEvents cncf;cloudnative;cloudevents;events;kafka - 8.0 enable diff --git a/src/CloudNative.CloudEvents.Mqtt/CloudNative.CloudEvents.Mqtt.csproj b/src/CloudNative.CloudEvents.Mqtt/CloudNative.CloudEvents.Mqtt.csproj index 58f622e..1cae6ed 100644 --- a/src/CloudNative.CloudEvents.Mqtt/CloudNative.CloudEvents.Mqtt.csproj +++ b/src/CloudNative.CloudEvents.Mqtt/CloudNative.CloudEvents.Mqtt.csproj @@ -1,10 +1,9 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 MQTT extensions for CloudNative.CloudEvents cncf;cloudnative;cloudevents;events;mqtt - 8.0 enable diff --git a/src/CloudNative.CloudEvents.NewtonsoftJson/CloudNative.CloudEvents.NewtonsoftJson.csproj b/src/CloudNative.CloudEvents.NewtonsoftJson/CloudNative.CloudEvents.NewtonsoftJson.csproj index 3ee42e3..0d178d8 100644 --- a/src/CloudNative.CloudEvents.NewtonsoftJson/CloudNative.CloudEvents.NewtonsoftJson.csproj +++ b/src/CloudNative.CloudEvents.NewtonsoftJson/CloudNative.CloudEvents.NewtonsoftJson.csproj @@ -1,9 +1,8 @@ - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 JSON support for the CNCF CloudEvents SDK, based on Newtonsoft.Json. - 8.0 enable cncf;cloudnative;cloudevents;events;json;newtonsoft diff --git a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs index 2d21be4..191ace4 100644 --- a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs @@ -345,7 +345,9 @@ protected virtual void DecodeStructuredModeDataBase64Property(JToken dataBase64T { 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); } /// @@ -524,7 +526,7 @@ protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, JsonWrite } else { - ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)); + ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent)!); if (IsJsonMediaType(dataContentType.MediaType)) { writer.WritePropertyName(DataPropertyName); @@ -696,7 +698,7 @@ public override void DecodeBinaryModeEventData(ReadOnlyMemory body, CloudE /// 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); } diff --git a/src/CloudNative.CloudEvents.Protobuf/CloudNative.CloudEvents.Protobuf.csproj b/src/CloudNative.CloudEvents.Protobuf/CloudNative.CloudEvents.Protobuf.csproj index c21c60e..628f82c 100644 --- a/src/CloudNative.CloudEvents.Protobuf/CloudNative.CloudEvents.Protobuf.csproj +++ b/src/CloudNative.CloudEvents.Protobuf/CloudNative.CloudEvents.Protobuf.csproj @@ -1,10 +1,9 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 Support for the Protobuf event format in for CloudNative.CloudEvents cncf;cloudnative;cloudevents;events;protobuf - 10.0 enable diff --git a/src/CloudNative.CloudEvents.SystemTextJson/CloudNative.CloudEvents.SystemTextJson.csproj b/src/CloudNative.CloudEvents.SystemTextJson/CloudNative.CloudEvents.SystemTextJson.csproj index d5c0916..81c21b9 100644 --- a/src/CloudNative.CloudEvents.SystemTextJson/CloudNative.CloudEvents.SystemTextJson.csproj +++ b/src/CloudNative.CloudEvents.SystemTextJson/CloudNative.CloudEvents.SystemTextJson.csproj @@ -1,9 +1,8 @@ - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 JSON support for the CNCF CloudEvents SDK, based on System.Text.Json. - 8.0 cncf;cloudnative;cloudevents;events;json;systemtextjson enable diff --git a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs index decdb77..46daab1 100644 --- a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs @@ -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 @@ public class JsonEventFormatter : CloudEventFormatter /// protected const string DataPropertyName = "data"; +#if NET7_0_OR_GREATER + /// + /// Json serialization context used to serialize and enable trimming and AOT. + /// + protected readonly JsonSerializerContext? JsonSerializerContext; +#endif + /// /// The options to use when serializing objects to JSON. /// @@ -108,22 +117,54 @@ public class JsonEventFormatter : CloudEventFormatter /// Creates a JsonEventFormatter that uses the default /// and for serializing and parsing. /// +#if NET7_0_OR_GREATER + [RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")] +#endif public JsonEventFormatter() : this(null, default) { } +#if NET7_0_OR_GREATER + /// + /// Creates a JsonEventFormatter that uses the default + /// and for serializing and parsing. + /// + /// The json context used for serializing objects to JSON. + public JsonEventFormatter(JsonSerializerContext jsonSerializerContext) : this(default, jsonSerializerContext) + { + } +#endif + /// /// Creates a JsonEventFormatter that uses the specified /// and for serializing and parsing. /// /// The options to use when serializing objects to JSON. May be null. /// The options to use when parsing JSON documents. +#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 + /// + /// Creates a JsonEventFormatter that uses the specified + /// and for serializing and parsing. + /// + /// The options to use when parsing JSON documents. + /// The json context used for serializing objects to JSON. + public JsonEventFormatter(JsonDocumentOptions documentOptions, JsonSerializerContext jsonSerializerContext) + { + Validation.CheckNotNull(jsonSerializerContext, nameof(jsonSerializerContext)); + DocumentOptions = documentOptions; + JsonSerializerContext = jsonSerializerContext; + } +#endif + /// public override async Task DecodeStructuredModeMessageAsync(Stream body, ContentType? contentType, IEnumerable? extensionAttributes) => await DecodeStructuredModeMessageImpl(body, contentType, extensionAttributes, true).ConfigureAwait(false); @@ -533,6 +574,10 @@ private void WriteCloudEventForBatchOrStructuredMode(Utf8JsonWriter writer, Clou /// The CloudEvent being encoded, which will have a non-null value for /// its property. /// The writer to serialize the data to. Will not be null. +#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 @@ protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonW 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 @@ protected virtual void EncodeStructuredModeData(CloudEvent cloudEvent, Utf8JsonW } /// +#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 EncodeBinaryModeEventData(CloudEvent cloudEvent) { Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); @@ -584,10 +638,18 @@ public override ReadOnlyMemory EncodeBinaryModeEventData(CloudEvent cloudE 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 @@ public class JsonEventFormatter : JsonEventFormatter /// Creates a JsonEventFormatter that uses the default /// and for serializing and parsing. /// +#if NET7_0_OR_GREATER + [RequiresUnreferencedCode("Use a constructor that takes a JsonSerializerContext.")] +#endif public JsonEventFormatter() { } +#if NET7_0_OR_GREATER + /// + /// Creates a JsonEventFormatter that uses the serializer + /// and for serializing and parsing. + /// + /// The json context used for serializing objects to JSON. + public JsonEventFormatter(JsonSerializerContext jsonSerializerContext) + : base(default, jsonSerializerContext) + { + } +#endif + /// /// Creates a JsonEventFormatter that uses the serializer /// and for serializing and parsing. /// /// The options to use when serializing and parsing. May be null. /// The options to use when parsing JSON documents. +#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 + /// + /// Creates a JsonEventFormatter that uses the serializer + /// and for serializing and parsing. + /// + /// The options to use when parsing JSON documents. + /// The json context used for serializing objects to JSON. + public JsonEventFormatter(JsonDocumentOptions documentOptions, JsonSerializerContext jsonSerializerContext) + : base(documentOptions, jsonSerializerContext) + { + } +#endif + /// +#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 EncodeBinaryModeEventData(CloudEvent cloudEvent) { Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); @@ -679,10 +776,18 @@ public override ReadOnlyMemory EncodeBinaryModeEventData(CloudEvent cloudE return Array.Empty(); } 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); } /// +#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 body, CloudEvent cloudEvent) { Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); @@ -692,22 +797,45 @@ public override void DecodeBinaryModeEventData(ReadOnlyMemory body, CloudE cloudEvent.Data = null; return; } - cloudEvent.Data = JsonSerializer.Deserialize(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(body.Span, 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 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); } /// - 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(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(dataElement.GetRawText(), SerializerOptions); + } // TODO: Consider decoding the base64 data as a byte array, then using DecodeBinaryModeData. /// diff --git a/src/CloudNative.CloudEvents/CloudEventFormatterAttribute.cs b/src/CloudNative.CloudEvents/CloudEventFormatterAttribute.cs index 74f30d7..757c207 100644 --- a/src/CloudNative.CloudEvents/CloudEventFormatterAttribute.cs +++ b/src/CloudNative.CloudEvents/CloudEventFormatterAttribute.cs @@ -4,6 +4,7 @@ using CloudNative.CloudEvents.Core; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace CloudNative.CloudEvents @@ -21,6 +22,9 @@ public sealed class CloudEventFormatterAttribute : Attribute /// /// The type to use for CloudEvent formatting. Must not be null. /// +#if NET7_0_OR_GREATER + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] +#endif public Type FormatterType { get; } /// @@ -56,7 +60,7 @@ public CloudEventFormatterAttribute(Type formatterType) => 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 @@ public CloudEventFormatterAttribute(Type formatterType) => } return formatter; - } + } } } \ No newline at end of file diff --git a/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj b/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj index 3561218..50c8649 100644 --- a/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj +++ b/src/CloudNative.CloudEvents/CloudNative.CloudEvents.csproj @@ -1,9 +1,8 @@  - netstandard2.0;netstandard2.1 + netstandard2.0;netstandard2.1;net7.0;net8.0 CNCF CloudEvents SDK - latest enable cloudnative;cloudevents;events diff --git a/src/CloudNative.CloudEvents/Core/BinaryDataUtilities.cs b/src/CloudNative.CloudEvents/Core/BinaryDataUtilities.cs index 3f951f0..2323fde 100644 --- a/src/CloudNative.CloudEvents/Core/BinaryDataUtilities.cs +++ b/src/CloudNative.CloudEvents/Core/BinaryDataUtilities.cs @@ -32,7 +32,7 @@ public async static Task> ToReadOnlyMemoryAsync(Stream stre // 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(buffer, 0, (int) memory.Position); + return new ReadOnlyMemory(buffer, 0, (int)memory.Position); } /// @@ -65,7 +65,7 @@ public static ReadOnlyMemory ToReadOnlyMemory(Stream stream) public static MemoryStream AsStream(ReadOnlyMemory 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); } /// @@ -79,7 +79,7 @@ public static string GetString(ReadOnlyMemory memory, Encoding encoding) // 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); } /// @@ -92,7 +92,7 @@ public static async Task CopyToStreamAsync(ReadOnlyMemory source, Stream d { 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); } /// @@ -108,7 +108,7 @@ public static byte[] AsArray(ReadOnlyMemory memory) 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(); } diff --git a/src/CloudNative.CloudEvents/Core/MimeUtilities.cs b/src/CloudNative.CloudEvents/Core/MimeUtilities.cs index b660086..09ddc36 100644 --- a/src/CloudNative.CloudEvents/Core/MimeUtilities.cs +++ b/src/CloudNative.CloudEvents/Core/MimeUtilities.cs @@ -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 @@ public static Encoding GetEncoding(ContentType? contentType) => 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 @@ public static Encoding GetEncoding(ContentType? contentType) => /// /// The content type to check. May be null, in which case the result is false. /// true if the given content type denotes a (non-batch) CloudEvent; false otherwise - 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 @@ contentType is string && /// /// The content type to check. May be null, in which case the result is false. /// true if the given content type represents a CloudEvent batch; false otherwise - public static bool IsCloudEventsBatchContentType(string? contentType) => + public static bool IsCloudEventsBatchContentType([NotNullWhen(true)] string? contentType) => contentType is string && contentType.StartsWith(BatchMediaType, StringComparison.InvariantCultureIgnoreCase); } } diff --git a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs index c1965d7..cf92ccb 100644 --- a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs @@ -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 @@ public static bool IsCloudEventBatch(this HttpRequestMessage httpRequestMessage) Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); return HasCloudEventsBatchContentType(httpRequestMessage.Content); } + /// /// Indicates whether this holds a batch of CloudEvents. @@ -83,7 +85,7 @@ public static Task ToCloudEventAsync( this HttpResponseMessage httpResponseMessage, CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => - ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable?) extensionAttributes); + ToCloudEventAsync(httpResponseMessage, formatter, (IEnumerable?)extensionAttributes); /// /// Converts this HTTP response message into a CloudEvent object @@ -112,7 +114,7 @@ public static Task ToCloudEventAsync( this HttpRequestMessage httpRequestMessage, CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => - ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable?) extensionAttributes); + ToCloudEventAsync(httpRequestMessage, formatter, (IEnumerable?)extensionAttributes); /// /// Converts this HTTP request message into a CloudEvent object. @@ -130,7 +132,7 @@ public static Task ToCloudEventAsync( return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); } - private static async Task ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content, + private static async Task ToCloudEventInternalAsync(HttpHeaders headers, HttpContent? content, CloudEventFormatter formatter, IEnumerable? extensionAttributes, string paramName) { Validation.CheckNotNull(formatter, nameof(formatter)); @@ -142,7 +144,7 @@ private static async Task ToCloudEventInternalAsync(HttpHeaders head } 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 @@ private static async Task ToCloudEventInternalAsync(HttpHeaders head ?? 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 @@ public static Task> ToCloudEventBatchAsync( return ToCloudEventBatchInternalAsync(httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); } - private static async Task> ToCloudEventBatchInternalAsync(HttpContent content, + private static async Task> ToCloudEventBatchInternalAsync(HttpContent? content, CloudEventFormatter formatter, IEnumerable? extensionAttributes, string paramName) { Validation.CheckNotNull(formatter, nameof(formatter)); @@ -332,15 +334,15 @@ public static HttpContent ToHttpContent(this IReadOnlyList cloudEven private static ByteArrayContent ToByteArrayContent(ReadOnlyMemory 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) => diff --git a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs index 7e83936..16ed690 100644 --- a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs @@ -151,7 +151,7 @@ public async static Task ToCloudEventAsync(this HttpListenerRequest /// A reference to a validated CloudEvent instance. public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => - ToCloudEvent(httpListenerRequest, formatter, (IEnumerable?) extensionAttributes); + ToCloudEvent(httpListenerRequest, formatter, (IEnumerable?)extensionAttributes); /// /// Converts this listener request into a CloudEvent object, with the given extension attributes. @@ -179,7 +179,7 @@ private async static Task ToCloudEventAsyncImpl(HttpListenerRequest } 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 @@ private async static Task ToCloudEventAsyncImpl(HttpListenerRequest 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 @@ public static Task> ToCloudEventBatchAsync( this HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, params CloudEventAttribute[]? extensionAttributes) => - ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable?)extensionAttributes); + ToCloudEventBatchAsync(httpListenerRequest, formatter, (IEnumerable?) extensionAttributes); /// /// Converts this HTTP request message into a CloudEvent batch. @@ -263,6 +263,7 @@ public static IReadOnlyList ToCloudEventBatch( CloudEventFormatter formatter, IEnumerable? extensionAttributes) => ToCloudEventBatchInternalAsync(httpListenerRequest, formatter, extensionAttributes, async: false).GetAwaiter().GetResult(); + private async static Task> ToCloudEventBatchInternalAsync(HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, IEnumerable? extensionAttributes, bool async) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0f81436..997763d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -27,6 +27,10 @@ Apache-2.0 https://cloudevents.io Copyright Cloud Native Foundation + + true + true + 12.0 diff --git a/test/CloudNative.CloudEvents.IntegrationTests/CloudNative.CloudEvents.IntegrationTests.csproj b/test/CloudNative.CloudEvents.IntegrationTests/CloudNative.CloudEvents.IntegrationTests.csproj index da33b50..ba679bb 100644 --- a/test/CloudNative.CloudEvents.IntegrationTests/CloudNative.CloudEvents.IntegrationTests.csproj +++ b/test/CloudNative.CloudEvents.IntegrationTests/CloudNative.CloudEvents.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 diff --git a/test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj b/test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj index 0fc8ce6..2c989ff 100644 --- a/test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj +++ b/test/CloudNative.CloudEvents.UnitTests/CloudNative.CloudEvents.UnitTests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTest.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTest.cs index 8cac6f9..2e23c71 100644 --- a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/ConformanceTest.cs @@ -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 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 ValidEventTestIds => SelectTestIds(ConformanceTestType.ValidSingleEvent); public static IEnumerable InvalidEventTestIds => SelectTestIds(ConformanceTestType.InvalidSingleEvent); @@ -29,20 +31,21 @@ private static IEnumerable SelectTestIds(ConformanceTestType type) => public static IEnumerable 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 void InvalidEvent(string testId) } [Theory, MemberData(nameof(ValidBatchTestIds))] - public void ValidBatch(string testId) + public void ValidBatch(string testId, bool useContext) { var test = GetTestById(testId); IReadOnlyList expected = SampleBatches.FromId(test.SampleId); @@ -58,15 +61,16 @@ public void ValidBatch(string testId) // 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 actual = new JsonEventFormatter().DecodeBatchModeMessage(body, contentType: null, extensions); + IReadOnlyList 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(); diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/GeneratedJsonContext.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/GeneratedJsonContext.cs new file mode 100644 index 0000000..5a113a8 --- /dev/null +++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/GeneratedJsonContext.cs @@ -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 +{ +} diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs index dd48c50..9858b30 100644 --- a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs @@ -32,8 +32,10 @@ public class JsonEventFormatterTest /// A simple test that populates all known v1.0 attributes, so we don't need to test that /// aspect in the future. /// - [Fact] - public void EncodeStructuredModeMessage_V1Attributes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_V1Attributes(bool useContext) { var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V1_0) { @@ -47,7 +49,8 @@ public void EncodeStructuredModeMessage_V1Attributes() Type = "event-type" }; - var encoded = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType); + var encoded = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeStructuredModeMessage(cloudEvent, out var contentType); Assert.Equal("application/cloudevents+json; charset=utf-8", contentType.ToString()); JsonElement obj = ParseJson(encoded); var asserter = new JsonElementAsserter @@ -65,8 +68,10 @@ public void EncodeStructuredModeMessage_V1Attributes() asserter.AssertProperties(obj, assertCount: true); } - [Fact] - public void EncodeStructuredModeMessage_AllAttributeTypes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_AllAttributeTypes(bool useContext) { var cloudEvent = new CloudEvent(AllTypesExtensions) { @@ -81,7 +86,7 @@ public void EncodeStructuredModeMessage_AllAttributeTypes() // We're not going to check these. cloudEvent.PopulateRequiredAttributes(); - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); var asserter = new JsonElementAsserter { { "binary", JsonValueKind.String, SampleBinaryDataBase64 }, @@ -101,7 +106,8 @@ public void EncodeStructuredModeMessage_JsonDataType_ObjectSerialization() var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new { Text = "simple text" }; cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + // anonymous type do not support source generation for json serialization + JsonElement element = EncodeAndParseStructured(cloudEvent, false); JsonElement dataProperty = element.GetProperty("data"); var asserter = new JsonElementAsserter { @@ -110,13 +116,15 @@ public void EncodeStructuredModeMessage_JsonDataType_ObjectSerialization() asserter.AssertProperties(dataProperty, assertCount: true); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_NumberSerialization() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_NumberSerialization(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = 10; cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); var asserter = new JsonElementAsserter { { "data", JsonValueKind.Number, 10 } @@ -146,13 +154,15 @@ public void EncodeStructuredModeMessage_JsonDataType_CustomSerializerOptions() asserter.AssertProperties(dataProperty, assertCount: true); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_AttributedModel() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_AttributedModel(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new AttributedModel { AttributedProperty = "simple text" }; cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); JsonElement dataProperty = element.GetProperty("data"); var asserter = new JsonElementAsserter { @@ -161,13 +171,15 @@ public void EncodeStructuredModeMessage_JsonDataType_AttributedModel() asserter.AssertProperties(dataProperty, assertCount: true); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_JsonElementObject() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_JsonElementObject(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = ParseJson("{ \"value\": { \"Key\": \"value\" } }").GetProperty("value"); cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); JsonElement data = element.GetProperty("data"); var asserter = new JsonElementAsserter { @@ -176,107 +188,125 @@ public void EncodeStructuredModeMessage_JsonDataType_JsonElementObject() asserter.AssertProperties(data, assertCount: true); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_JsonElementString() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_JsonElementString(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = ParseJson("{ \"value\": \"text\" }").GetProperty("value"); cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); JsonElement data = element.GetProperty("data"); Assert.Equal(JsonValueKind.String, data.ValueKind); Assert.Equal("text", data.GetString()); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_JsonElementNull() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_JsonElementNull(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = ParseJson("{ \"value\": null }").GetProperty("value"); cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); JsonElement data = element.GetProperty("data"); Assert.Equal(JsonValueKind.Null, data.ValueKind); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_JsonElementNumeric() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_JsonElementNumeric(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = ParseJson("{ \"value\": 100 }").GetProperty("value"); cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); JsonElement data = element.GetProperty("data"); Assert.Equal(JsonValueKind.Number, data.ValueKind); Assert.Equal(100, data.GetInt32()); } - [Fact] - public void EncodeStructuredModeMessage_JsonDataType_NullValue() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_JsonDataType_NullValue(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = null; cloudEvent.DataContentType = "application/json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); Assert.False(element.TryGetProperty("data", out _)); } - [Fact] - public void EncodeStructuredModeMessage_TextType_String() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_TextType_String(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = "some text"; cloudEvent.DataContentType = "text/anything"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); var dataProperty = element.GetProperty("data"); Assert.Equal(JsonValueKind.String, dataProperty.ValueKind); Assert.Equal("some text", dataProperty.GetString()); } // A text content type with bytes as data is serialized like any other bytes. - [Fact] - public void EncodeStructuredModeMessage_TextType_Bytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_TextType_Bytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = SampleBinaryData; cloudEvent.DataContentType = "text/anything"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); Assert.False(element.TryGetProperty("data", out _)); var dataBase64 = element.GetProperty("data_base64"); Assert.Equal(JsonValueKind.String, dataBase64.ValueKind); Assert.Equal(SampleBinaryDataBase64, dataBase64.GetString()); } - [Fact] - public void EncodeStructuredModeMessage_TextType_NotStringOrBytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_TextType_NotStringOrBytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new object(); cloudEvent.DataContentType = "text/anything"; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); Assert.Throws(() => formatter.EncodeStructuredModeMessage(cloudEvent, out _)); } - [Fact] - public void EncodeStructuredModeMessage_ArbitraryType_Bytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_ArbitraryType_Bytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = SampleBinaryData; cloudEvent.DataContentType = "not_text/or_json"; - JsonElement element = EncodeAndParseStructured(cloudEvent); + JsonElement element = EncodeAndParseStructured(cloudEvent, useContext); Assert.False(element.TryGetProperty("data", out _)); var dataBase64 = element.GetProperty("data_base64"); Assert.Equal(JsonValueKind.String, dataBase64.ValueKind); Assert.Equal(SampleBinaryDataBase64, dataBase64.GetString()); } - [Fact] - public void EncodeStructuredModeMessage_ArbitraryType_NotBytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeStructuredModeMessage_ArbitraryType_NotBytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new object(); cloudEvent.DataContentType = "not_text/or_json"; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); Assert.Throws(() => formatter.EncodeStructuredModeMessage(cloudEvent, out _)); } @@ -286,7 +316,9 @@ public void EncodeBinaryModeEventData_JsonDataType_ObjectSerialization() var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new { Text = "simple text" }; cloudEvent.DataContentType = "application/json"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + // anonymous type do not support source generation for json serialization + var bytes = new JsonEventFormatter() + .EncodeBinaryModeEventData(cloudEvent); JsonElement data = ParseJson(bytes); var asserter = new JsonElementAsserter { @@ -316,13 +348,16 @@ public void EncodeBinaryModeEventData_JsonDataType_CustomSerializer() asserter.AssertProperties(data, assertCount: true); } - [Fact] - public void EncodeBinaryModeEventData_JsonDataType_AttributedModel() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_JsonDataType_AttributedModel(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new AttributedModel { AttributedProperty = "simple text" }; cloudEvent.DataContentType = "application/json"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); JsonElement data = ParseJson(bytes); var asserter = new JsonElementAsserter { @@ -332,78 +367,97 @@ public void EncodeBinaryModeEventData_JsonDataType_AttributedModel() } [Theory] - [InlineData("utf-8")] - [InlineData("utf-16")] - public void EncodeBinaryModeEventData_JsonDataType_JsonElement(string charset) + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("utf-16", true)] + [InlineData("utf-16", false)] + public void EncodeBinaryModeEventData_JsonDataType_JsonElement(string charset, bool useContext) { // This would definitely be an odd thing to do, admittedly... var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = ParseJson($"{{ \"value\": \"some text\" }}").GetProperty("value"); cloudEvent.DataContentType = $"application/json; charset={charset}"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); Assert.Equal("\"some text\"", BinaryDataUtilities.GetString(bytes, Encoding.GetEncoding(charset))); } - [Fact] - public void EncodeBinaryModeEventData_JsonDataType_NullValue() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_JsonDataType_NullValue(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = null; cloudEvent.DataContentType = "application/json"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); Assert.True(bytes.IsEmpty); } [Theory] - [InlineData("utf-8")] - [InlineData("iso-8859-1")] - public void EncodeBinaryModeEventData_TextType_String(string charset) + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("iso-8859-1", true)] + [InlineData("iso-8859-1", false)] + public void EncodeBinaryModeEventData_TextType_String(string charset, bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = "some text"; cloudEvent.DataContentType = $"text/anything; charset={charset}"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); Assert.Equal("some text", BinaryDataUtilities.GetString(bytes, Encoding.GetEncoding(charset))); } // A text content type with bytes as data is serialized like any other bytes. - [Fact] - public void EncodeBinaryModeEventData_TextType_Bytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_TextType_Bytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = SampleBinaryData; cloudEvent.DataContentType = "text/anything"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); Assert.Equal(SampleBinaryData, bytes); } - [Fact] - public void EncodeBinaryModeEventData_TextType_NotStringOrBytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_TextType_NotStringOrBytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new object(); cloudEvent.DataContentType = "text/anything"; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); Assert.Throws(() => formatter.EncodeBinaryModeEventData(cloudEvent)); } - [Fact] - public void EncodeBinaryModeEventData_ArbitraryType_Bytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_ArbitraryType_Bytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = SampleBinaryData; cloudEvent.DataContentType = "not_text/or_json"; - var bytes = new JsonEventFormatter().EncodeBinaryModeEventData(cloudEvent); + var bytes = (useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter()) + .EncodeBinaryModeEventData(cloudEvent); Assert.Equal(SampleBinaryData, bytes); } - [Fact] - public void EncodeBinaryModeEventData_ArbitraryType_NotBytes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBinaryModeEventData_ArbitraryType_NotBytes(bool useContext) { var cloudEvent = new CloudEvent().PopulateRequiredAttributes(); cloudEvent.Data = new object(); cloudEvent.DataContentType = "not_text/or_json"; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); Assert.Throws(() => formatter.EncodeBinaryModeEventData(cloudEvent)); } @@ -440,11 +494,13 @@ public void EncodeBinaryModeEventData_NoContentType_LeavesBinaryData() // per-CloudEvent implementation is shared with structured mode, so we rely on // structured mode testing for things like custom serialization. - [Fact] - public void EncodeBatchModeMessage_Empty() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBatchModeMessage_Empty(bool useContext) { var cloudEvents = new CloudEvent[0]; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var bytes = formatter.EncodeBatchModeMessage(cloudEvents, out var contentType); Assert.Equal("application/cloudevents-batch+json; charset=utf-8", contentType.ToString()); var array = ParseJson(bytes); @@ -452,8 +508,10 @@ public void EncodeBatchModeMessage_Empty() Assert.Equal(0, array.GetArrayLength()); } - [Fact] - public void EncodeBatchModeMessage_TwoEvents() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBatchModeMessage_TwoEvents(bool useContext) { var event1 = new CloudEvent().PopulateRequiredAttributes(); event1.Id = "event1"; @@ -464,7 +522,7 @@ public void EncodeBatchModeMessage_TwoEvents() event2.Id = "event2"; var cloudEvents = new[] { event1, event2 }; - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var bytes = formatter.EncodeBatchModeMessage(cloudEvents, out var contentType); Assert.Equal("application/cloudevents-batch+json; charset=utf-8", contentType.ToString()); var array = ParseJson(bytes).EnumerateArray().ToList(); @@ -491,10 +549,12 @@ public void EncodeBatchModeMessage_TwoEvents() asserter2.AssertProperties(array[1], assertCount: true); } - [Fact] - public void EncodeBatchModeMessage_Invalid() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EncodeBatchModeMessage_Invalid(bool useContext) { - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); // Invalid CloudEvent Assert.Throws(() => formatter.EncodeBatchModeMessage(new[] { new CloudEvent() }, out _)); // Null argument @@ -504,18 +564,22 @@ public void EncodeBatchModeMessage_Invalid() Assert.Throws(() => formatter.EncodeBatchModeMessage(new CloudEvent[1], out _)); } - [Fact] - public void DecodeStructuredModeMessage_NotJson() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_NotJson(bool useContext) { - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); Assert.ThrowsAny(() => formatter.DecodeStructuredModeMessage(new byte[10], new ContentType("application/json"), null)); } // Just a single test for the code that parses asynchronously... the guts are all the same. [Theory] - [InlineData("utf-8")] - [InlineData("iso-8859-1")] - public async Task DecodeStructuredModeMessageAsync_Minimal(string charset) + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("iso-8859-1", true)] + [InlineData("iso-8859-1", false)] + public async Task DecodeStructuredModeMessageAsync_Minimal(string charset, bool useContext) { // Note: just using Json.NET to get the JSON in a simple way... var obj = new JObject @@ -528,7 +592,7 @@ public async Task DecodeStructuredModeMessageAsync_Minimal(string charset) }; var bytes = Encoding.GetEncoding(charset).GetBytes(obj.ToString()); var stream = new MemoryStream(bytes); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvent = await formatter.DecodeStructuredModeMessageAsync(stream, new ContentType($"application/cloudevents+json; charset={charset}"), null); Assert.Equal("test-type", cloudEvent.Type); Assert.Equal("test-id", cloudEvent.Id); @@ -537,9 +601,11 @@ public async Task DecodeStructuredModeMessageAsync_Minimal(string charset) } [Theory] - [InlineData("utf-8")] - [InlineData("iso-8859-1")] - public void DecodeStructuredModeMessage_Minimal(string charset) + [InlineData("utf-8", true)] + [InlineData("utf-8", false)] + [InlineData("iso-8859-1", true)] + [InlineData("iso-8859-1", false)] + public void DecodeStructuredModeMessage_Minimal(string charset, bool useContext) { var obj = new JObject { @@ -551,15 +617,17 @@ public void DecodeStructuredModeMessage_Minimal(string charset) }; var bytes = Encoding.GetEncoding(charset).GetBytes(obj.ToString()); var stream = new MemoryStream(bytes); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvent = formatter.DecodeStructuredModeMessage(stream, new ContentType($"application/cloudevents+json; charset={charset}"), null); Assert.Equal("test-type", cloudEvent.Type); Assert.Equal("test-id", cloudEvent.Id); Assert.Equal(SampleUri, cloudEvent.Source); } - [Fact] - public void DecodeStructuredModeMessage_NoSpecVersion() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_NoSpecVersion(bool useContext) { var obj = new JObject { @@ -567,11 +635,13 @@ public void DecodeStructuredModeMessage_NoSpecVersion() ["id"] = "test-id", ["source"] = SampleUriText, }; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_UnknownSpecVersion() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_UnknownSpecVersion(bool useContext) { var obj = new JObject { @@ -580,11 +650,13 @@ public void DecodeStructuredModeMessage_UnknownSpecVersion() ["id"] = "test-id", ["source"] = SampleUriText, }; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_MissingRequiredAttributes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_MissingRequiredAttributes(bool useContext) { var obj = new JObject { @@ -593,11 +665,13 @@ public void DecodeStructuredModeMessage_MissingRequiredAttributes() ["id"] = "test-id" // Source is missing }; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_SpecVersionNotString() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_SpecVersionNotString(bool useContext) { var obj = new JObject { @@ -606,11 +680,13 @@ public void DecodeStructuredModeMessage_SpecVersionNotString() ["id"] = "test-id", ["source"] = SampleUriText, }; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_TypeNotString() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_TypeNotString(bool useContext) { var obj = new JObject { @@ -619,11 +695,13 @@ public void DecodeStructuredModeMessage_TypeNotString() ["id"] = "test-id", ["source"] = SampleUriText, }; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_V1Attributes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_V1Attributes(bool useContext) { var obj = new JObject { @@ -637,7 +715,7 @@ public void DecodeStructuredModeMessage_V1Attributes() ["source"] = "//event-source", ["time"] = SampleTimestampText }; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal(CloudEventsSpecVersion.V1_0, cloudEvent.SpecVersion); Assert.Equal("test-type", cloudEvent.Type); Assert.Equal("test-id", cloudEvent.Id); @@ -648,8 +726,10 @@ public void DecodeStructuredModeMessage_V1Attributes() AssertTimestampsEqual(SampleTimestamp, cloudEvent.Time); } - [Fact] - public void DecodeStructuredModeMessage_AllAttributeTypes() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_AllAttributeTypes(bool useContext) { var obj = new JObject { @@ -669,7 +749,7 @@ public void DecodeStructuredModeMessage_AllAttributeTypes() }; var bytes = Encoding.UTF8.GetBytes(obj.ToString()); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvent = formatter.DecodeStructuredModeMessage(bytes, s_jsonCloudEventContentType, AllTypesExtensions); Assert.Equal(SampleBinaryData, cloudEvent["binary"]); Assert.True((bool)cloudEvent["boolean"]!); @@ -680,8 +760,10 @@ public void DecodeStructuredModeMessage_AllAttributeTypes() Assert.Equal(SampleUriReference, cloudEvent["urireference"]); } - [Fact] - public void DecodeStructuredModeMessage_IncorrectExtensionTypeWithValidValue() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_IncorrectExtensionTypeWithValidValue(bool useContext) { var obj = new JObject { @@ -694,7 +776,7 @@ public void DecodeStructuredModeMessage_IncorrectExtensionTypeWithValidValue() }; // Decode the event, providing the extension with the correct type. var bytes = Encoding.UTF8.GetBytes(obj.ToString()); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvent = formatter.DecodeStructuredModeMessage(bytes, s_jsonCloudEventContentType, AllTypesExtensions); // The value will have been decoded according to the extension. @@ -702,65 +784,81 @@ public void DecodeStructuredModeMessage_IncorrectExtensionTypeWithValidValue() } // There are other invalid token types as well; this is just one of them. - [Fact] - public void DecodeStructuredModeMessage_AttributeValueAsArrayToken() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_AttributeValueAsArrayToken(bool useContext) { var obj = CreateMinimalValidJObject(); obj["attr"] = new Newtonsoft.Json.Linq.JArray(); - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_Null() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_Null(bool useContext) { var obj = CreateMinimalValidJObject(); obj["attr"] = Newtonsoft.Json.Linq.JValue.CreateNull(); - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); // The JSON event format spec demands that we ignore null values, so we shouldn't // have created an extension attribute. Assert.Null(cloudEvent.GetAttribute("attr")); } [Theory] - [InlineData(null)] - [InlineData("application/json")] - [InlineData("text/plain")] - [InlineData("application/binary")] - public void DecodeStructuredModeMessage_NoData(string contentType) + [InlineData(null, true)] + [InlineData(null, false)] + [InlineData("application/json", true)] + [InlineData("application/json", false)] + [InlineData("text/plain", true)] + [InlineData("text/plain", false)] + [InlineData("application/binary", true)] + [InlineData("application/binary", false)] + public void DecodeStructuredModeMessage_NoData(string contentType, bool useContext) { var obj = CreateMinimalValidJObject(); if (contentType is object) { obj["datacontenttype"] = contentType; } - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Null(cloudEvent.Data); } - [Fact] - public void DecodeStructuredModeMessage_BothDataAndDataBase64() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_BothDataAndDataBase64(bool useContext) { var obj = CreateMinimalValidJObject(); obj["data"] = "text"; obj["data_base64"] = SampleBinaryDataBase64; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_DataBase64NonString() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_DataBase64NonString(bool useContext) { var obj = CreateMinimalValidJObject(); obj["data_base64"] = 10; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } // data_base64 always ends up as bytes, regardless of content type. [Theory] - [InlineData(null)] - [InlineData("application/json")] - [InlineData("text/plain")] - [InlineData("application/binary")] - public void DecodeStructuredModeMessage_Base64(string contentType) + [InlineData(null, true)] + [InlineData(null, false)] + [InlineData("application/json", true)] + [InlineData("application/json", false)] + [InlineData("text/plain", true)] + [InlineData("text/plain", false)] + [InlineData("application/binary", true)] + [InlineData("application/binary", false)] + public void DecodeStructuredModeMessage_Base64(string contentType, bool useContext) { var obj = CreateMinimalValidJObject(); if (contentType is object) @@ -768,27 +866,32 @@ public void DecodeStructuredModeMessage_Base64(string contentType) obj["datacontenttype"] = contentType; } obj["data_base64"] = SampleBinaryDataBase64; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal(SampleBinaryData, cloudEvent.Data); } [Theory] - [InlineData("text/plain")] - [InlineData("image/png")] - public void DecodeStructuredModeMessage_NonJsonContentType_JsonStringToken(string contentType) + [InlineData("text/plain", true)] + [InlineData("text/plain", false)] + [InlineData("image/png", true)] + [InlineData("image/png", false)] + public void DecodeStructuredModeMessage_NonJsonContentType_JsonStringToken(string contentType, bool useContext) { var obj = CreateMinimalValidJObject(); obj["datacontenttype"] = contentType; obj["data"] = "some text"; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal("some text", cloudEvent.Data); } [Theory] - [InlineData(null)] - [InlineData("application/json")] - [InlineData("application/json; charset=utf-8")] - public void DecodeStructuredModeMessage_JsonContentType_JsonStringToken(string contentType) + [InlineData(null, true)] + [InlineData(null, false)] + [InlineData("application/json", true)] + [InlineData("application/json", false)] + [InlineData("application/json; charset=utf-8", true)] + [InlineData("application/json; charset=utf-8", false)] + public void DecodeStructuredModeMessage_JsonContentType_JsonStringToken(string contentType, bool useContext) { var obj = CreateMinimalValidJObject(); if (contentType is object) @@ -796,18 +899,22 @@ public void DecodeStructuredModeMessage_JsonContentType_JsonStringToken(string c obj["datacontenttype"] = contentType; } obj["data"] = "text"; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); var element = (JsonElement) cloudEvent.Data!; Assert.Equal(JsonValueKind.String, element.ValueKind); Assert.Equal("text", element.GetString()); } [Theory] - [InlineData(null)] - [InlineData("application/json")] - [InlineData("application/xyz+json")] - [InlineData("application/xyz+json; charset=utf-8")] - public void DecodeStructuredModeMessage_JsonContentType_NonStringValue(string contentType) + [InlineData(null, true)] + [InlineData(null, false)] + [InlineData("application/json", true)] + [InlineData("application/json", false)] + [InlineData("application/xyz+json", true)] + [InlineData("application/xyz+json", false)] + [InlineData("application/xyz+json; charset=utf-8", true)] + [InlineData("application/xyz+json; charset=utf-8", false)] + public void DecodeStructuredModeMessage_JsonContentType_NonStringValue(string contentType, bool useContext) { var obj = CreateMinimalValidJObject(); if (contentType is object) @@ -815,40 +922,46 @@ public void DecodeStructuredModeMessage_JsonContentType_NonStringValue(string co obj["datacontenttype"] = contentType; } obj["data"] = 10; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); var element = (JsonElement) cloudEvent.Data!; Assert.Equal(JsonValueKind.Number, element.ValueKind); Assert.Equal(10, element.GetInt32()); } - [Fact] - public void DecodeStructuredModeMessage_NonJsonContentType_NonStringValue() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_NonJsonContentType_NonStringValue(bool useContext) { var obj = CreateMinimalValidJObject(); obj["datacontenttype"] = "text/plain"; obj["data"] = 10; - Assert.Throws(() => DecodeStructuredModeMessage(obj)); + Assert.Throws(() => DecodeStructuredModeMessage(obj, useContext)); } - [Fact] - public void DecodeStructuredModeMessage_NullDataBase64Ignored() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_NullDataBase64Ignored(bool useContext) { var obj = CreateMinimalValidJObject(); obj["data_base64"] = Newtonsoft.Json.Linq.JValue.CreateNull(); obj["data"] = "some text"; obj["datacontenttype"] = "text/plain"; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal("some text", cloudEvent.Data); } - [Fact] - public void DecodeStructuredModeMessage_NullDataIgnored() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructuredModeMessage_NullDataIgnored(bool useContext) { var obj = CreateMinimalValidJObject(); obj["data_base64"] = SampleBinaryDataBase64; obj["data"] = Newtonsoft.Json.Linq.JValue.CreateNull(); obj["datacontenttype"] = "application/binary"; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal(SampleBinaryData, cloudEvent.Data); } @@ -911,46 +1024,56 @@ public void DecodeBinaryModeEventData_Binary() Assert.Equal(bytes, data); } - [Fact] - public void DecodeBatchMode_NotArray() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_NotArray(bool useContext) { - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var data = Encoding.UTF8.GetBytes(CreateMinimalValidJObject().ToString()); Assert.Throws(() => formatter.DecodeBatchModeMessage(data, s_jsonCloudEventBatchContentType, extensionAttributes: null)); } - [Fact] - public void DecodeBatchMode_ArrayContainingNonObject() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_ArrayContainingNonObject(bool useContext) { - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var array = new JArray { CreateMinimalValidJObject(), "text" }; var data = Encoding.UTF8.GetBytes(array.ToString()); Assert.Throws(() => formatter.DecodeBatchModeMessage(data, s_jsonCloudEventBatchContentType, extensionAttributes: null)); } - [Fact] - public void DecodeBatchMode_Empty() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_Empty(bool useContext) { - var cloudEvents = DecodeBatchModeMessage(new JArray()); + var cloudEvents = DecodeBatchModeMessage(new JArray(), useContext); Assert.Empty(cloudEvents); } - [Fact] - public void DecodeBatchMode_Minimal() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_Minimal(bool useContext) { - var cloudEvents = DecodeBatchModeMessage(new JArray { CreateMinimalValidJObject() }); + var cloudEvents = DecodeBatchModeMessage(new JArray { CreateMinimalValidJObject() }, useContext); var cloudEvent = Assert.Single(cloudEvents); Assert.Equal("event-type", cloudEvent.Type); Assert.Equal("event-id", cloudEvent.Id); Assert.Equal(new Uri("//event-source", UriKind.RelativeOrAbsolute), cloudEvent.Source); } - [Fact] - public void DecodeBatchMode_Minimal_WithStream() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_Minimal_WithStream(bool useContext) { var array = new JArray { CreateMinimalValidJObject() }; var bytes = Encoding.UTF8.GetBytes(array.ToString()); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvents = formatter.DecodeBatchModeMessage(new MemoryStream(bytes), s_jsonCloudEventBatchContentType, null); var cloudEvent = Assert.Single(cloudEvents); Assert.Equal("event-type", cloudEvent.Type); @@ -959,8 +1082,10 @@ public void DecodeBatchMode_Minimal_WithStream() } // Just a single test for the code that parses asynchronously... the guts are all the same. - [Fact] - public async Task DecodeBatchModeMessageAsync_Minimal() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DecodeBatchModeMessageAsync_Minimal(bool useContext) { var obj = new JObject { @@ -971,7 +1096,7 @@ public async Task DecodeBatchModeMessageAsync_Minimal() }; var bytes = Encoding.UTF8.GetBytes(new JArray { obj }.ToString()); var stream = new MemoryStream(bytes); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var cloudEvents = await formatter.DecodeBatchModeMessageAsync(stream, s_jsonCloudEventBatchContentType, null); var cloudEvent = Assert.Single(cloudEvents); Assert.Equal("test-type", cloudEvent.Type); @@ -980,8 +1105,10 @@ public async Task DecodeBatchModeMessageAsync_Minimal() } - [Fact] - public void DecodeBatchMode_Multiple() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeBatchMode_Multiple(bool useContext) { var array = new JArray { @@ -1002,7 +1129,7 @@ public void DecodeBatchMode_Multiple() ["source"] = "//event-source2" }, }; - var cloudEvents = DecodeBatchModeMessage(array); + var cloudEvents = DecodeBatchModeMessage(array, useContext); Assert.Equal(2, cloudEvents.Count); var event1 = cloudEvents[0]; @@ -1069,8 +1196,10 @@ public void EncodeStructured_BinaryData_DefaultContentTypeIsNotImplied() asserter.AssertProperties(obj, assertCount: true); } - [Fact] - public void DecodeStructured_DefaultContentTypeToApplicationJson() + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DecodeStructured_DefaultContentTypeToApplicationJson(bool useContext) { var obj = new JObject { @@ -1080,7 +1209,7 @@ public void DecodeStructured_DefaultContentTypeToApplicationJson() ["source"] = SampleUriText, ["data"] = "some text" }; - var cloudEvent = DecodeStructuredModeMessage(obj); + var cloudEvent = DecodeStructuredModeMessage(obj, useContext); Assert.Equal("application/json", cloudEvent.DataContentType); var jsonData = Assert.IsType(cloudEvent.Data); Assert.Equal(JsonValueKind.String, jsonData.ValueKind); @@ -1189,9 +1318,9 @@ internal static JsonElement ParseJson(ReadOnlyMemory data) /// Convenience method to format a CloudEvent with the default JsonEventFormatter in /// structured mode, then parse the result as a JObject. /// - private static JsonElement EncodeAndParseStructured(CloudEvent cloudEvent) + private static JsonElement EncodeAndParseStructured(CloudEvent cloudEvent, bool useContext) { - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); var encoded = formatter.EncodeStructuredModeMessage(cloudEvent, out _); return ParseJson(encoded); } @@ -1200,10 +1329,10 @@ private static JsonElement EncodeAndParseStructured(CloudEvent cloudEvent) /// Convenience method to serialize a JObject to bytes, then /// decode it as a structured event with the default (System.Text.Json) JsonEventFormatter and no extension attributes. /// - private static CloudEvent DecodeStructuredModeMessage(Newtonsoft.Json.Linq.JObject obj) + private static CloudEvent DecodeStructuredModeMessage(Newtonsoft.Json.Linq.JObject obj, bool useContext) { var bytes = Encoding.UTF8.GetBytes(obj.ToString()); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); return formatter.DecodeStructuredModeMessage(bytes, s_jsonCloudEventContentType, null); } @@ -1211,10 +1340,10 @@ private static CloudEvent DecodeStructuredModeMessage(Newtonsoft.Json.Linq.JObje /// Convenience method to serialize a JArray to bytes, then /// decode it as a structured event with the default (System.Text.Json) JsonEventFormatter and no extension attributes. /// - private static IReadOnlyList DecodeBatchModeMessage(Newtonsoft.Json.Linq.JArray array) + private static IReadOnlyList DecodeBatchModeMessage(Newtonsoft.Json.Linq.JArray array, bool useContext) { var bytes = Encoding.UTF8.GetBytes(array.ToString()); - var formatter = new JsonEventFormatter(); + var formatter = useContext ? new JsonEventFormatter(GeneratedJsonContext.Default) : new JsonEventFormatter(); return formatter.DecodeBatchModeMessage(bytes, s_jsonCloudEventBatchContentType, null); } diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 7f8813f..96ea893 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -14,5 +14,6 @@ False + 12.0