From 46719a10a4d92ecc47e4f24e9dbbd9d5ce3f0a87 Mon Sep 17 00:00:00 2001 From: Jezz Santos Date: Sun, 12 May 2024 11:23:10 +1200 Subject: [PATCH] Fixed C# 'required' modifier in request DTO's throwing exceptions before validation step. Closes #34 --- README.md | 3 +- docs/decisions/0150-nullability.md | 45 +++ docs/design-principles/0020-api-framework.md | 4 +- docs/design-principles/0025-modularity.md | 2 +- .../0090-authentication-authorization.md | 4 +- .../Api/Audits/AuditsApi.cs | 2 +- .../Api/Emails/EmailsApi.cs | 2 +- .../Api/FeatureFlags/FeatureFlagsApi.cs | 4 +- .../Api/Provisionings/ProvisioningsApi.cs | 2 +- .../Api/Recording/RecordingApi.cs | 4 +- .../Api/Usages/UsagesApi.cs | 2 +- src/ApiHost1/Api/TestingOnly/TestingWebApi.cs | 16 +- ...alidatedGetTestingOnlyRequestValidator.cs} | 15 +- ...alidatedPostTestingOnlyRequestValidator.cs | 29 ++ src/ApiHost1/ApiHost1.csproj | 6 + src/ApiHost1/Resources.Designer.cs | 18 +- src/ApiHost1/Resources.resx | 8 +- .../MakeBookingRequestValidatorSpec.cs | 5 +- .../Api/Bookings/BookingsApi.cs | 4 +- .../Bookings/MakeBookingRequestValidator.cs | 10 +- src/CarsInfrastructure/Api/Cars/CarsApi.cs | 13 +- .../Api/EndUsers/EndUsersApi.cs | 4 +- .../Api/Invitations/InvitationsApi.cs | 6 +- .../Api/Memberships/MembershipsApi.cs | 2 +- .../Api/APIKeys/APIKeysApi.cs | 2 +- .../Api/AuthTokens/AuthTokensApi.cs | 4 +- .../MachineCredentialsApi.cs | 2 +- .../PasswordCredentialsApi.cs | 23 +- .../Api/SSO/SingleSignOnApi.cs | 3 +- .../Api/Images/ImagesApi.cs | 8 +- .../Extensions/OperationMethodExtensions.cs | 13 + .../ApiDocsSpec.cs | 5 +- .../AuthNApiSpec.cs | 5 +- .../AuthZApiSpec.cs | 5 +- .../ContentNegotiationApiSpec.cs | 5 +- .../DataFormatsApiSpec.cs | 5 +- .../DefaultStatusCodeApiSpec.cs | 5 +- .../ErrorApiSpec.cs | 5 +- .../HealthCheckApiSpec.cs | 5 +- .../MultiTenancySpec.cs | 5 +- .../RequestCorrelationApiSpec.cs | 9 +- .../ValidationApiSpec.cs | 284 +++++++++++++--- .../AuthorizeAttribute.cs | 8 +- ...thCreateEdgeIdentityFeatureStateRequest.cs | 8 +- ...hCreateEdgeIdentityFeatureStateResponse.cs | 5 + .../FlagsmithCreateEdgeIdentityRequest.cs | 4 +- .../FlagsmithCreateFeatureRequest.cs | 4 +- .../FlagsmithCreateFeatureStateRequest.cs | 2 +- .../FlagsmithCreateIdentityRequest.cs | 4 +- .../FlagsmithDeleteEdgeIdentitiesRequest.cs | 4 +- .../FlagsmithDeleteFeatureRequest.cs | 4 +- .../FlagsmithGetEdgeIdentitiesRequest.cs | 2 +- .../FlagsmithGetFeatureStatesRequest.cs | 2 +- .../Flagsmith/FlagsmithGetFeaturesRequest.cs | 2 +- .../Gravatar/GravatarGetImageRequest.cs | 2 +- .../ExchangeOAuth2CodeForTokensRequest.cs | 8 +- .../Ancillary/DeliverAuditRequest.cs | 5 +- .../Ancillary/DeliverEmailRequest.cs | 5 +- .../Ancillary/DeliverUsageRequest.cs | 5 +- .../GetFeatureFlagForCallerRequest.cs | 3 +- .../Ancillary/GetFeatureFlagRequest.cs | 5 +- .../Ancillary/NotifyProvisioningRequest.cs | 5 +- .../Ancillary/RecordMeasureRequest.cs | 3 +- .../Ancillary/RecordUsageRequest.cs | 3 +- .../BackEndForFrontEnd/AuthenticateRequest.cs | 3 +- .../GetFeatureFlagForCallerRequest.cs | 3 +- .../BackEndForFrontEnd/RecordCrashRequest.cs | 3 +- .../RecordMeasureRequest.cs | 3 +- .../RecordPageViewRequest.cs | 3 +- .../BackEndForFrontEnd/RecordTraceRequest.cs | 5 +- .../BackEndForFrontEnd/RecordUsageRequest.cs | 3 +- .../Bookings/CancelBookingRequest.cs | 3 +- .../Bookings/MakeBookingRequest.cs | 5 +- .../Cars/DeleteCarRequest.cs | 3 +- .../Cars/GetCarRequest.cs | 3 +- .../Cars/RegisterCarRequest.cs | 11 +- .../Cars/ReleaseCarAvailabilityRequest.cs | 7 +- .../Cars/ReserveCarIfAvailableRequest.cs | 9 +- .../Cars/ScheduleMaintenanceCarRequest.cs | 3 +- .../SearchAllCarUnavailabilitiesRequest.cs | 3 +- .../Cars/TakeOfflineCarRequest.cs | 3 +- .../EndUsers/AssignPlatformRolesRequest.cs | 3 +- .../ChangeDefaultOrganizationRequest.cs | 3 +- .../EndUsers/InviteGuestRequest.cs | 3 +- .../EndUsers/ResendGuestInvitationRequest.cs | 3 +- .../EndUsers/UnassignPlatformRolesRequest.cs | 3 +- .../EndUsers/VerifyGuestInvitationRequest.cs | 3 +- .../Identities/AuthenticatePasswordRequest.cs | 5 +- .../AuthenticateSingleSignOnRequest.cs | 5 +- .../CompletePasswordResetRequest.cs | 5 +- ...onfirmRegistrationPersonPasswordRequest.cs | 3 +- .../Identities/DeleteAPIKeyRequest.cs | 3 +- ...etRegistrationPersonConfirmationRequest.cs | 3 +- .../InitiatePasswordResetRequest.cs | 3 +- .../Identities/RefreshTokenRequest.cs | 3 +- .../Identities/RegisterMachineRequest.cs | 3 +- .../RegisterPersonPasswordRequest.cs | 9 +- .../Identities/ResendPasswordResetRequest.cs | 3 +- .../Identities/RevokeRefreshTokenRequest.cs | 3 +- .../Identities/VerifyPasswordResetRequest.cs | 3 +- .../Images/DeleteImageRequest.cs | 3 +- .../Images/DownloadImageRequest.cs | 3 +- .../Images/GetImageRequest.cs | 3 +- .../Images/UpdateImageRequest.cs | 3 +- .../AssignRolesToOrganizationRequest.cs | 3 +- .../CreateOrganizationRequest.cs | 3 +- .../UnInviteMemberFromOrganizationRequest.cs | 3 +- .../UnassignRolesFromOrganizationRequest.cs | 3 +- ...lidationsValidatedGetTestingOnlyRequest.cs | 16 + ...idationsValidatedPostTestingOnlyRequest.cs | 16 + .../ValidationsValidatedTestingOnlyRequest.cs | 15 - .../ChangeProfileAvatarRequest.cs | 3 +- .../ChangeProfileContactAddressRequest.cs | 3 +- .../UserProfiles/ChangeProfileRequest.cs | 3 +- .../DeleteProfileAvatarRequest.cs | 3 +- .../Documentation/FromFormMultiPartFilter.cs | 11 +- .../Api/Organizations/OrganizationsApi.cs | 8 +- src/SaaStack.sln.DotSettings | 4 + .../ApiLayerAnalyzerSpec.cs | 318 +++++++++++++++++- .../AnalyzerReleases.Shipped.md | 15 + .../ApiLayerAnalyzer.cs | 67 +++- .../ApplicationLayerAnalyzer.cs | 4 +- .../DomainDrivenDesignAnalyzer.cs | 20 +- .../DomainDrivenDesignCodeFix.cs | 18 +- .../EventingAnalyzer.cs | 4 +- .../Resources.Designer.cs | 54 +++ .../Resources.resx | 18 + .../Tools.Analyzers.NonPlatform.csproj | 3 + .../MinimalApiMediatRGenerator.cs | 6 +- .../Tools.Generators.Web.Api.csproj | 3 + .../Api/Profiles/UserProfilesApi.cs | 8 +- .../Api/AuthN/AuthenticationApi.cs | 2 +- .../Api/FeatureFlags/FeatureFlagsApi.cs | 2 +- src/WebsiteHost/Api/Recording/RecordingApi.cs | 12 +- 134 files changed, 1147 insertions(+), 351 deletions(-) create mode 100644 docs/decisions/0150-nullability.md rename src/ApiHost1/Api/TestingOnly/{ValidationsValidatedTestingOnlyRequestValidator.cs => ValidationsValidatedGetTestingOnlyRequestValidator.cs} (58%) create mode 100644 src/ApiHost1/Api/TestingOnly/ValidationsValidatedPostTestingOnlyRequestValidator.cs create mode 100644 src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateResponse.cs create mode 100644 src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedGetTestingOnlyRequest.cs create mode 100644 src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedPostTestingOnlyRequest.cs delete mode 100644 src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedTestingOnlyRequest.cs diff --git a/README.md b/README.md index 49f424c8..ebeaa50e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Under Construction](https://img.shields.io/badge/Status-UnderConstruction-yellow.svg)](README.md) +[![Backend API Ready](https://img.shields.io/badge/Status-BackendAPI_Ready-green.svg)](README.md) +[![Frontend WebApp Under Construction](https://img.shields.io/badge/Status-FrontendUI_UnderConstruction-yellow.svg)](README.md) [![Build and Test](https://github.com/jezzsantos/saastack/actions/workflows/build.yml/badge.svg)](https://github.com/jezzsantos/saastack/actions/workflows/build.yml) [![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/jezzsantos) diff --git a/docs/decisions/0150-nullability.md b/docs/decisions/0150-nullability.md new file mode 100644 index 00000000..fd5bf4bf --- /dev/null +++ b/docs/decisions/0150-nullability.md @@ -0,0 +1,45 @@ +# Nullability + +* status: accepted +* date: 2023-09-12 +* deciders: jezzsantos + +# Context and Problem Statement + +Nullability refers to the issue of managing nulls in code. +Managing null in C# has been a long-standing issue, and the language has made several advances to reduce the amount of handling required. +However, the issue still remains, and the language has not yet provided a definitive solution. Thus many developers are still guarding against nulls and handling them as results. + +Nullable value types were first introduced in C# 2.0 (circa, Nov 2005), which allowed "value types" (like `int?`, `DateTime?`, and `MyCustomEnum?` etc) to be explicitly assigned a null value. +Since C# 8 (circa, Sept 2019), we've had the ability to define nullable "reference types" (like `string?` or `MyCustomObject?`), which allow developers to annotate their code to indicate where nulls are allowed. We also had the ability to mark up code, and projects with nullable annotations to help the compiler identify potential null reference exceptions. (like `#nullable enable` or `#nullable disable`) and make whole projects nullable or not using `enable`. Which also added tooling like Roslyn rules to help identify potential null reference exceptions. + +Outside of official dotnet releases, we've seen the introduction of more functional programming concepts like `Option`, `OneOf` and `Result` types, which are used to wrap values that may or may not be null, and provide a more functional way of handling nulls. + +These help the programmer manage return types much easier (since they are often implemented as `struct`) and that can lead to the eradication of `null` as a value anywhere in the code (except dealing with 3rd party library code). + +## Considered Options + +The options are: + +1. Nullable Context - having the compiler and IDE help identify potential null reference exceptions. +2. Optional - representing a value that may or may not be present. +3. Nulls - same old, same old. + +## Decision Outcome + +`Nullable Context + Optional` + +- Neither just Nullable Context nor Optional alone is sufficient to completely eradicate nulls form the codebase. Both are useful in certain contexts. It largely depends on what is being expressed at the time. +- Nullable Context (in an IDE) is very useful to help the programmer recognize where nulls are allowed, and where they are not. It also helps the compiler to identify potential null reference exceptions. +- Optional is very useful when dealing with return types, and when you want to express that a value may or may not be present. It is also useful when you want to express that a value may or may not be present, and you want to provide a default value if it is not present. +- {justification3} + +## More Information + +The dotnet team has attempted to introduce a set of functional types including `Optional` in the past in the [dotNext](https://github.com/dotnet/dotNext) which many products/teams have adopted. However, there have been strong statements from the dotnet team (can't find it online) that they will NOT be adopting these types into the dotnet runtime (some statement like: "we don't want another `null`!"). + +However, in the interim, we have moved forward with our own slim version of `Optional` and `Result`types, which are based on the [dotNext](https://github.com/dotnet/dotNext) project: + +- We decided to reduce the number of dependencies in this codebase +- We decided to learn more about these types, and how they could be used in our codebase. +- We decided that one day they can be (relatively easily) ripped and replaced for another implementation (at some cost), either when the language official supports them, or when we want to commit to another library. diff --git a/docs/design-principles/0020-api-framework.md b/docs/design-principles/0020-api-framework.md index df7d4d41..2844363a 100644 --- a/docs/design-principles/0020-api-framework.md +++ b/docs/design-principles/0020-api-framework.md @@ -92,7 +92,7 @@ where each service operation (method above) would have a unique request DTO that [Route("/cars/{Id}", OperationMethod.Get)] public class GetCarRequest : IWebRequest { - public required string Id { get; set; } + public string? Id { get; set; } } ``` @@ -267,7 +267,7 @@ Then we use Roslyn analyzers (and other tooling) to guide the author in creating [Route("/cars/{Id}", OperationMethod.Get)] public class GetCarRequest : IWebRequest { - public required string Id { get; set; } + public string? Id { get; set; } } ``` and, in `Infrastructure.Web.Api.Interfaces/Operations/Cars/GetCarResponse.cs`: diff --git a/docs/design-principles/0025-modularity.md b/docs/design-principles/0025-modularity.md index 9d833a13..219b8213 100644 --- a/docs/design-principles/0025-modularity.md +++ b/docs/design-principles/0025-modularity.md @@ -139,7 +139,7 @@ For example, [Authorize(Roles.Platform_Operations)] public class GetOrganizationSettingsRequest : TenantedRequest { - public required string Id { get; set; } + public string? Id { get; set; } } ``` diff --git a/docs/design-principles/0090-authentication-authorization.md b/docs/design-principles/0090-authentication-authorization.md index 116d468c..c9a1f2d2 100644 --- a/docs/design-principles/0090-authentication-authorization.md +++ b/docs/design-principles/0090-authentication-authorization.md @@ -119,7 +119,7 @@ For example, [Authorize(Roles.Tenant_Member, Features.Tenant_Basic)] public class GetCarRequest : TenantedRequest { - public required string Id { get; set; } + public string? Id { get; set; } } ``` @@ -198,7 +198,7 @@ For example, [Authorize(Roles.Tenant_Member, Features.Tenant_Basic)] public class GetCarRequest : TenantedRequest { - public required string Id { get; set; } + public string? Id { get; set; } } ``` diff --git a/src/AncillaryInfrastructure/Api/Audits/AuditsApi.cs b/src/AncillaryInfrastructure/Api/Audits/AuditsApi.cs index c7d7ccd3..cd69af68 100644 --- a/src/AncillaryInfrastructure/Api/Audits/AuditsApi.cs +++ b/src/AncillaryInfrastructure/Api/Audits/AuditsApi.cs @@ -23,7 +23,7 @@ public async Task> Deliver(DeliverAu CancellationToken cancellationToken) { var delivered = - await _ancillaryApplication.DeliverAuditAsync(_callerFactory.Create(), request.Message, cancellationToken); + await _ancillaryApplication.DeliverAuditAsync(_callerFactory.Create(), request.Message!, cancellationToken); return () => delivered.HandleApplicationResult(_ => new PostResult(new DeliverMessageResponse { IsDelivered = true })); diff --git a/src/AncillaryInfrastructure/Api/Emails/EmailsApi.cs b/src/AncillaryInfrastructure/Api/Emails/EmailsApi.cs index 02380822..51ab4f85 100644 --- a/src/AncillaryInfrastructure/Api/Emails/EmailsApi.cs +++ b/src/AncillaryInfrastructure/Api/Emails/EmailsApi.cs @@ -23,7 +23,7 @@ public async Task> Deliver(DeliverEm CancellationToken cancellationToken) { var delivered = - await _ancillaryApplication.DeliverEmailAsync(_callerFactory.Create(), request.Message, cancellationToken); + await _ancillaryApplication.DeliverEmailAsync(_callerFactory.Create(), request.Message!, cancellationToken); return () => delivered.HandleApplicationResult(_ => new PostResult(new DeliverMessageResponse { IsDelivered = true })); diff --git a/src/AncillaryInfrastructure/Api/FeatureFlags/FeatureFlagsApi.cs b/src/AncillaryInfrastructure/Api/FeatureFlags/FeatureFlagsApi.cs index 5a7df428..f95e2e26 100644 --- a/src/AncillaryInfrastructure/Api/FeatureFlags/FeatureFlagsApi.cs +++ b/src/AncillaryInfrastructure/Api/FeatureFlags/FeatureFlagsApi.cs @@ -22,7 +22,7 @@ public async Task> Get(GetFeat CancellationToken cancellationToken) { var flag = await _featureFlagsApplication.GetFeatureFlagAsync(_callerFactory.Create(), - request.Name, request.TenantId, request.UserId, cancellationToken); + request.Name!, request.TenantId, request.UserId!, cancellationToken); return () => flag.HandleApplicationResult(f => new GetFeatureFlagResponse { Flag = f }); } @@ -41,7 +41,7 @@ public async Task> GetForCalle CancellationToken cancellationToken) { var flag = await _featureFlagsApplication.GetFeatureFlagForCallerAsync(_callerFactory.Create(), - request.Name, cancellationToken); + request.Name!, cancellationToken); return () => flag.HandleApplicationResult(f => new GetFeatureFlagResponse { Flag = f }); } diff --git a/src/AncillaryInfrastructure/Api/Provisionings/ProvisioningsApi.cs b/src/AncillaryInfrastructure/Api/Provisionings/ProvisioningsApi.cs index e3abea17..785b1b45 100644 --- a/src/AncillaryInfrastructure/Api/Provisionings/ProvisioningsApi.cs +++ b/src/AncillaryInfrastructure/Api/Provisionings/ProvisioningsApi.cs @@ -34,7 +34,7 @@ public async Task> Notify(NotifyProv CancellationToken cancellationToken) { var delivered = - await _ancillaryApplication.NotifyProvisioningAsync(_callerFactory.Create(), request.Message, + await _ancillaryApplication.NotifyProvisioningAsync(_callerFactory.Create(), request.Message!, cancellationToken); return () => delivered.HandleApplicationResult(_ => diff --git a/src/AncillaryInfrastructure/Api/Recording/RecordingApi.cs b/src/AncillaryInfrastructure/Api/Recording/RecordingApi.cs index bbbe4a1b..0f054f66 100644 --- a/src/AncillaryInfrastructure/Api/Recording/RecordingApi.cs +++ b/src/AncillaryInfrastructure/Api/Recording/RecordingApi.cs @@ -20,7 +20,7 @@ public RecordingApi(ICallerContextFactory callerFactory, IRecordingApplication r public async Task RecordMeasurement(RecordMeasureRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordMeasurementAsync(_callerFactory.Create(), request.EventName, + var result = await _recordingApplication.RecordMeasurementAsync(_callerFactory.Create(), request.EventName!, request.Additional, cancellationToken); @@ -31,7 +31,7 @@ public async Task RecordMeasurement(RecordMeasureRequest request public async Task RecordUsage(RecordUseRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordUsageAsync(_callerFactory.Create(), request.EventName, + var result = await _recordingApplication.RecordUsageAsync(_callerFactory.Create(), request.EventName!, request.Additional, cancellationToken); diff --git a/src/AncillaryInfrastructure/Api/Usages/UsagesApi.cs b/src/AncillaryInfrastructure/Api/Usages/UsagesApi.cs index 1f9f2edb..aed68205 100644 --- a/src/AncillaryInfrastructure/Api/Usages/UsagesApi.cs +++ b/src/AncillaryInfrastructure/Api/Usages/UsagesApi.cs @@ -22,7 +22,7 @@ public async Task> Deliver(DeliverUs CancellationToken cancellationToken) { var delivered = - await _ancillaryApplication.DeliverUsageAsync(_callerFactory.Create(), request.Message, cancellationToken); + await _ancillaryApplication.DeliverUsageAsync(_callerFactory.Create(), request.Message!, cancellationToken); return () => delivered.HandleApplicationResult(_ => new PostResult(new DeliverMessageResponse { IsDelivered = true })); diff --git a/src/ApiHost1/Api/TestingOnly/TestingWebApi.cs b/src/ApiHost1/Api/TestingOnly/TestingWebApi.cs index 979a6876..d9e0abb5 100644 --- a/src/ApiHost1/Api/TestingOnly/TestingWebApi.cs +++ b/src/ApiHost1/Api/TestingOnly/TestingWebApi.cs @@ -209,12 +209,22 @@ public async Task> Validatio { Message = $"amessage{request.Id}" }); } - public async Task> ValidationsValidated( - ValidationsValidatedTestingOnlyRequest request, CancellationToken cancellationToken) + public async Task> ValidationsValidatedGet( + ValidationsValidatedGetTestingOnlyRequest request, CancellationToken cancellationToken) { await Task.CompletedTask; return () => new Result(new StringMessageTestingOnlyResponse - { Message = $"amessage{request.Field1}" }); + { Message = $"amessage{request.RequiredField}" }); + } + + public async Task> ValidationsValidatedPost( + ValidationsValidatedPostTestingOnlyRequest request, CancellationToken cancellationToken) + { + await Task.CompletedTask; + return () => + new PostResult( + new StringMessageTestingOnlyResponse { Message = $"amessage{request.RequiredField}" }, + "alocation"); } private static IReadOnlyList GetRepositories(IServiceProvider services, diff --git a/src/ApiHost1/Api/TestingOnly/ValidationsValidatedTestingOnlyRequestValidator.cs b/src/ApiHost1/Api/TestingOnly/ValidationsValidatedGetTestingOnlyRequestValidator.cs similarity index 58% rename from src/ApiHost1/Api/TestingOnly/ValidationsValidatedTestingOnlyRequestValidator.cs rename to src/ApiHost1/Api/TestingOnly/ValidationsValidatedGetTestingOnlyRequestValidator.cs index 8d23a7c9..68688246 100644 --- a/src/ApiHost1/Api/TestingOnly/ValidationsValidatedTestingOnlyRequestValidator.cs +++ b/src/ApiHost1/Api/TestingOnly/ValidationsValidatedGetTestingOnlyRequestValidator.cs @@ -1,4 +1,5 @@ #if TESTINGONLY +using Common.Extensions; using FluentValidation; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using JetBrains.Annotations; @@ -6,21 +7,23 @@ namespace ApiHost1.Api.TestingOnly; [UsedImplicitly] -public class ValidationsValidatedTestingOnlyRequestValidator : AbstractValidator +public class + ValidationsValidatedGetTestingOnlyRequestValidator : AbstractValidator { - public ValidationsValidatedTestingOnlyRequestValidator() + public ValidationsValidatedGetTestingOnlyRequestValidator() { RuleFor(req => req.Id) .NotEmpty() .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidId); - RuleFor(req => req.Field1) + RuleFor(req => req.RequiredField) .NotEmpty() .Matches(@"[\d]{1,3}") - .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidField1); - RuleFor(req => req.Field2) + .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + RuleFor(req => req.OptionalField) .NotEmpty() .Matches(@"[\d]{1,3}") - .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidField2); + .When(req => req.OptionalField.HasValue()) + .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidOptionalField); } } #endif \ No newline at end of file diff --git a/src/ApiHost1/Api/TestingOnly/ValidationsValidatedPostTestingOnlyRequestValidator.cs b/src/ApiHost1/Api/TestingOnly/ValidationsValidatedPostTestingOnlyRequestValidator.cs new file mode 100644 index 00000000..25fb6642 --- /dev/null +++ b/src/ApiHost1/Api/TestingOnly/ValidationsValidatedPostTestingOnlyRequestValidator.cs @@ -0,0 +1,29 @@ +#if TESTINGONLY +using Common.Extensions; +using FluentValidation; +using Infrastructure.Web.Api.Operations.Shared.TestingOnly; +using JetBrains.Annotations; + +namespace ApiHost1.Api.TestingOnly; + +[UsedImplicitly] +public class + ValidationsValidatedPostTestingOnlyRequestValidator : AbstractValidator +{ + public ValidationsValidatedPostTestingOnlyRequestValidator() + { + RuleFor(req => req.Id) + .NotEmpty() + .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidId); + RuleFor(req => req.RequiredField) + .NotEmpty() + .Matches(@"[\d]{1,3}") + .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + RuleFor(req => req.OptionalField) + .NotEmpty() + .Matches(@"[\d]{1,3}") + .When(req => req.OptionalField.HasValue()) + .WithMessage(Resources.GetTestingOnlyValidatedRequestValidator_InvalidOptionalField); + } +} +#endif \ No newline at end of file diff --git a/src/ApiHost1/ApiHost1.csproj b/src/ApiHost1/ApiHost1.csproj index 59f8aec4..9a8b409a 100644 --- a/src/ApiHost1/ApiHost1.csproj +++ b/src/ApiHost1/ApiHost1.csproj @@ -16,6 +16,12 @@ + + + <_Parameter1>Infrastructure.Web.Api.IntegrationTests + + + diff --git a/src/ApiHost1/Resources.Designer.cs b/src/ApiHost1/Resources.Designer.cs index 091e5f84..f4362b80 100644 --- a/src/ApiHost1/Resources.Designer.cs +++ b/src/ApiHost1/Resources.Designer.cs @@ -60,29 +60,29 @@ internal Resources() { } /// - /// Looks up a localized string similar to The Field1 was either invalid or missing. + /// Looks up a localized string similar to The Id was either invalid or missing. /// - internal static string GetTestingOnlyValidatedRequestValidator_InvalidField1 { + internal static string GetTestingOnlyValidatedRequestValidator_InvalidId { get { - return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidField1", resourceCulture); + return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidId", resourceCulture); } } /// - /// Looks up a localized string similar to The Field2 was either invalid or missing. + /// Looks up a localized string similar to The OptionalField1 was either invalid or missing. /// - internal static string GetTestingOnlyValidatedRequestValidator_InvalidField2 { + internal static string GetTestingOnlyValidatedRequestValidator_InvalidOptionalField { get { - return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidField2", resourceCulture); + return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidOptionalField", resourceCulture); } } /// - /// Looks up a localized string similar to The Id was either invalid or missing. + /// Looks up a localized string similar to The RequiredField was either invalid or missing. /// - internal static string GetTestingOnlyValidatedRequestValidator_InvalidId { + internal static string GetTestingOnlyValidatedRequestValidator_InvalidRequiredField { get { - return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidId", resourceCulture); + return ResourceManager.GetString("GetTestingOnlyValidatedRequestValidator_InvalidRequiredField", resourceCulture); } } } diff --git a/src/ApiHost1/Resources.resx b/src/ApiHost1/Resources.resx index bcd1a22d..ff0122c7 100644 --- a/src/ApiHost1/Resources.resx +++ b/src/ApiHost1/Resources.resx @@ -27,10 +27,10 @@ The Id was either invalid or missing - - The Field1 was either invalid or missing + + The RequiredField was either invalid or missing - - The Field2 was either invalid or missing + + The OptionalField1 was either invalid or missing \ No newline at end of file diff --git a/src/BookingsInfrastructure.UnitTests/Api/Bookings/MakeBookingRequestValidatorSpec.cs b/src/BookingsInfrastructure.UnitTests/Api/Bookings/MakeBookingRequestValidatorSpec.cs index 88c1a8b4..fe505adf 100644 --- a/src/BookingsInfrastructure.UnitTests/Api/Bookings/MakeBookingRequestValidatorSpec.cs +++ b/src/BookingsInfrastructure.UnitTests/Api/Bookings/MakeBookingRequestValidatorSpec.cs @@ -62,7 +62,8 @@ public void WhenEndUtcIsBeforeStartUtc_ThenThrows() [Fact] public void WhenDurationIsTooShort_ThenThrows() { - _dto.EndUtc = _dto.StartUtc.Add(Validations.Booking.MinimumBookingDuration.Subtract(TimeSpan.FromSeconds(1))); + _dto.EndUtc = + _dto.StartUtc!.Value.Add(Validations.Booking.MinimumBookingDuration.Subtract(TimeSpan.FromSeconds(1))); _validator .Invoking(x => x.ValidateAndThrow(_dto)) @@ -74,7 +75,7 @@ public void WhenDurationIsTooShort_ThenThrows() public void WhenDurationIsTooLong_ThenThrows() { _dto.EndUtc = - _dto.StartUtc.Add(Validations.Booking.MaximumBookingDuration.Add(TimeSpan.FromSeconds(1))); + _dto.StartUtc!.Value.Add(Validations.Booking.MaximumBookingDuration.Add(TimeSpan.FromSeconds(1))); _validator .Invoking(x => x.ValidateAndThrow(_dto)) diff --git a/src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs b/src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs index 6fb910ea..fb547833 100644 --- a/src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs +++ b/src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs @@ -21,7 +21,7 @@ public BookingsApi(ICallerContextFactory callerFactory, IBookingsApplication boo public async Task Cancel(CancelBookingRequest request, CancellationToken cancellationToken) { var booking = - await _bookingsApplication.CancelBookingAsync(_callerFactory.Create(), request.OrganizationId!, request.Id, + await _bookingsApplication.CancelBookingAsync(_callerFactory.Create(), request.OrganizationId!, request.Id!, cancellationToken); return () => booking.HandleApplicationResult(); } @@ -30,7 +30,7 @@ public async Task> Make(MakeBookingR CancellationToken cancellationToken) { var booking = await _bookingsApplication.MakeBookingAsync(_callerFactory.Create(), request.OrganizationId!, - request.CarId, request.StartUtc, request.EndUtc, cancellationToken); + request.CarId!, request.StartUtc.GetValueOrDefault(), request.EndUtc, cancellationToken); return () => booking.HandleApplicationResult(c => new PostResult(new MakeBookingResponse { Booking = c })); diff --git a/src/BookingsInfrastructure/Api/Bookings/MakeBookingRequestValidator.cs b/src/BookingsInfrastructure/Api/Bookings/MakeBookingRequestValidator.cs index 7c11aac6..f818b30d 100644 --- a/src/BookingsInfrastructure/Api/Bookings/MakeBookingRequestValidator.cs +++ b/src/BookingsInfrastructure/Api/Bookings/MakeBookingRequestValidator.cs @@ -25,12 +25,14 @@ public MakeBookingRequestValidator(IIdentifierFactory idFactory) .WithMessage(Resources.MakeBookingRequestValidator_InvalidEndUtc) .When(req => req.EndUtc.HasValue); RuleFor(req => req) - .Must(req => req.EndUtc?.Subtract(req.StartUtc) >= Validations.Booking.MinimumBookingDuration) + .Must(req => req.EndUtc?.Subtract(req.StartUtc.GetValueOrDefault()) + >= Validations.Booking.MinimumBookingDuration) .WithMessage(Resources.MakeBookingRequestValidator_InvalidEndUtc) - .When(req => req.EndUtc.HasValue); + .When(req => req.StartUtc.HasValue && req.EndUtc.HasValue); RuleFor(req => req) - .Must(req => req.EndUtc?.Subtract(req.StartUtc) <= Validations.Booking.MaximumBookingDuration) + .Must(req => req.EndUtc?.Subtract(req.StartUtc.GetValueOrDefault()) + <= Validations.Booking.MaximumBookingDuration) .WithMessage(Resources.MakeBookingRequestValidator_InvalidEndUtc) - .When(req => req.EndUtc.HasValue); + .When(req => req.StartUtc.HasValue && req.EndUtc.HasValue); } } \ No newline at end of file diff --git a/src/CarsInfrastructure/Api/Cars/CarsApi.cs b/src/CarsInfrastructure/Api/Cars/CarsApi.cs index 91a89c47..778ff486 100644 --- a/src/CarsInfrastructure/Api/Cars/CarsApi.cs +++ b/src/CarsInfrastructure/Api/Cars/CarsApi.cs @@ -20,14 +20,14 @@ public CarsApi(ICallerContextFactory callerFactory, ICarsApplication carsApplica public async Task Delete(DeleteCarRequest request, CancellationToken cancellationToken) { - var car = await _carsApplication.DeleteCarAsync(_callerFactory.Create(), request.OrganizationId!, request.Id, + var car = await _carsApplication.DeleteCarAsync(_callerFactory.Create(), request.OrganizationId!, request.Id!, cancellationToken); return () => car.HandleApplicationResult(); } public async Task> Get(GetCarRequest request, CancellationToken cancellationToken) { - var car = await _carsApplication.GetCarAsync(_callerFactory.Create(), request.OrganizationId!, request.Id, + var car = await _carsApplication.GetCarAsync(_callerFactory.Create(), request.OrganizationId!, request.Id!, cancellationToken); return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c }); @@ -37,7 +37,8 @@ public async Task> Register(RegisterCarReques CancellationToken cancellationToken) { var car = await _carsApplication.RegisterCarAsync(_callerFactory.Create(), request.OrganizationId!, - request.Make, request.Model, request.Year, request.Jurisdiction, request.NumberPlate, cancellationToken); + request.Make!, request.Model!, request.Year, request.Jurisdiction!, request.NumberPlate!, + cancellationToken); return () => car.HandleApplicationResult(c => new PostResult(new GetCarResponse { Car = c }, new GetCarRequest { Id = c.Id }.ToUrl())); @@ -47,7 +48,7 @@ public async Task> ScheduleMaintenance(Sc CancellationToken cancellationToken) { var car = await _carsApplication.ScheduleMaintenanceCarAsync(_callerFactory.Create(), request.OrganizationId!, - request.Id, request.FromUtc, request.ToUtc, cancellationToken); + request.Id!, request.FromUtc, request.ToUtc, cancellationToken); return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c }); } @@ -77,7 +78,7 @@ public async Task unavailabilities.HandleApplicationResult(c => new SearchAllCarUnavailabilitiesResponse @@ -89,7 +90,7 @@ public async Task> TakeOffline(TakeOfflin CancellationToken cancellationToken) { var car = await _carsApplication.TakeOfflineCarAsync(_callerFactory.Create(), request.OrganizationId!, - request.Id, request.FromUtc, request.ToUtc, cancellationToken); + request.Id!, request.FromUtc, request.ToUtc, cancellationToken); return () => car.HandleApplicationResult(c => new GetCarResponse { Car = c }); } } \ No newline at end of file diff --git a/src/EndUsersInfrastructure/Api/EndUsers/EndUsersApi.cs b/src/EndUsersInfrastructure/Api/EndUsers/EndUsersApi.cs index ba0d23b0..794bb5e6 100644 --- a/src/EndUsersInfrastructure/Api/EndUsers/EndUsersApi.cs +++ b/src/EndUsersInfrastructure/Api/EndUsers/EndUsersApi.cs @@ -22,7 +22,7 @@ public async Task> AssignPlatformRoles( AssignPlatformRolesRequest request, CancellationToken cancellationToken) { var user = - await _endUsersApplication.AssignPlatformRolesAsync(_callerFactory.Create(), request.Id, + await _endUsersApplication.AssignPlatformRolesAsync(_callerFactory.Create(), request.Id!, request.Roles ?? new List(), cancellationToken); return () => user.HandleApplicationResult(usr => @@ -33,7 +33,7 @@ public async Task> UnassignPlatformRoles( UnassignPlatformRolesRequest request, CancellationToken cancellationToken) { var user = - await _endUsersApplication.UnassignPlatformRolesAsync(_callerFactory.Create(), request.Id, + await _endUsersApplication.UnassignPlatformRolesAsync(_callerFactory.Create(), request.Id!, request.Roles ?? new List(), cancellationToken); return () => diff --git a/src/EndUsersInfrastructure/Api/Invitations/InvitationsApi.cs b/src/EndUsersInfrastructure/Api/Invitations/InvitationsApi.cs index 8307916a..eddef1f1 100644 --- a/src/EndUsersInfrastructure/Api/Invitations/InvitationsApi.cs +++ b/src/EndUsersInfrastructure/Api/Invitations/InvitationsApi.cs @@ -22,7 +22,7 @@ public async Task> AcceptGu VerifyGuestInvitationRequest request, CancellationToken cancellationToken) { var invitation = - await _invitationsApplication.VerifyGuestInvitationAsync(_callerFactory.Create(), request.Token, + await _invitationsApplication.VerifyGuestInvitationAsync(_callerFactory.Create(), request.Token!, cancellationToken); return () => invitation.HandleApplicationResult(invite => @@ -33,7 +33,7 @@ public async Task> InviteGuest( InviteGuestRequest request, CancellationToken cancellationToken) { var invitation = - await _invitationsApplication.InviteGuestAsync(_callerFactory.Create(), request.Email, + await _invitationsApplication.InviteGuestAsync(_callerFactory.Create(), request.Email!, cancellationToken); return () => invitation.HandleApplicationResult(invite => @@ -44,7 +44,7 @@ public async Task ResendGuestInvitation( ResendGuestInvitationRequest request, CancellationToken cancellationToken) { var invitation = - await _invitationsApplication.ResendGuestInvitationAsync(_callerFactory.Create(), request.Token, + await _invitationsApplication.ResendGuestInvitationAsync(_callerFactory.Create(), request.Token!, cancellationToken); return () => invitation.HandleApplicationResult(); diff --git a/src/EndUsersInfrastructure/Api/Memberships/MembershipsApi.cs b/src/EndUsersInfrastructure/Api/Memberships/MembershipsApi.cs index 1e7fbdc5..e8175b67 100644 --- a/src/EndUsersInfrastructure/Api/Memberships/MembershipsApi.cs +++ b/src/EndUsersInfrastructure/Api/Memberships/MembershipsApi.cs @@ -22,7 +22,7 @@ public async Task> ChangeDefaultOrga ChangeDefaultOrganizationRequest request, CancellationToken cancellationToken) { var user = await _endUsersApplication.ChangeDefaultMembershipAsync(_callerFactory.Create(), - request.OrganizationId, cancellationToken); + request.OrganizationId!, cancellationToken); return () => user.HandleApplicationResult(x => new GetUserResponse { User = x }); } diff --git a/src/IdentityInfrastructure/Api/APIKeys/APIKeysApi.cs b/src/IdentityInfrastructure/Api/APIKeys/APIKeysApi.cs index 64cd5bf4..a3bc7773 100644 --- a/src/IdentityInfrastructure/Api/APIKeys/APIKeysApi.cs +++ b/src/IdentityInfrastructure/Api/APIKeys/APIKeysApi.cs @@ -31,7 +31,7 @@ public async Task> Create( public async Task DeleteAPIKey(DeleteAPIKeyRequest request, CancellationToken cancellationToken) { - var key = await _apiKeysApplication.DeleteAPIKeyAsync(_callerFactory.Create(), request.Id, cancellationToken); + var key = await _apiKeysApplication.DeleteAPIKeyAsync(_callerFactory.Create(), request.Id!, cancellationToken); return () => key.HandleApplicationResult(); } diff --git a/src/IdentityInfrastructure/Api/AuthTokens/AuthTokensApi.cs b/src/IdentityInfrastructure/Api/AuthTokens/AuthTokensApi.cs index 710618af..c86eece7 100644 --- a/src/IdentityInfrastructure/Api/AuthTokens/AuthTokensApi.cs +++ b/src/IdentityInfrastructure/Api/AuthTokens/AuthTokensApi.cs @@ -23,7 +23,7 @@ public async Task> Refre CancellationToken cancellationToken) { var tokens = - await _authTokensApplication.RefreshTokenAsync(_callerFactory.Create(), request.RefreshToken, + await _authTokensApplication.RefreshTokenAsync(_callerFactory.Create(), request.RefreshToken!, cancellationToken); return () => tokens.HandleApplicationResult(tok => @@ -37,7 +37,7 @@ public async Task Revoke(RevokeRefreshTokenRequest request, CancellationToken cancellationToken) { var tokens = - await _authTokensApplication.RevokeRefreshTokenAsync(_callerFactory.Create(), request.RefreshToken, + await _authTokensApplication.RevokeRefreshTokenAsync(_callerFactory.Create(), request.RefreshToken!, cancellationToken); return () => tokens.HandleApplicationResult(); diff --git a/src/IdentityInfrastructure/Api/MachineCredentials/MachineCredentialsApi.cs b/src/IdentityInfrastructure/Api/MachineCredentials/MachineCredentialsApi.cs index c28fafd6..9482f5ec 100644 --- a/src/IdentityInfrastructure/Api/MachineCredentials/MachineCredentialsApi.cs +++ b/src/IdentityInfrastructure/Api/MachineCredentials/MachineCredentialsApi.cs @@ -22,7 +22,7 @@ public MachineCredentialsApi(ICallerContextFactory callerFactory, public async Task> RegisterMachine( RegisterMachineRequest request, CancellationToken cancellationToken) { - var machine = await _machineCredentialsApplication.RegisterMachineAsync(_callerFactory.Create(), request.Name, + var machine = await _machineCredentialsApplication.RegisterMachineAsync(_callerFactory.Create(), request.Name!, request.Timezone, request.CountryCode, request.ApiKeyExpiresOnUtc, cancellationToken); return () => machine.HandleApplicationResult(x => diff --git a/src/IdentityInfrastructure/Api/PasswordCredentials/PasswordCredentialsApi.cs b/src/IdentityInfrastructure/Api/PasswordCredentials/PasswordCredentialsApi.cs index af62ef84..bd1e5fd0 100644 --- a/src/IdentityInfrastructure/Api/PasswordCredentials/PasswordCredentialsApi.cs +++ b/src/IdentityInfrastructure/Api/PasswordCredentials/PasswordCredentialsApi.cs @@ -25,9 +25,8 @@ public async Task> Authe CancellationToken cancellationToken) { var authenticated = - await _passwordCredentialsApplication.AuthenticateAsync(_callerFactory.Create(), request.Username, - request.Password, - cancellationToken); + await _passwordCredentialsApplication.AuthenticateAsync(_callerFactory.Create(), request.Username!, + request.Password!, cancellationToken); return () => authenticated.HandleApplicationResult(tok => new PostResult(new AuthenticateResponse @@ -40,7 +39,7 @@ public async Task CompletePasswordReset(CompletePasswordResetReq CancellationToken cancellationToken) { var completion = await _passwordCredentialsApplication.CompletePasswordResetAsync(_callerFactory.Create(), - request.Token, request.Password, cancellationToken); + request.Token!, request.Password!, cancellationToken); return () => completion.HandleApplicationResult(); } @@ -50,8 +49,7 @@ public async Task ConfirmRegistration(ConfirmRegistrationPersonP { var result = await _passwordCredentialsApplication.ConfirmPersonRegistrationAsync(_callerFactory.Create(), - request.Token, - cancellationToken); + request.Token!, cancellationToken); return () => result.Match(() => new Result(), error => new Result(error)); @@ -63,8 +61,7 @@ public async Task token.HandleApplicationResult( @@ -77,8 +74,8 @@ public async Task credential.HandleApplicationResult(creds => @@ -89,7 +86,7 @@ public async Task RequestPasswordReset(InitiatePasswordResetRequ CancellationToken cancellationToken) { var reset = await _passwordCredentialsApplication.InitiatePasswordResetAsync(_callerFactory.Create(), - request.EmailAddress, cancellationToken); + request.EmailAddress!, cancellationToken); return () => reset.HandleApplicationResult(); } @@ -98,7 +95,7 @@ public async Task ResendPasswordReset(ResendPasswordResetRequest CancellationToken cancellationToken) { var resent = - await _passwordCredentialsApplication.ResendPasswordResetAsync(_callerFactory.Create(), request.Token, + await _passwordCredentialsApplication.ResendPasswordResetAsync(_callerFactory.Create(), request.Token!, cancellationToken); return () => resent.HandleApplicationResult(); @@ -108,7 +105,7 @@ public async Task VerifyPasswordReset(VerifyPasswordResetRequest CancellationToken cancellationToken) { var verified = - await _passwordCredentialsApplication.VerifyPasswordResetAsync(_callerFactory.Create(), request.Token, + await _passwordCredentialsApplication.VerifyPasswordResetAsync(_callerFactory.Create(), request.Token!, cancellationToken); return () => verified.HandleApplicationResult(); diff --git a/src/IdentityInfrastructure/Api/SSO/SingleSignOnApi.cs b/src/IdentityInfrastructure/Api/SSO/SingleSignOnApi.cs index e02aeabf..e39c3f1f 100644 --- a/src/IdentityInfrastructure/Api/SSO/SingleSignOnApi.cs +++ b/src/IdentityInfrastructure/Api/SSO/SingleSignOnApi.cs @@ -24,8 +24,7 @@ public async Task> Authe { var authenticated = await _singleSignOnApplication.AuthenticateAsync(_callerFactory.Create(), request.InvitationToken, - request.Provider, - request.AuthCode, request.Username, cancellationToken); + request.Provider!, request.AuthCode!, request.Username, cancellationToken); return () => authenticated.HandleApplicationResult(tok => new PostResult(new AuthenticateResponse diff --git a/src/ImagesInfrastructure/Api/Images/ImagesApi.cs b/src/ImagesInfrastructure/Api/Images/ImagesApi.cs index 770d33b8..22a5c323 100644 --- a/src/ImagesInfrastructure/Api/Images/ImagesApi.cs +++ b/src/ImagesInfrastructure/Api/Images/ImagesApi.cs @@ -28,7 +28,7 @@ public ImagesApi(IHttpContextAccessor httpContextAccessor, IFileUploadService fi public async Task DownloadImage(DownloadImageRequest request, CancellationToken cancellationToken) { - var download = await _application.DownloadImageAsync(_callerFactory.Create(), request.Id, cancellationToken); + var download = await _application.DownloadImageAsync(_callerFactory.Create(), request.Id!, cancellationToken); return () => download.HandleApplicationResult(x => new StreamResult(x.Stream, x.ContentType)); } @@ -36,14 +36,14 @@ public async Task DownloadImage(DownloadImageRequest request, public async Task> GetImage(GetImageRequest request, CancellationToken cancellationToken) { - var image = await _application.GetImageAsync(_callerFactory.Create(), request.Id, cancellationToken); + var image = await _application.GetImageAsync(_callerFactory.Create(), request.Id!, cancellationToken); return () => image.HandleApplicationResult(x => new GetImageResponse { Image = x }); } public async Task ImageDelete(DeleteImageRequest request, CancellationToken cancellationToken) { - var image = await _application.DeleteImageAsync(_callerFactory.Create(), request.Id, cancellationToken); + var image = await _application.DeleteImageAsync(_callerFactory.Create(), request.Id!, cancellationToken); return () => image.HandleApplicationResult(); } @@ -51,7 +51,7 @@ public async Task ImageDelete(DeleteImageRequest request, Cance public async Task> UpdateImage(UpdateImageRequest request, CancellationToken cancellationToken) { - var image = await _application.UpdateImageAsync(_callerFactory.Create(), request.Id, request.Description, + var image = await _application.UpdateImageAsync(_callerFactory.Create(), request.Id!, request.Description, cancellationToken); return () => diff --git a/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs b/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs index 309641fa..2c503ffb 100644 --- a/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs +++ b/src/Infrastructure.Web.Api.Common/Extensions/OperationMethodExtensions.cs @@ -19,4 +19,17 @@ public static HttpMethod ToHttpMethod(this OperationMethod method) _ => HttpMethod.Get }; } + + /// + /// Whether the specified could have a request body + /// + public static bool CanHaveBody(this OperationMethod method) + { + return method switch + { + OperationMethod.Post => true, + OperationMethod.PutPatch => true, + _ => false + }; + } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ApiDocsSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ApiDocsSpec.cs index 912c07fd..050f24d6 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ApiDocsSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ApiDocsSpec.cs @@ -1,4 +1,3 @@ -using ApiHost1; using FluentAssertions; using HtmlAgilityPack; using Infrastructure.Hosting.Common; @@ -9,9 +8,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class ApiDocsSpec : WebApiSpec +public class ApiDocsSpec : WebApiSpec { - public ApiDocsSpec(WebApiSetup setup) : base(setup) + public ApiDocsSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs index da841bda..fabcfbef 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/AuthNApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using Application.Resources.Shared; using Common.Configuration; using Domain.Interfaces; @@ -18,12 +17,12 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class AuthNApiSpec : WebApiSpec +public class AuthNApiSpec : WebApiSpec { private readonly IConfigurationSettings _settings; private readonly ITokensService _tokensService; - public AuthNApiSpec(WebApiSetup setup) : base(setup) + public AuthNApiSpec(WebApiSetup setup) : base(setup) { EmptyAllRepositories(); _settings = setup.GetRequiredService(); diff --git a/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs index d6180b7c..3260388a 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/AuthZApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using Application.Resources.Shared; using Common.Configuration; using Domain.Interfaces; @@ -17,12 +16,12 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class AuthZApiSpec : WebApiSpec +public class AuthZApiSpec : WebApiSpec { private readonly IConfigurationSettings _settings; private readonly ITokensService _tokensService; - public AuthZApiSpec(WebApiSetup setup) : base(setup) + public AuthZApiSpec(WebApiSetup setup) : base(setup) { EmptyAllRepositories(); _settings = setup.GetRequiredService(); diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs index 0b626483..e4468a8b 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ContentNegotiationApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; @@ -11,9 +10,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class ContentNegotiationApiSpec : WebApiSpec +public class ContentNegotiationApiSpec : WebApiSpec { - public ContentNegotiationApiSpec(WebApiSetup setup) : base(setup) + public ContentNegotiationApiSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs index 7cc54315..b42c5d96 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/DataFormatsApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Text; -using ApiHost1; using Common.Extensions; using FluentAssertions; using Infrastructure.Web.Api.Interfaces; @@ -13,9 +12,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class DataFormatsApiSpec : WebApiSpec +public class DataFormatsApiSpec : WebApiSpec { - public DataFormatsApiSpec(WebApiSetup setup) : base(setup) + public DataFormatsApiSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/DefaultStatusCodeApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/DefaultStatusCodeApiSpec.cs index da34578a..9e3ff999 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/DefaultStatusCodeApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/DefaultStatusCodeApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; @@ -10,9 +9,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class DefaultStatusCodeApiSpec : WebApiSpec +public class DefaultStatusCodeApiSpec : WebApiSpec { - public DefaultStatusCodeApiSpec(WebApiSetup setup) : base(setup) + public DefaultStatusCodeApiSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ErrorApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ErrorApiSpec.cs index 921f61de..f69f4288 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ErrorApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ErrorApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; @@ -10,9 +9,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class ErrorApiSpec : WebApiSpec +public class ErrorApiSpec : WebApiSpec { - public ErrorApiSpec(WebApiSetup setup) : base(setup) + public ErrorApiSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/HealthCheckApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/HealthCheckApiSpec.cs index 01201ca8..1c68f5f1 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/HealthCheckApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/HealthCheckApiSpec.cs @@ -1,4 +1,3 @@ -using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Operations.Shared.Health; using IntegrationTesting.WebApi.Common; @@ -8,9 +7,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class HealthCheckApiSpec : WebApiSpec +public class HealthCheckApiSpec : WebApiSpec { - public HealthCheckApiSpec(WebApiSetup setup) : base(setup) + public HealthCheckApiSpec(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs index dc6240bb..10cd138d 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/MultiTenancySpec.cs @@ -1,5 +1,4 @@ using System.Net; -using ApiHost1; using Application.Resources.Shared; using CarsDomain; using Common.Extensions; @@ -27,9 +26,9 @@ namespace Infrastructure.Web.Api.IntegrationTests; [Trait("Category", "Integration.API")] [Collection("API")] -public class MultiTenancySpec : WebApiSpec +public class MultiTenancySpec : WebApiSpec { - public MultiTenancySpec(WebApiSetup setup) : base(setup, OverrideDependencies) + public MultiTenancySpec(WebApiSetup setup) : base(setup, OverrideDependencies) { EmptyAllRepositories(); DeleteAllPreviousTenants(StubTenantSettingsService.GetRepositoryPath(null)); diff --git a/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs index fe1b48f7..25a8ec65 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/RequestCorrelationApiSpec.cs @@ -1,6 +1,5 @@ #if TESTINGONLY using System.Net; -using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; @@ -15,9 +14,9 @@ public class RequestCorrelationApiSpec { [Trait("Category", "Integration.API")] [Collection("API")] - public class GivenAnHttpClient : WebApiSpec + public class GivenAnHttpClient : WebApiSpec { - public GivenAnHttpClient(WebApiSetup setup) : base(setup) + public GivenAnHttpClient(WebApiSetup setup) : base(setup) { } @@ -67,9 +66,9 @@ public async Task WhenGetWithXCorrelationId_ThenReturnsSameResponseHeader() [Trait("Category", "Integration.API")] [Collection("API")] - public class GivenAJsonClient : WebApiSpec + public class GivenAJsonClient : WebApiSpec { - public GivenAJsonClient(WebApiSetup setup) : base(setup) + public GivenAJsonClient(WebApiSetup setup) : base(setup) { } diff --git a/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs b/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs index 9dc17db3..11786513 100644 --- a/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs +++ b/src/Infrastructure.Web.Api.IntegrationTests/ValidationApiSpec.cs @@ -1,78 +1,262 @@ #if TESTINGONLY using System.Net; +using System.Text.Json; using ApiHost1; using FluentAssertions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Api.Operations.Shared.TestingOnly; using IntegrationTesting.WebApi.Common; +using JetBrains.Annotations; using Xunit; namespace Infrastructure.Web.Api.IntegrationTests; -[Trait("Category", "Integration.API")] -[Collection("API")] -public class ValidationApiSpec : WebApiSpec +[UsedImplicitly] +public class ValidationApiSpec { - public ValidationApiSpec(WebApiSetup setup) : base(setup) + [Trait("Category", "Integration.API")] + [Collection("API")] + public class GivenAnUnvalidatedRequest : WebApiSpec { - } + public GivenAnUnvalidatedRequest(WebApiSetup setup) : base(setup) + { + } - [Fact] - public async Task WhenGetUnvalidatedRequest_ThenReturns200() - { - var result = await Api.GetAsync(new ValidationsUnvalidatedTestingOnlyRequest()); + [Fact] + public async Task WhenGetRequest_ThenReturns200() + { + var result = await Api.GetAsync(new ValidationsUnvalidatedTestingOnlyRequest()); - result.StatusCode.Should().Be(HttpStatusCode.OK); - result.Content.Value.Message.Should().Be("amessage"); + result.StatusCode.Should().Be(HttpStatusCode.OK); + result.Content.Value.Message.Should().Be("amessage"); + } } - [Fact] - public async Task WhenGetValidatedRequestWithInvalidFields_ThenReturnsValidationError() + [Trait("Category", "Integration.API")] + [Collection("API")] + public class GivenAValidatedGetRequest : WebApiSpec { - var result = await Api.GetAsync(new ValidationsValidatedTestingOnlyRequest { Id = "1234" }); - - result.StatusCode.Should().Be(HttpStatusCode.BadRequest); - result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); - result.Content.Error.Title.Should().Be("Bad Request"); - result.Content.Error.Status.Should().Be(400); - result.Content.Error.Detail.Should().Be("'Field1' must not be empty."); - result.Content.Error.Instance.Should().Match(@"https://localhost:?????/testingonly/validations/validated/1234"); - result.Content.Error.Exception.Should().BeNull(); - result.Content.Error.Errors.Should().BeEquivalentTo(new ValidatorProblem[] + public GivenAValidatedGetRequest(WebApiSetup setup) : base(setup) { - new() { Rule = "NotEmptyValidator", Reason = "'Field1' must not be empty.", Value = null }, - new() { Rule = "NotEmptyValidator", Reason = "'Field2' must not be empty.", Value = null } - }); - } + } - [Fact] - public async Task WhenGetValidatedRequestWithPartialInvalidFields_ThenReturnsValidationError() - { - var result = await Api.GetAsync(new ValidationsValidatedTestingOnlyRequest { Id = "1234", Field1 = "123" }); - - result.StatusCode.Should().Be(HttpStatusCode.BadRequest); - result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); - result.Content.Error.Title.Should().Be("Bad Request"); - result.Content.Error.Status.Should().Be(400); - result.Content.Error.Detail.Should().Be("'Field2' must not be empty."); - result.Content.Error.Instance.Should() - .Match("https://localhost:?????/testingonly/validations/validated/1234?field1=123"); - result.Content.Error.Exception.Should().BeNull(); - result.Content.Error.Errors.Should().BeEquivalentTo(new ValidatorProblem[] + [Fact] + public async Task WhenGetRequestWithNullPathField_ThenReturnsMissingError() + { + var result = await Api.GetAsync(new ValidationsValidatedGetTestingOnlyRequest + { + Id = null!, + RequiredField = null! + }); + + result.StatusCode.Should().Be(HttpStatusCode.NotFound); + result.Content.Error.Type.Should().BeNull(); + result.Content.Error.Title.Should().Be("Not Found"); + result.Content.Error.Status.Should().Be(404); + result.Content.Error.Detail.Should().BeNull(); + result.Content.Error.Instance.Should().BeNull(); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors.Should().BeNull(); + } + + [Fact] + public async Task WhenGetRequestWithNullRequiredField_ThenReturnsValidationError() + { + var result = await Api.GetAsync(new ValidationsValidatedGetTestingOnlyRequest + { + Id = "1234", + RequiredField = null! + }); + + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); + result.Content.Error.Status.Should().Be(400); + result.Content.Error.Detail.Should().Be("'Required Field' must not be empty."); + result.Content.Error.Instance.Should() + .Match(@"https://localhost:?????/testingonly/validations/validated/1234"); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors.Should().BeEquivalentTo(new ValidatorProblem[] + { + new() + { + Rule = "NotEmptyValidator", Reason = "'Required Field' must not be empty.", Value = null + } + }); + } + + [Fact] + public async Task WhenGetRequestWithInvalidFields_ThenReturnsValidationError() + { + var result = await Api.GetAsync(new ValidationsValidatedGetTestingOnlyRequest + { + Id = "1234", + RequiredField = "invalid", + OptionalField = "invalid" + }); + + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); + result.Content.Error.Status.Should().Be(400); + result.Content.Error.Detail.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + result.Content.Error.Instance.Should() + .Match( + "https://localhost:?????/testingonly/validations/validated/1234?optionalfield=invalid&requiredfield=invalid"); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors!.Length.Should().Be(2); + result.Content.Error.Errors[0].Rule.Should().Be("RegularExpressionValidator"); + result.Content.Error.Errors[0].Reason.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + result.Content.Error.Errors[0].Value.As().GetString().Should().Be("invalid"); + result.Content.Error.Errors[1].Rule.Should().Be("RegularExpressionValidator"); + result.Content.Error.Errors[1].Reason.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidOptionalField); + result.Content.Error.Errors[1].Value.As().GetString().Should().Be("invalid"); + } + + [Fact] + public async Task WhenGetRequestWithOnlyRequiredFields_ThenReturnsResponse() + { + var result = + await Api.GetAsync(new ValidationsValidatedGetTestingOnlyRequest + { + Id = "1234", + RequiredField = "123" + }); + + result.StatusCode.Should().Be(HttpStatusCode.OK); + result.Content.Value.Message.Should().Be("amessage123"); + } + + [Fact] + public async Task WhenGetRequestWithAllFields_ThenReturnsResponse() { - new() { Rule = "NotEmptyValidator", Reason = "'Field2' must not be empty.", Value = null } - }); + var result = + await Api.GetAsync(new ValidationsValidatedGetTestingOnlyRequest + { + Id = "1234", + RequiredField = "123", + OptionalField = "789" + }); + + result.StatusCode.Should().Be(HttpStatusCode.OK); + result.Content.Value.Message.Should().Be("amessage123"); + } } - [Fact] - public async Task WhenGetValidatedRequestWithValidId_ThenReturnsResponse() + [Trait("Category", "Integration.API")] + [Collection("API")] + public class GivenAValidatedPostRequest : WebApiSpec { - var result = - await Api.GetAsync(new ValidationsValidatedTestingOnlyRequest - { Id = "1234", Field1 = "123", Field2 = "456" }); + public GivenAValidatedPostRequest(WebApiSetup setup) : base(setup) + { + } + + [Fact] + public async Task WhenGetRequestWithNullPathField_ThenReturnsMissingError() + { + var result = await Api.PostAsync(new ValidationsValidatedPostTestingOnlyRequest + { + Id = null!, + RequiredField = null! + }); + + result.StatusCode.Should().Be(HttpStatusCode.NotFound); + result.Content.Error.Type.Should().BeNull(); + result.Content.Error.Title.Should().Be("Not Found"); + result.Content.Error.Status.Should().Be(404); + result.Content.Error.Detail.Should().BeNull(); + result.Content.Error.Instance.Should().BeNull(); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors.Should().BeNull(); + } + + [Fact] + public async Task WhenPostRequestWithNullRequiredField_ThenReturnsValidationError() + { + var result = await Api.PostAsync(new ValidationsValidatedPostTestingOnlyRequest + { + Id = "1234", + RequiredField = null! + }); + + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); + result.Content.Error.Status.Should().Be(400); + result.Content.Error.Detail.Should().Be("'Required Field' must not be empty."); + result.Content.Error.Instance.Should() + .Match(@"https://localhost:?????/testingonly/validations/validated/1234"); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors.Should().BeEquivalentTo(new ValidatorProblem[] + { + new() + { + Rule = "NotEmptyValidator", Reason = "'Required Field' must not be empty.", Value = null + } + }); + } + + [Fact] + public async Task WhenPostRequestWithInvalidFields_ThenReturnsValidationError() + { + var result = await Api.PostAsync(new ValidationsValidatedPostTestingOnlyRequest + { + Id = "1234", + RequiredField = "invalid", + OptionalField = "invalid" + }); + + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Content.Error.Type.Should().Be("https://datatracker.ietf.org/doc/html/rfc9110#section-15.5"); + result.Content.Error.Title.Should().Be("Bad Request"); + result.Content.Error.Status.Should().Be(400); + result.Content.Error.Detail.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + result.Content.Error.Instance.Should() + .Match("https://localhost:?????/testingonly/validations/validated/1234"); + result.Content.Error.Exception.Should().BeNull(); + result.Content.Error.Errors!.Length.Should().Be(2); + result.Content.Error.Errors[0].Rule.Should().Be("RegularExpressionValidator"); + result.Content.Error.Errors[0].Reason.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidRequiredField); + result.Content.Error.Errors[0].Value.As().GetString().Should().Be("invalid"); + result.Content.Error.Errors[1].Rule.Should().Be("RegularExpressionValidator"); + result.Content.Error.Errors[1].Reason.Should() + .Be(Resources.GetTestingOnlyValidatedRequestValidator_InvalidOptionalField); + result.Content.Error.Errors[1].Value.As().GetString().Should().Be("invalid"); + } + + [Fact] + public async Task WhenPostRequestWithOnlyRequiredFields_ThenReturnsResponse() + { + var result = + await Api.PostAsync(new ValidationsValidatedPostTestingOnlyRequest + { + Id = "1234", + RequiredField = "123" + }); + + result.StatusCode.Should().Be(HttpStatusCode.Created); + result.Content.Value.Message.Should().Be("amessage123"); + } + + [Fact] + public async Task WhenPostRequestWithAllFields_ThenReturnsResponse() + { + var result = + await Api.PostAsync(new ValidationsValidatedPostTestingOnlyRequest + { + Id = "1234", + RequiredField = "123", + OptionalField = "789" + }); - result.StatusCode.Should().Be(HttpStatusCode.OK); - result.Content.Value.Message.Should().Be("amessage123"); + result.StatusCode.Should().Be(HttpStatusCode.Created); + result.Content.Value.Message.Should().Be("amessage123"); + } } } #endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Interfaces/AuthorizeAttribute.cs b/src/Infrastructure.Web.Api.Interfaces/AuthorizeAttribute.cs index 4d73bbb7..b7d14da7 100644 --- a/src/Infrastructure.Web.Api.Interfaces/AuthorizeAttribute.cs +++ b/src/Infrastructure.Web.Api.Interfaces/AuthorizeAttribute.cs @@ -121,20 +121,20 @@ internal static string FormatRoleName(Roles flag) ? new ICallerContext.CallerRoles() : new ICallerContext.CallerRoles(policy.Roles.Platform is not null ? policy.Roles.Platform.Select(rol => rol.ToRoleByName(RoleAndFeatureScope.Platform)).ToArray() - : Array.Empty(), policy.Roles.Tenant is not null + : [], policy.Roles.Tenant is not null ? policy.Roles.Tenant.Select(rol => rol.ToRoleByName(RoleAndFeatureScope.Tenant)).ToArray() - : Array.Empty()); + : []); var features = policy.Features is null ? new ICallerContext.CallerFeatures() : new ICallerContext.CallerFeatures(policy.Features.Platform is not null ? policy.Features.Platform .Select(feat => feat.ToFeatureByName(RoleAndFeatureScope.Platform)) .ToArray() - : Array.Empty(), + : [], policy.Features.Tenant is not null ? policy.Features.Tenant.Select(feat => feat.ToFeatureByName(RoleAndFeatureScope.Tenant)) .ToArray() - : Array.Empty()); + : []); return (roles, features); }) .ToList(); diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateRequest.cs index d288edb8..fdd00e84 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateRequest.cs @@ -9,11 +9,9 @@ public class { [JsonPropertyName("enabled")] public bool Enabled { get; set; } - public required string EnvironmentApiKey { get; set; } + public string? EnvironmentApiKey { get; set; } [JsonPropertyName("feature")] public int Feature { get; set; } - public required string IdentityUuid { get; set; } -} - -public class FlagsmithCreateEdgeIdentityFeatureStateResponse : IWebResponse; \ No newline at end of file + public string? IdentityUuid { get; set; } +} \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateResponse.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateResponse.cs new file mode 100644 index 00000000..7d508ea0 --- /dev/null +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityFeatureStateResponse.cs @@ -0,0 +1,5 @@ +using Infrastructure.Web.Api.Interfaces; + +namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; + +public class FlagsmithCreateEdgeIdentityFeatureStateResponse : IWebResponse; \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityRequest.cs index d9099c83..2e76b7a8 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateEdgeIdentityRequest.cs @@ -8,9 +8,9 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [UsedImplicitly] public class FlagsmithCreateEdgeIdentityRequest : IWebRequest { - public required string EnvironmentApiKey { get; set; } + public string? EnvironmentApiKey { get; set; } - [JsonPropertyName("identifier")] public required string Identifier { get; set; } + [JsonPropertyName("identifier")] public string? Identifier { get; set; } [JsonPropertyName("traits")] public List? Traits { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureRequest.cs index 4529793f..21c3eac3 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureRequest.cs @@ -6,7 +6,7 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/projects/{ProjectId}/features/", OperationMethod.Post)] public class FlagsmithCreateFeatureRequest : IWebRequest { - [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } - public required int ProjectId { get; set; } + public int ProjectId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureStateRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureStateRequest.cs index 109530ca..070aee0a 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureStateRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithCreateFeatureStateRequest.cs @@ -8,7 +8,7 @@ public class FlagsmithCreateFeatureStateRequest : IWebRequest { - [JsonPropertyName("identifier")] public required string Identifier { get; set; } + [JsonPropertyName("identifier")] public string? Identifier { get; set; } - [JsonPropertyName("traits")] public required List Traits { get; set; } + [JsonPropertyName("traits")] public List Traits { get; set; } = new(); } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteEdgeIdentitiesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteEdgeIdentitiesRequest.cs index d92ee8f2..ecc35886 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteEdgeIdentitiesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteEdgeIdentitiesRequest.cs @@ -5,7 +5,7 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/environments/{EnvironmentApiKey}/edge-identities/{IdentityUuid}/", OperationMethod.Delete)] public class FlagsmithDeleteEdgeIdentitiesRequest : IWebRequest { - public required string EnvironmentApiKey { get; set; } + public string? EnvironmentApiKey { get; set; } - public required string IdentityUuid { get; set; } + public string? IdentityUuid { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteFeatureRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteFeatureRequest.cs index e8dd7674..d2184447 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteFeatureRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithDeleteFeatureRequest.cs @@ -5,7 +5,7 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/projects/{ProjectId}/features/{FeatureId}/", OperationMethod.Delete)] public class FlagsmithDeleteFeatureRequest : IWebRequest { - public required int FeatureId { get; set; } + public int? FeatureId { get; set; } - public required int ProjectId { get; set; } + public int? ProjectId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetEdgeIdentitiesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetEdgeIdentitiesRequest.cs index be3c8078..b2eb5d01 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetEdgeIdentitiesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetEdgeIdentitiesRequest.cs @@ -5,5 +5,5 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/environments/{EnvironmentApiKey}/edge-identities/", OperationMethod.Get)] public class FlagsmithGetEdgeIdentitiesRequest : IWebRequest { - public required string EnvironmentApiKey { get; set; } + public string? EnvironmentApiKey { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeatureStatesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeatureStatesRequest.cs index 553efd1a..cdff9565 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeatureStatesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeatureStatesRequest.cs @@ -6,7 +6,7 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/environments/{EnvironmentApiKey}/featurestates/", OperationMethod.Post)] public class FlagsmithGetFeatureStatesRequest : IWebRequest { - public required string EnvironmentApiKey { get; set; } + public string? EnvironmentApiKey { get; set; } [JsonPropertyName("feature")] public int Feature { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeaturesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeaturesRequest.cs index 409f8594..cfa89631 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeaturesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Flagsmith/FlagsmithGetFeaturesRequest.cs @@ -5,5 +5,5 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.Flagsmith; [Route("/projects/{ProjectId}/features/", OperationMethod.Get)] public class FlagsmithGetFeaturesRequest : IWebRequest { - public int ProjectId { get; set; } + public int? ProjectId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs index 6a573ec2..f2a5b8ac 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/Gravatar/GravatarGetImageRequest.cs @@ -10,7 +10,7 @@ public class GravatarGetImageRequest : IWebRequestStream { [JsonPropertyName("d")] public string? Default { get; set; } - public required string Hash { get; set; } + public string? Hash { get; set; } [JsonPropertyName("s")] public int? Width { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/OAuth2/ExchangeOAuth2CodeForTokensRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/OAuth2/ExchangeOAuth2CodeForTokensRequest.cs index c75c8819..1f86565e 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/OAuth2/ExchangeOAuth2CodeForTokensRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/3rdParties/OAuth2/ExchangeOAuth2CodeForTokensRequest.cs @@ -6,15 +6,15 @@ namespace Infrastructure.Web.Api.Operations.Shared._3rdParties.OAuth2; [Route("/auth/token", OperationMethod.Post)] public class ExchangeOAuth2CodeForTokensRequest : UnTenantedRequest { - [JsonPropertyName("client_id")] public required string ClientId { get; set; } + [JsonPropertyName("client_id")] public string? ClientId { get; set; } [JsonPropertyName("client_secret")] public string? ClientSecret { get; set; } - [JsonPropertyName("code")] public required string Code { get; set; } + [JsonPropertyName("code")] public string? Code { get; set; } - [JsonPropertyName("grant_type")] public required string GrantType { get; set; } + [JsonPropertyName("grant_type")] public string? GrantType { get; set; } - [JsonPropertyName("redirect_uri")] public required string RedirectUri { get; set; } + [JsonPropertyName("redirect_uri")] public string? RedirectUri { get; set; } [JsonPropertyName("scope")] public string? Scope { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverAuditRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverAuditRequest.cs index f1439c68..0106ad2a 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverAuditRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverAuditRequest.cs @@ -1,4 +1,5 @@ -using Infrastructure.Web.Api.Interfaces; +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Authorize(Roles.Platform_ServiceAccount)] public class DeliverAuditRequest : UnTenantedRequest { - public required string Message { get; set; } + [Required] public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverEmailRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverEmailRequest.cs index d7732a54..7a0f2a7e 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverEmailRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverEmailRequest.cs @@ -1,4 +1,5 @@ -using Infrastructure.Web.Api.Interfaces; +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Authorize(Roles.Platform_ServiceAccount)] public class DeliverEmailRequest : UnTenantedRequest { - public required string Message { get; set; } + [Required] public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverUsageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverUsageRequest.cs index 8c92554e..1578caae 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverUsageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/DeliverUsageRequest.cs @@ -1,4 +1,5 @@ -using Infrastructure.Web.Api.Interfaces; +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Authorize(Roles.Platform_ServiceAccount)] public class DeliverUsageRequest : UnTenantedRequest { - public required string Message { get; set; } + [Required] public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagForCallerRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagForCallerRequest.cs index d0a0f340..c6631e92 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagForCallerRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagForCallerRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Route("/flags/{Name}", OperationMethod.Get)] public class GetFeatureFlagForCallerRequest : UnTenantedRequest { - public required string Name { get; set; } + [Required] public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagRequest.cs index 4e0bd085..051f3951 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/GetFeatureFlagRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -6,9 +7,9 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Authorize(Roles.Platform_ServiceAccount)] public class GetFeatureFlagRequest : UnTenantedRequest { - public required string Name { get; set; } + [Required] public string? Name { get; set; } public string? TenantId { get; set; } - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/NotifyProvisioningRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/NotifyProvisioningRequest.cs index 40d0ab51..c681a3c3 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/NotifyProvisioningRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/NotifyProvisioningRequest.cs @@ -1,4 +1,5 @@ -using Infrastructure.Web.Api.Interfaces; +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; [Authorize(Roles.Platform_ServiceAccount)] public class NotifyProvisioningRequest : UnTenantedRequest { - public required string Message { get; set; } + [Required] public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordMeasureRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordMeasureRequest.cs index 2474ffd4..b222e1fe 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordMeasureRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordMeasureRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -8,5 +9,5 @@ public class RecordMeasureRequest : UnTenantedEmptyRequest { public Dictionary? Additional { get; set; } - public required string EventName { get; set; } + [Required] public string? EventName { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordUsageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordUsageRequest.cs index 90e1e03c..3c62efbb 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordUsageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Ancillary/RecordUsageRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Ancillary; @@ -8,5 +9,5 @@ public class RecordUseRequest : UnTenantedEmptyRequest { public Dictionary? Additional { get; set; } - public required string EventName { get; set; } + [Required] public string? EventName { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/AuthenticateRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/AuthenticateRequest.cs index d8cc580f..177517af 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/AuthenticateRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/AuthenticateRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -9,7 +10,7 @@ public class AuthenticateRequest : UnTenantedRequest public string? Password { get; set; } - public required string Provider { get; set; } + [Required] public string? Provider { get; set; } public string? Username { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/GetFeatureFlagForCallerRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/GetFeatureFlagForCallerRequest.cs index a0d3f5ab..84bc4bac 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/GetFeatureFlagForCallerRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/GetFeatureFlagForCallerRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; [Route("/flags/{Name}", OperationMethod.Get)] public class GetFeatureFlagForCallerRequest : UnTenantedRequest { - public required string Name { get; set; } + [Required] public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordCrashRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordCrashRequest.cs index 8fc600bf..8914e9ba 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordCrashRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordCrashRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; [Route("/record/crash", OperationMethod.Post)] public class RecordCrashRequest : UnTenantedEmptyRequest { - public required string Message { get; set; } + [Required] public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordMeasureRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordMeasureRequest.cs index 2646b1fe..70eda573 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordMeasureRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordMeasureRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -7,5 +8,5 @@ public class RecordMeasureRequest : UnTenantedEmptyRequest { public Dictionary? Additional { get; set; } - public required string EventName { get; set; } + [Required] public string? EventName { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordPageViewRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordPageViewRequest.cs index 1f6e6736..dc29025d 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordPageViewRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordPageViewRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; [Route("/record/page_view", OperationMethod.Post)] public class RecordPageViewRequest : UnTenantedEmptyRequest { - public required string Path { get; set; } + [Required] public string? Path { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordTraceRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordTraceRequest.cs index e938fe7b..0f5fa0d8 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordTraceRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordTraceRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -7,7 +8,7 @@ public class RecordTraceRequest : UnTenantedEmptyRequest { public List? Arguments { get; set; } - public required string Level { get; set; } + [Required] public string? Level { get; set; } - public required string MessageTemplate { get; set; } + [Required] public string? MessageTemplate { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordUsageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordUsageRequest.cs index 7a42a3a5..09c79b36 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordUsageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/BackEndForFrontEnd/RecordUsageRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.BackEndForFrontEnd; @@ -7,5 +8,5 @@ public class RecordUseRequest : UnTenantedEmptyRequest { public Dictionary? Additional { get; set; } - public required string EventName { get; set; } + [Required] public string? EventName { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Bookings/CancelBookingRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Bookings/CancelBookingRequest.cs index 49927de5..652cdbb3 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Bookings/CancelBookingRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Bookings/CancelBookingRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Bookings; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Bookings; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class CancelBookingRequest : TenantedDeleteRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Bookings/MakeBookingRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Bookings/MakeBookingRequest.cs index 33e44370..f89f24a4 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Bookings/MakeBookingRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Bookings/MakeBookingRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Bookings; @@ -6,9 +7,9 @@ namespace Infrastructure.Web.Api.Operations.Shared.Bookings; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class MakeBookingRequest : TenantedRequest { - public required string CarId { get; set; } + [Required] public string? CarId { get; set; } public DateTime? EndUtc { get; set; } - public required DateTime StartUtc { get; set; } + [Required] public DateTime? StartUtc { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/DeleteCarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/DeleteCarRequest.cs index 2d10024a..2d7862fe 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/DeleteCarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/DeleteCarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class DeleteCarRequest : TenantedDeleteRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/GetCarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/GetCarRequest.cs index 4de1637f..135bc417 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/GetCarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/GetCarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Authorize(Roles.Tenant_Member, Features.Tenant_Basic)] public class GetCarRequest : TenantedRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/RegisterCarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/RegisterCarRequest.cs index d8eafc27..05971273 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/RegisterCarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/RegisterCarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,13 +7,13 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class RegisterCarRequest : TenantedRequest { - public required string Jurisdiction { get; set; } + [Required] public string? Jurisdiction { get; set; } - public required string Make { get; set; } + [Required] public string? Make { get; set; } - public required string Model { get; set; } + [Required] public string? Model { get; set; } - public required string NumberPlate { get; set; } + [Required] public string? NumberPlate { get; set; } - public required int Year { get; set; } + [Required] public int Year { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReleaseCarAvailabilityRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReleaseCarAvailabilityRequest.cs index 8f6910fc..cf573c2b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReleaseCarAvailabilityRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReleaseCarAvailabilityRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,9 +7,9 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class ReleaseCarAvailabilityRequest : TenantedRequest { - public required DateTime FromUtc { get; set; } + [Required] public DateTime? FromUtc { get; set; } - public required string Id { get; set; } + [Required] public string? Id { get; set; } - public required DateTime ToUtc { get; set; } + [Required] public DateTime? ToUtc { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReserveCarIfAvailableRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReserveCarIfAvailableRequest.cs index 2634baef..1427da7a 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReserveCarIfAvailableRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ReserveCarIfAvailableRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,11 +7,11 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Authorize(Roles.Tenant_Member, Features.Tenant_PaidTrial)] public class ReserveCarIfAvailableRequest : TenantedRequest { - public required DateTime FromUtc { get; set; } + [Required] public DateTime? FromUtc { get; set; } - public required string Id { get; set; } + [Required] public string? Id { get; set; } - public required string ReferenceId { get; set; } + [Required] public string? ReferenceId { get; set; } - public required DateTime ToUtc { get; set; } + [Required] public DateTime? ToUtc { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ScheduleMaintenanceCarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ScheduleMaintenanceCarRequest.cs index 38c2c209..11aa545b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/ScheduleMaintenanceCarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/ScheduleMaintenanceCarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -8,7 +9,7 @@ public class ScheduleMaintenanceCarRequest : TenantedRequest { public DateTime FromUtc { get; set; } - public required string Id { get; set; } + [Required] public string? Id { get; set; } public DateTime ToUtc { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/SearchAllCarUnavailabilitiesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/SearchAllCarUnavailabilitiesRequest.cs index 19daf482..57754c9c 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/SearchAllCarUnavailabilitiesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/SearchAllCarUnavailabilitiesRequest.cs @@ -1,4 +1,5 @@ #if TESTINGONLY +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -6,6 +7,6 @@ namespace Infrastructure.Web.Api.Operations.Shared.Cars; [Route("/cars/{Id}/unavailabilities", OperationMethod.Search, isTestingOnly: true)] public class SearchAllCarUnavailabilitiesRequest : TenantedSearchRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } #endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Cars/TakeOfflineCarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Cars/TakeOfflineCarRequest.cs index e4a3ca84..2873de6d 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Cars/TakeOfflineCarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Cars/TakeOfflineCarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Cars; @@ -8,7 +9,7 @@ public class TakeOfflineCarRequest : TenantedRequest { public DateTime? FromUtc { get; set; } - public required string Id { get; set; } + [Required] public string? Id { get; set; } public DateTime? ToUtc { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/AssignPlatformRolesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/AssignPlatformRolesRequest.cs index 6b891db5..20f9927b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/AssignPlatformRolesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/AssignPlatformRolesRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -6,7 +7,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Authorize(Interfaces.Roles.Platform_Operations)] public class AssignPlatformRolesRequest : UnTenantedRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } public List? Roles { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ChangeDefaultOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ChangeDefaultOrganizationRequest.cs index 6c09e4f7..2e1483e1 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ChangeDefaultOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ChangeDefaultOrganizationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Authorize(Roles.Platform_Standard, Features.Platform_PaidTrial)] public class ChangeDefaultOrganizationRequest : UnTenantedRequest { - public required string OrganizationId { get; set; } + [Required] public string? OrganizationId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/InviteGuestRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/InviteGuestRequest.cs index 0a3065a9..bf93d949 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/InviteGuestRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/InviteGuestRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Authorize(Roles.Platform_Standard, Features.Platform_Basic)] public class InviteGuestRequest : UnTenantedRequest { - public required string Email { get; set; } + [Required] public string? Email { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ResendGuestInvitationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ResendGuestInvitationRequest.cs index 62f7fe76..0c052600 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ResendGuestInvitationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/ResendGuestInvitationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Authorize(Roles.Platform_Standard, Features.Platform_Basic)] public class ResendGuestInvitationRequest : UnTenantedEmptyRequest { - public required string Token { get; set; } + [Required] public string? Token { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/UnassignPlatformRolesRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/UnassignPlatformRolesRequest.cs index b78dcc29..6d4ba462 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/UnassignPlatformRolesRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/UnassignPlatformRolesRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -6,7 +7,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Authorize(Interfaces.Roles.Platform_Operations)] public class UnassignPlatformRolesRequest : UnTenantedRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } public List? Roles { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/VerifyGuestInvitationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/VerifyGuestInvitationRequest.cs index 4fef03e4..84fbca97 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/VerifyGuestInvitationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/EndUsers/VerifyGuestInvitationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.EndUsers; [Route("/invitations/{Token}/verify", OperationMethod.Get)] public class VerifyGuestInvitationRequest : UnTenantedRequest { - public required string Token { get; set; } + [Required] public string? Token { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticatePasswordRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticatePasswordRequest.cs index 629a9a1f..f016230f 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticatePasswordRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticatePasswordRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -11,7 +12,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/passwords/auth", OperationMethod.Post)] public class AuthenticatePasswordRequest : UnTenantedRequest { - public required string Password { get; set; } + [Required] public string? Password { get; set; } - public required string Username { get; set; } + [Required] public string? Username { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticateSingleSignOnRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticateSingleSignOnRequest.cs index 1e3b297a..f950cc5e 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticateSingleSignOnRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/AuthenticateSingleSignOnRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -5,11 +6,11 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/sso/auth", OperationMethod.Post)] public class AuthenticateSingleSignOnRequest : UnTenantedRequest { - public required string AuthCode { get; set; } + [Required] public string? AuthCode { get; set; } public string? InvitationToken { get; set; } - public required string Provider { get; set; } + [Required] public string? Provider { get; set; } public string? Username { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/CompletePasswordResetRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/CompletePasswordResetRequest.cs index e0f5f5be..9c2dd604 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/CompletePasswordResetRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/CompletePasswordResetRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -5,7 +6,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/passwords/{Token}/reset/complete", OperationMethod.Post)] public class CompletePasswordResetRequest : UnTenantedEmptyRequest { - public required string Password { get; set; } + [Required] public string? Password { get; set; } - public required string Token { get; set; } + [Required] public string? Token { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/ConfirmRegistrationPersonPasswordRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/ConfirmRegistrationPersonPasswordRequest.cs index f63bc505..3c77a12b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/ConfirmRegistrationPersonPasswordRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/ConfirmRegistrationPersonPasswordRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/passwords/confirm-registration", OperationMethod.Post)] public class ConfirmRegistrationPersonPasswordRequest : UnTenantedRequest { - public required string Token { get; set; } + [Required] public string? Token { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/DeleteAPIKeyRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/DeleteAPIKeyRequest.cs index 45d1c4c1..1dbf4343 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/DeleteAPIKeyRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/DeleteAPIKeyRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -6,5 +7,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Authorize(Roles.Platform_Standard, Features.Platform_PaidTrial)] public class DeleteAPIKeyRequest : UnTenantedDeleteRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/GetRegistrationPersonConfirmationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/GetRegistrationPersonConfirmationRequest.cs index ffc0d0fd..4811a9de 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/GetRegistrationPersonConfirmationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/GetRegistrationPersonConfirmationRequest.cs @@ -1,4 +1,5 @@ #if TESTINGONLY +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -6,6 +7,6 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/passwords/confirm-registration", OperationMethod.Get, isTestingOnly: true)] public class GetRegistrationPersonConfirmationRequest : UnTenantedRequest { - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } } #endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/InitiatePasswordResetRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/InitiatePasswordResetRequest.cs index 6f2fc3cb..e715ac66 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/InitiatePasswordResetRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/InitiatePasswordResetRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/passwords/reset", OperationMethod.Post)] public class InitiatePasswordResetRequest : UnTenantedEmptyRequest { - public required string EmailAddress { get; set; } + [Required] public string? EmailAddress { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RefreshTokenRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RefreshTokenRequest.cs index 7aa398fd..1425332b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RefreshTokenRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RefreshTokenRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -5,5 +6,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Identities; [Route("/tokens/refresh", OperationMethod.Post)] public class RefreshTokenRequest : UnTenantedRequest { - public required string RefreshToken { get; set; } + [Required] public string? RefreshToken { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterMachineRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterMachineRequest.cs index 889f4e91..efcc6abc 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterMachineRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterMachineRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -9,7 +10,7 @@ public class RegisterMachineRequest : UnTenantedRequest public string? CountryCode { get; set; } - public required string Name { get; set; } + [Required] public string? Name { get; set; } public string? Timezone { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterPersonPasswordRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterPersonPasswordRequest.cs index a29e0d9f..e8aadbf0 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterPersonPasswordRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Identities/RegisterPersonPasswordRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Identities; @@ -7,15 +8,15 @@ public class RegisterPersonPasswordRequest : UnTenantedRequest { - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Images/UpdateImageRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Images/UpdateImageRequest.cs index e58f480f..0e9d6a2b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Images/UpdateImageRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Images/UpdateImageRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Images; @@ -11,5 +12,5 @@ public class UpdateImageRequest : UnTenantedRequest { public string? Description { get; set; } - public required string Id { get; set; } + [Required] public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/AssignRolesToOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/AssignRolesToOrganizationRequest.cs index 9a443546..6e330138 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/AssignRolesToOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/AssignRolesToOrganizationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Organizations; @@ -12,7 +13,7 @@ public class AssignRolesToOrganizationRequest : UnTenantedRequest Roles { get; set; } = []; - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/CreateOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/CreateOrganizationRequest.cs index b164ddf7..eae5aa54 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/CreateOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/CreateOrganizationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Organizations; @@ -9,5 +10,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.Organizations; [Authorize(Roles.Platform_Standard, Features.Platform_PaidTrial)] public class CreateOrganizationRequest : UnTenantedRequest { - public required string Name { get; set; } + [Required] public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnInviteMemberFromOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnInviteMemberFromOrganizationRequest.cs index 0be13944..3e9a39ee 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnInviteMemberFromOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnInviteMemberFromOrganizationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Organizations; @@ -10,7 +11,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Organizations; public class UnInviteMemberFromOrganizationRequest : UnTenantedRequest, IUnTenantedOrganizationRequest { - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnassignRolesFromOrganizationRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnassignRolesFromOrganizationRequest.cs index 9be8a411..c61a9020 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnassignRolesFromOrganizationRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/Organizations/UnassignRolesFromOrganizationRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Organizations; @@ -12,7 +13,7 @@ public class UnassignRolesFromOrganizationRequest : UnTenantedRequest Roles { get; set; } = []; - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } public string? Id { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedGetTestingOnlyRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedGetTestingOnlyRequest.cs new file mode 100644 index 00000000..c3e5936d --- /dev/null +++ b/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedGetTestingOnlyRequest.cs @@ -0,0 +1,16 @@ +#if TESTINGONLY +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; + +namespace Infrastructure.Web.Api.Operations.Shared.TestingOnly; + +[Route("/testingonly/validations/validated/{Id}", OperationMethod.Get, isTestingOnly: true)] +public class ValidationsValidatedGetTestingOnlyRequest : IWebRequest +{ + public string? Id { get; set; } + + public string? OptionalField { get; set; } + + [Required] public string? RequiredField { get; set; } +} +#endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedPostTestingOnlyRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedPostTestingOnlyRequest.cs new file mode 100644 index 00000000..08680234 --- /dev/null +++ b/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedPostTestingOnlyRequest.cs @@ -0,0 +1,16 @@ +#if TESTINGONLY +using System.ComponentModel.DataAnnotations; +using Infrastructure.Web.Api.Interfaces; + +namespace Infrastructure.Web.Api.Operations.Shared.TestingOnly; + +[Route("/testingonly/validations/validated/{Id}", OperationMethod.Post, isTestingOnly: true)] +public class ValidationsValidatedPostTestingOnlyRequest : IWebRequest +{ + public string? Id { get; set; } + + public string? OptionalField { get; set; } + + [Required] public string? RequiredField { get; set; } +} +#endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedTestingOnlyRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedTestingOnlyRequest.cs deleted file mode 100644 index f2e1c749..00000000 --- a/src/Infrastructure.Web.Api.Operations.Shared/TestingOnly/ValidationsValidatedTestingOnlyRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -#if TESTINGONLY -using Infrastructure.Web.Api.Interfaces; - -namespace Infrastructure.Web.Api.Operations.Shared.TestingOnly; - -[Route("/testingonly/validations/validated/{id}", OperationMethod.Get, isTestingOnly: true)] -public class ValidationsValidatedTestingOnlyRequest : IWebRequest -{ - public string? Field1 { get; set; } - - public string? Field2 { get; set; } - - public string? Id { get; set; } -} -#endif \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileAvatarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileAvatarRequest.cs index cde9e06c..d25ada3b 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileAvatarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileAvatarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.UserProfiles; @@ -10,5 +11,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.UserProfiles; public class ChangeProfileAvatarRequest : UnTenantedRequest, IHasMultipartForm { // Will also include bytes for the multipart-form image - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileContactAddressRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileContactAddressRequest.cs index 0293969d..b0b7ee6d 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileContactAddressRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/ChangeProfileContactAddressRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.UserProfiles; @@ -21,7 +22,7 @@ public class ChangeProfileContactAddressRequest : UnTenantedRequest public string? Timezone { get; set; } - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/DeleteProfileAvatarRequest.cs b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/DeleteProfileAvatarRequest.cs index 69964c32..9b6d29bd 100644 --- a/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/DeleteProfileAvatarRequest.cs +++ b/src/Infrastructure.Web.Api.Operations.Shared/UserProfiles/DeleteProfileAvatarRequest.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.UserProfiles; @@ -9,5 +10,5 @@ namespace Infrastructure.Web.Api.Operations.Shared.UserProfiles; [Authorize(Roles.Platform_Standard, Features.Platform_Basic)] public class DeleteProfileAvatarRequest : UnTenantedRequest { - public required string UserId { get; set; } + [Required] public string? UserId { get; set; } } \ No newline at end of file diff --git a/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs b/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs index d3b784de..bfeae8ea 100644 --- a/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs +++ b/src/Infrastructure.Web.Hosting.Common/Documentation/FromFormMultiPartFilter.cs @@ -1,6 +1,7 @@ using System.Reflection; using Common; using Common.Extensions; +using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Interfaces; using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc.ApiExplorer; @@ -108,19 +109,19 @@ private static Optional IsMultiPartFormRequest(Operatio return Optional.None; } - if (!RequestHasBody(context)) + if (!RequestCouldHaveBody(context)) { return Optional.None; } return requestParameters.First(); - static bool RequestHasBody(OperationFilterContext context) + static bool RequestCouldHaveBody(OperationFilterContext context) { var method = context.ApiDescription.HttpMethod; - return method == HttpMethod.Post.Method - || method == HttpMethod.Put.Method - || method == HttpMethod.Patch.Method; + var httpMethod = new HttpMethod(method ?? HttpMethod.Get.Method); + + return httpMethod.CanHaveBody(); } static bool IsFromFormRequest(ApiParameterDescription requestParameter) diff --git a/src/OrganizationsInfrastructure/Api/Organizations/OrganizationsApi.cs b/src/OrganizationsInfrastructure/Api/Organizations/OrganizationsApi.cs index a9f348dc..aa928238 100644 --- a/src/OrganizationsInfrastructure/Api/Organizations/OrganizationsApi.cs +++ b/src/OrganizationsInfrastructure/Api/Organizations/OrganizationsApi.cs @@ -29,7 +29,7 @@ public async Task> Assi AssignRolesToOrganizationRequest request, CancellationToken cancellationToken) { var organization = await _organizationsApplication.AssignRolesToOrganizationAsync(_callerFactory.Create(), - request.Id!, request.UserId, request.Roles, cancellationToken); + request.Id!, request.UserId!, request.Roles, cancellationToken); return () => organization.HandleApplicationResult(org => @@ -72,7 +72,7 @@ public async Task> Create(C CancellationToken cancellationToken) { var organization = - await _organizationsApplication.CreateSharedOrganizationAsync(_callerFactory.Create(), request.Name, + await _organizationsApplication.CreateSharedOrganizationAsync(_callerFactory.Create(), request.Name!, cancellationToken); return () => organization.HandleApplicationResult(org => @@ -160,7 +160,7 @@ public async Task> Unas UnassignRolesFromOrganizationRequest request, CancellationToken cancellationToken) { var organization = await _organizationsApplication.UnassignRolesFromOrganizationAsync(_callerFactory.Create(), - request.Id!, request.UserId, request.Roles, cancellationToken); + request.Id!, request.UserId!, request.Roles, cancellationToken); return () => organization.HandleApplicationResult(org => @@ -171,7 +171,7 @@ public async Task organization.HandleApplicationResult(org => diff --git a/src/SaaStack.sln.DotSettings b/src/SaaStack.sln.DotSettings index 33354bc2..323c3ae0 100644 --- a/src/SaaStack.sln.DotSettings +++ b/src/SaaStack.sln.DotSettings @@ -14,6 +14,7 @@ DO_NOT_SHOW WARNING WARNING + SUGGESTION WARNING True Required @@ -31,12 +32,15 @@ 1 True True + 10000 1 False + WRAP_IF_LONG True True WRAP_IF_LONG CHOP_ALWAYS + CHOP_ALWAYS CHOP_ALWAYS <?xml version="1.0" encoding="utf-16"?> <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> diff --git a/src/Tools.Analyzers.NonPlatform.UnitTests/ApiLayerAnalyzerSpec.cs b/src/Tools.Analyzers.NonPlatform.UnitTests/ApiLayerAnalyzerSpec.cs index 550b7512..c7f042ca 100644 --- a/src/Tools.Analyzers.NonPlatform.UnitTests/ApiLayerAnalyzerSpec.cs +++ b/src/Tools.Analyzers.NonPlatform.UnitTests/ApiLayerAnalyzerSpec.cs @@ -1703,7 +1703,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Test; [Route(""/apath"", OperationMethod.Get)] internal class ARequest : IWebRequest { - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1724,7 +1724,7 @@ namespace Infrastructure.Web.Api.Operations.Shared.Test; [Route(""/apath"", OperationMethod.Get)] public class AClass : IWebRequest { - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1745,7 +1745,7 @@ namespace anamespace; [Route(""/apath"", OperationMethod.Get)] public class ARequest : IWebRequest { - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1766,7 +1766,7 @@ public async Task WhenHasNoRouteAttribute_ThenAlerts() namespace Infrastructure.Web.Api.Operations.Shared.Test; public class ARequest : IWebRequest { - public required string AProperty { get; set; } + public string AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1792,7 +1792,7 @@ public ARequest(string value) AProperty = value; } - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1814,7 +1814,7 @@ private ARequest() AProperty = string.Empty; } - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.DiagnosticExists( @@ -1836,7 +1836,7 @@ public ARequest() AProperty = string.Empty; } - public required string AProperty { get; set; } + public string? AProperty { get; set; } }"; await Verify.NoDiagnosticExists(input); @@ -1875,7 +1875,7 @@ public async Task WhenAnyPropertyReferenceTypeIsOptional_ThenAlerts() using Common; using Infrastructure.Web.Api.Interfaces; namespace Infrastructure.Web.Api.Operations.Shared.Test; -[Route(""/apath"", OperationMethod.Get)] +[Route(""/apath"", OperationMethod.Post)] public class ARequest : IWebRequest { public Optional AProperty { get; set; } @@ -1901,6 +1901,308 @@ public class ARequest : IWebRequest await Verify.NoDiagnosticExists(input); } } + + [Trait("Category", "Unit")] + public class GivenRule037 + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsRequired_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public required string AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule037, input, 9, 28, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsRequired_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public required int AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule037, input, 9, 25, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyEnumIsRequired_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public required AnEnum AProperty { get; set; } +} + +public enum AnEnum +{ + AValue +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule037, input, 9, 28, "AProperty"); + } + } + + [UsedImplicitly] + public class GivenRule038 + { + [Trait("Category", "Unit")] + public class GivenAGetRequest + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNotNullable_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public string AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule038, input, 9, 19, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public int? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNotNullable_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public int AProperty { get; set; } +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule038, input, 9, 16, "AProperty"); + } + + [Fact] + public async Task WhenAnyPropertyEnumIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public AnEnum? AProperty { get; set; } +} + +public enum AnEnum +{ + AValue +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyEnumIsNotNullable_ThenAlerts() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Get)] +public class ARequest : IWebRequest +{ + public AnEnum AProperty { get; set; } +} + +public enum AnEnum +{ + AValue +}"; + + await Verify.DiagnosticExists( + ApiLayerAnalyzer.Rule038, input, 9, 19, "AProperty"); + } + } + + [Trait("Category", "Unit")] + public class GivenAPostRequest + { + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public string? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyReferenceTypeIsNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public string AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public int? AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyValueTypeIsNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public int AProperty { get; set; } +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyEnumIsNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public AnEnum? AProperty { get; set; } +} + +public enum AnEnum +{ + AValue +}"; + + await Verify.NoDiagnosticExists(input); + } + + [Fact] + public async Task WhenAnyPropertyEnumIsNotNullable_ThenNoAlert() + { + const string input = @" +using System; +using Common; +using Infrastructure.Web.Api.Interfaces; +namespace Infrastructure.Web.Api.Operations.Shared.Test; +[Route(""/apath"", OperationMethod.Post)] +public class ARequest : IWebRequest +{ + public AnEnum AProperty { get; set; } +} + +public enum AnEnum +{ + AValue +}"; + + await Verify.NoDiagnosticExists(input); + } + } + } } [UsedImplicitly] diff --git a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md index b871e0c1..ed16db23 100644 --- a/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md +++ b/src/Tools.Analyzers.NonPlatform/AnalyzerReleases.Shipped.md @@ -15,6 +15,20 @@ SAASWEB018 | SaaStackWebApi | Error | The request type should not be declared with a AuthorizeAttribute on it. SAASWEB019 | SaaStackWebApi | Warning | The request type should be declared with a AuthorizeAttribute on it. SAASWEB020 | SaaStackWebApi | Warning | There should be no methods in this class with the same route. + SAASWEB030 | SaaStackWebApi | Error | Request must be public + SAASWEB031 | SaaStackWebApi | Error | Request must be named with "Request" suffix + SAASWEB032 | SaaStackWebApi | Error | Request must be in namespace "Infrastructure.Web.Api.Operations.Shared" + SAASWEB033 | SaaStackWebApi | Error | Request must have a + SAASWEB034 | SaaStackWebApi | Error | Request must have a parameterless constructor + SAASWEB035 | SaaStackWebApi | Error | Properties must have public getters and setters + SAASWEB036 | SaaStackWebApi | Error | Properties should be nullable not Optional{T} for interoperability + SAASWEB037 | SaaStackWebApi | Error | Properties should NOT use required modifier + SAASWEB040 | SaaStackWebApi | Error | Response must be public + SAASWEB041 | SaaStackWebApi | Error | Response must be named with "Response" suffix + SAASWEB042 | SaaStackWebApi | Error | Response must be in namespace "Infrastructure.Web.Api.Operations.Shared" + SAASWEB043 | SaaStackWebApi | Error | Response must have a parameterless constructor + SAASWEB044 | SaaStackWebApi | Error | Properties must have public getters and setters + SAASWEB045 | SaaStackWebApi | Error | Properties should be nullable not Optional{T} for interoperability SAASDDD010 | SaaStackDDD | Error | Aggregate roots must have at least one Create() class factory method. SAASDDD011 | SaaStackDDD | Error | Create() class factory methods must return correct types. SAASDDD012 | SaaStackDDD | Error | Aggregate roots must raise a create event in the class factory. @@ -39,6 +53,7 @@ SAASDDD034 | SaaStackDDD | Error | Properties must not have public setters. SAASDDD035 | SaaStackDDD | Error | ValueObjects must only have immutable methods SAASDDD036 | SaaStackDDD | Warning | ValueObjects should be marked as sealed. + SAASDDD036 | SaaStackDDD | Warning | ValueObjects should be marked as sealed. SAASDDD040 | SaaStackDDD | Error | DomainEvents must be public SAASDDD041 | SaaStackDDD | Warning | DomainEvents should be sealed SAASDDD042 | SaaStackDDD | Error | DomainEvents must have a parameterless constructor diff --git a/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs index 6ab0112c..67ecab64 100644 --- a/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/ApiLayerAnalyzer.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using System.Text; using Common.Extensions; +using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Interfaces; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -42,6 +43,8 @@ namespace Tools.Analyzers.NonPlatform; /// SAASWEB34: Error: Request must have a parameterless constructor /// SAASWEB35: Error: Properties must have public getters and setters /// SAASWEB36: Error: Properties should be nullable not Optional{T} for interoperability +/// SAASWEB37: Error: Properties should NOT use required modifier +/// SAASWEB38: Error: Properties for GET/DELETE requests should all be nullable /// Responses: /// SAASWEB40: Error: Response must be public /// SAASWEB41: Error: Response must be named with "Response" suffix @@ -59,35 +62,31 @@ internal static readonly Dictionary> { { OperationMethod.Post, - new List { typeof(ApiEmptyResult), typeof(ApiPostResult<,>) } + [typeof(ApiEmptyResult), typeof(ApiPostResult<,>)] }, { OperationMethod.Get, - new List - { - typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiGetResult<,>), typeof(ApiStreamResult) - } + [typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiGetResult<,>), typeof(ApiStreamResult)] }, { OperationMethod.Search, - new List - { + [ typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiGetResult<,>), typeof(ApiSearchResult<,>) - } + ] }, { OperationMethod.PutPatch, - new List { typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiPutPatchResult<,>) } + [typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiPutPatchResult<,>)] }, { OperationMethod.Delete, - new List { typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiDeleteResult) } + [typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiDeleteResult)] } }; internal static readonly Type[] AllowableServiceOperationReturnTypes = - { + [ typeof(ApiEmptyResult), typeof(ApiResult<,>), typeof(ApiPostResult<,>), @@ -96,7 +95,7 @@ internal static readonly Dictionary> typeof(ApiPutPatchResult<,>), typeof(ApiDeleteResult), typeof(ApiStreamResult) - }; + ]; internal static readonly DiagnosticDescriptor Rule010 = "SAASWEB10".GetDescriptor(DiagnosticSeverity.Warning, AnalyzerConstants.Categories.WebApi, nameof(Resources.SAASWEB010Title), nameof(Resources.SAASWEB010Description), @@ -157,6 +156,12 @@ internal static readonly Dictionary> AnalyzerConstants.Categories.WebApi, nameof(Resources.Diagnostic_Title_PropertyMustBeNullableNotOptional), nameof(Resources.Diagnostic_Description_PropertyMustBeNullableNotOptional), nameof(Resources.Diagnostic_MessageFormat_PropertyMustBeNullableNotOptional)); + internal static readonly DiagnosticDescriptor Rule037 = "SAASWEB037".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.WebApi, nameof(Resources.SAASWEB037Title), nameof(Resources.SAASWEB037Description), + nameof(Resources.SAASWEB037MessageFormat)); + internal static readonly DiagnosticDescriptor Rule038 = "SAASWEB038".GetDescriptor(DiagnosticSeverity.Error, + AnalyzerConstants.Categories.WebApi, nameof(Resources.SAASWEB038Title), nameof(Resources.SAASWEB038Description), + nameof(Resources.SAASWEB038MessageFormat)); internal static readonly DiagnosticDescriptor Rule040 = "SAASWEB040".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.WebApi, nameof(Resources.Diagnostic_Title_ClassMustBePublic), @@ -184,7 +189,7 @@ internal static readonly Dictionary> public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( Rule010, Rule011, Rule012, Rule013, Rule014, Rule015, Rule016, Rule017, Rule018, Rule019, Rule020, - Rule030, Rule031, Rule032, Rule033, Rule034, Rule035, Rule036, + Rule030, Rule031, Rule032, Rule033, Rule034, Rule035, Rule036, Rule037, Rule038, Rule040, Rule041, Rule042, Rule043, Rule044, Rule045); public override void Initialize(AnalysisContext context) @@ -308,6 +313,7 @@ private static void AnalyzeRequestClass(SyntaxNodeAnalysisContext context) context.ReportDiagnostic(Rule034, classDeclarationSyntax); } + var operationMethod = GetOperationMethod(context, classDeclarationSyntax); var allProperties = classDeclarationSyntax.Members.Where(member => member is PropertyDeclarationSyntax) .Cast() .ToList(); @@ -324,6 +330,19 @@ private static void AnalyzeRequestClass(SyntaxNodeAnalysisContext context) { context.ReportDiagnostic(Rule036, property); } + + if (property.IsRequired()) + { + context.ReportDiagnostic(Rule037, property); + } + + var isGetOrDeleteMethod = operationMethod.Exists() + && !operationMethod.Value.CanHaveBody(); + if (isGetOrDeleteMethod + && !property.IsNullableType(context)) + { + context.ReportDiagnostic(Rule038, property); + } } } } @@ -403,6 +422,26 @@ private static bool OperationAndReturnsTypeDontMatch(SyntaxNodeAnalysisContext c return true; } + private static OperationMethod? GetOperationMethod(SyntaxNodeAnalysisContext context, + ClassDeclarationSyntax classDeclarationSyntax) + { + var requestTypeSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax); + var attribute = requestTypeSymbol.GetAttributeOfType(context); + if (attribute is null) + { + return null; + } + + var operation = attribute.ConstructorArguments[1].Value!.ToString()!; + + if (!Enum.TryParse(operation, true, out var operationMethod)) + { + return null; + } + + return operationMethod; + } + private static bool ReturnTypeIsNotCorrect(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclarationSyntax, out ITypeSymbol? returnType) { @@ -640,7 +679,7 @@ public void SetRouteSegments(string? routePath) { if (routePath.HasValue()) { - RouteSegments = routePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + RouteSegments = routePath.Split(['/'], StringSplitOptions.RemoveEmptyEntries); } } } diff --git a/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs index d7d09fc9..8704613e 100644 --- a/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/ApplicationLayerAnalyzer.cs @@ -35,7 +35,7 @@ namespace Tools.Analyzers.NonPlatform; public class ApplicationLayerAnalyzer : DiagnosticAnalyzer { internal static readonly SpecialType[] AllowableReadModelPrimitives = - { + [ SpecialType.System_Boolean, SpecialType.System_String, SpecialType.System_UInt64, @@ -45,7 +45,7 @@ public class ApplicationLayerAnalyzer : DiagnosticAnalyzer SpecialType.System_Decimal, SpecialType.System_DateTime, SpecialType.System_Byte - }; + ]; internal static readonly SpecialType[] AllowableResourcePrimitives = AllowableReadModelPrimitives; internal static readonly DiagnosticDescriptor Rule010 = "SAASAPP010".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Application, nameof(Resources.Diagnostic_Title_ClassMustBePublic), diff --git a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs index 531dabdd..ee374272 100644 --- a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignAnalyzer.cs @@ -65,7 +65,7 @@ public class DomainDrivenDesignAnalyzer : DiagnosticAnalyzer public const string ClassFactoryMethodName = "Create"; public const string ConstructorMethodCall = "RaiseCreateEvent"; // AggregateRootBase.RaiseCreateEvent internal static readonly SpecialType[] AllowableDomainEventPrimitives = - { + [ SpecialType.System_Boolean, SpecialType.System_String, SpecialType.System_UInt64, @@ -75,7 +75,7 @@ public class DomainDrivenDesignAnalyzer : DiagnosticAnalyzer SpecialType.System_Decimal, SpecialType.System_DateTime, SpecialType.System_Byte - }; + ]; internal static readonly DiagnosticDescriptor Rule010 = "SAASDDD010".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAASDDD010Title), nameof(Resources.SAASDDD010Description), nameof(Resources.SAASDDD010MessageFormat)); @@ -199,7 +199,7 @@ public class DomainDrivenDesignAnalyzer : DiagnosticAnalyzer internal static readonly DiagnosticDescriptor Rule049 = "SAASDDD049".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Ddd, nameof(Resources.SAASDDD049Title), nameof(Resources.SAASDDD049Description), nameof(Resources.SAASDDD049MessageFormat)); - private static readonly string[] IgnoredValueObjectMethodNames = { nameof(IDehydratableEntity.Dehydrate) }; + private static readonly string[] IgnoredValueObjectMethodNames = [nameof(IDehydratableEntity.Dehydrate)]; public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create( @@ -603,7 +603,7 @@ public static INamedTypeSymbol[] GetAllowableClassFactoryReturnTypes(this Syntax var resultType = context.Compilation.GetTypeByMetadataName(typeof(Result<,>).FullName!)!; var resultOfClassAndErrorType = resultType.Construct(classSymbol, errorType); - return new[] { classSymbol, resultOfClassAndErrorType }; + return [classSymbol, resultOfClassAndErrorType]; } public static INamedTypeSymbol[] GetAllowableDomainEventFactoryReturnTypes(this SyntaxNodeAnalysisContext context, @@ -615,7 +615,7 @@ public static INamedTypeSymbol[] GetAllowableDomainEventFactoryReturnTypes(this return Array.Empty(); } - return new[] { classSymbol }; + return [classSymbol]; } public static INamedTypeSymbol[] GetAllowableDomainEventPropertyReturnTypes(this SyntaxNodeAnalysisContext context) @@ -662,7 +662,7 @@ public static INamedTypeSymbol[] GetAllowableValueObjectMutableMethodReturnTypes var resultType = context.Compilation.GetTypeByMetadataName(typeof(Result<,>).FullName!)!; var resultOfClassAndErrorType = resultType.Construct(classSymbol, errorType); - return new[] { classSymbol, resultOfClassAndErrorType }; + return [classSymbol, resultOfClassAndErrorType]; } public static bool HasIncorrectReturnType(this SyntaxNodeAnalysisContext context, @@ -861,7 +861,7 @@ private static INamedTypeSymbol[] GetAllowableDehydrateReturnType(this SemanticM } var propertiesType = compilation.GetTypeByMetadataName(typeof(HydrationProperties).FullName!)!; - return new[] { propertiesType }; + return [propertiesType]; } /// @@ -917,7 +917,7 @@ private static INamedTypeSymbol[] GetAllowableAggregateRehydrateReturnType(this var factoryType = compilation.GetTypeByMetadataName(typeof(AggregateRootFactory<>).FullName!)!; var factoryOfClass = factoryType.Construct(classSymbol); - return new[] { factoryOfClass }; + return [factoryOfClass]; } private static INamedTypeSymbol[] GetAllowableEntityRehydrateReturnType(this SemanticModel semanticModel, @@ -932,7 +932,7 @@ private static INamedTypeSymbol[] GetAllowableEntityRehydrateReturnType(this Sem var factoryType = compilation.GetTypeByMetadataName(typeof(EntityFactory<>).FullName!)!; var factoryOfClass = factoryType.Construct(classSymbol); - return new[] { factoryOfClass }; + return [factoryOfClass]; } private static INamedTypeSymbol[] GetAllowableValueObjectRehydrateReturnType(this SemanticModel semanticModel, @@ -947,7 +947,7 @@ private static INamedTypeSymbol[] GetAllowableValueObjectRehydrateReturnType(thi var factoryType = compilation.GetTypeByMetadataName(typeof(ValueObjectFactory<>).FullName!)!; var factoryOfClass = factoryType.Construct(classSymbol); - return new[] { factoryOfClass }; + return [factoryOfClass]; } public class DehydratableStatus diff --git a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs index bb34bfba..0d6e6589 100644 --- a/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs +++ b/src/Tools.Analyzers.NonPlatform/DomainDrivenDesignCodeFix.cs @@ -232,7 +232,7 @@ private static async Task AddRehydrateMethodToAggregateRoot(Document d var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], nameof(IRehydratableObject.Rehydrate), $"Domain.Interfaces.AggregateRootFactory<{className}>", null, @@ -257,7 +257,7 @@ private static async Task AddCreateMethodToAggregateRoot(Document docu var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], DomainDrivenDesignAnalyzer.ClassFactoryMethodName, $"{nameof(Result)}<{className}, {nameof(Error)}>", new Dictionary @@ -285,7 +285,7 @@ private static async Task AddRehydrateMethodToEntity(Document document var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], nameof(IRehydratableObject.Rehydrate), $"Domain.Interfaces.EntityFactory<{className}>", null, @@ -308,7 +308,7 @@ private static async Task AddCreateMethodToEntity(Document document, var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], DomainDrivenDesignAnalyzer.ClassFactoryMethodName, $"{nameof(Result)}<{className}, {nameof(Error)}>", new Dictionary @@ -341,7 +341,7 @@ private static async Task AddRehydrateMethodToValueObject(Document doc var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], nameof(IRehydratableObject.Rehydrate), $"Domain.Interfaces.ValueObjectFactory<{className}>", null, @@ -371,7 +371,7 @@ private static async Task AddCreateMethodToValueObject(Document docume var className = classDeclarationSyntax.Identifier.Text; var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword], DomainDrivenDesignAnalyzer.ClassFactoryMethodName, $"{nameof(Result)}<{className}, {nameof(Error)}>", isSingleValueObject @@ -392,7 +392,7 @@ private static async Task AddCreateMethodToValueObject(Document docume if (!isSingleValueObject) { var newConstructor = GenerateConstructor( - new[] { SyntaxKind.PrivateKeyword }, + [SyntaxKind.PrivateKeyword], className, new Dictionary { @@ -420,7 +420,7 @@ private static async Task AddDehydrateMethodToAggregateRoot(Document d } var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword], nameof(IDehydratableEntity.Dehydrate), $"{nameof(HydrationProperties)}", null, @@ -442,7 +442,7 @@ private static async Task AddDehydrateMethodToEntity(Document document } var newMethod = GenerateMethod( - new[] { SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword }, + [SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword], nameof(IDehydratableEntity.Dehydrate), $"{nameof(HydrationProperties)}", null, diff --git a/src/Tools.Analyzers.NonPlatform/EventingAnalyzer.cs b/src/Tools.Analyzers.NonPlatform/EventingAnalyzer.cs index 6a586b32..0b0fe308 100644 --- a/src/Tools.Analyzers.NonPlatform/EventingAnalyzer.cs +++ b/src/Tools.Analyzers.NonPlatform/EventingAnalyzer.cs @@ -27,7 +27,7 @@ namespace Tools.Analyzers.NonPlatform; public class EventingAnalyzer : DiagnosticAnalyzer { internal static readonly SpecialType[] AllowableIntegrationEventPrimitives = - { + [ SpecialType.System_Boolean, SpecialType.System_String, SpecialType.System_UInt64, @@ -37,7 +37,7 @@ public class EventingAnalyzer : DiagnosticAnalyzer SpecialType.System_Decimal, SpecialType.System_DateTime, SpecialType.System_Byte - }; + ]; internal static readonly DiagnosticDescriptor Rule010 = "SAASEVT010".GetDescriptor(DiagnosticSeverity.Error, AnalyzerConstants.Categories.Eventing, nameof(Resources.Diagnostic_Title_ClassMustBePublic), nameof(Resources.Diagnostic_Description_ClassMustBePublic), diff --git a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs index 85883bc0..763e5448 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs +++ b/src/Tools.Analyzers.NonPlatform/Resources.Designer.cs @@ -1301,6 +1301,60 @@ internal static string SAASWEB033Title { } } + /// + /// Looks up a localized string similar to Property must not be marked as `required`.. + /// + internal static string SAASWEB037Description { + get { + return ResourceManager.GetString("SAASWEB037Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must not be marked as `required`. + /// + internal static string SAASWEB037MessageFormat { + get { + return ResourceManager.GetString("SAASWEB037MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wrong return type. + /// + internal static string SAASWEB037Title { + get { + return ResourceManager.GetString("SAASWEB037Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property must not be nullable.. + /// + internal static string SAASWEB038Description { + get { + return ResourceManager.GetString("SAASWEB038Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Property '{0}' must be nullable. + /// + internal static string SAASWEB038MessageFormat { + get { + return ResourceManager.GetString("SAASWEB038MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Wrong return type. + /// + internal static string SAASWEB038Title { + get { + return ResourceManager.GetString("SAASWEB038Title", resourceCulture); + } + } + /// /// Looks up a localized string similar to The request type should end with the word 'Response'.. /// diff --git a/src/Tools.Analyzers.NonPlatform/Resources.resx b/src/Tools.Analyzers.NonPlatform/Resources.resx index 09d94b33..b506db89 100644 --- a/src/Tools.Analyzers.NonPlatform/Resources.resx +++ b/src/Tools.Analyzers.NonPlatform/Resources.resx @@ -270,6 +270,24 @@ Missing '[RouteAttribute]' on request + + Property must not be marked as `required`. + + + Property '{0}' must not be marked as `required` + + + Wrong return type + + + Property must not be nullable. + + + Property '{0}' must be nullable + + + Wrong return type + The request type should end with the word 'Response'. diff --git a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj index 6f3bd128..c592f811 100644 --- a/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj +++ b/src/Tools.Analyzers.NonPlatform/Tools.Analyzers.NonPlatform.csproj @@ -101,6 +101,9 @@ Reference\Infrastructure.Web.Api.Interfaces\RouteAttribute.cs + + Reference\Infrastructure.Web.Api.Common\Extensions\OperationMethodExtensions.cs + Reference\Infrastructure.Interfaces\AuthenticationConstants.cs diff --git a/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs b/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs index d5bc3826..b839ec50 100644 --- a/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs +++ b/src/Tools.Generators.Web.Api/MinimalApiMediatRGenerator.cs @@ -1,6 +1,7 @@ using System.Text; using Common.Extensions; using Infrastructure.Interfaces; +using Infrastructure.Web.Api.Common.Extensions; using Infrastructure.Web.Api.Interfaces; using Infrastructure.Web.Hosting.Common; using Microsoft.CodeAnalysis; @@ -123,15 +124,14 @@ private static void BuildEndpointRegistrations( var endPointMethodName = $"Map{routeEndpointMethod}"; endpointRegistrations.AppendLine( $" {groupName}.{endPointMethodName}(\"{registration.RoutePath}\","); - if (registration.OperationMethod is OperationMethod.Get or OperationMethod.Search - or OperationMethod.Delete) + if (!registration.OperationMethod.CanHaveBody()) { endpointRegistrations.AppendLine( $" async (global::MediatR.IMediator mediator, [global::Microsoft.AspNetCore.Http.AsParameters] global::{registration.RequestDto.FullName} request) =>"); } else { - if (registration.OperationMethod is OperationMethod.Post or OperationMethod.PutPatch + if (registration.OperationMethod.CanHaveBody() && registration.IsMultipartFormData) { endpointRegistrations.AppendLine( diff --git a/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj b/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj index f11a71a4..05e03ba8 100644 --- a/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj +++ b/src/Tools.Generators.Web.Api/Tools.Generators.Web.Api.csproj @@ -68,6 +68,9 @@ Reference\Infrastructure.Web.Api.Interfaces\OperationMethod.cs + + Reference\Infrastructure.Web.Api.Common\Extensions\OperationMethodExtensions.cs + Reference\Infrastructure.Web.Hosting.Common\WebHostingConstants.cs diff --git a/src/UserProfilesInfrastructure/Api/Profiles/UserProfilesApi.cs b/src/UserProfilesInfrastructure/Api/Profiles/UserProfilesApi.cs index 0459ccb1..80cd39c1 100644 --- a/src/UserProfilesInfrastructure/Api/Profiles/UserProfilesApi.cs +++ b/src/UserProfilesInfrastructure/Api/Profiles/UserProfilesApi.cs @@ -29,7 +29,7 @@ public async Task> ChangeContactAddre ChangeProfileContactAddressRequest request, CancellationToken cancellationToken) { var profile = - await _userProfilesApplication.ChangeContactAddressAsync(_callerFactory.Create(), request.UserId, + await _userProfilesApplication.ChangeContactAddressAsync(_callerFactory.Create(), request.UserId!, request.Line1, request.Line2, request.Line3, request.City, request.State, request.CountryCode, request.Zip, cancellationToken); @@ -43,7 +43,7 @@ public async Task> ChangeProfile( ChangeProfileRequest request, CancellationToken cancellationToken) { var profile = - await _userProfilesApplication.ChangeProfileAsync(_callerFactory.Create(), request.UserId, + await _userProfilesApplication.ChangeProfileAsync(_callerFactory.Create(), request.UserId!, request.FirstName, request.LastName, request.DisplayName, request.PhoneNumber, request.Timezone, cancellationToken); @@ -64,7 +64,7 @@ public async Task> C } var profile = - await _userProfilesApplication.ChangeProfileAvatarAsync(_callerFactory.Create(), request.UserId, + await _userProfilesApplication.ChangeProfileAvatarAsync(_callerFactory.Create(), request.UserId!, uploaded.Value, cancellationToken); return () => @@ -76,7 +76,7 @@ public async Task> DeletePro DeleteProfileAvatarRequest request, CancellationToken cancellationToken) { var profile = - await _userProfilesApplication.DeleteProfileAvatarAsync(_callerFactory.Create(), request.UserId, + await _userProfilesApplication.DeleteProfileAvatarAsync(_callerFactory.Create(), request.UserId!, cancellationToken); return () => diff --git a/src/WebsiteHost/Api/AuthN/AuthenticationApi.cs b/src/WebsiteHost/Api/AuthN/AuthenticationApi.cs index 4bc1cf56..858b53a3 100644 --- a/src/WebsiteHost/Api/AuthN/AuthenticationApi.cs +++ b/src/WebsiteHost/Api/AuthN/AuthenticationApi.cs @@ -25,7 +25,7 @@ public AuthenticationApi(ICallerContextFactory callerFactory, IAuthenticationApp public async Task> Authenticate( AuthenticateRequest request, CancellationToken cancellationToken) { - var tokens = await _authenticationApplication.AuthenticateAsync(_callerFactory.Create(), request.Provider, + var tokens = await _authenticationApplication.AuthenticateAsync(_callerFactory.Create(), request.Provider!, request.AuthCode, request.Username, request.Password, cancellationToken); if (tokens.IsSuccessful) { diff --git a/src/WebsiteHost/Api/FeatureFlags/FeatureFlagsApi.cs b/src/WebsiteHost/Api/FeatureFlags/FeatureFlagsApi.cs index 3079ea6d..0f70d5d0 100644 --- a/src/WebsiteHost/Api/FeatureFlags/FeatureFlagsApi.cs +++ b/src/WebsiteHost/Api/FeatureFlags/FeatureFlagsApi.cs @@ -32,7 +32,7 @@ public async Task> GetForCalle CancellationToken cancellationToken) { var flag = await _featureFlagsApplication.GetFeatureFlagForCallerAsync(_callerFactory.Create(), - request.Name, cancellationToken); + request.Name!, cancellationToken); return () => flag.HandleApplicationResult(f => new GetFeatureFlagResponse { Flag = f }); } diff --git a/src/WebsiteHost/Api/Recording/RecordingApi.cs b/src/WebsiteHost/Api/Recording/RecordingApi.cs index 474cdffa..6c818cb5 100644 --- a/src/WebsiteHost/Api/Recording/RecordingApi.cs +++ b/src/WebsiteHost/Api/Recording/RecordingApi.cs @@ -25,7 +25,7 @@ public RecordingApi(ICallerContextFactory callerFactory, IRecordingApplication r public async Task RecordCrash(RecordCrashRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordCrashAsync(_callerFactory.Create(), request.Message, + var result = await _recordingApplication.RecordCrashAsync(_callerFactory.Create(), request.Message!, cancellationToken); return () => result.Match(() => new Result(), @@ -35,7 +35,7 @@ public async Task RecordCrash(RecordCrashRequest request, public async Task RecordMeasurement(RecordMeasureRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordMeasurementAsync(_callerFactory.Create(), request.EventName, + var result = await _recordingApplication.RecordMeasurementAsync(_callerFactory.Create(), request.EventName!, request.Additional, _httpContextAccessor.ToClientDetails(), cancellationToken); return () => result.Match(() => new Result(), @@ -45,7 +45,7 @@ public async Task RecordMeasurement(RecordMeasureRequest request public async Task RecordPageView(RecordPageViewRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordPageViewAsync(_callerFactory.Create(), request.Path, + var result = await _recordingApplication.RecordPageViewAsync(_callerFactory.Create(), request.Path!, _httpContextAccessor.ToClientDetails(), cancellationToken); return () => result.Match(() => new Result(), @@ -56,8 +56,8 @@ public async Task RecordTrace(RecordTraceRequest request, CancellationToken cancellationToken) { var result = await _recordingApplication.RecordTraceAsync(_callerFactory.Create(), - request.Level.ToEnum(), - request.MessageTemplate, + request.Level!.ToEnum(), + request.MessageTemplate!, request.Arguments, cancellationToken); @@ -68,7 +68,7 @@ public async Task RecordTrace(RecordTraceRequest request, public async Task RecordUsage(RecordUseRequest request, CancellationToken cancellationToken) { - var result = await _recordingApplication.RecordUsageAsync(_callerFactory.Create(), request.EventName, + var result = await _recordingApplication.RecordUsageAsync(_callerFactory.Create(), request.EventName!, request.Additional, _httpContextAccessor.ToClientDetails(), cancellationToken); return () => result.Match(() => new Result(),