From 897f8bc9eded893465d48486d2b21f4fe5ee2847 Mon Sep 17 00:00:00 2001 From: Norbert Truchsess Date: Tue, 31 Oct 2023 11:17:48 +0100 Subject: [PATCH] feat(errorhandling)! enhance exceptions with details for errortype, errorcode and parameters (#315) add new exceptions for errortype, errorcode and parameters ------- Refs: CPLP-3090 Reviewed-By: Phil Schneider --- .../RegistrationBusinessLogic.cs | 3 +- ...rationRegistrationErrorMessageContainer.cs | 38 +++++++++ .../Administration.Service/Program.cs | 6 ++ .../ConfigurationException.cs | 12 ++- .../ConflictException.cs | 14 +++- .../ControllerArgumentException.cs | 27 ++++--- .../DetailException.cs | 80 +++++++++++++++++++ .../ErrorDetails.cs | 32 ++++++++ .../ForbiddenException.cs | 14 +++- .../Library/ErrorMessageService.cs | 43 ++++++++++ .../Library/ErrorResponse.cs | 29 +++++++ .../IErrorMessageContainer.cs} | 30 +------ .../Library/IErrorMessageService.cs | 25 ++++++ .../NotFoundException.cs | 14 +++- .../ServiceException.cs | 30 ++++++- .../UnexpectedConditionException.cs | 12 ++- .../UnsupportedMediaTypeException.cs | 14 +++- .../GeneralHttpErrorHandler.cs | 39 ++++++--- .../GeneralHttpErrorHandlerTests.cs | 55 ++++++++++++- 19 files changed, 441 insertions(+), 76 deletions(-) create mode 100644 src/administration/Administration.Service/ErrorHandling/AdministrationRegistrationErrorMessageContainer.cs create mode 100644 src/framework/Framework.ErrorHandling.Library/DetailException.cs create mode 100644 src/framework/Framework.ErrorHandling.Library/ErrorDetails.cs create mode 100644 src/framework/Framework.ErrorHandling.Library/Library/ErrorMessageService.cs create mode 100644 src/framework/Framework.ErrorHandling.Library/Library/ErrorResponse.cs rename src/framework/Framework.ErrorHandling.Library/{ErrorResponse.cs => Library/IErrorMessageContainer.cs} (56%) create mode 100644 src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageService.cs diff --git a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs index 8e5a021842..76985bf10b 100644 --- a/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs +++ b/src/administration/Administration.Service/BusinessLogic/RegistrationBusinessLogic.cs @@ -20,6 +20,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using Org.Eclipse.TractusX.Portal.Backend.Administration.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.Models; using Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.BusinessLogic; using Org.Eclipse.TractusX.Portal.Backend.Clearinghouse.Library.Models; @@ -82,7 +83,7 @@ private async Task GetCompanyWithAddressAsyncInternal(Gu var companyWithAddress = await _portalRepositories.GetInstance().GetCompanyUserRoleWithAddressUntrackedAsync(applicationId).ConfigureAwait(false); if (companyWithAddress == null) { - throw new NotFoundException($"applicationId {applicationId} not found"); + throw NotFoundException.Create(AdministrationRegistrationErrors.APPLICATION_NOT_FOUND, new ErrorParameter[] { new("applicationId", applicationId.ToString()) }); } return new CompanyWithAddressData( companyWithAddress.CompanyId, diff --git a/src/administration/Administration.Service/ErrorHandling/AdministrationRegistrationErrorMessageContainer.cs b/src/administration/Administration.Service/ErrorHandling/AdministrationRegistrationErrorMessageContainer.cs new file mode 100644 index 0000000000..2b435ece39 --- /dev/null +++ b/src/administration/Administration.Service/ErrorHandling/AdministrationRegistrationErrorMessageContainer.cs @@ -0,0 +1,38 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Administration.ErrorHandling; + +public class AdministrationRegistrationErrorMessageContainer : IErrorMessageContainer +{ + private static readonly IReadOnlyDictionary _messageContainer = new Dictionary { + { AdministrationRegistrationErrors.APPLICATION_NOT_FOUND, "application {applicationId} does not exist" } + }.ToImmutableDictionary(x => (int)x.Key, x => x.Value); + + public Type Type { get => typeof(AdministrationRegistrationErrors); } + public IReadOnlyDictionary MessageContainer { get => _messageContainer; } +} + +public enum AdministrationRegistrationErrors +{ + APPLICATION_NOT_FOUND +} diff --git a/src/administration/Administration.Service/Program.cs b/src/administration/Administration.Service/Program.cs index 7af74c7c45..0ed5e57035 100644 --- a/src/administration/Administration.Service/Program.cs +++ b/src/administration/Administration.Service/Program.cs @@ -18,8 +18,10 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ +using Org.Eclipse.TractusX.Portal.Backend.Administration.ErrorHandling; using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.BusinessLogic; using Org.Eclipse.TractusX.Portal.Backend.Administration.Service.DependencyInjection; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; using Org.Eclipse.TractusX.Portal.Backend.Framework.Web; using Org.Eclipse.TractusX.Portal.Backend.Mailing.SendMail; using Org.Eclipse.TractusX.Portal.Backend.Notifications.Library; @@ -78,5 +80,9 @@ .AddPartnerRegistration(builder.Configuration) .AddNetworkRegistrationProcessHelper(); + builder.Services + .AddSingleton() + .AddSingleton(); + builder.Services.AddProvisioningDBAccess(builder.Configuration); }); diff --git a/src/framework/Framework.ErrorHandling.Library/ConfigurationException.cs b/src/framework/Framework.ErrorHandling.Library/ConfigurationException.cs index 55d25d7262..bddebf8406 100644 --- a/src/framework/Framework.ErrorHandling.Library/ConfigurationException.cs +++ b/src/framework/Framework.ErrorHandling.Library/ConfigurationException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class ConfigurationException : Exception +public class ConfigurationException : DetailException { - public ConfigurationException() { } + public ConfigurationException() : base() { } public ConfigurationException(string message) : base(message) { } public ConfigurationException(string message, Exception inner) : base(message, inner) { } protected ConfigurationException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected ConfigurationException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static ConfigurationException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static ConfigurationException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/ConflictException.cs b/src/framework/Framework.ErrorHandling.Library/ConflictException.cs index f5ef1b182a..cc6db2db0e 100644 --- a/src/framework/Framework.ErrorHandling.Library/ConflictException.cs +++ b/src/framework/Framework.ErrorHandling.Library/ConflictException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class ConflictException : Exception +public class ConflictException : DetailException { - public ConflictException() { } + public ConflictException() : base() { } public ConflictException(string message) : base(message) { } - public ConflictException(string message, System.Exception inner) : base(message, inner) { } + public ConflictException(string message, Exception inner) : base(message, inner) { } protected ConflictException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected ConflictException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static ConflictException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static ConflictException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/ControllerArgumentException.cs b/src/framework/Framework.ErrorHandling.Library/ControllerArgumentException.cs index 34b8bf8733..ba275fa2c6 100644 --- a/src/framework/Framework.ErrorHandling.Library/ControllerArgumentException.cs +++ b/src/framework/Framework.ErrorHandling.Library/ControllerArgumentException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -22,18 +21,14 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; /// [Serializable] -public class ControllerArgumentException : Exception +public class ControllerArgumentException : DetailException { - public ControllerArgumentException(string message) : base(message) { } + public ControllerArgumentException() : base() { } - public ControllerArgumentException(ArgumentException argumentException) - : this(argumentException.Message) - { - ParamName = argumentException.ParamName; - } + public ControllerArgumentException(string message) : base(message) { } public ControllerArgumentException(string message, string paramName) - : base(String.Format("{0} (Parameter '{1}')", message, paramName)) + : base(string.Format("{0} (Parameter '{1}')", message, paramName)) { ParamName = paramName; } @@ -43,4 +38,18 @@ public ControllerArgumentException(string message, string paramName) protected ControllerArgumentException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected ControllerArgumentException(Type errorType, int errorCode, IEnumerable? parameters = null, string? paramName = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) + { + ParamName = paramName; + } + + public static ControllerArgumentException Create(T error, IEnumerable? parameters = null, string? paramName = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, paramName, inner); + public static ControllerArgumentException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, null, inner); + public static ControllerArgumentException Create(T error, IEnumerable parameters, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), parameters, null, inner); + public static ControllerArgumentException Create(T error, string paramName, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), null, paramName, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/DetailException.cs b/src/framework/Framework.ErrorHandling.Library/DetailException.cs new file mode 100644 index 0000000000..79051f7336 --- /dev/null +++ b/src/framework/Framework.ErrorHandling.Library/DetailException.cs @@ -0,0 +1,80 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; +using System.Text.RegularExpressions; + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +[Serializable] +public abstract class DetailException : Exception +{ + private static readonly Regex _templateMatcherExpression = new Regex(@"\{(\w+)\}", RegexOptions.None, TimeSpan.FromSeconds(1)); // to replace any text surrounded by { and } + private enum NoDetailsErrorType + { + NONE = 0 + } + + protected DetailException() : base() { } + protected DetailException(string message) : base(message) { } + protected DetailException(string message, Exception inner) : base(message, inner) { } + protected DetailException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected DetailException( + Type errorType, int errorCode, IEnumerable? parameters, Exception? inner) : base(Enum.GetName(errorType, errorCode), inner) + { + ErrorType = errorType; + ErrorCode = errorCode; + Parameters = parameters ?? Enumerable.Empty(); + } + + protected static int ValueOf(T error) where T : Enum => (int)Convert.ChangeType(error, TypeCode.Int32); + + public Type ErrorType { get; } = typeof(NoDetailsErrorType); + public int ErrorCode { get; } = (int)NoDetailsErrorType.NONE; + public IEnumerable Parameters { get; } = Enumerable.Empty(); + public bool HasDetails { get => ErrorType != typeof(NoDetailsErrorType); } + + public string GetErrorMessage(IErrorMessageService messageService) => + _templateMatcherExpression.Replace( + messageService.GetMessage(ErrorType, ErrorCode), + m => Parameters.SingleOrDefault(x => x.Name == m.Groups[1].Value)?.Value ?? "null"); + + public IEnumerable GetErrorDetails(IErrorMessageService messageService) => + GetDetailExceptions().Select(x => + new ErrorDetails( + x.Message, + x.ErrorType.Name, + messageService.GetMessage(x.ErrorType, x.ErrorCode), + x.Parameters)); + + private IEnumerable GetDetailExceptions() + { + yield return this; + var inner = InnerException; + while (inner is not null) + { + if (inner is DetailException detail && detail.ErrorType != typeof(NoDetailsErrorType)) + yield return detail; + inner = inner.InnerException; + } + } +} diff --git a/src/framework/Framework.ErrorHandling.Library/ErrorDetails.cs b/src/framework/Framework.ErrorHandling.Library/ErrorDetails.cs new file mode 100644 index 0000000000..583732e7c7 --- /dev/null +++ b/src/framework/Framework.ErrorHandling.Library/ErrorDetails.cs @@ -0,0 +1,32 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; + +public record ErrorDetails( + string ErrorCode, + string Type, + string Message, + IEnumerable Parameters +); + +public record ErrorParameter( + string Name, + string Value +); diff --git a/src/framework/Framework.ErrorHandling.Library/ForbiddenException.cs b/src/framework/Framework.ErrorHandling.Library/ForbiddenException.cs index 00f342c5c4..ca7d10c719 100644 --- a/src/framework/Framework.ErrorHandling.Library/ForbiddenException.cs +++ b/src/framework/Framework.ErrorHandling.Library/ForbiddenException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class ForbiddenException : Exception +public class ForbiddenException : DetailException { - public ForbiddenException() { } + public ForbiddenException() : base() { } public ForbiddenException(string message) : base(message) { } - public ForbiddenException(string message, System.Exception inner) : base(message, inner) { } + public ForbiddenException(string message, Exception inner) : base(message, inner) { } protected ForbiddenException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected ForbiddenException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static ForbiddenException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static ForbiddenException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/Library/ErrorMessageService.cs b/src/framework/Framework.ErrorHandling.Library/Library/ErrorMessageService.cs new file mode 100644 index 0000000000..d1152949a4 --- /dev/null +++ b/src/framework/Framework.ErrorHandling.Library/Library/ErrorMessageService.cs @@ -0,0 +1,43 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +using System.Collections.Immutable; + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; + +public sealed class ErrorMessageService : IErrorMessageService +{ + private readonly IReadOnlyDictionary> _messageContainers; + + public ErrorMessageService(IEnumerable errorMessageContainers) + { + _messageContainers = errorMessageContainers.ToImmutableDictionary(x => x.Type, x => x.MessageContainer); + } + + public string GetMessage(Type type, int code) + { + if (!_messageContainers.TryGetValue(type, out var container)) + throw new ArgumentException($"unexpected type {type.Name}"); + + if (!container.TryGetValue(code, out var message)) + throw new ArgumentException($"no message defined for {type.Name}.{Enum.GetName(type, code)}"); + + return message; + } +} diff --git a/src/framework/Framework.ErrorHandling.Library/Library/ErrorResponse.cs b/src/framework/Framework.ErrorHandling.Library/Library/ErrorResponse.cs new file mode 100644 index 0000000000..7c8e6f782c --- /dev/null +++ b/src/framework/Framework.ErrorHandling.Library/Library/ErrorResponse.cs @@ -0,0 +1,29 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; + +public record ErrorResponse( + string Type, + string Title, + int Status, + IDictionary> Errors, + string ErrorId, + IEnumerable? Details +); diff --git a/src/framework/Framework.ErrorHandling.Library/ErrorResponse.cs b/src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageContainer.cs similarity index 56% rename from src/framework/Framework.ErrorHandling.Library/ErrorResponse.cs rename to src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageContainer.cs index 3cbcea09d4..b99f56a09f 100644 --- a/src/framework/Framework.ErrorHandling.Library/ErrorResponse.cs +++ b/src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageContainer.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -18,33 +17,10 @@ * SPDX-License-Identifier: Apache-2.0 ********************************************************************************/ -using System.Text.Json.Serialization; - namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; -public class ErrorResponse +public interface IErrorMessageContainer { - public ErrorResponse(string type, string title, int status, IDictionary> errors, string errorId) - { - Type = type; - Title = title; - Status = status; - Errors = errors; - ErrorId = errorId; - } - - [JsonPropertyName("type")] - public string Type { get; set; } - - [JsonPropertyName("title")] - public string Title { get; set; } - - [JsonPropertyName("status")] - public int Status { get; set; } - - [JsonPropertyName("errors")] - public IDictionary> Errors { get; set; } - - [JsonPropertyName("errorId")] - public string ErrorId { get; set; } + public Type Type { get; } + public IReadOnlyDictionary MessageContainer { get; } } diff --git a/src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageService.cs b/src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageService.cs new file mode 100644 index 0000000000..e37c437df5 --- /dev/null +++ b/src/framework/Framework.ErrorHandling.Library/Library/IErrorMessageService.cs @@ -0,0 +1,25 @@ +/******************************************************************************** + * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; + +public interface IErrorMessageService +{ + public string GetMessage(Type type, int code); +} diff --git a/src/framework/Framework.ErrorHandling.Library/NotFoundException.cs b/src/framework/Framework.ErrorHandling.Library/NotFoundException.cs index 75b72bd1c2..ce252ca177 100644 --- a/src/framework/Framework.ErrorHandling.Library/NotFoundException.cs +++ b/src/framework/Framework.ErrorHandling.Library/NotFoundException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class NotFoundException : Exception +public class NotFoundException : DetailException { - public NotFoundException() { } + public NotFoundException() : base() { } public NotFoundException(string message) : base(message) { } - public NotFoundException(string message, System.Exception inner) : base(message, inner) { } + public NotFoundException(string message, Exception inner) : base(message, inner) { } protected NotFoundException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected NotFoundException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static NotFoundException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static NotFoundException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/ServiceException.cs b/src/framework/Framework.ErrorHandling.Library/ServiceException.cs index 3fa4db3bcf..9511ec6f68 100644 --- a/src/framework/Framework.ErrorHandling.Library/ServiceException.cs +++ b/src/framework/Framework.ErrorHandling.Library/ServiceException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -23,11 +22,13 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class ServiceException : Exception +public class ServiceException : DetailException { public HttpStatusCode? StatusCode { get; } public bool IsRecoverable { get; } + public ServiceException() : base() { } + public ServiceException(string message, bool isRecoverable = false) : base(message) { StatusCode = null; @@ -40,13 +41,13 @@ public ServiceException(string message, HttpStatusCode httpStatusCode, bool isRe IsRecoverable = isRecoverable; } - public ServiceException(string message, System.Exception inner, bool isRecoverable = false) : base(message, inner) + public ServiceException(string message, Exception inner, bool isRecoverable = false) : base(message, inner) { StatusCode = null; IsRecoverable = isRecoverable; } - public ServiceException(string message, System.Exception inner, HttpStatusCode httpStatusCode, bool isRecoverable = false) : base(message, inner) + public ServiceException(string message, Exception inner, HttpStatusCode httpStatusCode, bool isRecoverable = false) : base(message, inner) { StatusCode = httpStatusCode; IsRecoverable = isRecoverable; @@ -55,4 +56,25 @@ public ServiceException(string message, System.Exception inner, HttpStatusCode h protected ServiceException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected ServiceException(Type errorType, int errorCode, IEnumerable? parameters = null, HttpStatusCode? httpStatusCode = null, bool isRecoverable = false, Exception? inner = null) : base(errorType, errorCode, parameters, inner) + { + StatusCode = httpStatusCode; + IsRecoverable = isRecoverable; + } + + public static ServiceException Create(T error, IEnumerable? parameters = null, HttpStatusCode? httpStatusCode = null, bool isRecoverable = false, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, httpStatusCode, isRecoverable, inner); + public static ServiceException Create(T error, HttpStatusCode httpStatusCode, bool isRecoverable = false, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), null, httpStatusCode, isRecoverable, inner); + public static ServiceException Create(T error, bool isRecoverable = false, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), null, null, isRecoverable, inner); + public static ServiceException Create(T error, IEnumerable parameters, bool isRecoverable, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, null, isRecoverable, inner); + public static ServiceException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, null, false, inner); + public static ServiceException Create(T error, IEnumerable parameters, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), parameters, null, false, inner); + public static ServiceException Create(T error, IEnumerable parameters, HttpStatusCode httpStatusCode, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), parameters, httpStatusCode, false, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/UnexpectedConditionException.cs b/src/framework/Framework.ErrorHandling.Library/UnexpectedConditionException.cs index 709678a6a8..90b53ad1e0 100644 --- a/src/framework/Framework.ErrorHandling.Library/UnexpectedConditionException.cs +++ b/src/framework/Framework.ErrorHandling.Library/UnexpectedConditionException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class UnexpectedConditionException : Exception +public class UnexpectedConditionException : DetailException { - public UnexpectedConditionException() { } + public UnexpectedConditionException() : base() { } public UnexpectedConditionException(string message) : base(message) { } public UnexpectedConditionException(string message, Exception inner) : base(message, inner) { } protected UnexpectedConditionException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected UnexpectedConditionException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static UnexpectedConditionException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static UnexpectedConditionException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Library/UnsupportedMediaTypeException.cs b/src/framework/Framework.ErrorHandling.Library/UnsupportedMediaTypeException.cs index ca664969f2..ccb9d5a0ff 100644 --- a/src/framework/Framework.ErrorHandling.Library/UnsupportedMediaTypeException.cs +++ b/src/framework/Framework.ErrorHandling.Library/UnsupportedMediaTypeException.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -21,12 +20,19 @@ namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling; [Serializable] -public class UnsupportedMediaTypeException : Exception +public class UnsupportedMediaTypeException : DetailException { - public UnsupportedMediaTypeException() { } + public UnsupportedMediaTypeException() : base() { } public UnsupportedMediaTypeException(string message) : base(message) { } - public UnsupportedMediaTypeException(string message, System.Exception inner) : base(message, inner) { } + public UnsupportedMediaTypeException(string message, Exception inner) : base(message, inner) { } protected UnsupportedMediaTypeException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + + protected UnsupportedMediaTypeException(Type errorType, int errorCode, IEnumerable? parameters = null, Exception? inner = null) : base(errorType, errorCode, parameters, inner) { } + + public static UnsupportedMediaTypeException Create(T error, IEnumerable? parameters = null, Exception? inner = null) where T : Enum => + new(typeof(T), ValueOf(error), parameters, inner); + public static UnsupportedMediaTypeException Create(T error, Exception inner) where T : Enum => + new(typeof(T), ValueOf(error), null, inner); } diff --git a/src/framework/Framework.ErrorHandling.Web/GeneralHttpErrorHandler.cs b/src/framework/Framework.ErrorHandling.Web/GeneralHttpErrorHandler.cs index ae514bb627..5a542b108a 100644 --- a/src/framework/Framework.ErrorHandling.Web/GeneralHttpErrorHandler.cs +++ b/src/framework/Framework.ErrorHandling.Web/GeneralHttpErrorHandler.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -25,12 +24,15 @@ using System.Collections.Immutable; using System.Net; using System.Text.Json; +using System.Text.Json.Serialization; namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; public class GeneralHttpErrorHandler { + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly RequestDelegate _next; + private readonly IErrorMessageService? _errorMessageService; private readonly ILogger _logger; private static readonly IReadOnlyDictionary Metadata = new Dictionary @@ -45,10 +47,11 @@ public class GeneralHttpErrorHandler { HttpStatusCode.InternalServerError, new MetaData("https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.1", "The server encountered an unexpected condition.") } }.ToImmutableDictionary(); - public GeneralHttpErrorHandler(RequestDelegate next, ILogger logger) + public GeneralHttpErrorHandler(RequestDelegate next, ILogger logger, IErrorMessageService? errorMessageService = null) //TODO make errorMessageService mandatory as soon all dependant services are adjusted accordingly { _next = next; _logger = logger; + _errorMessageService = errorMessageService; } public async Task Invoke(HttpContext context) @@ -60,13 +63,14 @@ public async Task Invoke(HttpContext context) catch (Exception error) { var errorId = Guid.NewGuid().ToString(); + var details = GetErrorDetails(error); + var message = GetErrorMessage(error); LogErrorInformation(errorId, error); var (statusCode, messageFunc, logLevel) = GetErrorInformation(error); - - _logger.Log(logLevel, error, "GeneralErrorHandler caught {Error} with errorId: {ErrorId} resulting in response status code {StatusCode}, message '{Message}'", error.GetType().Name, errorId, (int)statusCode, error.Message); + _logger.Log(logLevel, error, "GeneralErrorHandler caught {Error} with errorId: {ErrorId} resulting in response status code {StatusCode}, message '{Message}'", error.GetType().Name, errorId, (int)statusCode, message); context.Response.ContentType = "application/json"; context.Response.StatusCode = (int)statusCode; - await context.Response.WriteAsync(JsonSerializer.Serialize(CreateErrorResponse(statusCode, error, errorId, messageFunc))).ConfigureAwait(false); + await context.Response.WriteAsync(JsonSerializer.Serialize(CreateErrorResponse(statusCode, error, errorId, message, details, messageFunc), Options)).ConfigureAwait(false); } } @@ -128,20 +132,20 @@ private static (HttpStatusCode StatusCode, Func)>? getSourceAndMessages = null) + private ErrorResponse CreateErrorResponse(HttpStatusCode statusCode, Exception error, string errorId, string message, IEnumerable? details, Func)>? getSourceAndMessages = null) { var meta = Metadata.GetValueOrDefault(statusCode, Metadata[HttpStatusCode.InternalServerError]); - var (source, messages) = getSourceAndMessages?.Invoke(error) ?? (error.Source, Enumerable.Repeat(error.Message, 1)); + var (source, messages) = getSourceAndMessages?.Invoke(error) ?? (error.Source, Enumerable.Repeat(message, 1)); var messageMap = new Dictionary> { { source ?? "unknown", messages } }; while (error.InnerException != null) { - error = error.InnerException; - source = error.Source ?? "inner"; + var inner = error.InnerException; + source = inner.Source ?? "inner"; messageMap[source] = messageMap.TryGetValue(source, out messages) - ? messages.Append(error.Message) - : Enumerable.Repeat(error.Message, 1); + ? messages.Append(GetErrorMessage(inner)) + : Enumerable.Repeat(GetErrorMessage(inner), 1); } return new ErrorResponse( @@ -149,10 +153,21 @@ private static ErrorResponse CreateErrorResponse(HttpStatusCode statusCode, Exce meta.Description, (int)statusCode, messageMap, - errorId + errorId, + details ); } + private string GetErrorMessage(Exception exception) => + _errorMessageService is not null && exception is DetailException detail && detail.HasDetails + ? detail.GetErrorMessage(_errorMessageService) + : exception.Message; + + private IEnumerable GetErrorDetails(Exception exception) => + _errorMessageService is not null && exception is DetailException detail && detail.HasDetails + ? detail.GetErrorDetails(_errorMessageService) + : Enumerable.Empty(); + private static void LogErrorInformation(string errorId, Exception exception) { LogContext.PushProperty("ErrorId", errorId); diff --git a/tests/framework/Framework.ErrorHandling.Web.Tests/GeneralHttpErrorHandlerTests.cs b/tests/framework/Framework.ErrorHandling.Web.Tests/GeneralHttpErrorHandlerTests.cs index f1271ee330..f47f06b565 100644 --- a/tests/framework/Framework.ErrorHandling.Web.Tests/GeneralHttpErrorHandlerTests.cs +++ b/tests/framework/Framework.ErrorHandling.Web.Tests/GeneralHttpErrorHandlerTests.cs @@ -1,5 +1,4 @@ /******************************************************************************** - * Copyright (c) 2021, 2023 BMW Group AG * Copyright (c) 2021, 2023 Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional @@ -24,14 +23,19 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Library; using Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Web; +using Org.Eclipse.TractusX.Portal.Backend.Tests.Shared; using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; using Xunit; namespace Org.Eclipse.TractusX.Portal.Backend.Framework.ErrorHandling.Tests; public class GeneralHttpErrorHandlerTests { + private static readonly JsonSerializerOptions Options = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly IFixture _fixture; public GeneralHttpErrorHandlerTests() @@ -203,4 +207,53 @@ public async Task Invoke_WithUnhandledSpecificException_ContentStatusCodeIs500() // Assert ((HttpStatusCode)httpContext.Response.StatusCode).Should().Be(HttpStatusCode.InternalServerError); } + + [Fact] + public async Task Invoke_WithDetailedException() + { + // Arrange + var expectedException = ConflictException.Create(TestErrors.FIRST_ERROR, new ErrorParameter[] { new("first", "foo"), new("second", "bar") }); + Task MockNextMiddleware(HttpContext _) => Task.FromException(expectedException); + using var body = new MemoryStream(); + var httpContext = new DefaultHttpContext(); + httpContext.Response.Body = body; + + var mockLogger = A.Fake>(); + var logger = new MockLogger(mockLogger); + + var errorMessageService = A.Fake(); + A.CallTo(() => errorMessageService.GetMessage(A._, A._)) + .ReturnsLazily((Type type, int code) => $"type: {type.Name} code: {code} first: {{first}} second: {{second}}"); + + var generalHttpErrorHandler = new GeneralHttpErrorHandler(MockNextMiddleware, logger, errorMessageService); + + // Act + await generalHttpErrorHandler.Invoke(httpContext); + + // Assert + ((HttpStatusCode)httpContext.Response.StatusCode).Should().Be(HttpStatusCode.Conflict); + A.CallTo(() => mockLogger.Log( + A.That.IsEqualTo(LogLevel.Information), + expectedException, + A.That.Matches(x => + x.StartsWith("GeneralErrorHandler caught ConflictException with errorId:") && + x.EndsWith("resulting in response status code 409, message 'type: TestErrors code: 1 first: foo second: bar'")))) + .MustHaveHappenedOnceExactly(); + + body.Position = 0; + var errorResponse = JsonSerializer.Deserialize(body, Options); + errorResponse.Should().NotBeNull().And.BeOfType().Which.Details.Should().ContainSingle().Which.Should().Match(x => + x.Type == "TestErrors" && + x.ErrorCode == "FIRST_ERROR" && + x.Message == "type: TestErrors code: 1 first: {first} second: {second}" && + x.Parameters.Count() == 2 && + x.Parameters.First(p => p.Name == "first").Value == "foo" && + x.Parameters.First(p => p.Name == "second").Value == "bar"); + } + + private enum TestErrors + { + FIRST_ERROR = 1, + SECOND_ERROR = 2 + } }