{"id":26487707,"url":"https://github.com/vincenth-net/orleans.results","last_synced_at":"2025-03-20T06:52:04.239Z","repository":{"id":43664351,"uuid":"511503964","full_name":"VincentH-Net/Orleans.Results","owner":"VincentH-Net","description":"Concise, version-tolerant result pattern implementation for Microsoft Orleans 8","archived":false,"fork":false,"pushed_at":"2025-03-17T08:53:56.000Z","size":222,"stargazers_count":41,"open_issues_count":0,"forks_count":1,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-17T09:43:09.319Z","etag":null,"topics":["cloud-native","csharp","distributed-systems","dotnet","error-handling","orleans","orleans-applications","orleans-example","pattern","validation"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/VincentH-Net.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-07T11:41:07.000Z","updated_at":"2025-03-17T08:54:00.000Z","dependencies_parsed_at":"2025-03-17T10:46:46.894Z","dependency_job_id":null,"html_url":"https://github.com/VincentH-Net/Orleans.Results","commit_stats":null,"previous_names":["vincenth-net/orleans.results"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VincentH-Net%2FOrleans.Results","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VincentH-Net%2FOrleans.Results/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VincentH-Net%2FOrleans.Results/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/VincentH-Net%2FOrleans.Results/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/VincentH-Net","download_url":"https://codeload.github.com/VincentH-Net/Orleans.Results/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244566929,"owners_count":20473451,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloud-native","csharp","distributed-systems","dotnet","error-handling","orleans","orleans-applications","orleans-example","pattern","validation"],"created_at":"2025-03-20T06:52:03.606Z","updated_at":"2025-03-20T06:52:04.233Z","avatar_url":"https://github.com/VincentH-Net.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# \u003cimg src=\"CSharp-Toolkit-Icon.png\" alt=\"C# Toolkit\" width=\"64px\" /\u003e Orleans.Results\nConcise, version-tolerant result pattern implementation for [Microsoft Orleans 8](https://github.com/dotnet/orleans/releases/tag/v8.0.0).\n\nIncluded in [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/Modern.CSharp.Templates?color=gold\u0026label=NuGet:%20Modern.CSharp.Templates\u0026style=plastic)](https://www.nuget.org/packages/Modern.CSharp.Templates) (see [below](#how-do-i-get-it))\n\nThe result pattern solves a common problem: it returns an object indicating success or failure of an operation instead of throwing exceptions (see [why](#why) below).\n\nThis implementation leverages [immutability to optimize performance](#immutability-and-performance) and is fully tested (100% code coverage).\n\n_(Note: this repo was transferred from Applicita to VincentH-Net on March 17, 2025 to reflect who actively maintains it)_\n\n## Basic usage\n\nDefine error codes:\n```csharp\npublic enum ErrorNr\n{\n    UserNotFound = 1\n}\n```\n\u003e Note that this enum is used to define convenience classes:\u003cbr /\u003e`Result : ResultBase\u003cErrorNr\u003e` and `Result\u003cT\u003e : ResultBase\u003cErrorNr, T\u003e`\u003cbr /\u003eThese classes save you from having to specify `\u003cErrorNr\u003e` as type parameter in every grain method signature\n\nGrain contract:\n```csharp\ninterface ITenant : IGrainWithStringKey\n{\n    Task\u003cResult\u003cstring\u003e\u003e GetUser(int id);\n}\n```\nUse in ASP.NET Core minimal API's:\n```csharp\napp.MapGet(\"minimalapis/users/{id}\", async (IClusterClient client, int id)\n =\u003e await client.GetGrain\u003cITenant\u003e(\"\").GetUser(id) switch\n    {\n        { IsSuccess: true               } r =\u003e Results.Ok(r.Value),\n        { ErrorNr: ErrorNr.UserNotFound } r =\u003e Results.NotFound(r.ErrorsText),\n        {                               } r =\u003e throw r.UnhandledErrorException()\n    }\n);\n```\nUse in ASP.NET Core MVC:\n```csharp\n[HttpGet(\"mvc/users/{id}\")]\npublic async Task\u003cActionResult\u003cstring\u003e\u003e GetUser(int id)\n =\u003e await client.GetGrain\u003cITenant\u003e(\"\").GetUser(id) switch\n    {\n        { IsSuccess: true               } r =\u003e Ok(r.Value),\n        { ErrorNr: ErrorNr.UserNotFound } r =\u003e NotFound(r.ErrorsText),\n        {                               } r =\u003e throw r.UnhandledErrorException()\n    };\n```\nGrain implementation:\n```csharp\nclass Tenant : Grain, ITenant\n{\n    public Task\u003cResult\u003cstring\u003e\u003e GetUser(int id) =\u003e Task.FromResult\u003cResult\u003cstring\u003e\u003e(\n        id \u003e= 0 \u0026\u0026 id \u003c S.Users.Count ?\n            S.Users[id] :\n            Errors.UserNotFound(id)\n    );\n}\n\nstatic class Errors\n{\n    public static Result.Error UserNotFound(int id) =\u003e new(ErrorNr.UserNotFound, $\"User {id} not found\");\n}\n```\n\n## Convenience features\nThe `Result\u003cT\u003e` class is intended for methods that return either a value or error(s), while the `Result` class is intended for methods that return either success (`Result.Ok`) or error(s).\n\nThe `Result` and `Result\u003cT\u003e` convenience classes have implicit convertors to allow concise returning of errors and values:\n```csharp\nasync Task\u003cResult\u003cstring\u003e\u003e GetString(int i) =\u003e i switch {\n    0 =\u003e \"Success!\",\n    1 =\u003e ErrorNr.NotFound,\n    2 =\u003e (ErrorNr.NotFound, \"Not found\"),\n    3 =\u003e new Error(ErrorNr.NotFound, \"Not found\"),\n    4 =\u003e new Collection\u003cError\u003e(/*...*/)\n};\n```\nThe implicit convertor only supports multiple errors with `Collection\u003cError\u003e`; you can use the public constructor to specify multiple errors with any `IEnumerable\u003cError\u003e`:\n```csharp\nasync Task\u003cResult\u003cstring\u003e\u003e GetString()\n{\n    IEnumerable\u003cError\u003e errors = new HashSet\u003cError\u003e();\n    // ... check for errors\n    if (errors.Any()) return new(errors);\n    return \"Success!\";\n}\n```\n## Validation errors\nThe `TryAsValidationErrors` method is covenient for returning [RFC7807](https://tools.ietf.org/html/rfc7807) based problem detail responses. This method is designed to be used with [ValidationProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?view=aspnetcore-8.0) (in MVC):\u003cbr\u003e\n```csharp\nreturn result.TryAsValidationErrors(ErrorNr.ValidationError, out var validationErrors)\n    ? ValidationProblem(new ValidationProblemDetails(validationErrors))\n\n    : result switch\n    {\n        { IsSuccess: true                   } r =\u003e Ok(r.Value),\n        { ErrorNr: ErrorNr.NoUsersAtAddress } r =\u003e NotFound(r.ErrorsText),\n        {                                   } r =\u003e throw r.UnhandledErrorException()\n    };\n```\nand with [Results.ValidationProblem](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem?view=aspnetcore-8.0) (in minimal API's):\n```csharp\nreturn result.TryAsValidationErrors(ErrorNr.ValidationError, out var validationErrors)\n    ? Results.ValidationProblem(validationErrors)\n\n    : result switch\n    {\n        { IsSuccess: true                   } r =\u003e Results.Ok(r.Value),\n        { ErrorNr: ErrorNr.NoUsersAtAddress } r =\u003e Results.NotFound(r.ErrorsText),\n        {                                   } r =\u003e throw r.UnhandledErrorException()\n    };\n```\n\nTo use `TryAsValidationErrors`, your `ErrorNr` must be a `[Flags] enum` with a flag that identifies which error codes are validation errors:\n```csharp\n[Flags]\npublic enum ErrorNr\n{\n    NoUsersAtAddress = 1,\n\n    ValidationError = 1024,\n    InvalidZipCode = 1 | ValidationError,\n    InvalidHouseNr = 2 | ValidationError,\n}\n```\n`TryAsValidationErrors` will only return validation errors if the result is failed and **all** errors in it are validation errors; the method is designed to support a typical validation implementation pattern:\n```csharp\npublic async Task\u003cResult\u003cstring\u003e\u003e GetUsersAtAddress(string zip, string nr)\n{\n    Collection\u003cResult.Error\u003e errors = new();\n\n    // First check for validation errors - don't perform the operation if there are any.\n    if (!ZipRegex().IsMatch(zip)) errors.Add(Errors.InvalidZipCode(zip));\n    if (!HouseNrRegex().IsMatch(nr)) errors.Add(Errors.InvalidHouseNr(nr));\n    if (errors.Any()) return errors;\n\n    // If there are no validation errors, perform the operation - this may return non-validation errors\n    // ... do the operation\n    if (...) errors.Add(Errors.NoUsersAtAddress($\"{zip} {nr}\"));\n    return errors.Any() ? errors : \"Success!\";\n}\n```\n\n## Immutability and performance\nTo optimize performance, `Result` and `Error` are implemented as immutable types and are marked with the Orleans [[Immutable] attribute](https://github.com/dotnet/orleans/blob/b7bb116ba4f98b64428d449d26f20ea37d3501b6/src/Orleans.Serialization.Abstractions/Annotations.cs#L430). This means that Orleans will not create a deep copy of these types for grain calls within the same silo, passing instance references instead.\n\nThe performance of `Result\u003cT\u003e` can be optimized similarly by judiciously marking specific `T` types as `[Immutable]` - exactly the same way as when you would directly pass `T` around, instead of `Result\u003cT\u003e`. The fact that `Result\u003cT\u003e` itself is not marked immutable does not significantly reduce the performance benefits gained; in cases where immutability makes a difference `T` typically has a much higher serialization cost than the wrapping result (which is very lightweight).\n## Full example\nThe [example in the repo](https://github.com/Applicita/Orleans.Results/tree/main/src/Example) demonstrates using Orleans.Results with both ASP.NET Core minimal API's and MVC:\n![Orleans Results Example](Orleans-Results-Example.png)\n## How do I get it?\n1) On the command line, ensure that the [template](https://github.com/Applicita/Modern.CSharp.Templates) is installed\u003cbr /\u003e(note that below is .NET 8 cli syntax; Orleans 8 requires .NET 8):\n    ```\n    dotnet new install Modern.CSharp.Templates\n    ```\n\n2) In or below the project folder that contains grain interfaces (or that is referenced by projects that contain grain interfaces), type:\n    ```\n    dotnet new mcs-orleans-results\n    ```\n    This will add the [ErrorNr.cs](https://github.com/Applicita/Orleans.Results/blob/main/src/ErrorNr.cs) and [Result.cs](https://github.com/Applicita/Orleans.Results/blob/main/src/Result.cs) files there (if you prefer, you can copy the files there manually)\n\n3) Update the `Example` namespace in the added files to match your project\n4) Edit the `ErrorNr` enum to define error codes\n\n## Why?\nThe result pattern solves a common problem: it returns an object indicating success or failure of an operation instead of throwing/using exceptions.\n\n### Using exceptions for flow control is an antipattern:\n- It degrades performance: see [MS profiling rule da0007](https://docs.microsoft.com/en-us/visualstudio/profiling/da0007-avoid-using-exceptions-for-control-flow)\n- It degrades code readability and maintainability:\u003cbr /\u003eit is easy to miss expected flows when exceptions are used; an exception raised in a called method can exit the current method without any indication that this is intentional and expected.\n\n  Checking return values and returning them to the caller makes the expected flows clear and explicit in context in the code of every method.\n\n  Using return values also allows you to use [code analysis rule CA1806](https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1806) to alert you where you forgot to check the return value (you can use a *discard* `_ =` to express intent to ignore a return value)\n\n### Orleans 8 supports version-tolerant, high-performance serialization\nHowever existing Result pattern implementations like [FluentResults](https://github.com/altmann/FluentResults) are not designed for serialization, let alone Orleans serialization. Orleans requires that you annotate your result types - including all types contained within - with the Orleans `[GenerateSerializer]` and `[Id]` attributes, or alternatively that you write additional code to serialize external types.\n\nThis means that result objects that can contain contain arbitrary objects as part of the errors (like exceptions) require an open-ended amount of work. Orleans.Results avoids this work by defining an error to be an `enum` nr plus a `string` message.\n\nOrleans.Results adheres to the Orleans 8 serialization guidelines, which enables compatibility with future changes in the result object serialization.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvincenth-net%2Forleans.results","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvincenth-net%2Forleans.results","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvincenth-net%2Forleans.results/lists"}