From a5bdf172ef8bce92417fa2baf494bc1cda26184c Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:40:22 +0100 Subject: [PATCH 01/30] =?UTF-8?q?=E2=9C=A8=20add=20configurable=20response?= =?UTF-8?q?=20negotiator=20base=20class=20for=20serialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ConfigurableResponseNegotiator.cs | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs diff --git a/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs new file mode 100644 index 0000000..eecd1c6 --- /dev/null +++ b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs @@ -0,0 +1,129 @@ +using Carter; +using System; +using System.Linq; +using Cuemon.Configuration; +using Cuemon.Diagnostics; +using Cuemon.Net.Http; +using Cuemon.Runtime.Serialization.Formatters; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Cuemon; + +namespace Codebelt.Extensions.Carter.Response; + +/// +/// Provides an abstract, configurable base class for Carter response negotiators that serialize models using a . +/// +/// The type of the configured options. +/// +/// +/// +/// +/// +public abstract class ConfigurableResponseNegotiator : Configurable, IResponseNegotiator where TOptions : class, IExceptionDescriptorOptions, IContentNegotiation, IValidatableParameterObject, new() +{ + /// + /// Initializes a new instance of the class. + /// + /// The used to configure content negotiation and serialization behavior. + protected ConfigurableResponseNegotiator(TOptions options) : base(options) + { + } + + /// + /// Determines whether this negotiator can handle the specified media type. + /// + /// The from the HTTP request's Accept header. + /// true if the negotiator can handle the specified media type; otherwise, false. + public virtual bool CanHandle(MediaTypeHeaderValue accept) + { + return Options.SupportedMediaTypes.Any(mediaType => + { + if (accept.MatchesMediaType(mediaType.MediaType)) + { + ContentType = mediaType.MediaType; + return true; + } + return false; + }); + } + + /// + /// Gets the matched content type determined by the most recent successful call to . + /// + /// The matched content type media type string, or null if has not yet been called successfully. + public string ContentType { get; private set; } + + /// + /// Resolves the character encoding for the HTTP response by inspecting the Accept-Charset header + /// of the , falling back to when no + /// valid charset is indicated. + /// + /// The current . + /// The to use when writing the response body. + public virtual Encoding GetEncoding(HttpRequest request) + { + var acceptCharset = request.GetTypedHeaders().AcceptCharset; + if (acceptCharset.Count > 0) + { + var preferred = acceptCharset + .OrderByDescending(x => x.Quality ?? 1.0) + .Select(x => x.Value.Value) + .FirstOrDefault(charset => + { + return Patterns.TryInvoke(() => Encoding.GetEncoding(charset!)); + }); + + if (preferred is not null) + { + return Encoding.GetEncoding(preferred); + } + } + return GetDefaultEncoding(); + } + + /// + /// When overridden in a derived class, returns the default character encoding for this negotiator. + /// This encoding is used as the fallback by when the HTTP request does not + /// specify a resolvable Accept-Charset preference. + /// + /// The default for this negotiator. + protected abstract Encoding GetDefaultEncoding(); + + /// + /// When overridden in a derived class, returns the used to serialize the response model. + /// + /// A configured with the current . + public abstract StreamFormatter GetFormatter(); + + /// + /// Serializes the specified and writes it to the HTTP response body. + /// + /// The type of the model to serialize. + /// The current . + /// The current to which the serialized content is written. + /// The model to serialize. + /// A used to propagate notification that the operation should be canceled. + /// A that represents the asynchronous write operation. + public virtual async Task Handle(HttpRequest req, HttpResponse res, T model, CancellationToken cancellationToken) + { + var encoding = GetEncoding(req); + res.ContentType = ContentType + "; charset=" + encoding.WebName; + await using var textWriter = new StreamWriter(res.Body, encoding); + var formatter = GetFormatter(); + using (var streamReader = new StreamReader(formatter.Serialize(model), encoding)) + { + Memory memoryBuffer = new char[8192]; + int read; + while ((read = await streamReader.ReadAsync(memoryBuffer, cancellationToken).ConfigureAwait(false)) != 0) + { + await textWriter.WriteAsync(memoryBuffer.Slice(0, read), cancellationToken).ConfigureAwait(false); + } + } + await textWriter.FlushAsync(cancellationToken).ConfigureAwait(false); + } +} From b33a1e51c34b14d2b619876ffae3268178e74f23 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:40:49 +0100 Subject: [PATCH 02/30] =?UTF-8?q?=E2=9C=A8=20add=20extension=20methods=20f?= =?UTF-8?q?or=20IEndpointConventionBuilder=20metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EndpointConventionBuilderExtensions.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter/EndpointConventionBuilderExtensions.cs diff --git a/src/Codebelt.Extensions.Carter/EndpointConventionBuilderExtensions.cs b/src/Codebelt.Extensions.Carter/EndpointConventionBuilderExtensions.cs new file mode 100644 index 0000000..5def504 --- /dev/null +++ b/src/Codebelt.Extensions.Carter/EndpointConventionBuilderExtensions.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Codebelt.Extensions.Carter; + +/// +/// Extension methods for . +/// +public static class EndpointConventionBuilderExtensions +{ + /// The to add the metadata to. + extension(IEndpointConventionBuilder builder) + { + /// + /// Adds metadata indicating that the endpoint produces a response of the specified type with the given status code and content types. + /// + /// The type of the response body. + /// The HTTP status code of the response. Defaults to . + /// The content types produced by the endpoint. + /// The with the added metadata. + public IEndpointConventionBuilder Produces(int statusCode = StatusCodes.Status200OK, params string[] contentTypes) + { + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode, typeof(TResponse), contentTypes)); + } + + /// + /// Adds metadata indicating that the endpoint produces a response with the given status code and no response body. + /// + /// The HTTP status code of the response. + /// The with the added metadata. + public IEndpointConventionBuilder Produces(int statusCode) + { + return builder.WithMetadata(new ProducesResponseTypeMetadata(statusCode)); + } + } +} From 0eaecf6c7d3d5adcf489fac01ccccdca2e597011 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:55:13 +0100 Subject: [PATCH 03/30] =?UTF-8?q?=E2=9C=85=20add=20unit=20tests=20for=20en?= =?UTF-8?q?dpoint=20convention=20builder=20and=20response=20negotiator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Codebelt.Extensions.Carter.Tests.csproj | 15 ++ ...EndpointConventionBuilderExtensionsTest.cs | 98 ++++++++++++ .../ConfigurableResponseNegotiatorTest.cs | 149 ++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.Tests/Codebelt.Extensions.Carter.Tests.csproj create mode 100644 test/Codebelt.Extensions.Carter.Tests/EndpointConventionBuilderExtensionsTest.cs create mode 100644 test/Codebelt.Extensions.Carter.Tests/Response/ConfigurableResponseNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.Tests/Codebelt.Extensions.Carter.Tests.csproj b/test/Codebelt.Extensions.Carter.Tests/Codebelt.Extensions.Carter.Tests.csproj new file mode 100644 index 0000000..e85ffa0 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.Tests/Codebelt.Extensions.Carter.Tests.csproj @@ -0,0 +1,15 @@ + + + + Codebelt.Extensions.Carter + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.Tests/EndpointConventionBuilderExtensionsTest.cs b/test/Codebelt.Extensions.Carter.Tests/EndpointConventionBuilderExtensionsTest.cs new file mode 100644 index 0000000..dadc671 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.Tests/EndpointConventionBuilderExtensionsTest.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Codebelt.Extensions.Xunit; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; +using Xunit; + +namespace Codebelt.Extensions.Carter +{ + /// + /// Tests for the class. + /// + public class EndpointConventionBuilderExtensionsTest : Test + { + public EndpointConventionBuilderExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Produces_WithTResponse_ShouldAddProducesResponseTypeMetadata() + { + var delegateEndpointBuilder = new FakeEndpointConventionBuilder(); + + delegateEndpointBuilder.Produces(StatusCodes.Status201Created, "application/json", "text/plain"); + + var endpointBuilder = new RouteEndpointBuilder(context => System.Threading.Tasks.Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + foreach (var convention in delegateEndpointBuilder.Conventions) + { + convention(endpointBuilder); + } + + var typeMetadata = endpointBuilder.Metadata.OfType().FirstOrDefault(); + + Assert.NotNull(typeMetadata); + Assert.Equal(StatusCodes.Status201Created, typeMetadata.StatusCode); + Assert.Equal(typeof(string), typeMetadata.Type); + Assert.Contains("application/json", typeMetadata.ContentTypes); + Assert.Contains("text/plain", typeMetadata.ContentTypes); + } + + [Fact] + public void Produces_WithTResponse_DefaultStatusCode_ShouldAddProducesResponseTypeMetadata() + { + var delegateEndpointBuilder = new FakeEndpointConventionBuilder(); + + delegateEndpointBuilder.Produces(contentTypes: "application/xml"); + + var endpointBuilder = new RouteEndpointBuilder(context => System.Threading.Tasks.Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + foreach (var convention in delegateEndpointBuilder.Conventions) + { + convention(endpointBuilder); + } + + var typeMetadata = endpointBuilder.Metadata.OfType().FirstOrDefault(); + + Assert.NotNull(typeMetadata); + Assert.Equal(StatusCodes.Status200OK, typeMetadata.StatusCode); + Assert.Equal(typeof(int), typeMetadata.Type); + Assert.Contains("application/xml", typeMetadata.ContentTypes); + } + + [Fact] + public void Produces_WithStatusCode_ShouldAddProducesResponseTypeMetadata() + { + var delegateEndpointBuilder = new FakeEndpointConventionBuilder(); + + delegateEndpointBuilder.Produces(StatusCodes.Status404NotFound); + + var endpointBuilder = new RouteEndpointBuilder(context => System.Threading.Tasks.Task.CompletedTask, RoutePatternFactory.Parse("/"), 0); + foreach (var convention in delegateEndpointBuilder.Conventions) + { + convention(endpointBuilder); + } + + var typeMetadata = endpointBuilder.Metadata.OfType().FirstOrDefault(); + + Assert.NotNull(typeMetadata); + Assert.Equal(StatusCodes.Status404NotFound, typeMetadata.StatusCode); + Assert.Null(typeMetadata.Type); + // ContentTypes might be empty + Assert.Empty(typeMetadata.ContentTypes); + } + + private class FakeEndpointConventionBuilder : IEndpointConventionBuilder + { + public List> Conventions { get; } = []; + + public void Add(Action convention) + { + Conventions.Add(convention); + } + } + } +} diff --git a/test/Codebelt.Extensions.Carter.Tests/Response/ConfigurableResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.Tests/Response/ConfigurableResponseNegotiatorTest.cs new file mode 100644 index 0000000..0aa3dbd --- /dev/null +++ b/test/Codebelt.Extensions.Carter.Tests/Response/ConfigurableResponseNegotiatorTest.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Codebelt.Extensions.Xunit; +using Cuemon.Configuration; +using Cuemon.Diagnostics; +using Cuemon.Net.Http; +using Cuemon.Runtime.Serialization.Formatters; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Codebelt.Extensions.Carter.Response +{ + public class ConfigurableResponseNegotiatorTest : Test + { + public ConfigurableResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void CanHandle_ShouldReturnTrue_WhenAcceptHeaderMatches() + { + var options = new FakeOptions(); + options.SupportedMediaTypes = new[] { new System.Net.Http.Headers.MediaTypeHeaderValue("application/json") }; + var negotiator = new FakeResponseNegotiator(options); + + var result = negotiator.CanHandle(new MediaTypeHeaderValue("application/json")); + + Assert.True(result); + Assert.Equal("application/json", negotiator.ContentType); + } + + [Fact] + public void CanHandle_ShouldReturnFalse_WhenAcceptHeaderDoesNotMatch() + { + var options = new FakeOptions(); + options.SupportedMediaTypes = new[] { new System.Net.Http.Headers.MediaTypeHeaderValue("application/xml") }; + var negotiator = new FakeResponseNegotiator(options); + + var result = negotiator.CanHandle(new MediaTypeHeaderValue("application/json")); + + Assert.False(result); + } + + [Fact] + public void GetEncoding_ShouldReturnPreferredEncoding_FromRequest() + { + var options = new FakeOptions(); + var negotiator = new FakeResponseNegotiator(options); + + var context = new DefaultHttpContext(); + context.Request.Headers.Append("Accept-Charset", "utf-16;q=1.0, utf-8;q=0.8"); + + var encoding = negotiator.GetEncoding(context.Request); + + Assert.Equal(Encoding.Unicode, encoding); + } + + [Fact] + public void GetEncoding_ShouldReturnDefaultEncoding_WhenNoMatch() + { + var options = new FakeOptions(); + var negotiator = new FakeResponseNegotiator(options); + + var context = new DefaultHttpContext(); + context.Request.Headers.Append("Accept-Charset", "unknown-encoding;q=1.0"); + + var encoding = negotiator.GetEncoding(context.Request); + + Assert.Equal("utf-8", encoding.WebName); + } + + [Fact] + public async Task Handle_ShouldWriteToResponse() + { + var options = new FakeOptions(); + options.SupportedMediaTypes = new[] { new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain") }; + var negotiator = new FakeResponseNegotiator(options); + + var context = new DefaultHttpContext(); + var ms = new MemoryStream(); + context.Response.Body = ms; + + // Simulate CanHandle to set ContentType + negotiator.CanHandle(new MediaTypeHeaderValue("text/plain")); + + await negotiator.Handle(context.Request, context.Response, "Hello World", CancellationToken.None); + + var result = Encoding.UTF8.GetString(ms.ToArray()); + + Assert.Equal("Hello World", result); + Assert.Equal("text/plain; charset=utf-8", context.Response.ContentType); + } + } + + public class FakeResponseNegotiator : ConfigurableResponseNegotiator + { + public FakeResponseNegotiator(FakeOptions options) : base(options) + { + } + + protected override Encoding GetDefaultEncoding() + { + return new UTF8Encoding(false); + } + + public override StreamFormatter GetFormatter() + { + return new FakeStreamFormatter(o => { }); + } + } + + public class FakeOptions : IExceptionDescriptorOptions, IContentNegotiation, IValidatableParameterObject + { + public FaultSensitivityDetails SensitivityDetails { get; set; } + + public IReadOnlyCollection SupportedMediaTypes { get; set; } = new List(); + + public void ValidateOptions() + { + } + } + + public class FakeStreamFormatter : StreamFormatter + { + public FakeStreamFormatter(Action setup) : base(setup) + { + } + + public override Stream Serialize(object source, Type objectType) + { + var ms = new MemoryStream(); + var writer = new StreamWriter(ms); + writer.Write(source?.ToString()); + writer.Flush(); + ms.Position = 0; + return ms; + } + + public override object Deserialize(Stream stream, Type objectType) + { + throw new NotImplementedException(); + } + } +} From 855bf8979e963e03513da0003a3eaf75b72fa6ca Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:55:37 +0100 Subject: [PATCH 04/30] =?UTF-8?q?=E2=9C=A8=20add=20newtonsoft=20json=20neg?= =?UTF-8?q?otiator=20for=20json=20response=20serialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NewtonsoftJsonNegotiator.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/NewtonsoftJsonNegotiator.cs diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/NewtonsoftJsonNegotiator.cs b/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/NewtonsoftJsonNegotiator.cs new file mode 100644 index 0000000..ed4898d --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/NewtonsoftJsonNegotiator.cs @@ -0,0 +1,40 @@ +using System.Text; +using Codebelt.Extensions.Carter.Response; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Cuemon.Runtime.Serialization.Formatters; +using Microsoft.Extensions.Options; + +namespace Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; + +/// +/// Provides a JSON response negotiator for Carter, capable of serializing response models to JSON format using Newtonsoft.Json. +/// +/// +public class NewtonsoftJsonNegotiator : ConfigurableResponseNegotiator +{ + /// + /// Initializes a new instance of the class. + /// + /// The used to configure JSON serialization and supported media types. + public NewtonsoftJsonNegotiator(IOptions options) : base(options.Value) + { + } + + /// + /// Returns UTF-8 without a byte-order mark (BOM) as the default character encoding for this negotiator. + /// + /// A instance with the byte-order mark disabled. + protected override Encoding GetDefaultEncoding() + { + return new UTF8Encoding(false); + } + + /// + /// Returns a new configured with the current . + /// + /// A instance configured with the current . + public override StreamFormatter GetFormatter() + { + return new NewtonsoftJsonFormatter(Options); + } +} From 5100d67f5ed160f5a4e1007112545d1fe1ae55ca Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:56:07 +0100 Subject: [PATCH 05/30] =?UTF-8?q?=E2=9C=A8=20add=20json=20response=20negot?= =?UTF-8?q?iator=20for=20serialization=20using=20system.text.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../JsonResponseNegotiator.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/JsonResponseNegotiator.cs diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/JsonResponseNegotiator.cs b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/JsonResponseNegotiator.cs new file mode 100644 index 0000000..1f07aa5 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/JsonResponseNegotiator.cs @@ -0,0 +1,40 @@ +using System.Text; +using Codebelt.Extensions.Carter.Response; +using Cuemon.Extensions.Text.Json.Formatters; +using Cuemon.Runtime.Serialization.Formatters; +using Microsoft.Extensions.Options; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Provides a JSON response negotiator for Carter, capable of serializing response models to JSON format using System.Text.Json. +/// +/// +public class JsonResponseNegotiator : ConfigurableResponseNegotiator +{ + /// + /// Initializes a new instance of the class. + /// + /// The used to configure JSON serialization and supported media types. + public JsonResponseNegotiator(IOptions options) : base(options.Value) + { + } + + /// + /// Returns UTF-8 without a byte-order mark (BOM) as the default character encoding for this negotiator. + /// + /// A instance with the byte-order mark disabled. + protected override Encoding GetDefaultEncoding() + { + return new UTF8Encoding(false); + } + + /// + /// Returns a new configured with the current . + /// + /// A instance configured with the current . + public override StreamFormatter GetFormatter() + { + return new JsonFormatter(Options); + } +} From 61f68d25e772f093eef514eaa87565498e741cd6 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:56:25 +0100 Subject: [PATCH 06/30] =?UTF-8?q?=E2=9C=A8=20add=20yaml=20response=20negot?= =?UTF-8?q?iator=20for=20serialization=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YamlResponseNegotiator.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/YamlResponseNegotiator.cs diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/YamlResponseNegotiator.cs b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/YamlResponseNegotiator.cs new file mode 100644 index 0000000..63a4087 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/YamlResponseNegotiator.cs @@ -0,0 +1,37 @@ +using Codebelt.Extensions.YamlDotNet.Formatters; +using Microsoft.Extensions.Options; +using System.Text; +using Codebelt.Extensions.Carter.Response; +using Cuemon.Runtime.Serialization.Formatters; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; + +/// +/// Provides a YAML response negotiator for Carter, capable of serializing response models to YAML format. +/// +/// +public class YamlResponseNegotiator : ConfigurableResponseNegotiator +{ + /// + /// Initializes a new instance of the class. + /// + /// The used to configure YAML serialization and supported media types. + public YamlResponseNegotiator(IOptions options) : base(options.Value) + { + } + + /// + /// Returns the character encoding specified by the . + /// + /// The default for this negotiator. + protected override Encoding GetDefaultEncoding() => Options.Encoding; + + /// + /// Returns a new configured with the current . + /// + /// A instance configured with the current . + public override StreamFormatter GetFormatter() + { + return new YamlFormatter(Options); + } +} From 750c7927aa55955ec01cb3a9d0ddf24922855789 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:56:51 +0100 Subject: [PATCH 07/30] =?UTF-8?q?=E2=9C=A8=20add=20xml=20response=20negoti?= =?UTF-8?q?ator=20for=20serialization=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../XmlResponseNegotiator.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Xml/XmlResponseNegotiator.cs diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Xml/XmlResponseNegotiator.cs b/src/Codebelt.Extensions.Carter.AspNetCore.Xml/XmlResponseNegotiator.cs new file mode 100644 index 0000000..0420b18 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Xml/XmlResponseNegotiator.cs @@ -0,0 +1,38 @@ +using Codebelt.Extensions.Carter.Response; +using Cuemon.Xml.Serialization.Formatters; +using Microsoft.Extensions.Options; +using System.Text; +using Cuemon.Runtime.Serialization.Formatters; + +namespace Codebelt.Extensions.Carter.AspNetCore.Xml +{ + /// + /// Provides an XML response negotiator for Carter, capable of serializing response models to XML format. + /// + /// + public class XmlResponseNegotiator : ConfigurableResponseNegotiator + { + /// + /// Initializes a new instance of the class. + /// + /// The used to configure XML serialization and supported media types. + public XmlResponseNegotiator(IOptions options) : base(options.Value) + { + } + + /// + /// Returns the character encoding specified by the writer settings. + /// + /// The default for this negotiator. + protected override Encoding GetDefaultEncoding() => Options.Settings.Writer.Encoding; + + /// + /// Returns a new configured with the current . + /// + /// A instance configured with the current . + public override StreamFormatter GetFormatter() + { + return new XmlFormatter(Options); + } + } +} From 2050ab3db3c34a36fce8683fe04e36841ab41540 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:57:18 +0100 Subject: [PATCH 08/30] =?UTF-8?q?=E2=9C=85=20add=20newtonsoft=20json=20neg?= =?UTF-8?q?otiator=20tests=20for=20media=20type=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...er.AspNetCore.Newtonsoft.Json.Tests.csproj | 16 +++ .../NewtonsoftJsonNegotiatorTest.cs | 98 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests.csproj create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/NewtonsoftJsonNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests.csproj b/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests.csproj new file mode 100644 index 0000000..f41d9e9 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests.csproj @@ -0,0 +1,16 @@ + + + + Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json + + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/NewtonsoftJsonNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/NewtonsoftJsonNegotiatorTest.cs new file mode 100644 index 0000000..9b96867 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.Tests/NewtonsoftJsonNegotiatorTest.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Carter; +using Carter.Response; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Http; +using Xunit; +using AspNetCoreMediaType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + +namespace Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; + +/// +/// Tests for the class. +/// +public class NewtonsoftJsonNegotiatorTest : Test +{ + public NewtonsoftJsonNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("application/json")] + [InlineData("*/*")] + public void CanHandle_ShouldReturnTrue_WhenMediaTypeIsSupported(string mediaType) + { + var sut = new NewtonsoftJsonNegotiator(Options.Create(new NewtonsoftJsonFormatterOptions())); + + Assert.True(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Theory] + [InlineData("application/xml")] + [InlineData("text/xml")] + [InlineData("application/yaml")] + public void CanHandle_ShouldReturnFalse_WhenMediaTypeIsNotSupported(string mediaType) + { + var sut = new NewtonsoftJsonNegotiator(Options.Create(new NewtonsoftJsonFormatterOptions + { + SupportedMediaTypes = new List + { + new("application/json") + } + })); + + Assert.False(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Fact] + public async Task Handle_ShouldWriteJsonToResponseBody_WithCorrectContentType() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddNewtonsoftJsonFormatterOptions(); + services.AddCarter(configurator: c => c.WithResponseNegotiator()); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", (HttpResponse res, CancellationToken ct) => + res.Negotiate(new FakeModel { Id = 1, Name = "Newtonsoft" }, ct)); + }); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/json", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + Assert.Contains("1", body); + Assert.Contains("Newtonsoft", body); + + TestOutput.WriteLine(body); + } +} + +internal sealed class FakeModel +{ + public int Id { get; init; } + public string Name { get; init; } +} From 78120d69e18e973c00ef5fdd11265169d5f94f55 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:57:34 +0100 Subject: [PATCH 09/30] =?UTF-8?q?=E2=9C=85=20add=20json=20response=20negot?= =?UTF-8?q?iator=20tests=20for=20media=20type=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...s.Carter.AspNetCore.Text.Json.Tests.csproj | 16 +++ .../JsonResponseNegotiatorTest.cs | 98 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests.csproj create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/JsonResponseNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests.csproj b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests.csproj new file mode 100644 index 0000000..c72f3b2 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests.csproj @@ -0,0 +1,16 @@ + + + + Codebelt.Extensions.Carter.AspNetCore.Text.Json + + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/JsonResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/JsonResponseNegotiatorTest.cs new file mode 100644 index 0000000..9d97860 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Json.Tests/JsonResponseNegotiatorTest.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Carter; +using Carter.Response; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Cuemon.Extensions.Text.Json.Formatters; +using Microsoft.Extensions.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using AspNetCoreMediaType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Tests for the class. +/// +public class JsonResponseNegotiatorTest : Test +{ + public JsonResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("application/json")] + [InlineData("*/*")] + public void CanHandle_ShouldReturnTrue_WhenMediaTypeIsSupported(string mediaType) + { + var sut = new JsonResponseNegotiator(Options.Create(new JsonFormatterOptions())); + + Assert.True(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Theory] + [InlineData("application/xml")] + [InlineData("text/xml")] + [InlineData("application/yaml")] + public void CanHandle_ShouldReturnFalse_WhenMediaTypeIsNotSupported(string mediaType) + { + var sut = new JsonResponseNegotiator(Options.Create(new JsonFormatterOptions + { + SupportedMediaTypes = new List + { + new("application/json") + } + })); + + Assert.False(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Fact] + public async Task Handle_ShouldWriteJsonToResponseBody_WithCorrectContentType() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddSingleton(new JsonFormatterOptions()); + services.AddCarter(configurator: c => c.WithResponseNegotiator()); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", (HttpResponse res, CancellationToken ct) => + res.Negotiate(new FakeModel { Id = 1, Name = "Json" }, ct)); + }); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/json", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + Assert.Contains("1", body); + Assert.Contains("Json", body); + + TestOutput.WriteLine(body); + } +} + +internal sealed class FakeModel +{ + public int Id { get; init; } + public string Name { get; init; } +} From 0e306184176e9056bbcbaa7ca80c0195d8bd19e0 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:57:47 +0100 Subject: [PATCH 10/30] =?UTF-8?q?=E2=9C=85=20add=20yaml=20response=20negot?= =?UTF-8?q?iator=20tests=20for=20media=20type=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...s.Carter.AspNetCore.Text.Yaml.Tests.csproj | 16 +++ .../YamlResponseNegotiatorTest.cs | 102 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests.csproj create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests.csproj b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests.csproj new file mode 100644 index 0000000..cc0e523 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests.csproj @@ -0,0 +1,16 @@ + + + + Codebelt.Extensions.Carter.AspNetCore.Text.Yaml + + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs new file mode 100644 index 0000000..8721306 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Carter; +using Carter.Response; +using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Codebelt.Extensions.YamlDotNet.Formatters; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; +using Xunit; +using AspNetCoreMediaType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; + +/// +/// Tests for the class. +/// +public class YamlResponseNegotiatorTest : Test +{ + public YamlResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("application/yaml")] + [InlineData("text/yaml")] + [InlineData("text/plain")] + [InlineData("*/*")] + public void CanHandle_ShouldReturnTrue_WhenMediaTypeIsSupported(string mediaType) + { + var sut = new YamlResponseNegotiator(Options.Create(new YamlFormatterOptions())); + + Assert.True(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Theory] + [InlineData("application/json")] + [InlineData("text/xml")] + [InlineData("application/xml")] + public void CanHandle_ShouldReturnFalse_WhenMediaTypeIsNotSupported(string mediaType) + { + var sut = new YamlResponseNegotiator(Options.Create(new YamlFormatterOptions + { + SupportedMediaTypes = new List + { + new("application/x-yaml"), + new("application/yaml"), + new("text/yaml") + } + })); + + Assert.False(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Fact] + public async Task Handle_ShouldWriteYamlToResponseBody_WithCorrectContentType() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddYamlFormatterOptions(); + services.AddCarter(configurator: c => c.WithResponseNegotiator()); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", (HttpResponse res, CancellationToken ct) => + res.Negotiate(new FakeModel { Id = 1, Name = "YAML" }, ct)); + }); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/yaml")); + return await client.GetAsync("/"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/yaml", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + Assert.Contains("id: 1", body); + Assert.Contains("name: YAML", body); + + TestOutput.WriteLine(body); + } +} + +internal sealed class FakeModel +{ + public int Id { get; init; } + public string Name { get; init; } +} From e7167193e354fcf049f0f4f833ec0da9b348a2f5 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:58:04 +0100 Subject: [PATCH 11/30] =?UTF-8?q?=E2=9C=85add=20xml=20response=20negotiato?= =?UTF-8?q?r=20tests=20for=20media=20type=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ensions.Carter.AspNetCore.Xml.Tests.csproj | 16 +++ .../XmlResponseNegotiatorTest.cs | 100 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests.csproj create mode 100644 test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests.csproj b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests.csproj new file mode 100644 index 0000000..dd2b46c --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests.csproj @@ -0,0 +1,16 @@ + + + + Codebelt.Extensions.Carter.AspNetCore.Xml + + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs new file mode 100644 index 0000000..7a9fd77 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Carter; +using Carter.Response; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Cuemon.Xml.Serialization.Formatters; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; +using AspNetCoreMediaType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; + +namespace Codebelt.Extensions.Carter.AspNetCore.Xml; + +/// +/// Tests for the class. +/// +public class XmlResponseNegotiatorTest : Test +{ + public XmlResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData("application/xml")] + [InlineData("text/xml")] + [InlineData("*/*")] + public void CanHandle_ShouldReturnTrue_WhenMediaTypeIsSupported(string mediaType) + { + var sut = new XmlResponseNegotiator(Options.Create(new XmlFormatterOptions())); + + Assert.True(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Theory] + [InlineData("application/json")] + [InlineData("text/yaml")] + [InlineData("application/yaml")] + public void CanHandle_ShouldReturnFalse_WhenMediaTypeIsNotSupported(string mediaType) + { + var sut = new XmlResponseNegotiator(Options.Create(new XmlFormatterOptions + { + SupportedMediaTypes = new List + { + new("application/xml"), + new("text/xml") + } + })); + + Assert.False(sut.CanHandle(AspNetCoreMediaType.Parse(mediaType))); + } + + [Fact] + public async Task Handle_ShouldWriteXmlToResponseBody_WithCorrectContentType() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.Configure(_ => { }); + services.AddCarter(configurator: c => c.WithResponseNegotiator()); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/", (HttpResponse res, CancellationToken ct) => + res.Negotiate(new FakeModel { Id = 1, Name = "XML" }, ct)); + }); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + return await client.GetAsync("/"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/xml", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + Assert.Contains("1", body); + Assert.Contains("XML", body); + + TestOutput.WriteLine(body); + } +} + +internal sealed class FakeModel +{ + public int Id { get; init; } + public string Name { get; init; } +} From 73bb25c38e3693e4037d596e7cfedbf5a435cd18 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 04:58:32 +0100 Subject: [PATCH 12/30] =?UTF-8?q?=E2=9C=85=20add=20world=20module=20and=20?= =?UTF-8?q?response=20negotiators=20for=20json,=20xml,=20yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Assets/WorldModule.cs | 28 +++++++++ ...t.Extensions.Carter.FunctionalTests.csproj | 27 ++++++++ .../JsonResponseNegotiatorTest.cs | 61 ++++++++++++++++++ .../NewtonsoftJsonNegotiatorTest.cs | 63 +++++++++++++++++++ .../XmlResponseNegotiatorTest.cs | 61 ++++++++++++++++++ .../YamlResponseNegotiatorTest.cs | 58 +++++++++++++++++ 6 files changed, 298 insertions(+) create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs create mode 100644 test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs new file mode 100644 index 0000000..d4bd783 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs @@ -0,0 +1,28 @@ +using System.Linq; +using System.Threading; +using Carter; +using Carter.Response; +using Cuemon.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; + +internal sealed class WorldModule : ICarterModule +{ + public void AddRoutes(IEndpointRouteBuilder app) + { + app.MapGet("/world/statistical-regions", (HttpResponse res, CancellationToken ct) => + res.Negotiate(World.StatisticalRegions + .Where(r => r.Kind == StatisticalRegionKind.World) + .Select(r => new StatisticalRegionModel { Code = r.Code, Name = r.Name, Kind = r.Kind }), ct)); + } +} + +internal sealed class StatisticalRegionModel +{ + public string Code { get; init; } + public string Name { get; init; } + public StatisticalRegionKind Kind { get; init; } +} diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj b/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj new file mode 100644 index 0000000..2b0f7d2 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj @@ -0,0 +1,27 @@ + + + + Codebelt.Extensions.Carter.AspNetCore.Text.Json + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs new file mode 100644 index 0000000..d4a3abb --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Cuemon.Extensions.AspNetCore.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Functional tests verifying Carter bootstrapped with as the sole response negotiator. +/// +public class JsonResponseNegotiatorTest : Test +{ + public JsonResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task GetStatisticalRegions_ShouldReturnJsonResponse_WhenCarterDefaultsAreDisabled() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddMinimalJsonOptions(o => + { + o.Settings.Converters.Insert(0, new JsonStringEnumConverter()); // namingPolicy: null = preserves PascalCase + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/world/statistical-regions"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/json", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + + TestOutput.WriteLine(body); + } +} diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs new file mode 100644 index 0000000..b609209 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs @@ -0,0 +1,63 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Carter; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Functional tests verifying Carter bootstrapped with as the sole response negotiator. +/// +public class NewtonsoftJsonNegotiatorTest : Test +{ + public NewtonsoftJsonNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task GetStatisticalRegions_ShouldReturnJsonResponse_WhenCarterDefaultsAreDisabled() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddNewtonsoftJsonFormatterOptions(o => + { + o.Settings.Converters.Insert(0, new StringEnumConverter(new DefaultNamingStrategy(), false)); + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/world/statistical-regions"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/json", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + + TestOutput.WriteLine(body); + } +} diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs new file mode 100644 index 0000000..c813bbc --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs @@ -0,0 +1,61 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Carter.AspNetCore.Xml; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Cuemon.Extensions.AspNetCore.Xml; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Functional tests verifying Carter bootstrapped with as the sole response negotiator. +/// +public class XmlResponseNegotiatorTest : Test +{ + public XmlResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task GetStatisticalRegions_ShouldReturnXmlResponse_WhenCarterDefaultsAreDisabled() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddMinimalXmlOptions(o => + { + o.Settings.Writer.Indent = true; + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + return await client.GetAsync("/world/statistical-regions"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/xml", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + + TestOutput.WriteLine(body); + } +} diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs new file mode 100644 index 0000000..b0d5272 --- /dev/null +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs @@ -0,0 +1,58 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Carter; +using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; +using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; + +/// +/// Functional tests verifying Carter bootstrapped with as the sole response negotiator. +/// +public class YamlResponseNegotiatorTest : Test +{ + public YamlResponseNegotiatorTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task GetStatisticalRegions_ShouldReturnYamlResponse_WhenCarterDefaultsAreDisabled() + { + using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddYamlFormatterOptions(); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/yaml")); + return await client.GetAsync("/world/statistical-regions"); + }); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.StartsWith("application/yaml", response.Content.Headers.ContentType?.ToString()); + + var body = await response.Content.ReadAsStringAsync(); + + Assert.NotEmpty(body); + + TestOutput.WriteLine(body); + } +} From d635badac0503306e0689cbb087dcc92fc1d02bd Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:43:00 +0100 Subject: [PATCH 13/30] =?UTF-8?q?=E2=9C=A8=20add=20docfx=20configuration?= =?UTF-8?q?=20and=20build=20scripts=20for=20documentation=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/BuildDocfxImage.ps1 | 4 + .docfx/Dockerfile.docfx | 17 ++++ .docfx/PublishDocfxImage.ps1 | 3 + ...sions.Carter.AspNetCore.Newtonsoft.Json.md | 7 ++ ....Extensions.Carter.AspNetCore.Text.Json.md | 7 ++ ....Extensions.Carter.AspNetCore.Text.Yaml.md | 7 ++ ...debelt.Extensions.Carter.AspNetCore.Xml.md | 7 ++ .../namespaces/Codebelt.Extensions.Carter.md | 13 +++ .docfx/docfx.json | 91 ++++++++++++++++++ .docfx/filterConfig.yml | 4 + .docfx/images/32x32.png | Bin 0 -> 1807 bytes .docfx/images/50x50.png | Bin 0 -> 2864 bytes .docfx/images/favicon.ico | Bin 0 -> 15406 bytes .docfx/includes/availability-modern.md | 1 + .docfx/index.md | 11 +++ .docfx/toc.yml | 4 + 16 files changed, 176 insertions(+) create mode 100644 .docfx/BuildDocfxImage.ps1 create mode 100644 .docfx/Dockerfile.docfx create mode 100644 .docfx/PublishDocfxImage.ps1 create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Json.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Xml.md create mode 100644 .docfx/api/namespaces/Codebelt.Extensions.Carter.md create mode 100644 .docfx/docfx.json create mode 100644 .docfx/filterConfig.yml create mode 100644 .docfx/images/32x32.png create mode 100644 .docfx/images/50x50.png create mode 100644 .docfx/images/favicon.ico create mode 100644 .docfx/includes/availability-modern.md create mode 100644 .docfx/index.md create mode 100644 .docfx/toc.yml diff --git a/.docfx/BuildDocfxImage.ps1 b/.docfx/BuildDocfxImage.ps1 new file mode 100644 index 0000000..bbaa1d6 --- /dev/null +++ b/.docfx/BuildDocfxImage.ps1 @@ -0,0 +1,4 @@ +$version = minver -i -t v -v w +docfx metadata docfx.json +docker buildx build -t carter-docfx:$version --platform linux/arm64,linux/amd64 --load -f Dockerfile.docfx . +get-childItem -recurse -path api -include *.yml, .manifest | remove-item diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx new file mode 100644 index 0000000..dc87c70 --- /dev/null +++ b/.docfx/Dockerfile.docfx @@ -0,0 +1,17 @@ +ARG NGINX_VERSION=1.29.5-alpine + +FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base +RUN rm -rf /usr/share/nginx/html/* + +FROM --platform=$BUILDPLATFORM codebeltnet/docfx:2.78.5 AS build + +ADD [".", "docfx"] + +RUN cd docfx; \ + docfx build + +FROM nginx:${NGINX_VERSION} AS final +WORKDIR /usr/share/nginx/html +COPY --from=build /build/docfx/wwwroot /usr/share/nginx/html + +ENTRYPOINT ["nginx", "-g", "daemon off;"] diff --git a/.docfx/PublishDocfxImage.ps1 b/.docfx/PublishDocfxImage.ps1 new file mode 100644 index 0000000..872b347 --- /dev/null +++ b/.docfx/PublishDocfxImage.ps1 @@ -0,0 +1,3 @@ +$version = minver -i -t v -v w +docker tag carter-docfx:$version jcr.codebelt.net/geekle/carter-docfx:$version +docker push jcr.codebelt.net/geekle/carter-docfx:$version diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.md b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.md new file mode 100644 index 0000000..e5f145f --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json +summary: *content +--- +The `Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json` namespace contains types that provide JSON response negotiation for Carter, capable of serializing response models to JSON format using `Newtonsoft.Json`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Json.md b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Json.md new file mode 100644 index 0000000..dcdd906 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Json.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.Carter.AspNetCore.Text.Json +summary: *content +--- +The `Codebelt.Extensions.Carter.AspNetCore.Text.Json` namespace contains types that provide JSON response negotiation for Carter, capable of serializing response models to JSON format using `System.Text.Json`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.md b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.md new file mode 100644 index 0000000..1f26a0d --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.Carter.AspNetCore.Text.Yaml +summary: *content +--- +The `Codebelt.Extensions.Carter.AspNetCore.Text.Yaml` namespace contains types that provide YAML response negotiation for Carter, capable of serializing response models to YAML format using `YamlDotNet`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Xml.md b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Xml.md new file mode 100644 index 0000000..4876cc4 --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.Carter.AspNetCore.Xml.md @@ -0,0 +1,7 @@ +--- +uid: Codebelt.Extensions.Carter.AspNetCore.Xml +summary: *content +--- +The `Codebelt.Extensions.Carter.AspNetCore.Xml` namespace contains types that provide XML response negotiation for Carter, capable of serializing response models to XML format using `System.Xml.XmlWriter`. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] diff --git a/.docfx/api/namespaces/Codebelt.Extensions.Carter.md b/.docfx/api/namespaces/Codebelt.Extensions.Carter.md new file mode 100644 index 0000000..1c680cc --- /dev/null +++ b/.docfx/api/namespaces/Codebelt.Extensions.Carter.md @@ -0,0 +1,13 @@ +--- +uid: Codebelt.Extensions.Carter +summary: *content +--- +The Codebelt.Extensions.Carter namespace contains types and extension methods that complements the Carter namespace by providing configurable response negotiation, endpoint convention builder extensions and content negotiation for ASP.NET Core minimal APIs. + +[!INCLUDE [availability-modern](../../includes/availability-modern.md)] + +### Extension Methods + +|Type|Ext|Methods| +|--:|:-:|---| +|IEndpointConventionBuilder|⬇️|`Produces`, `Produces`| diff --git a/.docfx/docfx.json b/.docfx/docfx.json new file mode 100644 index 0000000..5f5bd8a --- /dev/null +++ b/.docfx/docfx.json @@ -0,0 +1,91 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ + "Codebelt.Extensions.Carter/**.csproj", + "Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/**.csproj", + "Codebelt.Extensions.Carter.AspNetCore.Text.Json/**.csproj", + "Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/**.csproj", + "Codebelt.Extensions.Carter.AspNetCore.Xml/**.csproj" + ], + "src": "../src" + } + ], + "dest": "api", + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "net10.0" + } + } + ], + "build": { + "xref": [ + "https://docs.cuemon.net/xrefmap.yml?v=20260120", + "https://newtonsoft.codebelt.net/xrefmap.yml", + "https://yamldotnet.codebelt.net/xrefmap.yml", + "https://github.com/dotnet/docfx/raw/main/.xrefmap.json" + ], + "content": [ + { + "files": [ + "api/**/*.yml", + "api/**/*.md", + "packages/**/*.md", + "toc.yml", + "*.md" + ], + "exclude": [ + "bin/**", + "obj/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "globalMetadata": { + "_appTitle": "Extensions for Carter by Codebelt", + "_appFooter": "Generated by DocFX. Copyright 2026 Geekle. All rights reserved.", + "_appLogoPath": "images/50x50.png", + "_appFaviconPath": "images/favicon.ico", + "_googleAnalyticsTagId": "G-GGL98GEYWE", + "_enableSearch": false, + "_disableContribution": false, + "_gitContribute": { + "repo": "https://github.com/codebeltnet/carter", + "branch": "main" + }, + "_gitUrlPattern": "github" + }, + "dest": "wwwroot", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default", + "modern" + ], + "overwrite": [ + { + "files": [ + "api/namespaces/**.md" + ], + "exclude": [ + "obj/**", + "wwwroot/**" + ] + } + ], + "postProcessors": [], + "markdownEngineName": "markdig", + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false + } +} diff --git a/.docfx/filterConfig.yml b/.docfx/filterConfig.yml new file mode 100644 index 0000000..3343240 --- /dev/null +++ b/.docfx/filterConfig.yml @@ -0,0 +1,4 @@ +apiRules: +- exclude: + uidRegex: ^System\.Object + type: Type \ No newline at end of file diff --git a/.docfx/images/32x32.png b/.docfx/images/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..8a17aadb2173aa92663dc19e9b0251aafcbf6163 GIT binary patch literal 1807 zcmV+q2k`ibP)F*bPrzs=$R00w_aL_t(og~gX!Y*gnF$A9ykbM~C|8Z3vT zgcc>?CPPi^VyuSHgs?;i+vTEA>Ld-NK#Pcq=tI>u4+W)GP#ZN3s#HNjnu1j+Y3l+G zgjhCJ5?4h$RJdR+mH}f3cA(O-p{H`w6@(36KE0!T6oKAZ>mIX>zZVDry&}YX)k|Xm2XKh>AR1RC1k6O) zdv@lU$18e&e{mRrfZT2nG=%W(l+*h+j1@R5GtM+OX4n0dE8iB_h$Tyb-wY$*n8?HF zmGs;%@R1TAaz;f!c{r=f6CetJa`&pJscCp<$Ts5uO6`*CM~}7)jP%(V4wse%gXX2S zw%a3Z4p9_Ak(|!XFKz<@MF=!DHd<#d#9I-2nWTSCqNzkHurllO&hqes&Y<~GFd#n| z^yAi^`bEu}vYXZ)K71|fV~8Rdl)Nst7{nrhAu)=;x&Figph58K6u(ySYc1cb&)Rp_ zhu1oTauNJ0G3FWZ^$E~PVusG|sSj_=XCRTmh>y+y_FWSIN;wce(RHY!h!yc=n+lsm zP2bv%kB_0M%?;t7tFP~Yyant6z#SbwN5GW$pfW5ykJmmoEjsSz)iTECbhB%hKQiRW zjou>%B6$XcjLHoa-M?qaqH9vOD5=O)F?=Ueera)i1S$Rr@Ibr_%dAmTyT{)i*MVvX zpsHrslSVn^#`XD;Sx8w!S8R2rfnHX)q3+;ReC;pvF>Voz+#x)I1Qk=F$((hFwZNka zzXn`H+C}7plU;Lnk0b!_?T(HZqUTk)u>Qc2$Vj_}G)YjAkRKdQpiwO2$zt(9T{)LE zc6T4Vm=YEs+yrbv+bX8I>s4R%N5sA+YOQ-`@!~Q>4jIe`yH`9lP7U7$a#cW(KJh?Y zd6sb41~Au`KCvWXO>M#RtHss6vM-coOBU2eJl^{>{+nb|L^9T=c&@`Z?3 zemz{)+}Bksz2S9DUMRvB6UO_`Us_(dRm`WkE?lP~KgP!r z?7Vet`nXiQ1UX;*V%$$UE`X|hzoMu4t-=jUm%d((o7@h(0C+%3gu|(nY;J4aa-kTJ zd&&9AH7{$rG%E3+FPu!+6l5LHK`8>V-^?;3z9J^OxVOXNdaA|c<3 zEzSn8?+k24^(zYJ01-nJP|$vb2Q6cMQ3yD1Uc>n6xhsA)e|}>Sz!x47Vd27s3oQU& zZ$4{Fq|}kyA5H1m_bwEaVSv~vhbRGwOw8&;Sq^kdx?!mz1HcWCa)rME%bS{-Ocs0~ zX}5!}j>T@}`%8fYBs$R{ibd>n?vUAqMI>EXTF@Jm2Nc;O_*YbIAB)mIiQSrof%f&dwBor4bPkMAaWfz!1-jf_$@9LoceY z=ffQW3<%+1#!k%^wi$a~s=!?0VT z5!smu^Im$FzLwejMdpLXKdfvvfKZU!SzfRS62_=u;N zVBUD}&sz>FYz6&RhR7z;GxK>x_J&Nwc7Wr@TQ3>kT9`htzBXZetovlwp{|pEKQwF> z;ASwqA_R&JBY=bDjAn2bH z1<-FqSI=lqsfh>tR5JL7zA?Rw23F6QJg6pMr4p_LtySqd)3c)p#7g6>rAhBBFkkV{ z7<`8A!^>zW;I|w6O2tYmgmW=Fu|Ipq%Ke0sc3r(ArRHhzbccA_EuQvEP{(w70P*yS xc-o(dxkEzcsY$!8-pScN@7HnBp8fxn{{i6$lqZ?O-qipA002ovPDHLkV1m+XV=({# literal 0 HcmV?d00001 diff --git a/.docfx/images/50x50.png b/.docfx/images/50x50.png new file mode 100644 index 0000000000000000000000000000000000000000..186e711e7de7a2822d5e1859fb40b296eaf1393b GIT binary patch literal 2864 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 O0000Q(=#QKKi?{m zWFrrXphPqt&L#V%EDn`Omqt2dOCxO;Vyv{Pvs$O{H+Us>;S{H50CYyllw?{)lu%DRoZ7hNWe*D;NKoyufR4d*D#gR%12 z?AT6~Y9O!H=U4`M3A}fqd~==8`L18DSEGEt$Ee*5cdN^!UCh|A92GknD_^MY{p*bE z{pR&PmzTHq6o!8-_Ji_NTo3E#SFq_}IY7X1iGGD#-hyMJom)Y#WHzg8o66K@z zR_EoZf))#Al|)idP=8~wt>HVx)`ohEwLX|@f9fX6%L0SDe0iNslt=Cg?Ut1t*YSO2 z(!K05>Uy0fjf&DNw)(Y}S@lybwuUvuwtBtA*7z;CV^FJbhnp##KIIJbf5AYW%dDT_ zGOBCbX6^UUS4%04@)y~*RfGRd%dCb4CDw+=Wcld-0@3(s2X`uU(eZcavoqZ$-4k}B z@m90he3ix8FdJom{0-Z){{(l7#ky@?CJX0eXfKSPksaP?QXS^!4_Hp}81-vi7%zu` zR+U6jSh`h(avO?m+tn1F%KCDQtUoq>4%Yn#JL@Q+wDEkd>~4!)-Tfya??To#e<|BD zv`p0!>k}a@(M6u!OIgcM4QpEUl`sm4-9@Ou@2e1$ldaJVNO;V*`-(zR!*+@ z^bT3&@!JyfO*Aa4?7xlbx^v+vg}gHIN8%x$7h0KEkIXYvE3ZIWdHBJ^w0~4sQ4zkE zcqqJ)jfr*2Y(uq*vJfY?4?U2W_Kyn7?BO-ULtz`sUr_}9mLX1Q8Owiz&rw+Gb3FqY z_7-Hw3+sLMxxmEB1u?b?gF%Wt_GPy@^Q+=`!LP7@KX@pLLSx#m$Q`Ox=7N82Jb&<8 z4|#VUa*L=J#PMOj^94Z`QT@c*WzzMyP1;>{rSckqr`T2Ggx3%cMP3sA`hi-devnfd zF7RJp;kHNlIqM;_&#$j=JTi)-5$*`+(Rkjv0}g{`7Gy*|5#$wZgRw7D5B|hMH#oNa z>Vet}HPX}rvGK3W)qS;j>O)+<>d*!LKF43d<3sR!8}2#KuRsGhf9=6s z+*#{)o)qRl41-D6QEE2x`wQ^Qq&^8qa8SmV&-&I7k6d=>dqSTGZYewPi&FJ|Zl-2` zOn#!R-v&SQz2gs2*ZEtx=fQs`+!36qel3(IcND{>0An{$IFbKM^?`ZB0~A-zpWRvM zRdtSlKD8=3zD8x~3%cFcgU3hU)rECH^Pk$!p+7niK3W%W%c41x|I+*ehIf(5_rE){ z{i|NqdBUse8u7C(zMa*qy}O#VaYXak-g^^k;?KTb=XUTtO}s(pu>M}F^Sd$<^NuGx zU)6q(z^_`}eqfb)&zXhBHm<Wey0S=Pf&XUeloacP6C*jm2}^j49z z(VeU=`TX#->8`#LHO6*s729)e)~tgDc~$tis9XqlvjBH>V!1NxEp+w^N+e%MOrvnI zb-Nj7;Wt48#kR&ri>&nz!G8qj%6BaGt-|?HVqVT&C7T!S;KWm&Ze)2Y?UL2+}C_!pLK^}#(1edc}WGw%Y!QRoI0;`UEe z{s?Up^B3a8E_n2VXHAK9dm-ZawQeb^pMkNMi?J9M@=jF#2<2R6jg=pN=wZ;G7?C$S z`xYVHQr55n{D(n1c%2S&0hoRZI$^PGyI;tcPX0jd)T$HYk5G>EHT3_hw3mzM6(OD6 z>DHfr4g5C~PtZw>!@rVlC(c9ihj>8G9xH!ThsMYsaar@nw%N9QC0UvvfAZL&G5lw$4%G6pLA5_7|B}`5S>j4r?;2jlu>)iHdsQ6|Rj^JD z^5N&7g-5!wkZ(U3dqWz7P6MV!nxnp%4FxuZy*{MCDHV z=>=JBS;^{I!Sd_{9*`~? zx>>s7c$sWfWE<>5uY-P1G-XxfiAxPax-zm!;77a>uAj0Z?2s9X*Sc%BhXzHIocbjk4ZcP$CY)733q)P1e2Ch`M$P2{Y6 zAzIYJ8hyWP!QKM~%S zBJbot?{&cPP81X7wEws+6(5;vNUtb|9T5b3W$eByRsPQ_9$1=UxI8DOrs4}*r(R)) z9TFsWgkDdT@AHnAJAq%oRTiF>iodKF_)CEwB%gJ0{2M)<+iHFGf?$Q+8mw^TY)WIp zNaShP>y70aO;VeEVqQfl@C&$R9!rgX#*kiF2wN*psqqILj<3Vkvl%v@4iMcP9s?Z$ zeOOoFxIYjG@OHIC975P-&{jcTJLoa#f09aH67rI(C=4|Txa37?@aF)39`J)Q@{;2Z zdYu}&XC=EW@J8)cerI^pFW6Ied^g~_ON@d17Tj^VO`18dUy|M9IP4$)OmT_%62q`7 zHwn1p)6?KL0>2ryUQmX4EPmMArvT@BusfcCosDe3Wc%!?^SQ|W4Evv>8@5T2EW5EVBzOXwBI?j;SIFmZl^)FP$04e zd5oH*dkgS13Ap6yi{OW?oowM5EFOQ*@6Lu@c|TzuFV%US{GF}GXuKWvs(m6Gh1gyZ z?>FnV*>jb+Gr#b`KGMWv0woMq6~H|NqIUwD>+o)Y)&MZR zIl7kV4FlTdcn0x5gCA{moZJ$~zmD!r@Q&a+z<-)=gHiVp=Kf|eA^j<&b-K;E2*zgq z1@sb078_`a;Y!p0#>{@ae(2|TV#ng}*Lqzwz;qmA@FLmv>8=6c5wznGK0G?NHu`KP zeqV(xn%Wbb=eeD*@NW$ax(7zvFyt|71GIJpf^Gil(KepzJs3AhaE zzLfiC4)Euw`#7Rpb~p|{`q>9OA<$mjldXmOI==_`9j`>gj`s;~t>69x*70}Yr@i&; zI;%BiPog$p?g!C^6QH9Wv%v{?F?%g+u=gXL!?@Im@#nI>CIQ#9H24cu2MP<-2RWi5 z^}&~^?ZQ{~v+y)8(EA8%KFBj74#2WJX( zCAwDi{!LbO4@YFv9Eja-Q9gc-R5*UY((vo+lo59>Nv&bqjA)_KO6-7%fg z#NUQ;>z$bQvrPJ8BF$b!{yIlN=qgMGDqH2p=oqY;+jSH5ii+=pp2d zwacB^ISsf@N3nICDpz%u5@xEeQr%fu$=aWs%R2hzvV9!f^K)3|vv~6po0np~(K}=Q z{*Gi9?WN@QjryZ|tJm>L&{LN3oGY$R?2q2F>1LNtOFS2GlYprK_|6l?d934bCEK^T zO1p1^VY?eDO-~?t(duX}3_2hE4hg+g;kMDOIf27HuZnu@6>Gd8%yll6&6clO1+TkB;9tZ_sB+nUgzm0 zx>k-bFW0n;EZ4U@TmFsimV8(LNJbU(mIaXzbhX77!$Ij5i!?3I#5!h{J);BejkpKs z0G$H0;%??7+#b50qC1MjGD28Zb-WtbhLpgV>+V12DeP%oVQ78cQ_yx}RscHZyzqAU zT;L~~6YigE3#VOsPNQxf_Ugak4D17s*jf|#iCw$>scUeDQwN;Az<(B)KPWES?kTi3 z+*w>kcbg4=;p1_qhx?5u&A9Ipm#23%z|#@%D!?c#O;m(nvB zdq*IY6Wl2!M@*Zvpg2P{xdS{BDDK8nC|PF=@X^IxGL?l<*sL>qgIx-$MGY>8wv!#Gin_ zFuD%(xQD0vciMx{{|!ADn=SNK0Bg4t;W7QMD1iPoiYu-DNcwLM@PjgP!?;@4=ye*@zP=)+=b<3rdt-yzI!Un(kV3?#*$1N@`7 zFGDvV~&{_dI9#tORszF?!^CRw6!vnd|= zM(7Q(xDH@x5^!Bq|J49HPx1PrxIXe@4Q#@E8Ns~&W@1`m7y7(VmCcW3G z*V|%b(wdPawh!6uuUtd#sDZ5;1i9PTg}(mw^pb5;2sh&Pflgu_d_r@K#FW7KnA@a(8hig%fzXBxHnS@3 zjf1G%Bzk{Sf;o;hY{#DY7{*2;yyq9?i+RoLaFc*5o&Gxm`tNl0Cmc}$J9JU}#2^rm zTtPZu9F6W#F#t(CbPl&#|Kp1|k6w`^IW7uVngm?a)9Am&nO*u4^#P7(CObGT{Bevf zQJ{qNHpOtM52Um|II=sn_;0~^5a(pwyDkx&30Lc6J#P>uBA2Sil&S*1sx!mKItP8M zgY&b_{r;@B3G0BV>i&D<@n0DROx-*tRYy3G)s^&(2-WY+ZoLI>C4UDxKcBU8_^-{u z1#It2)vCQO5aInw@bASj-g4mcn81&C14}PqAxu?l@2iz~>zTSPZqg!Ei(@Hk?Ztma zxUtlB$?s3Wd&gM5JeEU?vRmE3g!hSwZ3$YqD_64SoO@KwJJ+y1BWu`hPM~{P^Cic> znl+iR^yG%1M232`&Azo60OVHeaY9uI@L!S ef!|v8;fx2eKadiZWEd~UUG9O)J@Eg#2mTNElAQJc literal 0 HcmV?d00001 diff --git a/.docfx/includes/availability-modern.md b/.docfx/includes/availability-modern.md new file mode 100644 index 0000000..5ed3f53 --- /dev/null +++ b/.docfx/includes/availability-modern.md @@ -0,0 +1 @@ +Availability: .NET 10 \ No newline at end of file diff --git a/.docfx/index.md b/.docfx/index.md new file mode 100644 index 0000000..50d2310 --- /dev/null +++ b/.docfx/index.md @@ -0,0 +1,11 @@ +--- +uid: frontpage-md +title: Extensions for Carter by Codebelt +--- +![Extensions for Carter API by Codebelt](/images/128x128.png) + +# Extensions for Carter by Codebelt + +This project is part of [Extensions for Carter by Codebelt](https://github.com/codebeltnet/carter). + +Proceed to the [docs](/api/Codebelt.Extensions.Carter.html) to learn more about the capabilities of this project. diff --git a/.docfx/toc.yml b/.docfx/toc.yml new file mode 100644 index 0000000..ab77dcc --- /dev/null +++ b/.docfx/toc.yml @@ -0,0 +1,4 @@ +- name: Carter API + href: api/Codebelt.Extensions.Carter.html +- name: NuGet + href: packages From b6edb237b2ab7521bff65a6773c446801601db8b Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:43:27 +0100 Subject: [PATCH 14/30] =?UTF-8?q?=E2=9C=A8=20add=20ci=20pipeline,=20scorec?= =?UTF-8?q?ard,=20service=20update,=20and=20downstream=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 117 +++++++++++++++++++ .github/workflows/scorecard.yml | 42 +++++++ .github/workflows/service-update.yml | 139 +++++++++++++++++++++++ .github/workflows/trigger-downstream.yml | 78 +++++++++++++ 4 files changed, 376 insertions(+) create mode 100644 .github/workflows/ci-pipeline.yml create mode 100644 .github/workflows/scorecard.yml create mode 100644 .github/workflows/service-update.yml create mode 100644 .github/workflows/trigger-downstream.yml diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml new file mode 100644 index 0000000..eeac7e0 --- /dev/null +++ b/.github/workflows/ci-pipeline.yml @@ -0,0 +1,117 @@ +name: Carter CI Pipeline +on: + pull_request: + branches: [main] + workflow_dispatch: + inputs: + configuration: + type: choice + description: The build configuration to use in the deploy stage. + required: true + default: Release + options: + - Debug + - Release + +permissions: + contents: read + +jobs: + build: + name: call-build + strategy: + matrix: + arch: [X64, ARM64] + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-build/.github/workflows/default.yml@v3 + with: + configuration: ${{ matrix.configuration }} + strong-name-key-filename: carter.snk + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + upload-build-artifact-name: build-${{ matrix.configuration }}-${{ matrix.arch }} + secrets: inherit + + pack: + name: call-pack + needs: [build] + strategy: + matrix: + configuration: [Debug, Release] + uses: codebeltnet/jobs-dotnet-pack/.github/workflows/default.yml@v3 + with: + configuration: ${{ matrix.configuration }} + version: ${{ needs.build.outputs.version }} + download-build-artifact-pattern: build-${{ matrix.configuration }}-X64 + + test_linux: + name: call-test-linux + needs: [build] + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] + arch: [X64, ARM64] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + runs-on: ${{ matrix.arch == 'ARM64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + build: true # we need to build due to xUnitv3 + restore: true # we need to restore due to xUnitv3 + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + test_windows: + name: call-test-windows + needs: [build] + strategy: + fail-fast: false + matrix: + configuration: [Debug, Release] + arch: [X64, ARM64] + uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 + with: + runs-on: ${{ matrix.arch == 'ARM64' && 'windows-11-arm' || 'windows-2025' }} + configuration: ${{ matrix.configuration }} + build-switches: -p:SkipSignAssembly=true + build: true # we need to build due to xUnitv3 + restore: true # we need to restore due to xUnitv3 + download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }} + + sonarcloud: + name: call-sonarcloud + needs: [build, test_linux, test_windows] + uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 + with: + organization: geekle + projectKey: carter + version: ${{ needs.build.outputs.version }} + secrets: inherit + + codecov: + name: call-codecov + needs: [build, test_linux, test_windows] + uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1 + with: + repository: codebeltnet/carter + secrets: inherit + + codeql: + name: call-codeql + needs: [build, test_linux, test_windows] + uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3 + permissions: + security-events: write + + deploy: + if: github.event_name != 'pull_request' + name: call-nuget + needs: [build, pack, test_linux, test_windows, sonarcloud, codecov, codeql] + uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v2 + with: + version: ${{ needs.build.outputs.version }} + environment: Production + configuration: ${{ inputs.configuration == '' && 'Release' || inputs.configuration }} + permissions: + contents: write + packages: write + secrets: inherit diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..aabea97 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,42 @@ +name: Scorecard supply-chain security +on: + branch_protection_rule: + schedule: + - cron: '45 17 * * 2' + push: + branches: [ "main" ] + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@v2.4.0 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: "Upload artifact" + uses: actions/upload-artifact@v4 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/service-update.yml b/.github/workflows/service-update.yml new file mode 100644 index 0000000..ea92ea5 --- /dev/null +++ b/.github/workflows/service-update.yml @@ -0,0 +1,139 @@ +name: Service Update + +on: + repository_dispatch: + types: [codebelt-service-update] + workflow_dispatch: + inputs: + source_repo: + description: 'Triggering source repo name (e.g. cuemon)' + required: false + default: '' + source_version: + description: 'Version released by source (e.g. 10.3.0)' + required: false + default: '' + dry_run: + type: boolean + description: 'Dry run — show changes but do not commit or open PR' + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + service-update: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve trigger inputs + id: trigger + run: | + SOURCE="${{ github.event.client_payload.source_repo || github.event.inputs.source_repo }}" + VERSION="${{ github.event.client_payload.source_version || github.event.inputs.source_version }}" + echo "source=$SOURCE" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Determine new version for this repo + id: newver + run: | + CURRENT=$(grep -oP '(?<=## \[)[\d.]+(?=\])' CHANGELOG.md | head -1) + NEW=$(echo "$CURRENT" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') + BRANCH="v${NEW}/service-update" + echo "current=$CURRENT" >> $GITHUB_OUTPUT + echo "new=$NEW" >> $GITHUB_OUTPUT + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Generate codebelt-aicia token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.CODEBELT_AICIA_APP_ID }} + private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }} + owner: codebeltnet + + - name: Bump NuGet packages + run: python3 .github/scripts/bump-nuget.py + env: + TRIGGER_SOURCE: ${{ steps.trigger.outputs.source }} + TRIGGER_VERSION: ${{ steps.trigger.outputs.version }} + + - name: Update PackageReleaseNotes.txt + run: | + NEW="${{ steps.newver.outputs.new }}" + for f in .nuget/*/PackageReleaseNotes.txt; do + [ -f "$f" ] || continue + TFM=$(grep -m1 "^Availability:" "$f" | sed 's/Availability: //' || echo ".NET 10, .NET 9 and .NET Standard 2.0") + ENTRY="Version: ${NEW}\nAvailability: ${TFM}\n \n# ALM\n- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)\n \n" + { printf "$ENTRY"; cat "$f"; } > "$f.tmp" && mv "$f.tmp" "$f" + done + + - name: Update CHANGELOG.md + run: | + python3 - <<'EOF' + import os, re + from datetime import date + new_ver = os.environ['NEW_VERSION'] + today = date.today().isoformat() + entry = f"## [{new_ver}] - {today}\n\nThis is a service update that focuses on package dependencies.\n\n" + with open("CHANGELOG.md") as f: + content = f.read() + idx = content.find("## [") + content = (content[:idx] + entry + content[idx:]) if idx != -1 else (content + entry) + with open("CHANGELOG.md", "w") as f: + f.write(content) + print(f"CHANGELOG updated for v{new_ver}") + EOF + env: + NEW_VERSION: ${{ steps.newver.outputs.new }} + + # Note: Docker image bumps removed in favor of manual updates + # The automated selection was picking wrong variants (e.g., mono-* instead of standard) + # TODO: Move to hosted service for smarter image selection + + - name: Show diff (dry run) + if: ${{ github.event.inputs.dry_run == 'true' }} + run: git diff + + - name: Create branch and open PR + if: ${{ github.event.inputs.dry_run != 'true' }} + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + NEW="${{ steps.newver.outputs.new }}" + BRANCH="${{ steps.newver.outputs.branch }}" + SOURCE="${{ steps.trigger.outputs.source }}" + SRC_VER="${{ steps.trigger.outputs.version }}" + + git config user.name "codebelt-aicia[bot]" + git config user.email "codebelt-aicia[bot]@users.noreply.github.com" + git checkout -b "$BRANCH" + git add -A + git diff --cached --quiet && echo "Nothing changed - skipping PR." && exit 0 + git commit -m "V${NEW}/service update" + git push origin "$BRANCH" + + echo "This is a service update that focuses on package dependencies." > pr_body.txt + echo "" >> pr_body.txt + echo "Automated changes:" >> pr_body.txt + echo "- Codebelt/Cuemon package versions bumped to latest compatible" >> pr_body.txt + echo "- PackageReleaseNotes.txt updated for v${NEW}" >> pr_body.txt + echo "- CHANGELOG.md entry added for v${NEW}" >> pr_body.txt + echo "" >> pr_body.txt + echo "Note: Third-party packages (Microsoft.Extensions.*, BenchmarkDotNet, etc.) are not auto-updated." >> pr_body.txt + echo "Use Dependabot or manual updates for those." >> pr_body.txt + echo "" >> pr_body.txt + echo "Generated by codebelt-aicia" >> pr_body.txt + if [ -n "$SOURCE" ] && [ -n "$SRC_VER" ]; then + echo "Triggered by: ${SOURCE} @ ${SRC_VER}" >> pr_body.txt + else + echo "Triggered by: manual workflow dispatch" >> pr_body.txt + fi + + gh pr create --title "V${NEW}/service update" --body-file pr_body.txt --base main --head "$BRANCH" --assignee gimlichael diff --git a/.github/workflows/trigger-downstream.yml b/.github/workflows/trigger-downstream.yml new file mode 100644 index 0000000..29eb29c --- /dev/null +++ b/.github/workflows/trigger-downstream.yml @@ -0,0 +1,78 @@ +name: Trigger Downstream Service Updates + +on: + release: + types: [published] + +jobs: + dispatch: + if: github.event.release.prerelease == false + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout (to read dispatch-targets.json) + uses: actions/checkout@v4 + + - name: Check for dispatch targets + id: check + run: | + if [ ! -f .github/dispatch-targets.json ]; then + echo "No dispatch-targets.json found, skipping." + echo "has_targets=false" >> $GITHUB_OUTPUT + exit 0 + fi + COUNT=$(python3 -c "import json; print(len(json.load(open('.github/dispatch-targets.json'))))") + echo "has_targets=$([ $COUNT -gt 0 ] && echo true || echo false)" >> $GITHUB_OUTPUT + + - name: Extract version from release tag + if: steps.check.outputs.has_targets == 'true' + id: version + run: | + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Generate codebelt-aicia token + if: steps.check.outputs.has_targets == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ vars.CODEBELT_AICIA_APP_ID }} + private-key: ${{ secrets.CODEBELT_AICIA_PRIVATE_KEY }} + owner: codebeltnet + + - name: Dispatch to downstream repos + if: steps.check.outputs.has_targets == 'true' + run: | + python3 - <<'EOF' + import json, urllib.request, os, sys + + targets = json.load(open('.github/dispatch-targets.json')) + token = os.environ['GH_TOKEN'] + version = os.environ['VERSION'] + source = os.environ['SOURCE_REPO'] + + for repo in targets: + url = f'https://api.github.com/repos/codebeltnet/{repo}/dispatches' + payload = json.dumps({ + 'event_type': 'codebelt-service-update', + 'client_payload': { + 'source_repo': source, + 'source_version': version + } + }).encode() + req = urllib.request.Request(url, data=payload, method='POST', headers={ + 'Authorization': f'Bearer {token}', + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28' + }) + with urllib.request.urlopen(req) as r: + print(f'✓ Dispatched to {repo}: HTTP {r.status}') + EOF + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.version.outputs.version }} + SOURCE_REPO: ${{ github.event.repository.name }} From 78b3093baec559f5539e24e83d6cac81c8d090ff Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:43:39 +0100 Subject: [PATCH 15/30] =?UTF-8?q?=E2=9C=A8=20add=20bump-nuget=20script=20f?= =?UTF-8?q?or=20simplified=20package=20version=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/scripts/bump-nuget.py | 134 ++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 .github/scripts/bump-nuget.py diff --git a/.github/scripts/bump-nuget.py b/.github/scripts/bump-nuget.py new file mode 100644 index 0000000..288aaf5 --- /dev/null +++ b/.github/scripts/bump-nuget.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +""" +Simplified package bumping for Codebelt service updates (Option B). + +Only updates packages published by the triggering source repo. +Does NOT update Microsoft.Extensions.*, BenchmarkDotNet, or other third-party packages. +Does NOT parse TFM conditions - only bumps Codebelt/Cuemon/Savvyio packages to the triggering version. + +Usage: + TRIGGER_SOURCE=cuemon TRIGGER_VERSION=10.3.0 python3 bump-nuget.py + +Behavior: +- If TRIGGER_SOURCE is "cuemon" and TRIGGER_VERSION is "10.3.0": + - Cuemon.Core: 10.2.1 → 10.3.0 + - Cuemon.Extensions.IO: 10.2.1 → 10.3.0 + - Microsoft.Extensions.Hosting: 9.0.13 → UNCHANGED (not a Codebelt package) + - BenchmarkDotNet: 0.15.8 → UNCHANGED (not a Codebelt package) +""" + +import re +import os +import sys +from typing import Dict, List + +TRIGGER_SOURCE = os.environ.get("TRIGGER_SOURCE", "") +TRIGGER_VERSION = os.environ.get("TRIGGER_VERSION", "") + +# Map of source repos to their package ID prefixes +SOURCE_PACKAGE_MAP: Dict[str, List[str]] = { + "cuemon": ["Cuemon."], + "xunit": ["Codebelt.Extensions.Xunit"], + "benchmarkdotnet": ["Codebelt.Extensions.BenchmarkDotNet"], + "bootstrapper": ["Codebelt.Bootstrapper"], + "newtonsoft-json": [ + "Codebelt.Extensions.Newtonsoft.Json", + "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft", + ], + "aws-signature-v4": ["Codebelt.Extensions.AspNetCore.Authentication.AwsSignature"], + "unitify": ["Codebelt.Unitify"], + "yamldotnet": [ + "Codebelt.Extensions.YamlDotNet", + "Codebelt.Extensions.AspNetCore.Mvc.Formatters.Text.Yaml", + ], + "globalization": ["Codebelt.Extensions.Globalization"], + "asp-versioning": ["Codebelt.Extensions.Asp.Versioning"], + "swashbuckle-aspnetcore": ["Codebelt.Extensions.Swashbuckle"], + "savvyio": ["Savvyio."], + "shared-kernel": [], + "carter": ["Codebelt.Extensions.Carter"], +} + + +def is_triggered_package(package_name: str) -> bool: + """Check if package is published by the triggering source repo.""" + if not TRIGGER_SOURCE: + return False + prefixes = SOURCE_PACKAGE_MAP.get(TRIGGER_SOURCE, []) + return any(package_name.startswith(prefix) for prefix in prefixes) + + +def main(): + if not TRIGGER_SOURCE or not TRIGGER_VERSION: + print( + "Error: TRIGGER_SOURCE and TRIGGER_VERSION environment variables required" + ) + print(f" TRIGGER_SOURCE={TRIGGER_SOURCE}") + print(f" TRIGGER_VERSION={TRIGGER_VERSION}") + sys.exit(1) + + # Strip 'v' prefix if present in version + target_version = TRIGGER_VERSION.lstrip("v") + + print(f"Trigger: {TRIGGER_SOURCE} @ {target_version}") + print(f"Only updating packages from: {TRIGGER_SOURCE}") + print() + + try: + with open("Directory.Packages.props", "r") as f: + content = f.read() + except FileNotFoundError: + print("Error: Directory.Packages.props not found") + sys.exit(1) + + changes = [] + skipped_third_party = [] + + def replace_version(m: re.Match) -> str: + pkg = m.group(1) + current = m.group(2) + + if not is_triggered_package(pkg): + skipped_third_party.append(f" {pkg} (skipped - not from {TRIGGER_SOURCE})") + return m.group(0) + + if target_version != current: + changes.append(f" {pkg}: {current} → {target_version}") + return m.group(0).replace( + f'Version="{current}"', f'Version="{target_version}"' + ) + + return m.group(0) + + # Match PackageVersion elements (handles multiline) + pattern = re.compile( + r"]*\bInclude="([^"]+)")' + r'(?=[^>]*\bVersion="([^"]+)")' + r"[^>]*>", + re.DOTALL, + ) + new_content = pattern.sub(replace_version, content) + + # Show results + if changes: + print(f"Updated {len(changes)} package(s) from {TRIGGER_SOURCE}:") + print("\n".join(changes)) + else: + print(f"No packages from {TRIGGER_SOURCE} needed updating.") + + if skipped_third_party: + print() + print(f"Skipped {len(skipped_third_party)} third-party package(s):") + print("\n".join(skipped_third_party[:5])) # Show first 5 + if len(skipped_third_party) > 5: + print(f" ... and {len(skipped_third_party) - 5} more") + + with open("Directory.Packages.props", "w") as f: + f.write(new_content) + + return 0 if changes else 0 # Return 0 even if no changes (not an error) + + +if __name__ == "__main__": + sys.exit(main()) From bff5ccaca775735cd3204fa872cab67e9908f984 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:43:57 +0100 Subject: [PATCH 16/30] =?UTF-8?q?=E2=9C=A8=20add=20code=20of=20conduct,=20?= =?UTF-8?q?contributing=20guidelines,=20and=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODE_OF_CONDUCT.md | 135 +++++++ .github/CONTRIBUTING.md | 52 +++ .github/codecov.yml | 2 + .github/copilot-instructions.md | 610 ++++++++++++++++++++++++++++++++ .github/dependabot.yml | 17 + .github/dispatch-targets.json | 1 + 6 files changed, 817 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/codecov.yml create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml create mode 100644 .github/dispatch-targets.json diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..e5be83d --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,135 @@ +# Contributor Covenant Code of Conduct + +This document is adapted from the Contributor Covenant which is used by many open source projects, +including those under the [.NET Foundation](https://dotnetfoundation.org/code-of-conduct). + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e962e41 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to `Carter Extensions by Codebelt` +When contributing to this repository, please first discuss the change you wish to make via issue, +email, or any other method with the owners of this repository before making a change. + +Please note we have a code of conduct, please follow it in all your interactions with the project. + +## Code of Conduct +Please review our [code of conduct](CODE_OF_CONDUCT.md). + +## Our Development Process +We use `trunk` based branching model that is aligned with todays DevSecOps practices. +All new features and/or fixes are merged into the `main` branch by creating a Pull Request. + +## Pull Requests +We actively welcome your pull requests. + +1. Fork the repo and create your branch from `main` +2. If you've added code that should be tested, add tests (DO follow [Microsoft Engineering Guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines)) +3. Any changes or additions requires documentation in the form of documenting public members +4. Ensure that all existing as well as new test passes +5. Issue that pull request with a big and heartful thanks for contributing + +## Issues +We use GitHub issues to track public bugs. Please ensure your description is +clear and has sufficient instructions to be able to reproduce the issue. + +## Coding Guidelines +* Please follow [Framework Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/) +* Please follow SOLID principles +* Please follow [Microsoft Engineering Guidelines](https://github.com/dotnet/aspnetcore/wiki/Engineering-guidelines) + +## Manifesto +As aspiring Software Craftsmen we are raising the bar of professional software development by practicing it and helping others learn the craft. + +Through this work we have come to value: + +* Not only working software, +but also well-crafted software +* Not only responding to change, +but also steadily adding value +* Not only individuals and interactions, +but also a community of professionals +* Not only customer collaboration, +but also productive partnerships + +That is, in pursuit of the items on the left we have found the items on the right to be indispensable. + +[Manifesto for Software Craftsmanship](https://manifesto.softwarecraftsmanship.org/) is the originator of this text. + +## License +By contributing to `Carter Extensions by Codebelt`, you agree that your contributions will be licensed +under the MIT license. diff --git a/.github/codecov.yml b/.github/codecov.yml new file mode 100644 index 0000000..2515756 --- /dev/null +++ b/.github/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "test" \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..8dcb15d --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,610 @@ +--- +description: 'Writing Unit Tests in Codebelt.Extensions.Carter' +applyTo: "**/*.{cs,csproj}" +--- + +# Writing Unit Tests in Codebelt.Extensions.Carter +This document provides instructions for writing unit tests in the Codebelt.Extensions.Carter codebase. Please follow these guidelines to ensure consistency and maintainability. + +## 1. Base Class + +**Always inherit from the `Test` base class** for all unit test classes. +This ensures consistent setup, teardown, and output handling across all tests. + +> Important: Do NOT add `using Xunit.Abstractions`. xUnit v3 no longer exposes that namespace; including it is incorrect and will cause compilation errors. Use the `Codebelt.Extensions.Xunit` Test base class and `using Xunit;` as shown in the examples below. If you need access to test output, rely on the Test base class (which accepts the appropriate output helper) rather than importing `Xunit.Abstractions`. + +```csharp +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Your.Namespace +{ + public class YourTestClass : Test + { + public YourTestClass(ITestOutputHelper output) : base(output) + { + } + + // Your tests here + } +} +``` + +## 2. Test Method Attributes + +- Use `[Fact]` for standard unit tests. +- Use `[Theory]` with `[InlineData]` or other data sources for parameterized tests. + +## 3. Naming Conventions + +- **Test classes**: End with `Test` (e.g., `DateSpanTest`). +- **Test methods**: Use descriptive names that state the expected behavior (e.g., `ShouldReturnTrue_WhenConditionIsMet`). + +## 4. Assertions + +- Use `Assert` methods from xUnit for all assertions. +- Prefer explicit and expressive assertions (e.g., `Assert.Equal`, `Assert.NotNull`, `Assert.Contains`). + +## 5. File and Namespace Organization + +- Place test files in the appropriate test project and folder structure. +- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: Codebelt.Extensions.Carter.Foo.Tests assembly, but namespace Codebelt.Extensions.Carter.Foo). + - Example: If the SUT class is declared as: + ```csharp + namespace Codebelt.Extensions.Carter.Foo.Bar + { + public class Zoo { /* ... */ } + } + ``` + then the corresponding unit test class must use the exact same namespace: + ```csharp + namespace Codebelt.Extensions.Carter.Foo.Bar + { + public class ZooTest : Test { /* ... */ } + } + ``` + - Do NOT use: + ```csharp + namespace Codebelt.Extensions.Carter.Foo.Bar.Tests { /* ... */ } // ❌ + namespace Codebelt.Extensions.Carter.Foo.Bar.Benchmarks { /* ... */ } // ❌ + ``` +- The unit tests for the Codebelt.Extensions.Carter.Foo assembly live in the Codebelt.Extensions.Carter.Foo.Tests assembly. +- The functional tests for the Codebelt.Extensions.Carter.Foo assembly live in the Codebelt.Extensions.Carter.Foo.FunctionalTests assembly. +- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the Codebelt.Extensions.Carter.Foo assembly would be named BooTest and placed in the Codebelt.Extensions.Carter.Foo namespace in the Codebelt.Extensions.Carter.Foo.Tests assembly. +- Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: + ```xml + + Codebelt.Extensions.Carter.Foo + + ``` +- When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. + +- Notes: + - This rule ensures type discovery and XML doc links behave consistently and reduces confusion when reading tests. + - Keep folder structure aligned with the production code layout to make locating SUT <-> test pairs straightforward. + +## 6. Example Test + +```csharp +using System; +using System.Globalization; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Codebelt.Extensions.Carter +{ + /// + /// Tests for the class. + /// + public class DateSpanTest : Test + { + public DateSpanTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Parse_ShouldGetOneMonthOfDifference_UsingIso8601String() + { + var start = new DateTime(2021, 3, 5).ToString("O"); + var end = new DateTime(2021, 4, 5).ToString("O"); + + var span = DateSpan.Parse(start, end); + + Assert.Equal("0:01:31:00:00:00.0", span.ToString()); + Assert.Equal(0, span.Years); + Assert.Equal(1, span.Months); + Assert.Equal(31, span.Days); + Assert.Equal(0, span.Hours); + Assert.Equal(0, span.Minutes); + Assert.Equal(0, span.Seconds); + Assert.Equal(0, span.Milliseconds); + + Assert.Equal(0.08493150684931507, span.TotalYears); + Assert.Equal(1, span.TotalMonths); + Assert.Equal(31, span.TotalDays); + Assert.Equal(744, span.TotalHours); + Assert.Equal(44640, span.TotalMinutes); + Assert.Equal(2678400, span.TotalSeconds); + Assert.Equal(2678400000, span.TotalMilliseconds); + + Assert.Equal(6, span.GetWeeks()); + Assert.Equal(-1566296493, span.GetHashCode()); + + TestOutput.WriteLine(span.ToString()); + } + } +} +``` + +## 7. Additional Guidelines + +- Keep tests focused and isolated. +- Do not rely on external systems except for xUnit itself and Codebelt.Extensions.Xunit (and derived from this). +- Ensure tests are deterministic and repeatable. + +## 8. Test Doubles + +- Preferred test doubles include dummies, fakes, stubs and spies if and when the design allows it. +- Under special circumstances, mock can be used (using Moq library). +- Before overriding methods, verify that the method is virtual or abstract; this rule also applies to mocks. +- Never mock IMarshaller; always use a new instance of JsonMarshaller. + +## 9. Avoid `InternalsVisibleTo` in Tests + +- **Do not** use `InternalsVisibleTo` to access internal types or members from test projects. +- Prefer **indirect testing via public APIs** that depend on the internal implementation (public facades, public extension methods, or other public entry points). + +### Preferred Pattern + +**Pattern name:** Public Facade Testing (also referred to as *Public API Proxy Testing*) + +**Description:** +Internal classes and methods must be validated by exercising the public API that consumes them. Tests should assert observable behavior exposed by the public surface rather than targeting internal implementation details directly. + +### Example Mapping + +- **Internal helper:** `DelimitedString` (internal static class) +- **Public API:** `TestOutputHelperExtensions.WriteLines()` (public extension method) +- **Test strategy:** Write tests for `WriteLines()` and verify its public behavior. The internal call to `DelimitedString.Create()` is exercised implicitly. + +### Benefits + +- Avoids exposing internal types to test assemblies. +- Ensures tests reflect real-world usage patterns. +- Maintains strong encapsulation and a clean public API. +- Tests remain resilient to internal refactoring as long as public behavior is preserved. + +### When to Apply + +- Internal logic is fully exercised through existing public APIs. +- Public entry points provide sufficient coverage of internal code paths. +- The internal implementation exists solely as a helper or utility for public-facing functionality. + +--- +description: 'Writing Performance Tests in Codebelt.Extensions.Carter' +applyTo: "tuning/**, **/*Benchmark*.cs" +--- + +# Writing Performance Tests in Codebelt.Extensions.Carter +This document provides guidance for writing performance tests (benchmarks) in the Codebelt.Extensions.Carter codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. + +## 1. Naming and Placement + +- Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. +- Place benchmark files in the appropriate benchmark project and folder structure. +- Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. +- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace Codebelt.Extensions.Carter.Security.Cryptography`, the benchmark file should also use: + ``` + namespace Codebelt.Extensions.Carter.Security.Cryptography + { + public class Sha512256Benchmark { /* ... */ } + } + ``` +The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). +- The benchmarks for the Codebelt.Extensions.Carter.Bar assembly live in the Codebelt.Extensions.Carter.Bar.Benchmarks assembly. +- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the Codebelt.Extensions.Carter.Bar assembly would be named ZooBenchmark and placed in the Codebelt.Extensions.Carter.Bar namespace in the Codebelt.Extensions.Carter.Bar.Benchmarks assembly. +- Modify the associated .csproj file to override the root namespace, e.g., Codebelt.Extensions.Carter.Bar. + +## 2. Attributes and Configuration + +- Use `BenchmarkDotNet` attributes to express intent and collect relevant metrics: + - `[MemoryDiagnoser]` to capture memory allocations. + - `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` to group related benchmarks. + - `[Params]` for input sizes or variations to exercise multiple scenarios. + - `[GlobalSetup]` for one-time initialization that's not part of measured work. + - `[Benchmark]` on methods representing measured operations; consider `Baseline = true` and `Description` to improve report clarity. +- Keep benchmark configuration minimal and explicit; prefer in-class attributes over large shared configs unless re-used widely. + +## 3. Structure and Best Practices + +- Keep benchmarks focused: each `Benchmark` method should measure a single logical operation. +- Avoid doing expensive setup work inside a measured method; use `[GlobalSetup]`, `[IterationSetup]`, or cached fields instead. +- Use `Params` to cover micro, mid and macro input sizes (for example: small, medium, large) and verify performance trends across them. +- Use small, deterministic data sets and avoid external systems (network, disk, DB). If external systems are necessary, mark them clearly and do not include them in CI benchmark runs by default. +- Capture results that are meaningful: time, allocations, and if needed custom counters. Prefer `MemoryDiagnoser` and descriptive `Description` values. + +## 4. Naming Conventions for Methods + +- Method names should be descriptive and indicate the scenario, e.g., `Parse_Short`, `ComputeHash_Large`. +- When comparing implementations, mark one method with `Baseline = true` and use similar names so reports are easy to read. + +## 5. Example Benchmark + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Codebelt.Extensions.Carter +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +## 6. Reporting and CI + +- Benchmarks are primarily for local and tuning runs; be cautious about running heavy BenchmarkDotNet workloads in CI. Prefer targeted runs or harnesses for CI where appropriate. +- Keep benchmark projects isolated (e.g., `tuning/*.csproj`) so they don't affect package builds or production artifacts. + +## 7. Additional Guidelines + +- Keep benchmarks readable and well-documented; add comments explaining non-obvious choices. +- If a benchmark exposes regressions or optimizations, add a short note in the benchmark file referencing the relevant issue or PR. +- For any shared helpers for benchmarking, prefer small utility classes inside the `tuning` projects rather than cross-cutting changes to production code. + +For further examples, refer to the benchmark files under the `tuning/` folder. + +--- +description: 'Writing XML documentation in Codebelt.Extensions.Carter' +applyTo: "**/*.cs" +--- + +# Writing XML documentation in Codebelt.Extensions.Carter +This document provides instructions for writing XML documentation. + +## 1. Documentation Style + +- Use the same documentation style as found throughout the codebase. +- Add XML doc comments to public and protected classes and methods where appropriate. +- Example: + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using Cuemon.Collections.Generic; +using Cuemon.Configuration; +using Cuemon.IO; +using Cuemon.Text; + +namespace Cuemon.Security +{ + /// + /// Represents the base class from which all implementations of hash algorithms and checksums should derive. + /// + /// The type of the configured options. + /// + /// + /// + public abstract class Hash : Hash, IConfigurable where TOptions : ConvertibleOptions, new() + { + /// + /// Initializes a new instance of the class. + /// + /// The which may be configured. + protected Hash(Action setup) + { + Options = Patterns.Configure(setup); + } + + /// + /// Gets the configured options of this instance. + /// + /// The configured options of this instance. + public TOptions Options { get; } + + + /// + /// The endian-initializer of this instance. + /// + /// An instance of the configured options. + protected sealed override void EndianInitializer(EndianOptions options) + { + options.ByteOrder = Options.ByteOrder; + } + } + + /// + /// Represents the base class that defines the public facing structure to expose. + /// + /// + public abstract class Hash : IHash + { + /// + /// Initializes a new instance of the class. + /// + protected Hash() + { + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(bool input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(byte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(char input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DateTime input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DBNull input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(decimal input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(double input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(short input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(int input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(long input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(sbyte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(float input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ushort input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(uint input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ulong input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// The which may be configured. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(string input, Action setup = null) + { + return ComputeHash(Convertible.GetBytes(input, setup)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Enum input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(params IConvertible[] input) + { + return ComputeHash(Arguments.ToEnumerableOf(input)); + } + + /// + /// Computes the hash value for the specified sequence of . + /// + /// The sequence of to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(IEnumerable input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public abstract HashResult ComputeHash(byte[] input); + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Stream input) + { + return ComputeHash(Patterns.SafeInvoke(() => new MemoryStream(), destination => + { + Decorator.Enclose(input).CopyStream(destination); + return destination; + }).ToArray()); + } + + /// + /// Defines the initializer that must implement. + /// + /// An instance of the configured options. + protected abstract void EndianInitializer(EndianOptions options); + } +} + +namespace Cuemon.Security +{ + /// + /// Configuration options for . + /// + public class FowlerNollVoOptions : ConvertibleOptions + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The following table shows the initial property values for an instance of . + /// + /// + /// Property + /// Initial Value + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FowlerNollVoOptions() + { + Algorithm = FowlerNollVoAlgorithm.Fnv1a; + ByteOrder = Endianness.BigEndian; + } + + /// + /// Gets or sets the algorithm of the Fowler-Noll-Vo hash function. + /// + /// The algorithm of the Fowler-Noll-Vo hash function. + public FowlerNollVoAlgorithm Algorithm { get; set; } + } +} +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1fca42a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "nuget" + directory: "/src" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 + - package-ecosystem: "nuget" + directory: "/test" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 + - package-ecosystem: "github-actions" + directory: "/.github/workflows" + schedule: + interval: "weekly" + open-pull-requests-limit: 0 diff --git a/.github/dispatch-targets.json b/.github/dispatch-targets.json new file mode 100644 index 0000000..1e3ec72 --- /dev/null +++ b/.github/dispatch-targets.json @@ -0,0 +1 @@ +[ ] From 01dde5f659c8fed79b6628c9650e9e54a753304a Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:45:44 +0100 Subject: [PATCH 17/30] =?UTF-8?q?=E2=9C=A8=20add=20NuGet=20package=20defin?= =?UTF-8?q?ition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PackageReleaseNotes.txt | 6 ++ .../README.md | 79 ++++++++++++++++++ .../icon.png | Bin 0 -> 7384 bytes .../PackageReleaseNotes.txt | 6 ++ .../README.md | 78 +++++++++++++++++ .../icon.png | Bin 0 -> 7384 bytes .../PackageReleaseNotes.txt | 6 ++ .../README.md | 73 ++++++++++++++++ .../icon.png | Bin 0 -> 7384 bytes .../PackageReleaseNotes.txt | 6 ++ .../README.md | 77 +++++++++++++++++ .../icon.png | Bin 0 -> 7384 bytes .../PackageReleaseNotes.txt | 7 ++ .nuget/Codebelt.Extensions.Carter/README.md | 46 ++++++++++ .nuget/Codebelt.Extensions.Carter/icon.png | Bin 0 -> 6946 bytes 15 files changed, 384 insertions(+) create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/README.md create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/icon.png create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Json/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Json/README.md create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Json/icon.png create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/README.md create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/icon.png create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Xml/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Xml/README.md create mode 100644 .nuget/Codebelt.Extensions.Carter.AspNetCore.Xml/icon.png create mode 100644 .nuget/Codebelt.Extensions.Carter/PackageReleaseNotes.txt create mode 100644 .nuget/Codebelt.Extensions.Carter/README.md create mode 100644 .nuget/Codebelt.Extensions.Carter/icon.png diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt new file mode 100644 index 0000000..04565d3 --- /dev/null +++ b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt @@ -0,0 +1,6 @@ +Version: 1.0.0 +Availability: .NET 10 +  +# New Features +- ADDED NewtonsoftJsonNegotiator class in the Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json namespace that provides a JSON response negotiator for Carter, capable of serializing response models to JSON format using Newtonsoft.Json +  \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/README.md b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/README.md new file mode 100644 index 0000000..6260aa8 --- /dev/null +++ b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/README.md @@ -0,0 +1,79 @@ +# Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json + +A Newtonsoft.Json-powered response negotiator for Carter in ASP.NET Core minimal APIs. + +## About + +**Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json** extends the **Codebelt.Extensions.Carter** package with a dedicated JSON response negotiator for Carter, capable of serializing response models to JSON format using Newtonsoft.Json. + +This package is useful when you need Newtonsoft.Json-specific behavior (for example custom converters, contract resolvers, or legacy JSON compatibility) while keeping Carter modules and content negotiation straightforward. + +## CSharp Example + +Functional-test style sample (same bootstrapping pattern used by this repository): + +```csharp +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Carter; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; +using Codebelt.Extensions.Carter.Assets; +using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +using var response = await MinimalWebHostTestFactory.RunAsync( + services => + { + services.AddNewtonsoftJsonFormatterOptions(o => + { + o.Settings.Converters.Insert(0, new StringEnumConverter(new DefaultNamingStrategy(), false)); + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/world/statistical-regions"); + }); +``` + +Program-style usage for production apps (remember to inherit from ICarterModule for your endpoints and add other services as needed): + +```csharp +using Carter; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddNewtonsoftJsonFormatterOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +## Related Packages + +* [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) 📦 diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/icon.png b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6aef8389482e533ca4d876cf836971055203bd75 GIT binary patch literal 7384 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 + { + services.AddMinimalJsonOptions(o => + { + o.Settings.Converters.Insert(0, new JsonStringEnumConverter()); + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + return await client.GetAsync("/world/statistical-regions"); + }); +``` + +Program-style usage for production apps (remember to inherit from ICarterModule for your endpoints and add other services as needed): + +```csharp +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json; +using Cuemon.Extensions.AspNetCore.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalJsonOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +## Related Packages + +* [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) 📦 diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Json/icon.png b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Json/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6aef8389482e533ca4d876cf836971055203bd75 GIT binary patch literal 7384 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 + { + services.AddMinimalYamlOptions(); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/yaml")); + return await client.GetAsync("/world/statistical-regions"); + }); +``` + +Program-style usage for production apps (remember to inherit from ICarterModule for your endpoints and add other services as needed): + +```csharp +using Carter; +using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalYamlOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +## Related Packages + +* [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) 📦 diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/icon.png b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6aef8389482e533ca4d876cf836971055203bd75 GIT binary patch literal 7384 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 + { + services.AddMinimalXmlOptions(o => + { + o.Settings.Writer.Indent = true; + }); + services.AddCarter(configurator: c => c + .WithModule() + .WithResponseNegotiator()); + services.AddRouting(); + }, + app => + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapCarter()); + }, + _ => { }, + async client => + { + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/xml")); + return await client.GetAsync("/world/statistical-regions"); + }); +``` + +Program-style usage for production apps (remember to inherit from ICarterModule for your endpoints and add other services as needed): + +```csharp +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Xml; +using Cuemon.Extensions.AspNetCore.Xml; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalXmlOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +## Related Packages + +* [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) 📦 diff --git a/.nuget/Codebelt.Extensions.Carter.AspNetCore.Xml/icon.png b/.nuget/Codebelt.Extensions.Carter.AspNetCore.Xml/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6aef8389482e533ca4d876cf836971055203bd75 GIT binary patch literal 7384 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 class in the Codebelt.Extensions.Carter.Response namespace that provides an abstract, configurable base class for Carter response negotiators that serialize models using a StreamFormatter implementation +- ADDED EndpointConventionBuilderExtensions class in the Codebelt.Extensions.Carter namespace that consist of extension methods for the IEndpointConventionBuilder interface: Produces and Produces +  \ No newline at end of file diff --git a/.nuget/Codebelt.Extensions.Carter/README.md b/.nuget/Codebelt.Extensions.Carter/README.md new file mode 100644 index 0000000..e0f4e0e --- /dev/null +++ b/.nuget/Codebelt.Extensions.Carter/README.md @@ -0,0 +1,46 @@ +# Codebelt.Extensions.Carter + +A focused extension layer for Carter with configurable response negotiation primitives and endpoint metadata helpers for ASP.NET Core minimal APIs. + +## About + +**Codebelt.Extensions.Carter** complements Carter by adding reusable negotiation infrastructure and endpoint convention extensions. + +Use this package when you want to: + +- Add explicit response metadata from Carter route mappings. +- Build on configurable negotiator abstractions. +- Keep minimal API modules expressive and OpenAPI-friendly. + +For concrete JSON, YAML, or XML negotiators, use one of the companion packages listed below. + +## CSharp Example + +```csharp +using Carter; +using Codebelt.Extensions.Carter; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddCarter(); + +var app = builder.Build(); + +app.MapGet("/status", () => new StatusResponse("ok")) + .Produces(StatusCodes.Status200OK, "application/json") + .ProducesProblem(StatusCodes.Status503ServiceUnavailable); + +app.MapCarter(); +app.Run(); + +public sealed record StatusResponse(string State); +``` + +## Related Packages + +* [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) 📦 +* [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) 📦 diff --git a/.nuget/Codebelt.Extensions.Carter/icon.png b/.nuget/Codebelt.Extensions.Carter/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..73b6edbb9321b95f56cb9e2f04ad3328304700a8 GIT binary patch literal 6946 zcmV+-8{OoIP)F*bPrzs=$R02-1>L_t(|ob8=^d{ou7$A4>|nMpz(fZ`ji z7%1`x0g_U!?bQU45M#na$F^1s_@Je}ZIxCa2GCotRqMxU)rtwFddnyXA;ds|)VEi| zBY+4Jz-p;tK_Cfv&78gNA0e2`oH=Ko$w>%gzMs$eW6s%U?|o*i{a$NtVrE3?xrh^; zfhZ@Ir~>qGq6*N%i7G%3C#nEFoTviya9mDCwq@u&qe+MnklY_cX94UneL-?@$|q00 z?_?fMHmZi*H%fqiZW@Tm4}tx}*hfTLhkg8bB^(;RcUS>-47vSuom{_W!cPR@0h~*g3i`9efjQM!j zFN%ruCkcT*ga9JB0Dq+UK80wG=)Y^Z^h9vrzqb$lYGw7HJCh~z<(&9XX~EE((S{&=6x>1^lh?pSmN)AWtQFdjEJs6v_Q@jgdb^0dIrF) zc3!eo1MC_$_4~y7UAEh-uzdh&>kCgsw3e#enTrFrRHx)$WhZ@G3P9k;2|78IVMLn% zMAQj1xmed9v(qV5ZUXkC{3IQq%PdBC3}7iSErljOM?f^01@^=BnQHo{DsGpyUT3wg4jRKtm+X6JQx*z=QCit@9zZtK8wDrn2T)@bk8&r_ zp~;FUj^PKHlt|S{wx|M}qzKDNG)*ZU8wwBz`z$)8+w~CqFkg2!oq{=zBQ*d;RiMie zkdFN8(I()eMK>+~@y<2@Ux5&(7BotiU|k@)vE-O~q-NPP1Ua3YO2h=@`~=lqmF z1q2K_-ubbicd2Q4g;g6?Jmh4)p3TTe=oN!sGH4cnKA_ePF(W|!CyFLy7HKY;H+AK@ zeNLtesK`TYj*nD~OZTUy#%nt2OoF`KZ(G%WDWzzW;lsTiS4`7O48Ea*Q3dp-*uLAg z`OLmUiN)i`$LrL5JA>{AaGFK%W)O-=a6$g6imy7Fb}I)`QdfbI9X5b;pWAPH<(F3R zIMf7m9wMgUPYmj)@BzT(@zliuriyIJt6s0C?#O|B5DEc0Tk$my7`L%bHx!M}TqI%gO-Vs!2VBAV_0Wo4FAwPs0~&jhL!%WgdkB%#^-KCm0-*rFMJ8%~yqebno?0Evw=?Jg0DTR@_GY4o znq}jiFHIQxbPUBj5s4yL*k(Xgw2pcPImwXkPd?oP{(-@xXHQG2y{n?4!czDhVbN8Y zm%01(E&_OvNd?zTz@MdbJu-dGnt!*s|7dYW#`7FQ_W-yXfYHj_X~bO6%#A;PX~NiF zOn+nD>kfZ{65ZA}(4)(N2%XTbudfN*+u6cjn3Z|86nGQhL1BXU0eaD;YlHI3*WVr_ zd;maxMaAK1tM*$oS6W3Y9yzHf{j9)tT0XFEi3IOJi>XU)bcI_%to8gF% zS8{&TR{$@=cr`_5V6hN1WLo*UTW|dP-#@pCZ|d5$2d9;W$oJi z`Q;V&Xl@z^;783>{f{RtFvdnc&Bxy+CZlbMDnKZ~!@R1t!Y{t+@<9@=0gZn7-pq;$r8$LH zy#G3Zrm}Uri?TA;1DJX|Amd!+T&k`0SfXweD$K7_hR^}@@$pts zR>mzXv>haPbzBU6UX+zN_l0ref}4xuJe{2#Q#>JaW?jvJYJyx(z~-W?vA=5*q}|w> zY|N<%hvZ?S55Oi@PjT_0x%l=!>kFXJt?7G%6`@t|39@k)GoAr({0{ z8q8&)NVEyC0Rs&NMYL5B?k>v8`~wjVBrs-iJE6BDpvT2^+%Mj$51?2kPuM6RJ$;6I z(4g!X`)`J4_;7E>OO^?Oa1X+d=lcRE3@!2P(7|-?9c2y-Wvxa5ik(0;(VcCPm`w#p zP0M@8&tKN|N^;eursY19o<75EFNXG{jLu6>P0Rg6i)pM)P0QV$l9o53Bk_eJ|Bdx! zE_Sy}>@e*3K}Z0v67%qBtJnR>k<4gtL>|u605+BbM~t3$CBOv$V$jkA%?$9DzDSiI zH7)lNB3=jJ;$ydl2*RRB;!@$p)vLdrR=#eQ3+@XQ6rok{!={z5_vWvz*x^`494e5G zi~@v>AqkN_D)=4)6C+WA)U@170F+bkTsw1$*Xwm?pkz#|A~3rK5@TUV07hA-2)-4I zQGj9#(DgNdl-j#6SmLv*1g*kH=&T*U_`v7$b;^ zUw*k90)``m5otMT0Lox2{I5yJm76!O`qW;IPC^`73s4FwnstH(guNoLD%i5IWG;Y* z4IdJ+33wvpzQj2Mu@1(D7GFuoD>iQ~`PhDLPD0G&p#k+WR;gGjDS#*5f2NtXu?Rah zHl=Dq$?R}70T=^9?h#IdNy*>3q4bkT@^Mn4alM&LG_ARxN;FCepfUG1T?-%_iu60o zngd0?CLNhQrsTImbl6NLnnvL!quQaQ0MaGLnfE$rSA?GdpaV+4*zdUTiQ7-2vULL| zh>wKalyunYJq~I-nQiQwM_g?&5*VuzjkN+4l7PT97*#xe>@{Yu~H4Z0P>#3jX9nQtx}Ki1g!g_)0v z*sGa%8klBM6JK(j?>)mA!s4qhPbnIou}pwJ0B}b8r}U0$KASu-??S+DB9{wl4ji%E zHPqBIsKGnWO$*{u>pasm57zUGho*IpIU_L1y=u-mmT46D~y9 z8sPAx0e$=kEQi3^rXv7wk!prj30o6%vhV@m#?`Bjf@uoE?t4VzSkn7NSz}igWsMze z72l%lvB^bQnTwcc8xj8)#{anv=IM5X4*)DsY|%G6okXCd&O7LgaB&#c0CnC$L!~BP z2k>2sJp3~$sqY{>U)d@9zK=}LopnqRTmU{{!sIQLC7n0D+J@rs85c9*C4eq93B~|k zm&^q>uddkE_QB$D<1X-P`fLL70hqPLJB7r}H?3J8-1J!EC9OugXgjRsv{wo_xzEdc zHwQmxrvNoK4jc%{cN1`%MWz7&%zR_gD|`NGH{W&`Jv#dgT@z=WmT+`OXZr&fQ8Ydy z2PEbJ9I}Yj1_@vZgjfLGUkeiV19U53Jevss1niZ>^AZ!!SbSXv)-CF8NS@5VQiY%a zFcks%6N#_vJzj`t&wou?p$Yw5cU;X`o?eH}_r%r#WNlK!jI~KG@43pUvunCFL{xlL zW)2h212D3KvHgp{zpoW>&pBck0-*NBVNXv%<};Zw)y&h z2>dvj@Kk<9g`smuq}2X&a6fhjHj5-JvD@@S<;S?w=@Tu zV-2f&S4=&6pIeRie*2Es3Ix=%6jvrL+SOwZdupQyj{q2G5&R&5A8Ym`K7CtBNvCb7 zS$E68;jHspXtPdIfABmU*_RA|qp_u&aQP+e962ql0<_{uJdD#X+6GtL(Zi6On^?5R zbdsZ|&|>d@a5{k>L>mW>V;lm}6YObvYWj+IYP%voHMd+ehL~0bn&SF>Q=NjQTB9|y zZ)s70QB#}sCUXS<03md#CDi_E;QLFSbS%e;WMmY&;`_PAu;dgXIT#QLU`|BKR;UL+ z{HYZDQJJq`dspWT0ABTawWBrv87Enc1hE@L+eD*$!|AGCg*}&E`*VMNpFhfgW@)7% zTz*NrM*XZ=Z;DYh#gYL3a`49spX*>gEg=GBPnvh7Aj)ST7l4Yz@0ZN%Bk+r*%L=wx z2S2s=bm+}fnP?#t7w9#YUsCm`AKOs?pylks8wY<}{BmU3wC3u`3$g_9BMg`gz06<+ zN{Q)#(q*%I#1iqYlxdTh*#}m)&(hohNxMgRoAoA-U5nf%V*Fs>_Qe}*zGq5K&hPpn zHt;-{uX89$KY<^K(}v9V`K~)WTAHNW)41-*FDjpd9~fc=x1n|ezi6zV9ZJ0zQoYL(+=5F9==47ECU;w zdWEA7YW;#^&^9d~u3WL=xBKnpuQN$a%gZ9-F#tZ;Tv@Wl?)%llz4sE+;|f9l4Z=f* zWBU}RRX%@w>fUfIVEb@yLM+S0047>QS|dFAcP`xYx~1_$IC8I@KTI;-g7toTZ8;#B zt`{v1A<7W2Nd#gpU$Nr;E*X|tF)}@G4l~bd{msmCwrpIfT%f*8&+XOF==lsBysR9; zTY-RQ%6E6Zav=DD;cDQ}ZN9pTwx>)X=st^c>XNt_H>+kin7q9INtBG&5mxxL5I{(# z!FaZ8+3fWMtT%Y+U4RYO)fpJ2yPHFuI8FdWM9h3&(7pS~#+iQ&_1aCu`WdD&qQI)p z&pbHRNce!6K?oPf*f@O}vA)IpgerY%*f=cEUCmDdFxrTzmYF9qXg-YkB#aUOkc<_f zk8nXRZO9c2%z@D!bIjM}Ts!ZkNb~FzgD=ZXK&XLhVp`hAiK=l3(x;jiQ2D9e)cn&c zOcv2hwe6hBl6sdbaHk+0VK1SN*gN!U+a0%SCeObVU?T^PMu78HEt|W7NdCy+`TkYQ z=WGN3A?S_ZhXkBo>)(F`onjrXcQhl#1W%;g=Az906*|`O8TT9Y^{9V_AX+! zAw9+H6B(>xBcNs$PT+SeI)fLf<+_D{6eqk52GFljYTn)ZS+j;%ztH?7S5~qxQtNTLu+Llp0!oJz9?v`>*G~A(LP}OLq6p@=@ChNcA~BRZ zaUFr6Cz;AbYf?I+K&c6Ev+J-lKY~R0Jeg~}51$9?HT155+Lc}<%!Chs8T^t1IkSXF zk1^Xe)9n{Z8&G%onL#Y|;RbDcEr`n72jsp0t_%vNgJ@Y$xIO@a`x6pGOM}A203kKc zgiGB=SIkNi;oq@k!%CyM3CA%z#;*l6^T`u%W{|kZV0ga0^tIXE zwl}VsJpWRr5Cu3A9BzVl!C+125KG}zOJ`rtZBA53pYekG>;wB7(Z1QEqs?LffUQ=! zPE8#j4_C}80GAs)ZWaDBR`EE6Q``3|&pMeLRIH$l-@@Qf?_NSkd!>Vns+5F=cjDTtK)ES)+t$i{X;&d_jBZUxXS*5d(uzy|fG7Tb6WfFE{cMi^MLsdB}t zu1pg+0SZu2Q4j#|R#&HM2-8~Q`-V%@oykrQ!HKgGZPVon006}Nzb7KyAXvEIGkQRf zC|#YJmisV(2g7{>12u$*Iz|l)>?6ke5aN}p%H{tT zK^%^47k@$&z~{TCA!qV}CqM?vx{S(RT0*wZcTdALQx^P&!H)!m*i`!3oJ#;8H@C2t zbc=%s7O=pC(pTraJz-Meg_;oW05~rs84{+pcU2ooW{()1E5q$nLX3CnYXg%z`xLXV zICN%^x>(J3q_WM?-uhDjLf6d+S2V~vUv~D>F~nr#gP5M1Ti6c(q?4P>nJp zhvJTWC|Ncj^qf6ScZ;h4fCv*pB4&IsM0`I^;K?HEK*ISstOxGK{IFyK%BV-3I0Dzwa->}M8U~6B4Zm9$gof&k( znt)cymd$w>M1KzRI2J&B@J$9X>!tWBjJ=pn4-*$NaNEjP=i2fmXr z_F4dICr*6$eBCAUn2`w}6)o=LZQI!<0M!6KBIdPvBd;tgD^N0s>?ljqfCmjQj2?Fo*SPaJ$^+_A1{VYI_lcjT*qz z4W-LC=E?KTv8!89+2U{CWx@vlkhExf)%w!Wvx84otkwb)aYg~0>;*ngW~!1DA?6ocbHacRw5|kV?^g`reN`&ldwjA6`)0K8^Q_~}6hQL?h=AB7kh}pR`QJm$FH@P{QiMjcArNaKk80(()KleE7G`0;sQFoPAu=)| zI7&K-5?<13f0{f?2)@tM98920HA9NN zknr4|7eE*bfeq0k07*qoM6N<$f^lgB@c;k- literal 0 HcmV?d00001 From a4d901f8b724ed917ff1b563687a0f713f542105 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:46:11 +0100 Subject: [PATCH 18/30] =?UTF-8?q?=E2=9C=A8=20add=20new=20project=20files?= =?UTF-8?q?=20for=20json,=20yaml,=20and=20xml=20response=20negotiators?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ons.Carter.AspNetCore.Newtonsoft.Json.csproj | 14 ++++++++++++++ ...xtensions.Carter.AspNetCore.Text.Json.csproj | 14 ++++++++++++++ ...xtensions.Carter.AspNetCore.Text.Yaml.csproj | 16 ++++++++++++++++ ...belt.Extensions.Carter.AspNetCore.Xml.csproj | 17 +++++++++++++++++ .../Codebelt.Extensions.Carter.csproj | 13 +++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.csproj create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/Codebelt.Extensions.Carter.AspNetCore.Text.Json.csproj create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.csproj create mode 100644 src/Codebelt.Extensions.Carter.AspNetCore.Xml/Codebelt.Extensions.Carter.AspNetCore.Xml.csproj create mode 100644 src/Codebelt.Extensions.Carter/Codebelt.Extensions.Carter.csproj diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.csproj b/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.csproj new file mode 100644 index 0000000..dd741b0 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json.csproj @@ -0,0 +1,14 @@ + + + + The Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json namespace contains types that complements the Codebelt.Extensions.Carter namespace by providing an opinionated JSON response negotiator for Carter using Newtonsoft.Json in the context of ASP.NET Core. This is a Codebelt ecosystem alternative to Carter's built-in Newtonsoft.Json negotiator. + extension-methods extensions carter response-negotiation json newtonsoft-json asp-net-core minimal-api codebelt + + + + + + + + + diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/Codebelt.Extensions.Carter.AspNetCore.Text.Json.csproj b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/Codebelt.Extensions.Carter.AspNetCore.Text.Json.csproj new file mode 100644 index 0000000..b6bfb89 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Json/Codebelt.Extensions.Carter.AspNetCore.Text.Json.csproj @@ -0,0 +1,14 @@ + + + + The Codebelt.Extensions.Carter.AspNetCore.Text.Json namespace contains types that complements the Codebelt.Extensions.Carter namespace by providing an opinionated JSON response negotiator for Carter using System.Text.Json in the context of ASP.NET Core. This is a Codebelt ecosystem alternative to Carter's built-in JSON negotiator. + extension-methods extensions carter response-negotiation json system-text-json asp-net-core minimal-api codebelt + + + + + + + + + diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.csproj b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.csproj new file mode 100644 index 0000000..f7722d8 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.csproj @@ -0,0 +1,16 @@ + + + + The Codebelt.Extensions.Carter.AspNetCore.Text.Yaml namespace contains types that complements the Codebelt.Extensions.Carter namespace by providing a YAML response negotiator for Carter using YamlDotNet in the context of ASP.NET Core. + extension-methods extensions carter response-negotiation yaml yamldotnet asp-net-core minimal-api + + + + + + + + + + + diff --git a/src/Codebelt.Extensions.Carter.AspNetCore.Xml/Codebelt.Extensions.Carter.AspNetCore.Xml.csproj b/src/Codebelt.Extensions.Carter.AspNetCore.Xml/Codebelt.Extensions.Carter.AspNetCore.Xml.csproj new file mode 100644 index 0000000..b7eb7f9 --- /dev/null +++ b/src/Codebelt.Extensions.Carter.AspNetCore.Xml/Codebelt.Extensions.Carter.AspNetCore.Xml.csproj @@ -0,0 +1,17 @@ + + + + The Codebelt.Extensions.Carter.AspNetCore.Xml namespace contains types that complements the Codebelt.Extensions.Carter namespace by providing an XML response negotiator for Carter using System.Xml in the context of ASP.NET Core. + extension-methods extensions carter response-negotiation xml system-xml asp-net-core minimal-api + + + + + + + + + + + + diff --git a/src/Codebelt.Extensions.Carter/Codebelt.Extensions.Carter.csproj b/src/Codebelt.Extensions.Carter/Codebelt.Extensions.Carter.csproj new file mode 100644 index 0000000..e7ac12d --- /dev/null +++ b/src/Codebelt.Extensions.Carter/Codebelt.Extensions.Carter.csproj @@ -0,0 +1,13 @@ + + + + The Codebelt.Extensions.Carter namespace contains types and extension methods that complements the Carter namespace by providing configurable response negotiation, endpoint convention builder extensions and content negotiation for ASP.NET Core minimal APIs. + extension-methods extensions carter response-negotiation content-negotiation asp-net-core minimal-api + + + + + + + + From de31691d6c3c10430a7a6b639677789872735554 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:46:46 +0100 Subject: [PATCH 19/30] =?UTF-8?q?=E2=9C=85=20add=20test=20for=20solution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../YamlResponseNegotiatorTest.cs | 4 ++-- .../XmlResponseNegotiatorTest.cs | 4 ++-- .../Assets/WorldModule.cs | 2 +- .../Codebelt.Extensions.Carter.FunctionalTests.csproj | 2 +- .../JsonResponseNegotiatorTest.cs | 11 ++++++----- .../NewtonsoftJsonNegotiatorTest.cs | 4 ++-- .../XmlResponseNegotiatorTest.cs | 4 ++-- .../YamlResponseNegotiatorTest.cs | 8 ++++---- 8 files changed, 20 insertions(+), 19 deletions(-) diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs index 8721306..91f0630 100644 --- a/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml.Tests/YamlResponseNegotiatorTest.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; using Carter; using Carter.Response; -using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; +using Codebelt.Extensions.AspNetCore.Text.Yaml; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; using Codebelt.Extensions.YamlDotNet.Formatters; @@ -63,7 +63,7 @@ public async Task Handle_ShouldWriteYamlToResponseBody_WithCorrectContentType() using var response = await MinimalWebHostTestFactory.RunAsync( services => { - services.AddYamlFormatterOptions(); + services.AddMinimalYamlOptions(); services.AddCarter(configurator: c => c.WithResponseNegotiator()); }, app => diff --git a/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs index 7a9fd77..b4ff25f 100644 --- a/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.AspNetCore.Xml.Tests/XmlResponseNegotiatorTest.cs @@ -7,10 +7,10 @@ using Carter.Response; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; +using Cuemon.Extensions.AspNetCore.Xml; using Cuemon.Xml.Serialization.Formatters; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; using AspNetCoreMediaType = Microsoft.Net.Http.Headers.MediaTypeHeaderValue; @@ -61,7 +61,7 @@ public async Task Handle_ShouldWriteXmlToResponseBody_WithCorrectContentType() using var response = await MinimalWebHostTestFactory.RunAsync( services => { - services.Configure(_ => { }); + services.AddMinimalXmlOptions(); services.AddCarter(configurator: c => c.WithResponseNegotiator()); }, app => diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs index d4bd783..7a2733c 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/Assets/WorldModule.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +namespace Codebelt.Extensions.Carter.Assets; internal sealed class WorldModule : ICarterModule { diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj b/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj index 2b0f7d2..8e38256 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/Codebelt.Extensions.Carter.FunctionalTests.csproj @@ -1,7 +1,7 @@ - Codebelt.Extensions.Carter.AspNetCore.Text.Json + Codebelt.Extensions.Carter diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs index d4a3abb..66d55b1 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/JsonResponseNegotiatorTest.cs @@ -1,9 +1,10 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Text.Json.Serialization; +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Carter; -using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json; +using Codebelt.Extensions.Carter.Assets; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; using Cuemon.Extensions.AspNetCore.Text.Json; @@ -11,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; +namespace Codebelt.Extensions.Carter; /// /// Functional tests verifying Carter bootstrapped with as the sole response negotiator. diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs index b609209..b129372 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/NewtonsoftJsonNegotiatorTest.cs @@ -4,7 +4,7 @@ using Carter; using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; using Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; -using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.Carter.Assets; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; using Microsoft.AspNetCore.Builder; @@ -13,7 +13,7 @@ using Newtonsoft.Json.Serialization; using Xunit; -namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; +namespace Codebelt.Extensions.Carter; /// /// Functional tests verifying Carter bootstrapped with as the sole response negotiator. diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs index c813bbc..cec3a78 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/XmlResponseNegotiatorTest.cs @@ -2,8 +2,8 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Carter; -using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; using Codebelt.Extensions.Carter.AspNetCore.Xml; +using Codebelt.Extensions.Carter.Assets; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; using Cuemon.Extensions.AspNetCore.Xml; @@ -11,7 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; +namespace Codebelt.Extensions.Carter; /// /// Functional tests verifying Carter bootstrapped with as the sole response negotiator. diff --git a/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs b/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs index b0d5272..a514406 100644 --- a/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs +++ b/test/Codebelt.Extensions.Carter.FunctionalTests/YamlResponseNegotiatorTest.cs @@ -2,16 +2,16 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Carter; -using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; -using Codebelt.Extensions.Carter.AspNetCore.Text.Json.Assets; +using Codebelt.Extensions.AspNetCore.Text.Yaml; using Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; +using Codebelt.Extensions.Carter.Assets; using Codebelt.Extensions.Xunit; using Codebelt.Extensions.Xunit.Hosting.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace Codebelt.Extensions.Carter.AspNetCore.Text.Json; +namespace Codebelt.Extensions.Carter; /// /// Functional tests verifying Carter bootstrapped with as the sole response negotiator. @@ -28,7 +28,7 @@ public async Task GetStatisticalRegions_ShouldReturnYamlResponse_WhenCarterDefau using var response = await MinimalWebHostTestFactory.RunAsync( services => { - services.AddYamlFormatterOptions(); + services.AddMinimalYamlOptions(); services.AddCarter(configurator: c => c .WithModule() .WithResponseNegotiator()); From 656344ea8660041e60aebcb4b1c198ad223aead2 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:47:30 +0100 Subject: [PATCH 20/30] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20add=20dependencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 119 +++++++++++++++++++++++++++++++++++++++ Directory.Build.targets | 17 ++++++ Directory.Packages.props | 24 ++++++++ testenvironments.json | 15 +++++ 4 files changed, 175 insertions(+) create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 Directory.Packages.props create mode 100644 testenvironments.json diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..841b128 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,119 @@ + + + $(MSBuildProjectName.EndsWith('Tests')) + $(MSBuildProjectName.EndsWith('Benchmarks')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())src')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())tooling')) + $([MSBuild]::IsOSPlatform('Linux')) + $([MSBuild]::IsOSPlatform('Windows')) + true + false + ..\..\.nuget\$(MSBuildProjectName)\PackageReleaseNotes.txt + latest + + + + true + true + + + + net10.0 + Copyright © Geekle 2026. All rights reserved. + gimlichael + Geekle + Extensions for Carter API by Codebelt + icon.png + README.md + https://carter.codebelt.net/ + MIT + https://github.com/codebeltnet/carter + git + en-US + true + true + true + snupkg + true + true + $(MSBuildThisFileDirectory)carter.snk + true + latest + Recommended + 7035,CA2260,S6618 + v + true + + + + + + + + + + + + + + net10.0 + false + Exe + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false + true + + + + 0 + + + + false + Exe + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + net10.0 + false + false + false + false + true + none + NU1701,NU1902,NU1903 + false + false + + + + + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..0c72407 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,17 @@ + + + + + + + @(PackageReleaseNotesLines, '%0A') + + + + + + 0 + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch).$(GITHUB_RUN_NUMBER) + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..641a9a3 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,24 @@ + + + true + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testenvironments.json b/testenvironments.json new file mode 100644 index 0000000..a12cd13 --- /dev/null +++ b/testenvironments.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "environments": [ + { + "name": "WSL-Ubuntu", + "type": "wsl", + "wslDistribution": "Ubuntu-24.04" + }, + { + "name": "Docker-Ubuntu", + "type": "docker", + "dockerImage": "codebeltnet/ubuntu-testrunner:net8.0.418-9.0.311-10.0.103" + } + ] +} From d3a02baeb11ebc8ae0e91ea7f86a0155247b579a Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:47:42 +0100 Subject: [PATCH 21/30] =?UTF-8?q?=E2=9C=A8=20add=20.editorconfig=20and=20.?= =?UTF-8?q?gitignore=20for=20project=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 193 +++++++++++++++++++++++++++++++++++++++++ .gitignore | 232 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 425 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..df96769 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,193 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# Default: prefer spaces for data/markup +indent_style = space +indent_size = 2 +tab_width = 2 + +# This style rule concern the use of the range operator, which is available in C# 8.0 and later. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0057 +[*.{cs,vb}] +dotnet_diagnostic.IDE0057.severity = none + +# This style rule concerns the use of switch expressions versus switch statements. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0066 +[*.{cs,vb}] +dotnet_diagnostic.IDE0066.severity = none + +# Performance rules +# https://docs.microsoft.com/da-dk/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings +[*.{cs,vb}] +dotnet_analyzer_diagnostic.category-Performance.severity = none # Because many of the suggestions by performance analyzers are not compatible with .NET Standard 2.0 + +# This style rule concerns the use of using statements without curly braces, also known as using declarations. This alternative syntax was introduced in C# 8.0. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0063 +[*.{cs,vb}] +dotnet_diagnostic.IDE0063.severity = none + +# This style rule concerns with simplification of interpolated strings to improve code readability. It recommends removal of certain explicit method calls, such as ToString(), when the same method would be implicitly invoked by the compiler if the explicit method call is removed. +# https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0071 +[*.{cs,vb}] +dotnet_diagnostic.IDE0071.severity = none + +# S3267: Loops should be simplified with "LINQ" expressions +# https://rules.sonarsource.com/csharp/RSPEC-3267 +dotnet_diagnostic.S3267.severity = none + +# CA1859: Use concrete types when possible for improved performance +# This is a violation of Framework Design Guidelines. +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1859 +[*.{cs,vb}] +dotnet_diagnostic.CA1859.severity = none + +# IDE0008: Use explicit type +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0007-ide0008 +[*.{cs,vb}] +dotnet_diagnostic.IDE0008.severity = none + +[*.{cs,vb}] +indent_style = space +indent_size = 4 + +# IDE0161: Namespace declaration preferences +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0161 +# Prefer file-scoped namespaces for new files; existing block-scoped files should not be converted unless explicitly asked +[*.cs] +csharp_style_namespace_declarations = file_scoped:suggestion + +# Top-level statements: DO NOT USE +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0210-ide0211 +# This is enforced via a style preference set to error severity, not a language-level prohibition. +# Always use explicit class declarations with a proper namespace and Main method where applicable. +[*.cs] +csharp_style_prefer_top_level_statements = false:error + +[*.xml] +indent_style = space +indent_size = 2 + +# IDE0078: Use pattern matching +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0078 +[*.{cs,vb}] +dotnet_diagnostic.IDE0078.severity = none + +# IDE0290: Use primary constructor +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0290 +[*.{cs,vb}] +dotnet_diagnostic.IDE0290.severity = none + +# CA1200: Avoid using cref tags with a prefix +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1200 +[*.{cs,vb}] +dotnet_diagnostic.CA1200.severity = none + +# IDE0305: Use collection expression for fluent +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0305 +[*.{cs,vb}] +dotnet_diagnostic.IDE0305.severity = none + +# IDE0011: Add braces +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0011 +[*.{cs,vb}] +dotnet_diagnostic.IDE0011.severity = none + +# IDE0028: Use collection initializers or expressions +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0028 +[*.{cs,vb}] +dotnet_diagnostic.IDE0028.severity = none + +# IDE0039: Use collection expression for array +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0300 +[*.{cs,vb}] +dotnet_diagnostic.IDE0300.severity = none + +# IDE0031: Use collection expression for empty +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0301 +[*.{cs,vb}] +dotnet_diagnostic.IDE0301.severity = none + +# IDE0046: Use conditional expression for return +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0046 +[*.{cs,vb}] +dotnet_diagnostic.IDE0046.severity = none + +# IDE0047: Parentheses preferences +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0047-ide0048 +[*.{cs,vb}] +dotnet_diagnostic.IDE0047.severity = none + +# CA1716: Identifiers should not match keywords +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1716 +[*.{cs,vb}] +dotnet_diagnostic.CA1716.severity = none + +# CA1720: Identifiers should not contain type names +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1720 +[*.{cs,vb}] +dotnet_diagnostic.CA1720.severity = none + +# CA1846: Prefer AsSpan over Substring +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1846.severity = none + +# CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1847 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1847.severity = none + +# CA1865-CA1867: Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1865-ca1867 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA1865.severity = none +dotnet_diagnostic.CA1866.severity = none +dotnet_diagnostic.CA1867.severity = none + +# CA2263: Prefer generic overload when type is known +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2263 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA2263.severity = none + +# CA2249: Consider using String.Contains instead of String.IndexOf +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2249 +# Excluded while TFMs include netstandard2.0 +[*.{cs,vb}] +dotnet_diagnostic.CA2249.severity = none + +# IDE0022: Use expression body for methods +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0022 +[*.{cs,vb}] +dotnet_diagnostic.IDE0022.severity = none + +# IDE0032: Use auto-property +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0032 +[*.{cs,vb}] +dotnet_diagnostic.IDE0032.severity = none + +# Order modifiers +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Excluded becuase of inconsistency with other analyzers +[*.{cs,vb}] +dotnet_diagnostic.IDE0036.severity = none + +# Order modifiers +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0036 +# Excluded becuase of inconsistency with other analyzers +[*.{cs,vb}] +dotnet_diagnostic.IDE0036.severity = none + +# Use 'System.Threading.Lock' +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0330 +# Excluded while TFMs are less than net9.0 +[*.{cs,vb}] +dotnet_diagnostic.IDE0330.severity = none diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c587fe9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,232 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +# Strong-Name Key +*.snk + +# SonarLint +.sonarlint/ + +# Resharper +*.DotSettings + +# DocFx +/.docfx/wwwroot +/.docfx/api/**/*.yml +/.docfx/**/*.manifest +/.docfx/.vscode/docfx-assistant +/.vscode/docfx-assistant + +# Tooling +/tooling/gse/Surrogates +/.vscode From bb30db40f2050ab7ec40cb1aaca01d7e1d235dcb Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:48:01 +0100 Subject: [PATCH 22/30] =?UTF-8?q?=F0=9F=92=AC=20add=20community=20health?= =?UTF-8?q?=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 18 ++++++++ LICENSE | 21 +++++++++ README.md | 123 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..79df611 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. + +## [1.0.0] - 2026-03-02 + +This is the initial stable release of the `Codebelt.Extensions.Carter`, `Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json`, `Codebelt.Extensions.Carter.AspNetCore.Text.Json`, `Codebelt.Extensions.Carter.AspNetCore.Text.Yaml` and `Codebelt.Extensions.Carter.AspNetCore.Xml` packages. + +### Added + +- `ConfigurableResponseNegotiator` class in the Codebelt.Extensions.Carter.Response namespace that provides an abstract, configurable base class for Carter response negotiators that serialize models using a `StreamFormatter` implementation, +- `EndpointConventionBuilderExtensions` class in the Codebelt.Extensions.Carter namespace that consist of extension methods for the `IEndpointConventionBuilder` interface: `Produces` and `Produces`, +- `NewtonsoftJsonNegotiator` class in the Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json namespace that provides a JSON response negotiator for Carter, capable of serializing response models to JSON format using `Newtonsoft.Json`, +- `JsonResponseNegotiator` class in the Codebelt.Extensions.Carter.AspNetCore.Text.Json namespace that provides a JSON response negotiator for Carter, capable of serializing response models to JSON format using `System.Text.Json`, +- `YamlResponseNegotiator` class in the Codebelt.Extensions.Carter.AspNetCore.Text.Yaml namespace that provides a YAML response negotiator for Carter, capable of serializing response models to YAML format using `YamlDotNet`, +- `XmlResponseNegotiator` class in the Codebelt.Extensions.Carter.AspNetCore.Xml namespace that provides an XML response negotiator for Carter, capable of serializing response models to XML format using `System.Xml.XmlWriter`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..503ef81 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Geekle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f7f94e7 --- /dev/null +++ b/README.md @@ -0,0 +1,123 @@ +![Extensions for Carter API by Codebelt](.nuget/Codebelt.Extensions.Carter/icon.png) + +# Extensions for Carter API by Codebelt + +[![Carter CI/CD Pipeline](https://github.com/codebeltnet/carter/actions/workflows/ci-pipeline.yml/badge.svg)](https://github.com/codebeltnet/carter/actions/workflows/ci-pipeline.yml)[![codecov](https://codecov.io/gh/codebeltnet/carter/graph/badge.svg?token=qX3lHGvFS4)](https://codecov.io/gh/codebeltnet/carter) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=carter&metric=alert_status)](https://sonarcloud.io/dashboard?id=carter) [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=carter&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=carter) [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=carter&metric=reliability_rating)](https://sonarcloud.io/dashboard?id=carter) [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=carter&metric=security_rating)](https://sonarcloud.io/dashboard?id=carter) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/codebeltnet/carter/badge)](https://scorecard.dev/viewer/?uri=github.com/codebeltnet/carter) + +An open-source project (MIT license) that targets and complements the [Carter](https://github.com/dotnet/Carter) API library. It provides a uniform and convenient way of doing API development for all project types in .NET. + +Your versatile Carter companion for modern development with `.NET 9` and `.NET 10`. + +It is, by heart, free, flexible and built to extend and boost your agile codebelt. + +## Concept + +The Extensions for Carter API by Codebelt is designed to bring clarity, structure, and consistency to Carter projects. It provides a focused extension layer for Carter with configurable response negotiation primitives and endpoint metadata helpers for ASP.NET Core minimal APIs. + +### Codebelt.Extensions.Carter Negotiator Examples + +These are the standalone response negotiator packages that extend the core `Codebelt.Extensions.Carter` package with specific content negotiation capabilities: + +#### Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json + +```csharp +using Carter; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddNewtonsoftJsonFormatterOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +#### Codebelt.Extensions.Carter.AspNetCore.Text.Json + +```csharp +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Text.Json; +using Cuemon.Extensions.AspNetCore.Text.Json; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalJsonOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +#### Codebelt.Extensions.Carter.AspNetCore.Text.Yaml + +```csharp +using Carter; +using Codebelt.Extensions.AspNetCore.Text.Yaml.Formatters; +using Codebelt.Extensions.Carter.AspNetCore.Text.Yaml; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalYamlOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +#### Codebelt.Extensions.Carter.AspNetCore.Xml + +```csharp +using Carter; +using Codebelt.Extensions.Carter.AspNetCore.Xml; +using Cuemon.Extensions.AspNetCore.Xml; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMinimalXmlOptions(); +builder.Services.AddCarter(c => c + .WithResponseNegotiator()); + +var app = builder.Build(); +app.MapCarter(); +app.Run(); +``` + +## 📚 Documentation + +Full documentation (generated by [DocFx](https://github.com/dotnet/docfx)) located here: https://carter.codebelt.net/ + +## 📦 Standalone Packages + +Provides a focused API for BenchmarkDotNet projects. + +|Package|vNext|Stable|Downloads| +|:--|:-:|:-:|:-:| +| [Codebelt.Extensions.Carter](https://www.nuget.org/packages/Codebelt.Extensions.Carter/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.Carter?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.Carter?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.Carter?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.Carter.AspNetCore.Text.Json](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Json/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.Carter.AspNetCore.Text.Json?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.Carter.AspNetCore.Text.Json?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.Carter.AspNetCore.Text.Json?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.Carter.AspNetCore.Text.Yaml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.Carter.AspNetCore.Text.Yaml?color=blueviolet&logo=nuget) | +| [Codebelt.Extensions.Carter.AspNetCore.Xml](https://www.nuget.org/packages/Codebelt.Extensions.Carter.AspNetCore.Xml/) | ![vNext](https://img.shields.io/nuget/vpre/Codebelt.Extensions.Carter.AspNetCore.Xml?logo=nuget) | ![Stable](https://img.shields.io/nuget/v/Codebelt.Extensions.Carter.AspNetCore.Xml?logo=nuget) | ![Downloads](https://img.shields.io/nuget/dt/Codebelt.Extensions.Carter.AspNetCore.Xml?color=blueviolet&logo=nuget) | + +### Contributing to `Extensions for Carter API by Codebelt` +[Contributions](.github/CONTRIBUTING.md) are welcome and appreciated. + +Feel free to submit issues, feature requests, or pull requests to help improve this library. + +### License +This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. From 0b273fef758d627782b6811b1d750cb02d65e679 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:48:06 +0100 Subject: [PATCH 23/30] =?UTF-8?q?=E2=9C=A8=20add=20solution=20file=20with?= =?UTF-8?q?=20project=20structure=20for=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Codebelt.Extensions.Carter.slnx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Codebelt.Extensions.Carter.slnx diff --git a/Codebelt.Extensions.Carter.slnx b/Codebelt.Extensions.Carter.slnx new file mode 100644 index 0000000..83dda2f --- /dev/null +++ b/Codebelt.Extensions.Carter.slnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From 80ddf47c16a85c127226ea2475f7bae09959078c Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 22:58:53 +0100 Subject: [PATCH 24/30] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20update=20sonarcloud?= =?UTF-8?q?=20project=20key=20due=20to=20key=20taken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index eeac7e0..9f187f2 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -83,7 +83,7 @@ jobs: uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle - projectKey: carter + projectKey: ccodebelt-carter version: ${{ needs.build.outputs.version }} secrets: inherit From b37514ddc2f868ee876db0fcc4b2e472066e8436 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 23:00:15 +0100 Subject: [PATCH 25/30] =?UTF-8?q?=F0=9F=93=9D=20update=20changelog=20date?= =?UTF-8?q?=20for=20initial=20stable=20release?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79df611..b9a835f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. -## [1.0.0] - 2026-03-02 +## [1.0.0] - 2026-03-01 This is the initial stable release of the `Codebelt.Extensions.Carter`, `Codebelt.Extensions.Carter.AspNetCore.Newtonsoft.Json`, `Codebelt.Extensions.Carter.AspNetCore.Text.Json`, `Codebelt.Extensions.Carter.AspNetCore.Text.Yaml` and `Codebelt.Extensions.Carter.AspNetCore.Xml` packages. From 1dc9444725ab622a877a32834d65c4f63c2609c3 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 23:17:06 +0100 Subject: [PATCH 26/30] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20fixed=20typo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-pipeline.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 9f187f2..ddf2323 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -83,7 +83,7 @@ jobs: uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3 with: organization: geekle - projectKey: ccodebelt-carter + projectKey: codebelt-carter version: ${{ needs.build.outputs.version }} secrets: inherit From a1fe5d1fed8973d8e35ff53d1c92ff5ca174361a Mon Sep 17 00:00:00 2001 From: gimlichael Date: Sun, 1 Mar 2026 23:36:30 +0100 Subject: [PATCH 27/30] =?UTF-8?q?=E2=9C=A8=20add=20128x128=20png=20image?= =?UTF-8?q?=20for=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docfx/images/128x128.png | Bin 0 -> 6946 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .docfx/images/128x128.png diff --git a/.docfx/images/128x128.png b/.docfx/images/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..73b6edbb9321b95f56cb9e2f04ad3328304700a8 GIT binary patch literal 6946 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 literal 0 HcmV?d00001 From 04a271a049f659bfe9c4c991d8d3599a0a671b37 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:40:12 +0100 Subject: [PATCH 28/30] fix: correct LICENSE link in README.md (#2) * Initial plan * fix: update LICENSE link in README.md to use correct filename Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7f94e7..f7a032d 100644 --- a/README.md +++ b/README.md @@ -120,4 +120,4 @@ Provides a focused API for BenchmarkDotNet projects. Feel free to submit issues, feature requests, or pull requests to help improve this library. ### License -This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. From fe638391329c1fc330d8cf162c2953eb2244e2d2 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:40:30 +0100 Subject: [PATCH 29/30] Fix NullReferenceException in ConfigurableResponseNegotiator when Accept-Charset header is absent (#3) * Initial plan * fix: add null-conditional operator to prevent NullReferenceException on AcceptCharset.Count Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --- .../Response/ConfigurableResponseNegotiator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs index eecd1c6..af32800 100644 --- a/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs +++ b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs @@ -68,7 +68,7 @@ public virtual bool CanHandle(MediaTypeHeaderValue accept) public virtual Encoding GetEncoding(HttpRequest request) { var acceptCharset = request.GetTypedHeaders().AcceptCharset; - if (acceptCharset.Count > 0) + if (acceptCharset?.Count > 0) { var preferred = acceptCharset .OrderByDescending(x => x.Quality ?? 1.0) From 7e405e0c6f6a9f180eecefd208ceb7bce69214bf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:40:46 +0100 Subject: [PATCH 30/30] fix: prevent StreamWriter from disposing HttpResponse.Body in ConfigurableResponseNegotiator (#4) * Initial plan * fix: use leaveOpen:true in StreamWriter to avoid disposing HttpResponse.Body Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gimlichael <8550919+gimlichael@users.noreply.github.com> --- .../Response/ConfigurableResponseNegotiator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs index af32800..b7b7dc6 100644 --- a/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs +++ b/src/Codebelt.Extensions.Carter/Response/ConfigurableResponseNegotiator.cs @@ -113,7 +113,7 @@ public virtual async Task Handle(HttpRequest req, HttpResponse res, T model, { var encoding = GetEncoding(req); res.ContentType = ContentType + "; charset=" + encoding.WebName; - await using var textWriter = new StreamWriter(res.Body, encoding); + await using var textWriter = new StreamWriter(res.Body, encoding, bufferSize: -1, leaveOpen: true); var formatter = GetFormatter(); using (var streamReader = new StreamReader(formatter.Serialize(model), encoding)) {