diff --git a/HotwiredBooks.sln.DotSettings.user b/HotwiredBooks.sln.DotSettings.user index 44bb357..0a6041a 100644 --- a/HotwiredBooks.sln.DotSettings.user +++ b/HotwiredBooks.sln.DotSettings.user @@ -1,8 +1,11 @@  + <AssemblyExplorer> + <Assembly Path="/home/andreas/.nuget/packages/erroror/1.3.0/lib/net6.0/ErrorOr.dll" /> +</AssemblyExplorer> - <SessionState ContinuousTestingMode="0" Name="Create_inserts_value_into_repository" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="Create_inserts_value_into_repository" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Project Location="/home/andreas/code/HotwiredBooks/HotwiredBooksTests" Presentation="&lt;HotwiredBooksTests&gt;" /> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="Create_inserts_value_into_repository #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" Name="Create_inserts_value_into_repository #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Project Location="/home/andreas/code/HotwiredBooks/HotwiredBooksTests" Presentation="&lt;HotwiredBooksTests&gt;" /> </SessionState> \ No newline at end of file diff --git a/HotwiredBooks/Components/IBooksRepository.cs b/HotwiredBooks/Components/IBooksRepository.cs index 8ea9555..21248a1 100644 --- a/HotwiredBooks/Components/IBooksRepository.cs +++ b/HotwiredBooks/Components/IBooksRepository.cs @@ -1,13 +1,13 @@ using HotwiredBooks.Models; -using MonadicBits; +using ErrorOr; namespace HotwiredBooks.Components; public interface IBooksRepository { - Task> Lookup(Guid id); + Task> Lookup(Guid id); Task> All(); - Task> Create(string title, string author); - Task> Update(Book book); - Task> Delete(Book book); + Task> Create(string title, string author); + Task> Update(Book book); + Task> Delete(Book book); } diff --git a/HotwiredBooks/Components/MemoryBasedBooksRepository.cs b/HotwiredBooks/Components/MemoryBasedBooksRepository.cs index 6487a86..2176cec 100644 --- a/HotwiredBooks/Components/MemoryBasedBooksRepository.cs +++ b/HotwiredBooks/Components/MemoryBasedBooksRepository.cs @@ -1,12 +1,12 @@ using System.Collections.Concurrent; using bridgefield.FoundationalBits; using HotwiredBooks.Models; -using MonadicBits; +using ErrorOr; +using ErrorOr.Extensions; +using HotwiredBooks.Extensions; namespace HotwiredBooks.Components; -using static Functional; - internal interface IBooksCommand; internal sealed record Create(string Title, string Author, DateTime CreatedAt) : IBooksCommand; @@ -17,44 +17,45 @@ internal sealed record Delete(Book Book) : IBooksCommand; public sealed class MemoryBasedBooksRepository : IBooksRepository { - private readonly IAgent> agent; + private readonly IAgent> agent; private readonly ConcurrentDictionary books = new(InitialBooks()); public MemoryBasedBooksRepository() => - agent = Agent.Start, IBooksCommand, Maybe>( + agent = Agent.Start, IBooksCommand, ErrorOr>( books, (current, command) => command switch { - Create create => Task.FromResult((current, - from book in new Book(Guid.NewGuid(), create.Title, create.Author, create.CreatedAt).Just() - from createdBook in current.TryAdd(book.Id, book) ? book.Just() : Nothing - select createdBook)), - Delete delete => Task.FromResult((current, - current.TryRemove(delete.Book.Id, out var deletedBook) ? deletedBook.Just() : Nothing)), + Create create => (current, + from book in new Book(Guid.NewGuid(), create.Title, create.Author, create.CreatedAt).Success() + from createdBook in current.TryAdd(book.Id, book) ? book.Success() : ErrorOr.From([Error.Failure()]) + select createdBook).AsTask(), + Delete delete => (current, + current.TryRemove(delete.Book.Id, out var deletedBook) ? deletedBook.Success() : ErrorOr.From( + [Error.Failure()])).AsTask(), Update update => Task.FromResult((current, from currentBook in current.TryGetValue(update.Book.Id, out var currentValue) - ? currentValue.Just() - : Nothing + ? currentValue.Success() + : ErrorOr.From([Error.Failure()]) from updatedBook in current.TryUpdate(update.Book.Id, update.Book, currentBook) - ? update.Book.Just() - : Nothing + ? update.Book.Success() + : ErrorOr.From([Error.Failure()]) select updatedBook)), _ => throw new ArgumentOutOfRangeException(nameof(command)) }); - public Task> Lookup(Guid id) => - Task.FromResult(books.TryGetValue(id, out var book) ? book.Just() : Nothing); + public Task> Lookup(Guid id) => + Task.FromResult(books.TryGetValue(id, out var book) ? ErrorOrFactory.From(book) : Error.NotFound()); public Task> All() => Task.FromResult>(books.Values); - public Task> Create(string title, string author) => + public Task> Create(string title, string author) => agent.Tell(new Create(title, author, DateTime.Now)); - public Task> Update(Book book) => + public Task> Update(Book book) => agent.Tell(new Update(book)); - public Task> Delete(Book book) => + public Task> Delete(Book book) => agent.Tell(new Delete(book)); private static IEnumerable> InitialBooks() diff --git a/HotwiredBooks/Controllers/BooksController.cs b/HotwiredBooks/Controllers/BooksController.cs index 9291af5..205226c 100644 --- a/HotwiredBooks/Controllers/BooksController.cs +++ b/HotwiredBooks/Controllers/BooksController.cs @@ -1,3 +1,5 @@ +using ErrorOr; +using ErrorOr.Extensions; using HotwiredBooks.Attributes; using HotwiredBooks.Components; using HotwiredBooks.Extensions; @@ -5,7 +7,6 @@ using HotwiredBooks.ViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; -using MonadicBits; namespace HotwiredBooks.Controllers; @@ -32,7 +33,7 @@ from formData in ParseFormData(collection) from book in booksRepository.Create(formData.Title, formData.Author) select book ) - .MapAsync(async book => + .Select(async book => View(new BooksCreateViewModel(book, (await booksRepository.All()).Count()))) .OrElse(StatusCode(500, "An unexpected error occurred on the server.")); @@ -40,7 +41,7 @@ select book public Task Edit(Guid id) => booksRepository .Lookup(id) - .MapAsync(book => View(new BooksEditViewModel(book))) + .Select(book => View(new BooksEditViewModel(book))) .OrElse(StatusCode(500, "An unexpected error occurred on the server.")); [HttpPatch, HttpPut] @@ -58,7 +59,7 @@ book with ) select updatedBook ) - .MapAsync(book => ViewComponentRenderer.RenderAsync("Book", new BooksEditViewModel(book))) + .Select(book => ViewComponentRenderer.RenderAsync("Book", new BooksEditViewModel(book))) .OrElse(StatusCode(500, "An unexpected error occurred on the server.")); [HttpPost] @@ -67,15 +68,15 @@ select updatedBook public Task Delete(Guid id) => booksRepository .Lookup(id) - .BindAsync(booksRepository.Delete) - .MapAsync(async book => + .SelectMany(booksRepository.Delete) + .Select(async book => View(new BooksDeleteViewModel(book, (await booksRepository.All()).Count()))) .OrElse(StatusCode(500, "An unexpected error occurred on the server.")); - private static Task> ParseFormData(IFormCollection collection) => - Task.FromResult( - from title in collection.JustGetValue("title") - from author in collection.JustGetValue("author") - select new FormData(title, author) - ); + private static Task> ParseFormData(IFormCollection collection) => + ( + from title in collection.JustGetValue("title") + from author in collection.JustGetValue("author") + select new FormData(title, author) + ).AsTask(); } diff --git a/HotwiredBooks/Extensions/AsyncExtensions.cs b/HotwiredBooks/Extensions/AsyncExtensions.cs new file mode 100644 index 0000000..9f6ea81 --- /dev/null +++ b/HotwiredBooks/Extensions/AsyncExtensions.cs @@ -0,0 +1,7 @@ +namespace HotwiredBooks.Extensions; + +public static class AsyncExtensions +{ + public static Task AsTask(this T value) => + Task.FromResult(value); +} diff --git a/HotwiredBooks/Extensions/ErrorOrExtensions.cs b/HotwiredBooks/Extensions/ErrorOrExtensions.cs new file mode 100644 index 0000000..2c910b0 --- /dev/null +++ b/HotwiredBooks/Extensions/ErrorOrExtensions.cs @@ -0,0 +1,9 @@ +using ErrorOr; + +namespace HotwiredBooks.Extensions; + +public static class ErrorOrExtensions +{ + public static ErrorOr Success(this T value) => + ErrorOrFactory.From(value); +} diff --git a/HotwiredBooks/Extensions/FunctionalExtensions.cs b/HotwiredBooks/Extensions/FunctionalExtensions.cs index f202ff4..300b68c 100644 --- a/HotwiredBooks/Extensions/FunctionalExtensions.cs +++ b/HotwiredBooks/Extensions/FunctionalExtensions.cs @@ -1,26 +1,27 @@ using Microsoft.Extensions.Primitives; -using MonadicBits; +using ErrorOr; namespace HotwiredBooks.Extensions; -using static Functional; - public static class FunctionalExtensions { - public static Maybe ToMaybe(this T value) => value?.Just() ?? Nothing; + public static ErrorOr ToErrorOr(this T value) => + value is not null ? ErrorOrFactory.From(value) : ErrorOr.From([Error.Failure()]); - public static Maybe JustGetValue(this IFormCollection collection, string key) => - collection.TryGetValue(key, out var value) ? value.Just() : Nothing; + public static ErrorOr JustGetValue(this IFormCollection collection, string key) => + collection.TryGetValue(key, out var value) + ? ErrorOrFactory.From(value) + : ErrorOr.From([Error.NotFound()]); - public static T OrElse(this Maybe maybe, T orElse) => - maybe.Match(value => value, () => orElse); + public static T OrElse(this ErrorOr maybe, T orElse) => + maybe.Match(value => value, _ => orElse); - public static T OrElse(this Maybe maybe, Func orElse) => - maybe.Match(value => value, orElse); + public static T OrElse(this ErrorOr maybe, Func orElse) => + maybe.Match(value => value, _ => orElse()); - public static async Task OrElse(this Task> maybe, Func orElse) => - (await maybe).Match(value => value, orElse); + public static async Task OrElse(this Task> maybe, Func orElse) => + (await maybe).Match(value => value, _ => orElse()); - public static async Task OrElse(this Task> maybe, T orElse) => - (await maybe).Match(value => value, () => orElse); + public static async Task OrElse(this Task> maybe, T orElse) => + (await maybe).Match(value => value, _ => orElse); } diff --git a/HotwiredBooks/Extensions/HtmlHelperExtensions.cs b/HotwiredBooks/Extensions/HtmlHelperExtensions.cs index 395d1cc..0d94575 100644 --- a/HotwiredBooks/Extensions/HtmlHelperExtensions.cs +++ b/HotwiredBooks/Extensions/HtmlHelperExtensions.cs @@ -1,7 +1,7 @@ using System.Reflection; +using ErrorOr.Extensions; using Humanizer; using Microsoft.AspNetCore.Mvc.Rendering; -using MonadicBits; namespace HotwiredBooks.Extensions; @@ -12,12 +12,12 @@ public static class HtmlHelperExtensions public static string DomId(this IHtmlHelper htmlHelper, object @object, string prefix = null) => ( - from property in @object.GetType().GetProperty("Id").ToMaybe() - from value in property.GetValue(@object).ToMaybe() + from property in @object.GetType().GetProperty("Id").ToErrorOr() + from value in property.GetValue(@object).ToErrorOr() select value.ToString() ).Match( id => $"{DomClass(htmlHelper, @object, prefix)}{Join}{id}", - () => DomClass(htmlHelper, @object, prefix ?? New) + _ => DomClass(htmlHelper, @object, prefix ?? New) ); public static string DomClass(this IHtmlHelper _, object objectOrType, string prefix = null) diff --git a/HotwiredBooks/HotwiredBooks.csproj b/HotwiredBooks/HotwiredBooks.csproj index d9f8ba2..d768915 100644 --- a/HotwiredBooks/HotwiredBooks.csproj +++ b/HotwiredBooks/HotwiredBooks.csproj @@ -8,7 +8,8 @@ - + + diff --git a/HotwiredBooks/ViewComponents/BookFormViewComponent.cs b/HotwiredBooks/ViewComponents/BookFormViewComponent.cs index 7f2f8ea..c76510f 100644 --- a/HotwiredBooks/ViewComponents/BookFormViewComponent.cs +++ b/HotwiredBooks/ViewComponents/BookFormViewComponent.cs @@ -1,11 +1,11 @@ using HotwiredBooks.Models; using HotwiredBooks.ViewModels; using Microsoft.AspNetCore.Mvc; -using MonadicBits; +using ErrorOr; namespace HotwiredBooks.ViewComponents; public sealed class BookFormViewComponent : ViewComponent { - public IViewComponentResult Invoke(Maybe book) => View(new BookFormViewModel(book)); + public IViewComponentResult Invoke(ErrorOr book) => View(new BookFormViewModel(book)); } diff --git a/HotwiredBooks/ViewModels/BookFormViewModel.cs b/HotwiredBooks/ViewModels/BookFormViewModel.cs index 7343897..8fb3628 100644 --- a/HotwiredBooks/ViewModels/BookFormViewModel.cs +++ b/HotwiredBooks/ViewModels/BookFormViewModel.cs @@ -1,6 +1,6 @@ using HotwiredBooks.Models; -using MonadicBits; +using ErrorOr; namespace HotwiredBooks.ViewModels; -public sealed record BookFormViewModel(Maybe Book); +public sealed record BookFormViewModel(ErrorOr Book); diff --git a/HotwiredBooks/Views/Books/Edit.cshtml b/HotwiredBooks/Views/Books/Edit.cshtml index 98dc2f2..c501454 100644 --- a/HotwiredBooks/Views/Books/Edit.cshtml +++ b/HotwiredBooks/Views/Books/Edit.cshtml @@ -1,10 +1,11 @@ @using HotwiredBooks.Extensions -@using MonadicBits +@using HotwiredBooks.TagHelpers +@using HotwiredBooks.ViewComponents @model HotwiredBooks.ViewModels.BooksEditViewModel @{ Layout = null; } - + diff --git a/HotwiredBooks/Views/Shared/Components/BookForm/Default.cshtml b/HotwiredBooks/Views/Shared/Components/BookForm/Default.cshtml index da2b05b..8f15fac 100644 --- a/HotwiredBooks/Views/Shared/Components/BookForm/Default.cshtml +++ b/HotwiredBooks/Views/Shared/Components/BookForm/Default.cshtml @@ -1,11 +1,12 @@ +@using ErrorOr.Extensions @model HotwiredBooks.ViewModels.BookFormViewModel @{ - var id = Model.Book.Map(book => book.Id).Match(id => id.ToString(), () => string.Empty); - var action = Model.Book.Match(_ => "Update", () => "Create"); - var method = Model.Book.Match(_ => "put", () => "post"); - var title = Model.Book.Map(book => book.Title).Match(title => title, () => string.Empty); - var author = Model.Book.Map(book => book.Author).Match(author => author, () => string.Empty); + var id = Model.Book.Select(book => book.Id).Match(id => id.ToString(), _ => string.Empty); + var action = Model.Book.Match(_ => "Update", _ => "Create"); + var method = Model.Book.Match(_ => "put", _ => "post"); + var title = Model.Book.Select(book => book.Title).Match(title => title, _ => string.Empty); + var author = Model.Book.Select(book => book.Author).Match(author => author, _ => string.Empty); }
Assert.Multiple(() => + var book = await repository.Create(title, author); + book.Switch(b => Assert.Multiple(() => { - Assert.That(book.Title, Is.EqualTo(title)); - Assert.That(book.Author, Is.EqualTo(author)); - Assert.That(book.Id, Is.Not.EqualTo(new Guid())); + Assert.That(b.Title, Is.EqualTo(title)); + Assert.That(b.Author, Is.EqualTo(author)); + Assert.That(b.Id, Is.Not.EqualTo(new Guid())); }), - Assert.Fail + _ => Assert.Fail() ); var areEqual = - from created in Task.FromResult(maybe) + from created in Task.FromResult(book) from read in repository.Lookup(created.Id) select created == read; - (await areEqual).Match(Assert.True, Assert.Fail); + (await areEqual).Switch(Assert.True, _ => Assert.Fail()); } }