Skip to content

Commit

Permalink
Merge pull request #90 from Worth-NL/fature/ApiResponses_Serializatio…
Browse files Browse the repository at this point in the history
…n_TreatAs200

Fature/api responses serialization treat as200
  • Loading branch information
Thomas-M-Krystyan authored Nov 12, 2024
2 parents a542beb + d928d1a commit 63760bf
Show file tree
Hide file tree
Showing 49 changed files with 1,126 additions and 899 deletions.
2 changes: 1 addition & 1 deletion Documentation/OMC - Documentation.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1 id="start">OMC Documentation</h1>

v.1.12.0
v.1.12.1

© 2023-2024, Worth Systems.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

using EventsHandler.Constants;
using EventsHandler.Controllers;
using EventsHandler.Mapping.Enums;
using EventsHandler.Mapping.Models.POCOs.NotificatieApi;
using EventsHandler.Properties;
using EventsHandler.Services.Responding;
using EventsHandler.Services.Responding.Interfaces;
using EventsHandler.Services.Responding.Messages.Models.Base;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -31,9 +31,9 @@ internal sealed class StandardizeApiResponsesAttribute : ActionFilterAttribute
static StandardizeApiResponsesAttribute()
{
// NOTE: Concept similar to strategy design pattern => decide how and which API Controllers are responding to the end-user
s_mappedControllersToResponders.TryAdd(typeof(EventsController), typeof(IRespondingService<NotificationEvent>));
s_mappedControllersToResponders.TryAdd(typeof(NotifyController), typeof(IRespondingService<ProcessingResult, string>));
s_mappedControllersToResponders.TryAdd(typeof(TestController), typeof(IRespondingService<ProcessingResult, string>));
s_mappedControllersToResponders.TryAdd(typeof(EventsController), typeof(OmcResponder));
s_mappedControllersToResponders.TryAdd(typeof(NotifyController), typeof(NotifyResponder));
s_mappedControllersToResponders.TryAdd(typeof(TestController), typeof(NotifyResponder));
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion EventsHandler/Api/EventsHandler/Constants/DefaultValues.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal static class ApiController
{
internal const string Route = "[controller]";

internal const string Version = "1.120";
internal const string Version = "1.121";
}
#endregion

Expand Down
18 changes: 15 additions & 3 deletions EventsHandler/Api/EventsHandler/Controllers/Base/OmcController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using Asp.Versioning;
using EventsHandler.Constants;
using EventsHandler.Extensions;
using EventsHandler.Properties;
using EventsHandler.Services.Responding.Messages.Models.Base;
using Microsoft.AspNetCore.Mvc;
Expand All @@ -17,7 +18,10 @@ namespace EventsHandler.Controllers.Base
[Consumes(DefaultValues.Request.ContentType)]
[Produces(DefaultValues.Request.ContentType)]
// Swagger UI
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(string))] // REASON: JWT Token is invalid or expired
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(BaseEnhancedStandardResponseBody))] // REASON: The HTTP Request wasn't successful
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(BaseStandardResponseBody))] // REASON: JWT Token is invalid or expired
[ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(BaseStandardResponseBody))] // REASON: Unexpected internal error
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(BaseStandardResponseBody))] // REASON: Something is not implemented
public abstract class OmcController : Controller
{
/// <summary>
Expand Down Expand Up @@ -68,7 +72,9 @@ protected internal static ObjectResult LogApiResponse(Exception exception, Objec
/// <inheritdoc cref="SentrySdk.CaptureMessage(string, SentryLevel)"/>
internal static void LogMessage(LogLevel logLevel, string logMessage)
{
_ = SentrySdk.CaptureMessage($"{Resources.Application_Name} | {logLevel:G} | {logMessage}", s_logMapping[logLevel]);
_ = SentrySdk.CaptureMessage(
message: string.Format(Resources.API_Response_STATUS_Logging, Resources.Application_Name, logLevel.GetEnumName(), logMessage),
level: s_logMapping[logLevel]);
}

/// <inheritdoc cref="SentrySdk.CaptureException(Exception)"/>
Expand All @@ -81,6 +87,12 @@ private static void LogException(Exception exception)
#region Helper methods
/// <summary>
/// Determines the log message based on the received <see cref="ObjectResult"/>.
/// <para>
/// The format:
/// <code>
/// HTTP Status Code | Description | Message (optional) | Cases (optional)
/// </code>
/// </para>
/// </summary>
private static string DetermineResultMessage(ObjectResult objectResult)
{
Expand All @@ -94,7 +106,7 @@ private static string DetermineResultMessage(ObjectResult objectResult)
BaseStandardResponseBody baseResponse => baseResponse.ToString(),

// Unknown object result
_ => $"{Resources.Processing_ERROR_UnspecifiedResponse} | {objectResult.StatusCode} | {nameof(objectResult.Value)}"
_ => string.Format(Resources.API_Response_ERROR_UnspecifiedResponse, objectResult.StatusCode, $"The response type {nameof(objectResult.Value)}")
};
}
#endregion
Expand Down
85 changes: 22 additions & 63 deletions EventsHandler/Api/EventsHandler/Controllers/EventsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
using EventsHandler.Attributes.Validation;
using EventsHandler.Controllers.Base;
using EventsHandler.Extensions;
using EventsHandler.Mapping.Enums;
using EventsHandler.Mapping.Models.POCOs.NotificatieApi;
using EventsHandler.Services.DataProcessing.Interfaces;
using EventsHandler.Services.DataProcessing.Strategy.Responses;
using EventsHandler.Services.Responding;
using EventsHandler.Services.Responding.Interfaces;
using EventsHandler.Services.Responding.Messages.Models.Errors;
using EventsHandler.Services.Serialization.Interfaces;
using EventsHandler.Services.Validation.Interfaces;
using EventsHandler.Services.Versioning.Interfaces;
using EventsHandler.Utilities.Swagger.Examples;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Filters;
using System.ComponentModel.DataAnnotations;
using EventsHandler.Services.Responding.Messages.Models.Base;
using Resources = EventsHandler.Properties.Resources;

namespace EventsHandler.Controllers
Expand All @@ -27,29 +26,21 @@ namespace EventsHandler.Controllers
/// <seealso cref="OmcController"/>
public sealed class EventsController : OmcController
{
private readonly ISerializationService _serializer;
private readonly IValidationService<NotificationEvent> _validator;
private readonly IProcessingService<NotificationEvent> _processor;
private readonly IRespondingService<NotificationEvent> _responder;
private readonly IProcessingService _processor;
private readonly IRespondingService<ProcessingResult> _responder;
private readonly IVersionsRegister _register;

/// <summary>
/// Initializes a new instance of the <see cref="EventsController"/> class.
/// </summary>
/// <param name="serializer">The input de(serializing) service.</param>
/// <param name="validator">The input validating service.</param>
/// <param name="processor">The input processing service (business logic).</param>
/// <param name="responder">The output standardization service (UX/UI).</param>
/// <param name="register">The register of versioned services.</param>
public EventsController(
ISerializationService serializer,
IValidationService<NotificationEvent> validator,
IProcessingService<NotificationEvent> processor,
IRespondingService<NotificationEvent> responder,
IProcessingService processor,
OmcResponder responder,
IVersionsRegister register)
{
this._serializer = serializer;
this._validator = validator;
this._processor = processor;
this._responder = responder;
this._register = register;
Expand All @@ -70,43 +61,28 @@ public EventsController(
[StandardizeApiResponses] // NOTE: Replace errors raised by ASP.NET Core with standardized API responses
// Swagger UI
[SwaggerRequestExample(typeof(NotificationEvent), typeof(NotificationEventExample))] // NOTE: Documentation of expected JSON schema with sample and valid payload values
[ProducesResponseType(StatusCodes.Status202Accepted)] // REASON: The notification was valid, and it was successfully sent to "Notify NL" Web API service
[ProducesResponseType(StatusCodes.Status206PartialContent)] // REASON: The notification was not sent (e.g., "test" ping received or scenario is not yet implemented. No need to retry sending it)
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProcessingFailed.Detailed))] // REASON: The notification was not sent (e.g., it was invalid due to missing data or improper structure. Retry sending is required)
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProcessingFailed.Detailed))] // REASON: Input deserialization error (e.g. model binding of required properties)
[ProducesResponseType(StatusCodes.Status500InternalServerError, Type = typeof(ProcessingFailed.Detailed))] // REASON: Internal server error (if-else / try-catch-finally handle)
[ProducesResponseType(StatusCodes.Status501NotImplemented, Type = typeof(string))] // REASON: Operation is not implemented (a new case is not yet supported)
[ProducesResponseType(StatusCodes.Status202Accepted, Type = typeof(BaseStandardResponseBody))] // REASON: The notification was sent to "Notify NL" Web API service
[ProducesResponseType(StatusCodes.Status206PartialContent, Type = typeof(BaseEnhancedStandardResponseBody))] // REASON: Test ping notification was received, serialization failed
[ProducesResponseType(StatusCodes.Status412PreconditionFailed, Type = typeof(BaseEnhancedStandardResponseBody))] // REASON: Some conditions predeceasing the request were not met
public async Task<IActionResult> ListenAsync([Required, FromBody] object json)
{
/* Validation #1: The validation of JSON payload structure and model-binding of [Required] properties are
* happening on the level of [FromBody] annotation. The attribute [StandardizeApiResponses]
* is meant to intercept native framework errors, raised immediately by ASP.NET Core validation
* mechanism, and to re-pack them ("beautify") into user-friendly standardized API responses */
/* The validation of JSON payload structure and model-binding of [Required] properties are
* happening on the level of [FromBody] annotation. The attribute [StandardizeApiResponses]
* is meant to intercept native framework errors, raised immediately by ASP.NET Core validation
* mechanism, and to re-pack them ("beautify") into user-friendly standardized API responses */
try
{
// Deserialize received JSON payload
NotificationEvent notification = this._serializer.Deserialize<NotificationEvent>(json);
// Try to process the received notification
ProcessingResult result = await this._processor.ProcessAsync(json);

// Validation #2: Structural and data inconsistencies analysis of optional properties
return this._validator.Validate(ref notification) is HealthCheck.OK_Valid
or HealthCheck.OK_Inconsistent
// Try to process the received notification
? await Task.Run<IActionResult>(async () =>
{
(ProcessingResult Status, string _) result = await this._processor.ProcessAsync(notification);

return LogApiResponse(result.Status.ConvertToLogLevel(), // LogLevel
this._responder.GetResponse(GetResult(result, json), notification.Details));
})

// The notification cannot be processed
: LogApiResponse(LogLevel.Error,
this._responder.GetResponse(GetAbortedResult(notification.Details.Message, json), notification.Details));
return LogApiResponse(result.Status.ConvertToLogLevel(), // LogLevel
this._responder.GetResponse(result));
}
catch (Exception exception)
{
// Serious problems occurred during the attempt to process the notification
return LogApiResponse(exception, this._responder.GetExceptionResponse(exception));
// Unhandled problems occurred during the attempt to process the notification
return LogApiResponse(exception,
this._responder.GetExceptionResponse(exception));
}
}

Expand All @@ -120,30 +96,13 @@ or HealthCheck.OK_Inconsistent
// User experience
[StandardizeApiResponses] // NOTE: Replace errors raised by ASP.NET Core with standardized API responses
// Swagger UI
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
public IActionResult Version()
{
LogApiResponse(LogLevel.Trace, Resources.Events_ApiVersionRequested);

return Ok(this._register.GetOmcVersion(
this._register.GetApisVersions()));
}

#region Helper methods
private static (ProcessingResult, string) GetResult((ProcessingResult Status, string Description) result, object json)
{
return (result.Status, EnrichDescription(result.Description, json));
}

private static (ProcessingResult, string) GetAbortedResult(string message, object json)
{
return (ProcessingResult.NotPossible, EnrichDescription(message, json));
}

private static string EnrichDescription(string originalText, object json)
{
return $"{originalText} | Notification: {json}";
}
#endregion
}
}
15 changes: 6 additions & 9 deletions EventsHandler/Api/EventsHandler/Controllers/NotifyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
using EventsHandler.Attributes.Authorization;
using EventsHandler.Attributes.Validation;
using EventsHandler.Controllers.Base;
using EventsHandler.Mapping.Enums;
using EventsHandler.Mapping.Models.POCOs.NotifyNL;
using EventsHandler.Services.Responding;
using EventsHandler.Services.Responding.Interfaces;
using EventsHandler.Services.Responding.Messages.Models.Errors;
using EventsHandler.Services.Responding.Messages.Models.Base;
using EventsHandler.Utilities.Swagger.Examples;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Filters;
Expand All @@ -19,6 +17,8 @@ namespace EventsHandler.Controllers
/// Controller used to get feedback from "Notify NL" Web API service.
/// </summary>
/// <seealso cref="OmcController"/>
// Swagger UI
[ProducesResponseType(StatusCodes.Status202Accepted, Type = typeof(BaseStandardResponseBody))] // REASON: The API service is up and running
public sealed class NotifyController : OmcController
{
private readonly NotifyResponder _responder;
Expand All @@ -27,9 +27,9 @@ public sealed class NotifyController : OmcController
/// Initializes a new instance of the <see cref="NotifyController"/> class.
/// </summary>
/// <param name="responder">The output standardization service (UX/UI).</param>
public NotifyController(IRespondingService<ProcessingResult, string> responder)
public NotifyController(NotifyResponder responder)
{
this._responder = (NotifyResponder)responder;
this._responder = responder;
}

/// <summary>
Expand All @@ -47,10 +47,7 @@ public NotifyController(IRespondingService<ProcessingResult, string> responder)
[StandardizeApiResponses] // NOTE: Replace errors raised by ASP.NET Core with standardized API responses
// Swagger UI
[SwaggerRequestExample(typeof(DeliveryReceipt), typeof(DeliveryReceiptExample))] // NOTE: Documentation of expected JSON schema with sample and valid payload values
[ProducesResponseType(StatusCodes.Status202Accepted)] // REASON: The delivery receipt with successful status
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProcessingFailed.Simplified))] // REASON: The delivery receipt with failure status
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(ProcessingFailed.Simplified))] // REASON: The JSON structure is invalid
[ProducesResponseType(StatusCodes.Status500InternalServerError)] // REASON: Internal server error (if-else / try-catch-finally handle)
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity, Type = typeof(BaseEnhancedStandardResponseBody))] // REASON: The JSON structure is invalid
public async Task<IActionResult> ConfirmAsync([Required, FromBody] object json)
{
return await this._responder.HandleNotifyCallbackAsync(json);
Expand Down
Loading

0 comments on commit 63760bf

Please sign in to comment.