Skip to content

Commit

Permalink
Merge pull request #2462 from RocketSurgeonsGuild/feature/primitives
Browse files Browse the repository at this point in the history
Feature/primitives
  • Loading branch information
david-driscoll authored Jan 5, 2025
2 parents b00087d + 884a5de commit 7a87c95
Show file tree
Hide file tree
Showing 91 changed files with 1,101 additions and 694 deletions.
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<PackageVersion Include="Serilog.Enrichers.AssemblyName" Version="2.0.0" />
<PackageVersion Include="Serilog.Enrichers.Demystifier" Version="1.0.3" />
<PackageVersion Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageVersion Include="Serilog.Sinks.BrowserConsole" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.1.1" />
<PackageVersion Include="Serilog.Sinks.Spectre" Version="0.5.0" />
<PackageVersion Include="Serilog.Sinks.Trace" Version="4.0.0" />
Expand Down
30 changes: 30 additions & 0 deletions LaunchPad.sln
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Minimal", "sample\Sa
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Minimal.Tests", "test\Sample.Minimal.Tests\Sample.Minimal.Tests.csproj", "{3A72EB2F-D0C0-4AA4-9CCB-7EA04DC95C8D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rocket.Surgery.LaunchPad.Primitives", "src\Primitives\Rocket.Surgery.LaunchPad.Primitives.csproj", "{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rocket.Surgery.LaunchPad.WebAssembly.Hosting", "src\WebAssembly.Hosting\Rocket.Surgery.LaunchPad.WebAssembly.Hosting.csproj", "{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -845,6 +849,30 @@ Global
{3A72EB2F-D0C0-4AA4-9CCB-7EA04DC95C8D}.Release|x64.Build.0 = Release|Any CPU
{3A72EB2F-D0C0-4AA4-9CCB-7EA04DC95C8D}.Release|x86.ActiveCfg = Release|Any CPU
{3A72EB2F-D0C0-4AA4-9CCB-7EA04DC95C8D}.Release|x86.Build.0 = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|x64.ActiveCfg = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|x64.Build.0 = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|x86.ActiveCfg = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Debug|x86.Build.0 = Debug|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|Any CPU.Build.0 = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|x64.ActiveCfg = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|x64.Build.0 = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|x86.ActiveCfg = Release|Any CPU
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2}.Release|x86.Build.0 = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|x64.ActiveCfg = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|x64.Build.0 = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|x86.ActiveCfg = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Debug|x86.Build.0 = Debug|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|Any CPU.Build.0 = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|x64.ActiveCfg = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|x64.Build.0 = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|x86.ActiveCfg = Release|Any CPU
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -908,6 +936,8 @@ Global
{F0DD1D80-6948-4F10-8789-228ACC1F412F} = {DF33E0FB-9790-4654-B60F-8AB22E0CC3D1}
{75C5361A-B1E8-4194-8BBC-CD68D4FC62CA} = {5D11C19B-E8E4-4CE3-9C8A-1D368578EBCB}
{3A72EB2F-D0C0-4AA4-9CCB-7EA04DC95C8D} = {DF33E0FB-9790-4654-B60F-8AB22E0CC3D1}
{8E25E25B-622E-4A15-BE2F-13EA01AA8CA2} = {8FFDF555-DB50-45F9-9A2D-6410F39151C3}
{6EA9A9C8-8AFC-46F6-84C0-1B97F4B59FC3} = {8FFDF555-DB50-45F9-9A2D-6410F39151C3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {439897C2-CCBD-44FE-B2DC-A3E4670ADA59}
Expand Down
1 change: 0 additions & 1 deletion sample/Sample.Restful.Client/Sample.Restful.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
/>
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NSwag.ApiDescription.Client" />
<PackageReference Include="System.ComponentModel.Annotations" Condition="'$(TargetFramework)' == 'netstandard2.1'" />
</ItemGroup>
<ItemGroup>
<OpenApiReference
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Microsoft.Extensions.Configuration;

using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.LaunchPad.Telemetry;

Expand All @@ -15,7 +17,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// <seealso cref="IOpenTelemetryConvention" />
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public class AspNetCoreConventionInstrumentationConvention : IOpenTelemetryConvention
{
Expand Down
12 changes: 7 additions & 5 deletions src/AspNetCore/Conventions/FluentValidationConvention.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using FluentValidation.AspNetCore;
using FluentValidation.AspNetCore;

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.DependencyInjection;
using Rocket.Surgery.LaunchPad.AspNetCore.Validation;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;
using HttpJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;

namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;

Expand All @@ -18,7 +20,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// <seealso cref="IServiceConvention" />
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public partial class FluentValidationConvention : IServiceConvention
{
Expand All @@ -33,9 +35,9 @@ public void Register(IConventionContext context, IConfiguration configuration, I
services.AddFluentValidationClientsideAdapters();
services
.Configure<MvcOptions>(mvcOptions => mvcOptions.Filters.Insert(0, new ValidationExceptionFilter()))
.Configure<MvcJsonOptions>(options => options.JsonSerializerOptions.Converters.Add(new ValidationProblemDetailsConverter ()))
.Configure<MvcJsonOptions>(options => options.JsonSerializerOptions.Converters.Add(new ValidationProblemDetailsConverter()))
.Configure<HttpJsonOptions>(options => options.SerializerOptions.Converters.Add(new ValidationProblemDetailsConverter()));

// AddFluentValidationRules(services);
// AddFluentValidationRules(services);
}
}
28 changes: 12 additions & 16 deletions src/AspNetCore/Conventions/OpenApiConvention.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using System.Text;
using System.Text.Json;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OpenApi;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.DependencyInjection;
using Rocket.Surgery.LaunchPad.AspNetCore.Composition;
Expand All @@ -24,17 +20,10 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// <seealso cref="IServiceConvention" />
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public partial class OpenApiConvention : IServiceConvention
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "Error adding XML comments from {XmlFile}"
)]
internal static partial void ErrorAddingXMLComments(ILogger logger, Exception exception, string xmlFile);

/// <summary>
/// Registers the specified context.
/// </summary>
Expand All @@ -56,17 +45,24 @@ public void Register(IConventionContext context, IConfiguration configuration, I
options.AddOperationTransformer<StatusCode201Filter>();
options.AddOperationTransformer<OperationMediaTypesFilter>();
options.AddOperationTransformer<AuthorizeFilter>();

});
}
);
services.AddFluentValidationOpenApi();
}

[LoggerMessage(
EventId = 0,
Level = LogLevel.Debug,
Message = "Error adding XML comments from {XmlFile}"
)]
internal static partial void ErrorAddingXMLComments(ILogger logger, Exception exception, string xmlFile);
}

internal class NestedTypeSchemaFilter : IOpenApiSchemaTransformer
{
public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken)
{
if (context is not { JsonTypeInfo.Type.DeclaringType: {} }) return Task.CompletedTask;
if (context is not { JsonTypeInfo.Type.DeclaringType: { } }) return Task.CompletedTask;
schema.Annotations["x-schema-id"] = $"{context.JsonTypeInfo.Type.DeclaringType.Name}{context.JsonTypeInfo.Type.Name}";
return Task.CompletedTask;
}
Expand Down
40 changes: 22 additions & 18 deletions src/AspNetCore/Conventions/ProblemDetailsConvention.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using FluentValidation;
using FluentValidation;
using FluentValidation.Results;

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.DependencyInjection;
using Rocket.Surgery.LaunchPad.AspNetCore.Validation;
Expand All @@ -21,7 +23,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// <seealso cref="IServiceConvention" />
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public class ProblemDetailsConvention : IServiceConvention
{
Expand All @@ -33,14 +35,14 @@ public void Register(IConventionContext context, IConfiguration configuration, I
options =>
{
var old = options.StatusCodeSelector;
options.StatusCodeSelector = (exception) => exception switch
{
NotFoundException => StatusCodes.Status404NotFound,
RequestFailedException => StatusCodes.Status400BadRequest,
NotAuthorizedException => StatusCodes.Status403Forbidden,
ValidationException => StatusCodes.Status422UnprocessableEntity,
_ => old?.Invoke(exception) ?? StatusCodes.Status500InternalServerError,
};
options.StatusCodeSelector = exception => exception switch
{
NotFoundException => StatusCodes.Status404NotFound,
RequestFailedException => StatusCodes.Status400BadRequest,
NotAuthorizedException => StatusCodes.Status403Forbidden,
ValidationException => StatusCodes.Status422UnprocessableEntity,
_ => old?.Invoke(exception) ?? StatusCodes.Status500InternalServerError,
};
}
);
services.TryAddEnumerable(ServiceDescriptor.Singleton<IProblemDetailsWriter, OnBeforeWriteProblemDetailsWriter>());
Expand All @@ -53,28 +55,33 @@ public void Register(IConventionContext context, IConfiguration configuration, I
}
}

class OnBeforeWriteProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
internal class OnBeforeWriteProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
{
public ValueTask WriteAsync(ProblemDetailsContext context) => throw new NotImplementedException();

public bool CanWrite(ProblemDetailsContext context)
{
if (!context.ProblemDetails.Status.HasValue
|| !apiBehaviorOptions.Value.ClientErrorMapping.TryGetValue(context.ProblemDetails.Status.Value, out var clientErrorData))
{
return false;
}

context.ProblemDetails.Title ??= clientErrorData.Title;
context.ProblemDetails.Type ??= clientErrorData.Link;
return false;
}
}

class FluentValidationProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
internal class FluentValidationProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
{
public ValueTask WriteAsync(ProblemDetailsContext context)
{
if (context is not { Exception: IProblemDetailsData details }
|| context.HttpContext.Items[typeof(ValidationResult)] is not ValidationResult validationResult) return ValueTask.CompletedTask;
|| context.HttpContext.Items[typeof(ValidationResult)] is not ValidationResult validationResult)
{
return ValueTask.CompletedTask;
}

context.ProblemDetails = new FluentValidationProblemDetails(validationResult.Errors)
{
Expand All @@ -88,13 +95,10 @@ public ValueTask WriteAsync(ProblemDetailsContext context)
return ValueTask.CompletedTask;
}

public bool CanWrite(ProblemDetailsContext context)
{
return context.Exception is not IProblemDetailsData && context.HttpContext.Items[typeof(ValidationResult)] is ValidationResult;
}
public bool CanWrite(ProblemDetailsContext context) => context.Exception is not IProblemDetailsData && context.HttpContext.Items[typeof(ValidationResult)] is ValidationResult;
}

class ValidationExceptionProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
internal class ValidationExceptionProblemDetailsWriter(IOptions<ApiBehaviorOptions> apiBehaviorOptions) : IProblemDetailsWriter
{
public ValueTask WriteAsync(ProblemDetailsContext context)
{
Expand Down
5 changes: 3 additions & 2 deletions src/AspNetCore/Conventions/RestfulConvention.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.DependencyInjection;
using Rocket.Surgery.LaunchPad.AspNetCore.Composition;
Expand All @@ -16,7 +17,7 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// <seealso cref="IServiceConvention" />
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public class RestfulConvention : IServiceConvention
{
Expand Down
25 changes: 10 additions & 15 deletions src/AspNetCore/Conventions/SystemJsonTextConvention.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

using Rocket.Surgery.Conventions;
using Rocket.Surgery.Conventions.DependencyInjection;
using Rocket.Surgery.LaunchPad.Foundation;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;
using HttpJsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;
using MvcJsonOptions = Microsoft.AspNetCore.Mvc.JsonOptions;

namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;

Expand All @@ -16,23 +16,16 @@ namespace Rocket.Surgery.LaunchPad.AspNetCore.Conventions;
/// </summary>
/// <seealso cref="IServiceConvention" />
/// <seealso cref="IServiceConvention" />
/// <remarks>
/// Create a new SystemJsonTextConvention
/// </remarks>
/// <param name="options"></param>
[PublicAPI]
[ExportConvention]
[AfterConvention(typeof(AspNetCoreConvention))]
[AfterConvention<AspNetCoreConvention>]
[ConventionCategory(ConventionCategory.Application)]
public class SystemJsonTextConvention : IServiceConvention
public class SystemJsonTextConvention(FoundationOptions? options = null) : IServiceConvention
{
private readonly FoundationOptions _options;

/// <summary>
/// Create a new SystemJsonTextConvention
/// </summary>
/// <param name="options"></param>
public SystemJsonTextConvention(FoundationOptions? options = null)
{
_options = options ?? new FoundationOptions();
}

/// <summary>
/// Registers the specified context.
/// </summary>
Expand All @@ -50,4 +43,6 @@ public void Register(IConventionContext context, IConfiguration configuration, I
.AddOptions<HttpJsonOptions>()
.Configure<IServiceProvider>((options, provider) => ExistingValueOptions.Apply(provider, options.SerializerOptions, Options.DefaultName));
}

private readonly FoundationOptions _options = options ?? new FoundationOptions();
}
Loading

0 comments on commit 7a87c95

Please sign in to comment.