diff --git a/src/Codehard.Core.sln b/src/Codehard.Core.sln index 1d58b2a..74b53d9 100644 --- a/src/Codehard.Core.sln +++ b/src/Codehard.Core.sln @@ -49,6 +49,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.Marten" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.Marten.Tests", "Codehard.Functional\Codehard.Functional.Marten.Tests\Codehard.Functional.Marten.Tests.csproj", "{BCB6D4C3-DD27-43AD-B0E9-6C8DE6821AD5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.MediatR", "Codehard.Functional\Codehard.Functional.Mediatr\Codehard.Functional.MediatR.csproj", "{8952083F-5A2A-4A37-9039-8588B08CA6EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Codehard.Functional.MediatR.Tests", "Codehard.Functional\Codehard.Functional.Mediatr.Tests\Codehard.Functional.MediatR.Tests.csproj", "{569F15A1-ABDE-4037-825C-90164D9074E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -143,6 +147,14 @@ Global {BCB6D4C3-DD27-43AD-B0E9-6C8DE6821AD5}.Debug|Any CPU.Build.0 = Debug|Any CPU {BCB6D4C3-DD27-43AD-B0E9-6C8DE6821AD5}.Release|Any CPU.ActiveCfg = Release|Any CPU {BCB6D4C3-DD27-43AD-B0E9-6C8DE6821AD5}.Release|Any CPU.Build.0 = Release|Any CPU + {8952083F-5A2A-4A37-9039-8588B08CA6EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8952083F-5A2A-4A37-9039-8588B08CA6EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8952083F-5A2A-4A37-9039-8588B08CA6EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8952083F-5A2A-4A37-9039-8588B08CA6EE}.Release|Any CPU.Build.0 = Release|Any CPU + {569F15A1-ABDE-4037-825C-90164D9074E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {569F15A1-ABDE-4037-825C-90164D9074E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {569F15A1-ABDE-4037-825C-90164D9074E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {569F15A1-ABDE-4037-825C-90164D9074E2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -173,5 +185,7 @@ Global {6277AC9F-217C-4F89-8589-51FCB4E9BADF} = {2F98EF48-527C-44C5-8FC3-65F25C808AC9} {8D62918D-C27F-45AF-B26A-437B594C0D5C} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} {BCB6D4C3-DD27-43AD-B0E9-6C8DE6821AD5} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} + {8952083F-5A2A-4A37-9039-8588B08CA6EE} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} + {569F15A1-ABDE-4037-825C-90164D9074E2} = {0C257E94-AD98-4AFB-93B7-B6F64EB7D2BA} EndGlobalSection EndGlobal diff --git a/src/Codehard.Core.sln.DotSettings b/src/Codehard.Core.sln.DotSettings index f78115f..649abbd 100644 --- a/src/Codehard.Core.sln.DotSettings +++ b/src/Codehard.Core.sln.DotSettings @@ -1,2 +1,3 @@  - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/Codehard.Functional/CodeHard.Function.FSharp.Tests.Types/CodeHard.Functional.FSharp.Tests.Types.fsproj b/src/Codehard.Functional/CodeHard.Function.FSharp.Tests.Types/CodeHard.Functional.FSharp.Tests.Types.fsproj index 2c92068..78d3020 100644 --- a/src/Codehard.Functional/CodeHard.Function.FSharp.Tests.Types/CodeHard.Functional.FSharp.Tests.Types.fsproj +++ b/src/Codehard.Functional/CodeHard.Function.FSharp.Tests.Types/CodeHard.Functional.FSharp.Tests.Types.fsproj @@ -10,7 +10,7 @@ - + diff --git a/src/Codehard.Functional/Codehard.Functional.EntityFramework/Codehard.Functional.EntityFramework.csproj b/src/Codehard.Functional/Codehard.Functional.EntityFramework/Codehard.Functional.EntityFramework.csproj index b448570..72f7cf1 100644 --- a/src/Codehard.Functional/Codehard.Functional.EntityFramework/Codehard.Functional.EntityFramework.csproj +++ b/src/Codehard.Functional/Codehard.Functional.EntityFramework/Codehard.Functional.EntityFramework.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Codehard.Functional/Codehard.Functional.FSharp/Codehard.Functional.FSharp.csproj b/src/Codehard.Functional/Codehard.Functional.FSharp/Codehard.Functional.FSharp.csproj index 0e5f807..ea7c1d5 100644 --- a/src/Codehard.Functional/Codehard.Functional.FSharp/Codehard.Functional.FSharp.csproj +++ b/src/Codehard.Functional/Codehard.Functional.FSharp/Codehard.Functional.FSharp.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Codehard.Functional/Codehard.Functional.Marten/Codehard.Functional.Marten.csproj b/src/Codehard.Functional/Codehard.Functional.Marten/Codehard.Functional.Marten.csproj index 01eea4e..1a77991 100644 --- a/src/Codehard.Functional/Codehard.Functional.Marten/Codehard.Functional.Marten.csproj +++ b/src/Codehard.Functional/Codehard.Functional.Marten/Codehard.Functional.Marten.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - 3.0.0-preview-1 + 3.0.0-preview-2 A functional extensions for Marten. https://github.com/codehardth/Codehard.Functional https://github.com/codehardth/Codehard.Functional diff --git a/src/Codehard.Functional/Codehard.Functional.Marten/DocumentSessionExtensions.cs b/src/Codehard.Functional/Codehard.Functional.Marten/DocumentSessionExtensions.cs index be1b04b..7df6e31 100644 --- a/src/Codehard.Functional/Codehard.Functional.Marten/DocumentSessionExtensions.cs +++ b/src/Codehard.Functional/Codehard.Functional.Marten/DocumentSessionExtensions.cs @@ -13,9 +13,12 @@ public static class DocumentSessionExtensions /// /// Save changes to the database /// - public static Aff SaveChangesAff(this IDocumentSession documentSession) + public static Aff SaveChangesAff( + this IDocumentSession documentSession, CancellationToken cancellationToken = default) { - return Aff(async () => await documentSession.SaveChangesAsync().ToUnit()); + return Aff( + async () => + await documentSession.SaveChangesAsync(cancellationToken).ToUnit()); } /// diff --git a/src/Codehard.Functional/Codehard.Functional.Marten/QueryableExtensions.cs b/src/Codehard.Functional/Codehard.Functional.Marten/QueryableExtensions.cs index 546ebeb..ea0184c 100644 --- a/src/Codehard.Functional/Codehard.Functional.Marten/QueryableExtensions.cs +++ b/src/Codehard.Functional/Codehard.Functional.Marten/QueryableExtensions.cs @@ -8,6 +8,19 @@ namespace Marten; public static class QueryableExtensions { + /// + /// Asynchronously converts an IQueryable<T> into a read-only list within an Aff monad. + /// + /// The type of elements in the IQueryable. + /// The IQueryable to be converted to a read-only list. + /// A CancellationToken to observe while waiting for the task to complete. + /// An Aff<IReadOnlyList<T>> representing the asynchronous operation. + /// The Aff monad wraps the result, which is the read-only list of elements. + public static Aff> ToListAff(this IQueryable source, CancellationToken ct = default) + { + return Aff(async () => await source.ToListAsync(ct)); + } + /// /// Asynchronously returns the only element of a sequence, or a None value if the sequence is empty; /// this method returns an Option<TSource>. diff --git a/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Codehard.Functional.MediatR.Tests.csproj b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Codehard.Functional.MediatR.Tests.csproj new file mode 100644 index 0000000..a0a0193 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Codehard.Functional.MediatR.Tests.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + + false + + Codehard.Functional.MediatR.Tests + + Codehard.Functional.MediatR.Tests + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/PublishTests.cs b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/PublishTests.cs new file mode 100644 index 0000000..000f9ab --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/PublishTests.cs @@ -0,0 +1,81 @@ +using System.Text; +using Lamar; +using MediatR; +using Shouldly; + +namespace Codehard.Functional.MediatR.Tests; + +public class PublishTests +{ + public class Ping : INotification + { + public string Message { get; init; } + } + + [Fact] + public async Task WhenPublishMessage_ShouldNotifidEachHandlersCorrectly() + { + // Arrange + var builder = new StringBuilder(); + var writer = new StringWriter(builder); + var container = BuildMediatr(); + + // Act + var mediator = container.GetInstance(); + + await mediator.Publish(new Ping { Message = "Ping" }); + + // Assert + var result = builder.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); + result.ShouldContain("Ping Pong"); + result.ShouldContain("Ping Pung"); + + Container BuildMediatr() + { + var container = new Container(cfg => + { + cfg.Scan(scanner => + { + scanner.AssemblyContainingType(typeof(PublishTests)); + scanner.IncludeNamespaceContainingType(); + scanner.WithDefaultConventions(); + scanner.AddAllTypesOf(typeof (INotificationHandler<>)); + }); + cfg.For().Use(writer); + cfg.For().Use(); + }); + + return container; + } + } + + public class PongHandler : INotificationHandler + { + private readonly TextWriter _writer; + + public PongHandler(TextWriter writer) + { + _writer = writer; + } + + public Task Handle(Ping notification, CancellationToken cancellationToken) + { + return _writer.WriteLineAsync(notification.Message + " Pong"); + } + } + + public class PungHandler : INotificationHandler + { + private readonly TextWriter _writer; + + public PungHandler(TextWriter writer) + { + _writer = writer; + } + + public Task Handle(Ping notification, CancellationToken cancellationToken) + { + return _writer.WriteLineAsync(notification.Message + " Pung"); + } + } +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/QueryTests.cs b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/QueryTests.cs new file mode 100644 index 0000000..081abe1 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/QueryTests.cs @@ -0,0 +1,145 @@ +using System.Text; +using Lamar; +using LanguageExt; +using MediatR; +using Shouldly; + +namespace Codehard.Functional.MediatR.Tests; + +public class QueryTests +{ + public class Ping : IQuery + { + public string? Message { get; set; } + } + + public class PingNotFound : IQuery + { + public string? Message { get; set; } + } + + public class Pong + { + public string? Message { get; set; } + } + + public class PingHandler + : IQueryHandler, + IQueryHandler + { + public Task> Handle(Ping request, CancellationToken cancellationToken) + { + return Task.FromResult( + Fin.Succ( + new PongQueryResult.Success(new Pong { Message = request.Message + " Pong" }))); + } + + public Task> Handle(PingNotFound request, CancellationToken cancellationToken) + { + return Task.FromResult( + Fin.Fail( + new ExpectedResultError(new PongQueryResult.NotFound()))); + } + } + + public abstract record PongQueryResult + { + private PongQueryResult() + { + } + + public sealed record Success(Pong Pong) : PongQueryResult; + + public sealed record NotFound : PongQueryResult; + } + + [Fact] + public async Task WhenSendQuery_ShouldResponseCorrectly() + { + // Arrange + var builder = new StringBuilder(); + var writer = new StringWriter(builder); + var container = BuildMediatr(); + + // Act + var mediator = container.GetInstance(); + + var response = + await mediator + .SendQueryAff(new Ping { Message = "Ping" }) + .MapExpectedResultError() + .Run(); + + // Assert + Assert.True(response.IsSucc); + + var result = response.ThrowIfFail(); + + var successValue = result.ShouldBeOfType(); + + successValue.Pong.Message.ShouldBe("Ping Pong"); + + return; + + Container BuildMediatr() + { + var container = new Container(cfg => + { + cfg.Scan(scanner => + { + scanner.AssemblyContainingType(typeof(QueryTests)); + scanner.IncludeNamespaceContainingType(); + scanner.WithDefaultConventions(); + scanner.AddAllTypesOf(typeof(IRequestHandler<,>)); + }); + cfg.For().Use(); + }); + + return container; + } + } + + [Fact] + public async Task WhenSendNotFoundQuery_ShouldResponseNotFoundCorrectly() + { + // Arrange + var builder = new StringBuilder(); + var writer = new StringWriter(builder); + var container = BuildMediatr(); + + // Act + var mediator = container.GetInstance(); + + var response = + await mediator + .SendQueryAff(new PingNotFound { Message = "Ping" }) + .MapExpectedResultError() + .Run(); + + // Assert + Assert.True(response.IsSucc); + + var result = response.ThrowIfFail(); + + result.ShouldBeOfType(); + + return; + + Container BuildMediatr() + { + var container = new Container(cfg => + { + cfg.Scan(scanner => + { + scanner.AssemblyContainingType(typeof(QueryTests)); + scanner.IncludeNamespaceContainingType(); + scanner.WithDefaultConventions(); + scanner.AddAllTypesOf(typeof(IRequestHandler<,>)); + }); + cfg.For().Use(); + }); + + return container; + } + } +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Usings.cs b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Usings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.MediatR.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/Codehard.Functional.Mediatr.csproj b/src/Codehard.Functional/Codehard.Functional.Mediatr/Codehard.Functional.Mediatr.csproj new file mode 100644 index 0000000..bcd2170 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/Codehard.Functional.Mediatr.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + Codehard.Functional.MediatR + Codehard.Functional.MediatR + + + + + + + + diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommand.cs b/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommand.cs new file mode 100644 index 0000000..eb89cdc --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommand.cs @@ -0,0 +1,8 @@ +using LanguageExt; + +// ReSharper disable once CheckNamespace +namespace MediatR; + +public interface ICommand : IRequest> +{ +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommandHandler.cs b/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommandHandler.cs new file mode 100644 index 0000000..166a122 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/ICommandHandler.cs @@ -0,0 +1,9 @@ +using LanguageExt; + +// ReSharper disable once CheckNamespace +namespace MediatR; + +public interface ICommandHandler : IRequestHandler> + where TCommand : ICommand +{ +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/IMediatorExtensions.cs b/src/Codehard.Functional/Codehard.Functional.Mediatr/IMediatorExtensions.cs new file mode 100644 index 0000000..7b350b4 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/IMediatorExtensions.cs @@ -0,0 +1,33 @@ +using LanguageExt; + +using static LanguageExt.Prelude; + +// ReSharper disable once CheckNamespace +namespace MediatR; + +public static class IMediatorExtensions +{ + public static Aff SendCommandAff( + this IMediator mediator, + TCommand command) + where TCommand : ICommand + { + return + Aff(async () => + await mediator.Send(command) + .Map(x => x.ToAff())) + .Flatten(); + } + + public static Aff SendQueryAff( + this IMediator mediator, + TQuery query) + where TQuery : IQuery + { + return + Aff(async () => + await mediator.Send(query) + .Map(x => x.ToAff())) + .Flatten(); + } +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/IQuery.cs b/src/Codehard.Functional/Codehard.Functional.Mediatr/IQuery.cs new file mode 100644 index 0000000..90030c0 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/IQuery.cs @@ -0,0 +1,8 @@ +using LanguageExt; + +// ReSharper disable once CheckNamespace +namespace MediatR; + +public interface IQuery : IRequest> +{ +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional.Mediatr/IQueryHandler.cs b/src/Codehard.Functional/Codehard.Functional.Mediatr/IQueryHandler.cs new file mode 100644 index 0000000..e05dce1 --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional.Mediatr/IQueryHandler.cs @@ -0,0 +1,9 @@ +using LanguageExt; + +// ReSharper disable once CheckNamespace +namespace MediatR; + +public interface IQueryHandler : IRequestHandler> + where TQuery : IQuery +{ +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional/Codehard.Functional.csproj b/src/Codehard.Functional/Codehard.Functional/Codehard.Functional.csproj index 1102e51..10b53fd 100644 --- a/src/Codehard.Functional/Codehard.Functional/Codehard.Functional.csproj +++ b/src/Codehard.Functional/Codehard.Functional/Codehard.Functional.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Codehard.Functional/Codehard.Functional/ExpectedResultError.cs b/src/Codehard.Functional/Codehard.Functional/ExpectedResultError.cs new file mode 100644 index 0000000..02dcb7b --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional/ExpectedResultError.cs @@ -0,0 +1,29 @@ +using LanguageExt.Common; + +namespace Codehard.Functional; + +public record ExpectedResultError : Expected +{ + public ExpectedResultError(object errorObject, Error? inner) + : base(string.Empty, 0, Optional(inner)) + { + this.ErrorObject = errorObject; + } + + public ExpectedResultError(object errorObject, Exception? exception = null) + : base(string.Empty, 0, exception == null ? Option.None : Option.Some(ErrorException.New(exception))) + { + this.ErrorObject = errorObject; + } + + public override ErrorException ToErrorException() => ErrorException.New(this.ErrorObject.ToString() ?? string.Empty); + + public override string ToString() => this.ErrorObject.ToString() ?? string.Empty; + + public object ErrorObject { get; init; } + + public void Deconstruct(out object errorObject) + { + errorObject = this.ErrorObject; + } +} \ No newline at end of file diff --git a/src/Codehard.Functional/Codehard.Functional/ExpectedResultErrorExtensions.cs b/src/Codehard.Functional/Codehard.Functional/ExpectedResultErrorExtensions.cs new file mode 100644 index 0000000..4d4198a --- /dev/null +++ b/src/Codehard.Functional/Codehard.Functional/ExpectedResultErrorExtensions.cs @@ -0,0 +1,24 @@ +using LanguageExt.Common; + +namespace Codehard.Functional; + +public static class ExpectedResultErrorExtensions +{ + public static Aff MapExpectedResultError( + this Error error) + { + return + error is ExpectedResultError { ErrorObject: TResult } expected + ? SuccessAff((TResult)expected.ErrorObject) + : FailAff(error); + } + + public static Aff MapExpectedResultError( + this Aff aff) + { + return + aff.MatchAff( + Succ: SuccessAff, + Fail: err => err.MapExpectedResultError()); + } +} \ No newline at end of file