Skip to content

Commit

Permalink
Update tests and texts to Orleans 8, .NET 8 and C# 12
Browse files Browse the repository at this point in the history
  • Loading branch information
VincentH-Net committed Apr 5, 2024
1 parent 70553d7 commit c78aba4
Show file tree
Hide file tree
Showing 9 changed files with 40 additions and 39 deletions.
Binary file modified Orleans-Results-Example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# <img src="CSharp-Toolkit-Icon.png" alt="C# Toolkit" width="64px" /> Orleans.Results
Concise, version-tolerant result pattern implementation for [Microsoft Orleans 7](https://github.com/dotnet/orleans/releases/tag/v7.0.0).
# <img src="CSharp-Toolkit-Icon.png" alt="C# Toolkit" width="64px" /> Orleans.Results
Concise, version-tolerant result pattern implementation for [Microsoft Orleans 8](https://github.com/dotnet/orleans/releases/tag/v8.0.0).

Included in [![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/Modern.CSharp.Templates?color=gold&label=NuGet:%20Modern.CSharp.Templates&style=plastic)](https://www.nuget.org/packages/Modern.CSharp.Templates) (see [below](#how-do-i-get-it))

Expand Down Expand Up @@ -74,10 +74,10 @@ async Task<Result<string>> GetString(int i) => i switch {
1 => ErrorNr.NotFound,
2 => (ErrorNr.NotFound, "Not found"),
3 => new Error(ErrorNr.NotFound, "Not found"),
4 => new List<Error>(/*...*/)
4 => new Collection<Error>(/*...*/)
};
```
The implicit convertor only supports multiple errors with `List<Error>`; you can use the public constructor to specify multiple errors with any `IEnumerable<Error>`:
The implicit convertor only supports multiple errors with `Collection<Error>`; you can use the public constructor to specify multiple errors with any `IEnumerable<Error>`:
```csharp
async Task<Result<string>> GetString()
{
Expand All @@ -88,7 +88,7 @@ async Task<Result<string>> GetString()
}
```
## Validation errors
The `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-7.0) (in MVC):<br>
The `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):<br>
```csharp
return result.TryAsValidationErrors(ErrorNr.ValidationError, out var validationErrors)
? ValidationProblem(new ValidationProblemDetails(validationErrors))
Expand All @@ -100,7 +100,7 @@ return result.TryAsValidationErrors(ErrorNr.ValidationError, out var validationE
{ } r => throw r.UnhandledErrorException()
};
```
and with [Results.ValidationProblem](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem?view=aspnetcore-7.0) (in minimal API's):
and with [Results.ValidationProblem](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.results.validationproblem?view=aspnetcore-8.0) (in minimal API's):
```csharp
return result.TryAsValidationErrors(ErrorNr.ValidationError, out var validationErrors)
? Results.ValidationProblem(validationErrors)
Expand Down Expand Up @@ -129,7 +129,7 @@ public enum ErrorNr
```csharp
public async Task<Result<string>> GetUsersAtAddress(string zip, string nr)
{
List<Result.Error> errors = new();
Collection<Result.Error> errors = new();

// First check for validation errors - don't perform the operation if there are any.
if (!ZipRegex().IsMatch(zip)) errors.Add(Errors.InvalidZipCode(zip));
Expand All @@ -151,7 +151,7 @@ The performance of `Result<T>` can be optimized similarly by judiciously marking
The [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:
![Orleans Results Example](Orleans-Results-Example.png)
## How do I get it?
1) On the command line, ensure that the [template](https://github.com/Applicita/Modern.CSharp.Templates) is installed<br />(note that below is .NET 7 cli syntax; Orleans 7 requires .NET 7):
1) On the command line, ensure that the [template](https://github.com/Applicita/Modern.CSharp.Templates) is installed<br />(note that below is .NET 8 cli syntax; Orleans 8 requires .NET 8):
```
dotnet new install Modern.CSharp.Templates
```
Expand All @@ -176,9 +176,9 @@ The result pattern solves a common problem: it returns an object indicating succ
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)
### Orleans 7 introduces version-tolerant, high-performance serialization
### Orleans 8 supports version-tolerant, high-performance serialization
However 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.
This 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.
Orleans.Results adheres to the Orleans 7 serialization guidelines, which enables compatibility with future changes in the result object serialization.
Orleans.Results adheres to the Orleans 8 serialization guidelines, which enables compatibility with future changes in the result object serialization.
2 changes: 1 addition & 1 deletion src/Example/Example.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<AnalysisLevel>preview-All</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile;CA1852</NoWarn>
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Example/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("v1", new OpenApiInfo { Title = "Example API with Orleans.Results for Orleans 7" }));
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("v1", new OpenApiInfo { Title = "Example API with Orleans.Results for Orleans 8" }));

var app = builder.Build();

Expand Down
2 changes: 2 additions & 0 deletions src/Tests/Foundation/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@

[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices#naming-your-tests", Scope = "namespaceanddescendants", Target = "~N:Orleans.Results.Tests.UnitTests")]
[assembly: SuppressMessage("Style", "IDE0005:Using directive is unnecessary.", Justification = "This project includes shared source files that are also used outside the context of this project's global usings")]
[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Not relevant for ASP.NET Core / Orleans applications")]
[assembly: SuppressMessage("Naming", "CA1724:Type names should not match namespaces", Justification = "Namespace is generated by Orleans")]
20 changes: 12 additions & 8 deletions src/Tests/Orleans.Results.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>preview-All</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile</NoWarn>

<IsPackable>false</IsPackable>
</PropertyGroup>
Expand All @@ -14,15 +18,15 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.0.0" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="7.0.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="8.0.0" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
15 changes: 6 additions & 9 deletions src/Tests/Orleans.Results/Example.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Text.RegularExpressions;
using Orleans.Runtime;

Expand All @@ -11,12 +12,8 @@ public interface ITenant : IGrainWithStringKey
Task<Result<ImmutableArray<int>>> GetUsersAtAddress(string zip, string nr);
}

public partial class Tenant : Grain, ITenant
public partial class Tenant([PersistentState("state")] IPersistentState<Tenant.State> state) : Grain, ITenant
{
readonly IPersistentState<State> state;

public Tenant([PersistentState("state")] IPersistentState<State> state) => this.state = state;

State S => state.State;

public Task<Result<string>> GetUser(int id) => Task.FromResult<Result<string>>(
Expand All @@ -33,23 +30,23 @@ public Task<Result> UpdateUser(int id, string name)

public async Task<Result<ImmutableArray<int>>> GetUsersAtAddress(string zip, string nr)
{
List<Result.Error> errors = new ();
Collection<Result.Error> errors = [];

// First check for validation errors - don't perform the operation if there are any.
if (!ZipCodeRegex().IsMatch(zip)) errors.Add(Errors.InvalidZipCode(zip));
if (!HouseNrRegex().IsMatch(nr)) errors.Add(Errors.InvalidHouseNr(nr));
if (errors.Any()) return errors;
if (errors.Count != 0) return errors;

// If there are no validation errors, perform the operation - this may return non-validation errors
await Task.CompletedTask; // Simulate an operation
if (!(int.TryParse(nr, out int number) && number % 2 == 1)) errors.Add(Errors.NoUsersAtAddress($"{zip} {nr}"));
return errors.Any() ? errors : ImmutableArray.Create(0);
return errors.Count != 0 ? errors : ImmutableArray.Create(0);
}

[GenerateSerializer]
public class State
{
[Id(0)] public List<string> Users { get; set; } = new(new[] { "John", "Vincent" });
[Id(0)] public Collection<string> Users { get; } = new(["John", "Vincent"]);
}

[GeneratedRegex("^\\d\\d\\d\\d[A-Z]{2}$")]
Expand Down
9 changes: 4 additions & 5 deletions src/Tests/UnitTests/ResultWithValueTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using Example;

namespace Orleans.Results.Tests.UnitTests;

[Collection(TestCluster.Name)]
public class ResultWithValueTests
public class ResultWithValueTests(ClusterFixture fixture)
{
readonly TestingHost.TestCluster cluster;

public ResultWithValueTests(ClusterFixture fixture) => cluster = fixture.Cluster;
readonly TestingHost.TestCluster cluster = fixture.Cluster;

ITenant Tenant => cluster.GrainFactory.GetGrain<ITenant>("");

Expand Down Expand Up @@ -176,7 +175,7 @@ public void ImplicitConversion_OfListOfErrorsToResult_GivesResultWithErrors()
var error2 = error1 with { Nr = ErrorNr.NoUsersAtAddress, Message = "Error message 2" };
// With is used to get coverage of the generated set_Code and set_Message

List<Result.Error> errors = new(new Result.Error[] { error1, error2 });
Collection<Result.Error> errors = new([error1, error2]);
Result<bool> result = errors;

Assert.Equal(errors, result.Errors);
Expand Down
9 changes: 4 additions & 5 deletions src/Tests/UnitTests/ResultWithoutValueTests.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using Example;

namespace Orleans.Results.Tests.UnitTests;

[Collection(TestCluster.Name)]
public class ResultWithoutValueTests
public class ResultWithoutValueTests(ClusterFixture fixture)
{
readonly TestingHost.TestCluster cluster;

public ResultWithoutValueTests(ClusterFixture fixture) => cluster = fixture.Cluster;
readonly TestingHost.TestCluster cluster = fixture.Cluster;

ITenant Tenant => cluster.GrainFactory.GetGrain<ITenant>("");

Expand Down Expand Up @@ -87,7 +86,7 @@ public void ImplicitConversion_OfListOfErrorsToResult_GivesResultWithErrors()
Result.Error error1 = new(ErrorNr.UserNotFound, "Error message 1");
var error2 = error1 with { Nr = ErrorNr.NoUsersAtAddress, Message = "Error message 2" };

List<Result.Error> errors = new(new Result.Error[] { error1, error2 });
Collection<Result.Error> errors = new([error1, error2]);
Result result = errors;

Assert.Equal(errors, result.Errors);
Expand Down

0 comments on commit c78aba4

Please sign in to comment.