From 9fb6c67d4ceb600016de20e5dbd7aa027ac45bbe Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Mon, 5 Jan 2026 14:39:20 +0200 Subject: [PATCH 1/6] Use TargetFramework-conditional PackageReferences for net8, net9 and net10 in all .csproj files --- .../Core/Odin.BackgroundProcessing.csproj | 36 +++++++---- .../Odin.Configuration.AzureBlobJson.csproj | 15 ++++- .../Odin.Data.SqlScriptsRunner.csproj | 14 +++++ Email/Core/Odin.Email.csproj | 16 ++++- Logging/Core/Odin.Logging.csproj | 59 +++++++++++-------- .../RabbitMq/Odin.Messaging.RabbitMq.csproj | 16 ++++- RemoteFiles/Core/Odin.RemoteFiles.csproj | 14 ++++- System/Result/Odin.System.Result.csproj | 12 +++- .../Odin.Utility.VaryingValues.csproj | 18 +++++- 9 files changed, 158 insertions(+), 42 deletions(-) diff --git a/BackgroundProcessing/Core/Odin.BackgroundProcessing.csproj b/BackgroundProcessing/Core/Odin.BackgroundProcessing.csproj index c30b3ae..54fc045 100644 --- a/BackgroundProcessing/Core/Odin.BackgroundProcessing.csproj +++ b/BackgroundProcessing/Core/Odin.BackgroundProcessing.csproj @@ -10,24 +10,40 @@ has static entry points to its functionality. - 1591;1573; - - - - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/Configuration/AzureBlobJson/Odin.Configuration.AzureBlobJson.csproj b/Configuration/AzureBlobJson/Odin.Configuration.AzureBlobJson.csproj index 0a50e08..3fa86f8 100644 --- a/Configuration/AzureBlobJson/Odin.Configuration.AzureBlobJson.csproj +++ b/Configuration/AzureBlobJson/Odin.Configuration.AzureBlobJson.csproj @@ -13,8 +13,21 @@ - + + + + + + + + + + + + + + diff --git a/Data/SqlScriptsRunner/Odin.Data.SqlScriptsRunner.csproj b/Data/SqlScriptsRunner/Odin.Data.SqlScriptsRunner.csproj index b62cb6b..f11cd37 100644 --- a/Data/SqlScriptsRunner/Odin.Data.SqlScriptsRunner.csproj +++ b/Data/SqlScriptsRunner/Odin.Data.SqlScriptsRunner.csproj @@ -15,9 +15,23 @@ + + + + + + + + + + + + + + diff --git a/Email/Core/Odin.Email.csproj b/Email/Core/Odin.Email.csproj index 0a458fa..e15b3f2 100644 --- a/Email/Core/Odin.Email.csproj +++ b/Email/Core/Odin.Email.csproj @@ -14,11 +14,25 @@ 1591;1573; - + + + + + + + + + + + + + + + diff --git a/Logging/Core/Odin.Logging.csproj b/Logging/Core/Odin.Logging.csproj index e7fa4b8..02c3f31 100644 --- a/Logging/Core/Odin.Logging.csproj +++ b/Logging/Core/Odin.Logging.csproj @@ -1,27 +1,38 @@  - - net8.0;net9.0;net10.0 - Odin - true - enable - icon.png - README.md - Provides ILoggerWrapper that extends .NET's ILogger of T - with all the LogXXX(...) calls as provided by the .NET LoggerExtensions extension methods. - The primary reason being for much more convenient assertions of logger calls compared to mocking ILogger, - and asserting ILogger -> Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, etc... - - - + + net8.0;net9.0;net10.0 + Odin + true + enable + icon.png + README.md + Provides ILoggerWrapper that extends .NET's ILogger of T + with all the LogXXX(...) calls as provided by the .NET LoggerExtensions extension methods. + The primary reason being for much more convenient assertions of logger calls compared to mocking ILogger, + and asserting ILogger -> Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, etc... + + - 1591;1573; - - - - - - - - - + 1591;1573; + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Messaging/RabbitMq/Odin.Messaging.RabbitMq.csproj b/Messaging/RabbitMq/Odin.Messaging.RabbitMq.csproj index 2b68de9..0120b53 100644 --- a/Messaging/RabbitMq/Odin.Messaging.RabbitMq.csproj +++ b/Messaging/RabbitMq/Odin.Messaging.RabbitMq.csproj @@ -13,10 +13,24 @@ CS1591 + + + + + + + + + + + + + + - + diff --git a/RemoteFiles/Core/Odin.RemoteFiles.csproj b/RemoteFiles/Core/Odin.RemoteFiles.csproj index f414ddf..3f7edb8 100644 --- a/RemoteFiles/Core/Odin.RemoteFiles.csproj +++ b/RemoteFiles/Core/Odin.RemoteFiles.csproj @@ -16,10 +16,22 @@ - + + + + + + + + + + + + + diff --git a/System/Result/Odin.System.Result.csproj b/System/Result/Odin.System.Result.csproj index 1b75e62..c2ccee8 100644 --- a/System/Result/Odin.System.Result.csproj +++ b/System/Result/Odin.System.Result.csproj @@ -15,9 +15,19 @@ 1591;1573; - + + + + + + + + + + + diff --git a/Utility/VaryingValues/Odin.Utility.VaryingValues.csproj b/Utility/VaryingValues/Odin.Utility.VaryingValues.csproj index deb20f1..c6dc8da 100644 --- a/Utility/VaryingValues/Odin.Utility.VaryingValues.csproj +++ b/Utility/VaryingValues/Odin.Utility.VaryingValues.csproj @@ -14,10 +14,22 @@ 1591;1573; - - - + + + + + + + + + + + + + + + From 44480096894f8639f010b9323a563a7f5070d5a1 Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Mon, 5 Jan 2026 14:43:35 +0200 Subject: [PATCH 2/6] Add notnull TValue type annotation to ResultValue and ToFailedResult method --- System/Result/ResultOfTMessage.cs | 13 ++++--- System/Result/ResultValue.cs | 19 +++++++++- System/Result/ResultValueEx.cs | 2 +- System/Result/ResultValueOfTMessage.cs | 51 +++++++++++++++++++++----- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/System/Result/ResultOfTMessage.cs b/System/Result/ResultOfTMessage.cs index 5fe8ee4..1fc5c3f 100644 --- a/System/Result/ResultOfTMessage.cs +++ b/System/Result/ResultOfTMessage.cs @@ -11,7 +11,7 @@ public record Result where TMessage : class /// True if successful /// public bool IsSuccess { get; init; } - + /// /// Messages list /// @@ -28,12 +28,12 @@ public IReadOnlyList Messages _messages ??= new List(); return _messages; } - init // For deserialisation + init // For deserialisation { _messages = value.ToList(); } } - + /// /// All messages flattened into 1 message. /// Assumes a decent implementation of TMessage.ToString() @@ -44,6 +44,7 @@ public string MessagesToString(string separator = " | ") { return string.Empty; } + return string.Join(separator, _messages.Select(c => c.ToString())); } @@ -55,7 +56,7 @@ public Result() { IsSuccess = false; } - + /// /// Result constructor. /// @@ -104,7 +105,7 @@ public static Result Failure(IEnumerable messages) { return new Result(false, messages); } - + /// /// Success. /// @@ -113,7 +114,7 @@ public static Result Success() { return new Result(true); } - + /// /// Success, optionally including a message /// diff --git a/System/Result/ResultValue.cs b/System/Result/ResultValue.cs index 3225b24..66bff37 100644 --- a/System/Result/ResultValue.cs +++ b/System/Result/ResultValue.cs @@ -6,7 +6,7 @@ /// /// /// To be renamed to ResultValue of TValue - public record ResultValue : ResultValue + public record ResultValue : ResultValue where TValue : notnull { /// /// Parameterless constructor for serialization. @@ -100,5 +100,22 @@ public ResultValue(bool isSuccess, TValue? value, string? message = null) { return new ResultValue(true, value, messages); } + + /// + /// + /// + /// + /// + /// + public override ResultValue ToFailedResult() + { + if (IsSuccess) + { + throw new ArgumentException($"Cannot convert a successful result of type {GetType().FullName} " + + $"to a failed result of type {typeof(ResultValue).FullName}."); + } + + return ResultValue.Failure(Messages); + } } } \ No newline at end of file diff --git a/System/Result/ResultValueEx.cs b/System/Result/ResultValueEx.cs index 06f8bf8..68d2e5e 100644 --- a/System/Result/ResultValueEx.cs +++ b/System/Result/ResultValueEx.cs @@ -7,7 +7,7 @@ /// /// /// To be renamed to ResultValueEx of TValue - public record ResultValueEx : ResultValue + public record ResultValueEx : ResultValue where TValue : notnull { /// /// Parameterless constructor for serialization. diff --git a/System/Result/ResultValueOfTMessage.cs b/System/Result/ResultValueOfTMessage.cs index 5925d43..c747176 100644 --- a/System/Result/ResultValueOfTMessage.cs +++ b/System/Result/ResultValueOfTMessage.cs @@ -8,20 +8,20 @@ namespace Odin.System /// /// /// - public record ResultValue where TMessage : class + public record ResultValue where TMessage : class where TValue : notnull { /// /// True if successful /// [MemberNotNullWhen(true, nameof(Value))] public bool IsSuccess { get; init; } - + /// /// Value is typically set when Success is True. /// Value is null when Success is false. /// public TValue? Value { get; init; } - + /// /// Messages list /// @@ -38,12 +38,12 @@ public IReadOnlyList Messages _messages ??= new List(); return _messages; } - init // For deserialisation + init // For deserialisation { _messages = value.ToList(); } } - + /// /// All messages flattened into 1 message. /// Assumes a decent implementation of TMessage.ToString() @@ -54,6 +54,7 @@ public string MessagesToString(string separator = " | ") { return string.Empty; } + return string.Join(separator, _messages.Select(c => c.ToString())); } @@ -101,7 +102,7 @@ protected ResultValue(bool success, TValue? value, TMessage? message = null) public static ResultValue Success(TValue value, IEnumerable? messages) { Precondition.RequiresNotNull(value); - return new ResultValue(true, value, messages); + return new ResultValue(true, value, messages); } /// @@ -114,7 +115,7 @@ public static ResultValue Success(TValue value) Precondition.RequiresNotNull(value); return new ResultValue(true, value, null as TMessage); } - + /// /// Creates a successful Result with Value set, and 1 Message item. /// @@ -133,7 +134,7 @@ public static ResultValue Success(TValue value, TMessage? mess /// At least 1 not null message is required. /// Normally null\default for a failure, but not necessarily. /// - public static ResultValue Failure(IEnumerable messages, TValue? value = default(TValue) ) + public static ResultValue Failure(IEnumerable messages, TValue? value = default(TValue)) { Precondition.RequiresNotNull(messages); List messagesList = messages.ToList(); @@ -147,10 +148,42 @@ public static ResultValue Success(TValue value, TMessage? mess /// Required for failed operations. /// Normally null\default for a failure, but not necessarily. /// - public static ResultValue Failure(TMessage message, TValue? value = default(TValue) ) + public static ResultValue Failure(TMessage message, TValue? value = default(TValue)) { Precondition.RequiresNotNull(message); return new ResultValue(false, value, new List() { message }); } + + + /// + /// + /// + /// + public Result ToResult() + { + return IsSuccess + ? Result.Success() + : Result.Failure(Messages.Select(m => + m.ToString() + ?? $"No string representation of message of type {typeof(TMessage).FullName}")); + } + + /// + /// + /// + /// + /// + public virtual ResultValue ToFailedResult() where TOtherValue : notnull + { + if (IsSuccess) + { + throw new ArgumentException($"Cannot convert a successful result of type {GetType().FullName} " + + $"to a failed result of type {typeof(ResultValue).FullName}."); + } + + return ResultValue.Failure(Messages); + } + + } } \ No newline at end of file From 8c5e492c51c4cb9a98b91c850011ca7f2f40513c Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Mon, 5 Jan 2026 14:54:40 +0200 Subject: [PATCH 3/6] Fix compiler warnings resulting from notnull constraint on ResultValue --- Email/Core/IEmailSender.cs | 2 +- Email/Core/NullEmailSender.cs | 4 ++-- Email/Mailgun/MailgunEmailSender.cs | 6 +++--- Email/Office365/Office365EmailSender.cs | 6 +++--- Email/Tests/Mailgun/MailgunEmailSenderTests.cs | 2 +- System/Activator2/Activator2.cs | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Email/Core/IEmailSender.cs b/Email/Core/IEmailSender.cs index 06b47ca..7ae2ed4 100644 --- a/Email/Core/IEmailSender.cs +++ b/Email/Core/IEmailSender.cs @@ -12,7 +12,7 @@ public interface IEmailSender /// /// /// Success and the Mailgun messageId populated in the Value of the Outcome if available. - Task> SendEmail(IEmailMessage email); + Task> SendEmail(IEmailMessage email); } } \ No newline at end of file diff --git a/Email/Core/NullEmailSender.cs b/Email/Core/NullEmailSender.cs index e4b7806..72c7106 100644 --- a/Email/Core/NullEmailSender.cs +++ b/Email/Core/NullEmailSender.cs @@ -12,9 +12,9 @@ public sealed class NullEmailSender : IEmailSender /// /// /// - public async Task> SendEmail(IEmailMessage email) + public async Task> SendEmail(IEmailMessage email) { - return await Task.FromResult(ResultValue.Success("12345")); + return await Task.FromResult(ResultValue.Success("12345")); } } } diff --git a/Email/Mailgun/MailgunEmailSender.cs b/Email/Mailgun/MailgunEmailSender.cs index 6f1383f..62c474b 100644 --- a/Email/Mailgun/MailgunEmailSender.cs +++ b/Email/Mailgun/MailgunEmailSender.cs @@ -110,7 +110,7 @@ private static ByteArrayContent ToByteArrayContent(Stream stream) /// /// An Outcome containing the Mailgun messageId. /// - public async Task> SendEmail(IEmailMessage email) + public async Task> SendEmail(IEmailMessage email) { Precondition.RequiresNotNull(email); Precondition.Requires(email.To.Any(), "Mailgun requires one or more to addresses."); @@ -182,11 +182,11 @@ private static ByteArrayContent ToByteArrayContent(Stream stream) MailgunSendResponse? response = await responseMessage.Content.ReadFromJsonAsync(); LogSendEmailResult(email, true, LogLevel.Information, $"Sent with Mailgun reference {response?.Id}."); - return ResultValue.Success(response?.Id); + return ResultValue.Success(response?.Id ?? ""); } catch (Exception e) { - return ResultValue.Failure(e.ToString()); + return ResultValue.Failure(e.ToString()); } } diff --git a/Email/Office365/Office365EmailSender.cs b/Email/Office365/Office365EmailSender.cs index e8d8ad1..2028f92 100644 --- a/Email/Office365/Office365EmailSender.cs +++ b/Email/Office365/Office365EmailSender.cs @@ -56,7 +56,7 @@ public Office365EmailSender(Office365Options office365Options, EmailSendingOptio const string MicrosoftGraphFileAttachmentOdataType = "#microsoft.graph.fileAttachment"; /// - public async Task> SendEmail(IEmailMessage email) + public async Task> SendEmail(IEmailMessage email) { if (email.From is null) { @@ -122,12 +122,12 @@ public Office365EmailSender(Office365Options office365Options, EmailSendingOptio await _graphClient.Users[_senderUserId].SendMail.PostAsync(requestBody); LogSendEmailResult(email, true, LogLevel.Information, $"Sent with Office365 via user {_senderUserId}"); - return ResultValue.Success("Success"); + return ResultValue.Success("Success"); } catch (Exception ex) { LogSendEmailResult(email, false, LogLevel.Error, $"Failed to send with Office365 via user {_senderUserId}", ex); - return ResultValue.Failure("Fail: " + ex.Message); + return ResultValue.Failure("Fail: " + ex.Message); } } diff --git a/Email/Tests/Mailgun/MailgunEmailSenderTests.cs b/Email/Tests/Mailgun/MailgunEmailSenderTests.cs index b46d079..81caf96 100644 --- a/Email/Tests/Mailgun/MailgunEmailSenderTests.cs +++ b/Email/Tests/Mailgun/MailgunEmailSenderTests.cs @@ -115,7 +115,7 @@ public async Task Send_with_attachment() VerifySuccessfulSendAndLogging(scenario, message, result); } - private void VerifySuccessfulSendAndLogging(MailgunEmailSenderTestBuilder scenario,EmailMessage message, ResultValue result) + private void VerifySuccessfulSendAndLogging(MailgunEmailSenderTestBuilder scenario,EmailMessage message, ResultValue result) { // Result Assert.That(result.IsSuccess, Is.True, result.MessagesToString()); diff --git a/System/Activator2/Activator2.cs b/System/Activator2/Activator2.cs index e90ccf9..b99780a 100644 --- a/System/Activator2/Activator2.cs +++ b/System/Activator2/Activator2.cs @@ -37,7 +37,7 @@ public static ResultValue TryCreate(string typeName, string assemblyName) return ResultValue.Failure($"Type {typeName} is not of type {nameof(T)}"); } - private static ResultValue CreateInstanceFailure(string typeName, string assemblyName, string? errorMessage = "") + private static ResultValue CreateInstanceFailure(string typeName, string assemblyName, string? errorMessage = "") where T : notnull { return ResultValue.Failure($"Could not create instance of type {typeName} from assembly {assemblyName}. {errorMessage}"); } From 5eeb12366ae1102368edb225c2030d17c9005f9a Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Tue, 6 Jan 2026 07:26:46 +0200 Subject: [PATCH 4/6] Result records changed to classes --- System/Result/MessageEx.cs | 2 +- System/Result/Result.cs | 2 +- System/Result/ResultEx.cs | 2 +- System/Result/ResultOfTMessage.cs | 2 +- System/Result/ResultValue.cs | 2 +- System/Result/ResultValueEx.cs | 2 +- System/Result/ResultValueOfTMessage.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/System/Result/MessageEx.cs b/System/Result/MessageEx.cs index ba30c44..42ca6f7 100644 --- a/System/Result/MessageEx.cs +++ b/System/Result/MessageEx.cs @@ -5,7 +5,7 @@ namespace Odin.System; /// /// Extended 'Message' including a Severity, and optional Exception. /// -public record MessageEx +public class MessageEx { /// /// Message content. Can be null. diff --git a/System/Result/Result.cs b/System/Result/Result.cs index 0c63deb..4f22e38 100644 --- a/System/Result/Result.cs +++ b/System/Result/Result.cs @@ -5,7 +5,7 @@ /// Represents the outcome of an operation that was successful or failed, /// together with a list of Messages. /// - public record Result : Result + public class Result : Result { /// public Result() diff --git a/System/Result/ResultEx.cs b/System/Result/ResultEx.cs index 3c2691b..58536a8 100644 --- a/System/Result/ResultEx.cs +++ b/System/Result/ResultEx.cs @@ -7,7 +7,7 @@ namespace Odin.System /// Like Result, but with a list of Messages of type ResultMessage2 /// which includes a Severity, a Message and optionally an Exception. /// - public record ResultEx : Result + public class ResultEx : Result { /// public ResultEx() diff --git a/System/Result/ResultOfTMessage.cs b/System/Result/ResultOfTMessage.cs index 1fc5c3f..a23574d 100644 --- a/System/Result/ResultOfTMessage.cs +++ b/System/Result/ResultOfTMessage.cs @@ -5,7 +5,7 @@ /// with a list of messages of type TMessage. /// /// - public record Result where TMessage : class + public class Result where TMessage : class { /// /// True if successful diff --git a/System/Result/ResultValue.cs b/System/Result/ResultValue.cs index 66bff37..9caca79 100644 --- a/System/Result/ResultValue.cs +++ b/System/Result/ResultValue.cs @@ -6,7 +6,7 @@ /// /// /// To be renamed to ResultValue of TValue - public record ResultValue : ResultValue where TValue : notnull + public class ResultValue : ResultValue where TValue : notnull { /// /// Parameterless constructor for serialization. diff --git a/System/Result/ResultValueEx.cs b/System/Result/ResultValueEx.cs index 68d2e5e..192ea68 100644 --- a/System/Result/ResultValueEx.cs +++ b/System/Result/ResultValueEx.cs @@ -7,7 +7,7 @@ /// /// /// To be renamed to ResultValueEx of TValue - public record ResultValueEx : ResultValue where TValue : notnull + public class ResultValueEx : ResultValue where TValue : notnull { /// /// Parameterless constructor for serialization. diff --git a/System/Result/ResultValueOfTMessage.cs b/System/Result/ResultValueOfTMessage.cs index c747176..e763cb3 100644 --- a/System/Result/ResultValueOfTMessage.cs +++ b/System/Result/ResultValueOfTMessage.cs @@ -8,7 +8,7 @@ namespace Odin.System /// /// /// - public record ResultValue where TMessage : class where TValue : notnull + public class ResultValue where TMessage : class where TValue : notnull { /// /// True if successful From 098c8e15ea2923b3583bd36be9b2fb48c49eabff Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Tue, 6 Jan 2026 07:48:29 +0200 Subject: [PATCH 5/6] Fix: IsSuccess not set by constructors --- System/Result/ResultValue.cs | 1 + System/Result/ResultValueEx.cs | 1 + System/Result/ResultValueOfTMessage.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/System/Result/ResultValue.cs b/System/Result/ResultValue.cs index 9caca79..e6b24d0 100644 --- a/System/Result/ResultValue.cs +++ b/System/Result/ResultValue.cs @@ -25,6 +25,7 @@ public ResultValue() public ResultValue(bool isSuccess, TValue? value, IEnumerable? messages) { Precondition.Requires(!(value == null && isSuccess), "Value is required for a successful result."); + IsSuccess = isSuccess; Value = value; _messages = messages?.ToList(); } diff --git a/System/Result/ResultValueEx.cs b/System/Result/ResultValueEx.cs index 192ea68..785544a 100644 --- a/System/Result/ResultValueEx.cs +++ b/System/Result/ResultValueEx.cs @@ -26,6 +26,7 @@ public ResultValueEx() public ResultValueEx(bool isSuccess, TValue? value, IEnumerable? messages) { Precondition.Requires(!(value == null && isSuccess), "Value is required for a successful result."); + IsSuccess = isSuccess; Value = value; _messages = messages?.ToList(); } diff --git a/System/Result/ResultValueOfTMessage.cs b/System/Result/ResultValueOfTMessage.cs index e763cb3..593122f 100644 --- a/System/Result/ResultValueOfTMessage.cs +++ b/System/Result/ResultValueOfTMessage.cs @@ -76,6 +76,7 @@ public ResultValue() protected ResultValue(bool success, TValue? value, IEnumerable? messages) { Precondition.Requires(!(value == null && success), "Value is required for a successful result."); + IsSuccess = success; Value = value; _messages = messages?.ToList(); } @@ -89,6 +90,7 @@ protected ResultValue(bool success, TValue? value, IEnumerable? messag protected ResultValue(bool success, TValue? value, TMessage? message = null) { Precondition.Requires(!(value == null && success), "Value is required for a successful result."); + IsSuccess = success; Value = value; _messages = message != null ? [message] : null; } From 61aaf40a331711a4d4d6b341639a5c4de289db00 Mon Sep 17 00:00:00 2001 From: Angus Thring Date: Tue, 6 Jan 2026 07:53:29 +0200 Subject: [PATCH 6/6] Add ResultValueNullable --- System/Result/ResultValue.cs | 2 +- System/Result/ResultValueNullable.cs | 120 ++++++++++++ .../Result/ResultValueNullableOfTMessage.cs | 180 ++++++++++++++++++ System/Result/ResultValueOfTMessage.cs | 2 +- 4 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 System/Result/ResultValueNullable.cs create mode 100644 System/Result/ResultValueNullableOfTMessage.cs diff --git a/System/Result/ResultValue.cs b/System/Result/ResultValue.cs index e6b24d0..ac04b5f 100644 --- a/System/Result/ResultValue.cs +++ b/System/Result/ResultValue.cs @@ -1,7 +1,7 @@ namespace Odin.System { /// - /// Represents the success or failure of an operation that returns a Value\Result on success, + /// Represents the success or failure of an operation that returns a non-null Value\Result on success, /// and list of string messages. /// /// diff --git a/System/Result/ResultValueNullable.cs b/System/Result/ResultValueNullable.cs new file mode 100644 index 0000000..ec9aa3a --- /dev/null +++ b/System/Result/ResultValueNullable.cs @@ -0,0 +1,120 @@ +namespace Odin.System; + +/// +/// Represents the success or failure of an operation that returns a possibly null Value\Result on success, +/// and list of string messages. +/// +/// +public class ResultValueNullable : ResultValueNullable +{ + /// + /// Parameterless constructor for serialization. + /// + public ResultValueNullable() + { + IsSuccess = false; + } + + /// + /// Default constructor. + /// + /// true or false + /// Required if successful + /// Optional, but good practice is to provide messages for failed results. + public ResultValueNullable(bool isSuccess, TValue? value, IEnumerable? messages) + { + IsSuccess = isSuccess; + Value = value; + _messages = messages?.ToList(); + } + + /// + /// Default constructor. + /// + /// true or false + /// Required if successful + /// Optional, but good practice is to provide messages for failed results. + public ResultValueNullable(bool isSuccess, TValue? value, string? message = null) + { + IsSuccess = isSuccess; + Value = value; + _messages = message != null ? [message] : null; + } + + /// + /// Success. + /// + /// Normally included as best practice for failed operations, but not mandatory. + /// Normally null\default for a failure, but not necessarily. + /// + public new static ResultValueNullable Failure(IEnumerable messages, TValue? value = default(TValue) ) + { + Precondition.RequiresNotNull(messages); + List list = messages.ToList(); + Precondition.Requires(list.Any(s => !string.IsNullOrWhiteSpace(s)),"At least 1 message is required."); + return new ResultValueNullable(false, value, list); + } + + /// + /// Success. + /// + /// Required for failed operations. + /// Normally null\default for a failure, but not necessarily. + /// + public new static ResultValueNullable Failure(string message, TValue? value = default(TValue) ) + { + Precondition.Requires(!string.IsNullOrWhiteSpace(message), $"{nameof(message)} is required."); + return new ResultValueNullable(false, value, new List() { message }); + } + + /// + /// Creates a successful Result with Value set. + /// + /// + /// + public new static ResultValueNullable Success(TValue value) + { + return new ResultValueNullable(true, value, null as string); + } + + /// + /// Creates a successful Result with Value set and a single Message. + /// + /// + /// + /// + public new static ResultValueNullable Success(TValue value, string? message) + { + return new ResultValueNullable(true, value, message); + } + + /// + /// Creates a successful Result with Value set, and several Messages. + /// + /// + /// + /// + public new static ResultValueNullable Success(TValue value, IEnumerable messages) + { + return new ResultValueNullable(true, value, messages); + } + + /// + /// + /// + /// + /// + /// + public override ResultValueNullable ToFailedResult() + { + if (IsSuccess) + { + throw new ArgumentException($"Cannot convert a successful result of type {GetType().FullName} " + + $"to a failed result of type {typeof(ResultValue).FullName}."); + } + + return ResultValueNullable.Failure(Messages); + } + + +} \ No newline at end of file diff --git a/System/Result/ResultValueNullableOfTMessage.cs b/System/Result/ResultValueNullableOfTMessage.cs new file mode 100644 index 0000000..1e2685f --- /dev/null +++ b/System/Result/ResultValueNullableOfTMessage.cs @@ -0,0 +1,180 @@ +namespace Odin.System; + +/// +/// Represents the success or failure of an operation that returns a Value\Result on success, +/// and list of messages, of type TMessage. +/// +/// +/// +public class ResultValueNullable where TMessage : class +{ + /// + /// True if successful + /// + public bool IsSuccess { get; init; } + + /// + /// Value may or may not be set when successful. + /// Value is null when Success is false. + /// + public TValue? Value { get; init; } + + /// + /// Messages list + /// + // ReSharper disable once InconsistentNaming + protected List? _messages; + + /// + /// Messages + /// + public IReadOnlyList Messages + { + get + { + _messages ??= new List(); + return _messages; + } + init // For deserialisation + { + _messages = value.ToList(); + } + } + + /// + /// All messages flattened into 1 message. + /// Assumes a decent implementation of TMessage.ToString() + /// + public string MessagesToString(string separator = " | ") + { + if (_messages == null || _messages.Count == 0) + { + return string.Empty; + } + + return string.Join(separator, _messages.Select(c => c.ToString())); + } + + + /// + /// Parameterless constructor for serialisation, etc. + /// + public ResultValueNullable() + { + Value = default(TValue); + } + + /// + /// Default constructor. + /// + /// true or false + /// Required if successful + /// Optional, but good practice is to provide messages for failed results. + protected ResultValueNullable(bool success, TValue? value, IEnumerable? messages) + { + IsSuccess = success; + Value = value; + _messages = messages?.ToList(); + } + + /// + /// Default constructor. + /// + /// true or false + /// Required if successful + /// Optional, but good practice is to provide messages for failed results. + protected ResultValueNullable(bool success, TValue? value, TMessage? message = null) + { + IsSuccess = success; + Value = value; + _messages = message != null ? [message] : null; + } + + /// + /// Success. + /// + /// Required. + /// Not normally used for successful operations, but can be for informational purposes. + /// + public static ResultValueNullable Success(TValue value, IEnumerable? messages) + { + return new ResultValueNullable(true, value, messages); + } + + /// + /// Creates a successful Result with Value set. + /// + /// Required. + /// + public static ResultValueNullable Success(TValue value) + { + return new ResultValueNullable(true, value, null as TMessage); + } + + /// + /// Creates a successful Result with Value set, and 1 Message item. + /// + /// Required. + /// Not normally used for successful operations, but can be for informational purposes. + /// + public static ResultValueNullable Success(TValue value, TMessage? message) + { + return new ResultValueNullable(true, value, message); + } + + /// + /// Creates a successful Result with Value set, and several Messages. + /// + /// At least 1 not null message is required. + /// Normally null\default for a failure, but not necessarily. + /// + public static ResultValueNullable Failure(IEnumerable messages, TValue? value = default(TValue)) + { + Precondition.RequiresNotNull(messages); + List messagesList = messages.ToList(); + Precondition.Requires(messagesList.Any(m => m != null!), "At least 1 message is required."); + return new ResultValueNullable(false, value, messagesList); + } + + /// + /// Success. + /// + /// Required for failed operations. + /// Normally null\default for a failure, but not necessarily. + /// + public static ResultValueNullable Failure(TMessage message, TValue? value = default(TValue)) + { + Precondition.RequiresNotNull(message); + return new ResultValueNullable(false, value, new List() { message }); + } + + + /// + /// + /// + /// + public Result ToResult() + { + return IsSuccess + ? Result.Success() + : Result.Failure(Messages.Select(m => + m.ToString() + ?? $"No string representation of message of type {typeof(TMessage).FullName}")); + } + + /// + /// + /// + /// + /// + public virtual ResultValueNullable ToFailedResult() where TOtherValue : notnull + { + if (IsSuccess) + { + throw new ArgumentException($"Cannot convert a successful result of type {GetType().FullName} " + + $"to a failed result of type {typeof(ResultValueNullable).FullName}."); + } + + return ResultValueNullable.Failure(Messages); + } +} \ No newline at end of file diff --git a/System/Result/ResultValueOfTMessage.cs b/System/Result/ResultValueOfTMessage.cs index 593122f..edcb091 100644 --- a/System/Result/ResultValueOfTMessage.cs +++ b/System/Result/ResultValueOfTMessage.cs @@ -17,7 +17,7 @@ public class ResultValue where TMessage : class where TValue : public bool IsSuccess { get; init; } /// - /// Value is typically set when Success is True. + /// Value is always set when Success is True. /// Value is null when Success is false. /// public TValue? Value { get; init; }