Skip to content

Commit

Permalink
Merge pull request #1 from Applicita/v2
Browse files Browse the repository at this point in the history
V2
  • Loading branch information
VincentH-Net authored Apr 16, 2024
2 parents cb6244b + 6e9563e commit 6a6f33f
Show file tree
Hide file tree
Showing 48 changed files with 716 additions and 568 deletions.
469 changes: 221 additions & 248 deletions .editorconfig

Large diffs are not rendered by default.

23 changes: 8 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# <img src="img/CSharp-Toolkit-Icon.png" alt="Backend Toolkit" width="64px" />Orleans.Multiservice
Prevent microservices pain with logical service separation in a modular monolith for [Microsoft Orleans 7](https://learn.microsoft.com/en-us/dotnet/orleans/)
# <img src="img/CSharp-Toolkit-Icon.png" alt="Backend Toolkit" width="64px" />Orleans.Multiservice
Prevent microservices pain with logical service separation in a modular monolith for [Microsoft Orleans 8](https://learn.microsoft.com/en-us/dotnet/orleans/)

Orleans.Multiservice is an automated code structuring pattern for logical service separation within a Microsoft Orleans (micro)service.

Expand Down Expand Up @@ -37,18 +37,18 @@ Orleans.Multiservice consists of:
> The code analyzer / unit tests will be added in a future release. Note that the multiservice pattern can be used without the analyzer by following the code structure of the template and the [pattern rules](#pattern-rules)
## Template usage
1) On the command line, ensure that the [mcs-orleans-multiservice template](https://github.com/Applicita/Modern.CSharp.Templates#readme) is installed<br />(note that below is .NET 7 cli syntax; Orleans 7 requires .NET 7):
1) On the command line, ensure that the [mcs-orleans-multiservice template](https://github.com/Applicita/Modern.CSharp.Templates#readme) is installed:
```
dotnet new install Modern.CSharp.Templates
```
**Note** that the `dotnet new mcs-orleans-multiservice` template requires **PowerShell** to be installed
2) Type this command to read the documentation for the template parameters:
2) Enter this command to read the documentation for the template parameters:
```
dotnet new mcs-orleans-multiservice -h
```
3) To create a new multiservice with one logical service in it, type e.g.:
3) To create a new multiservice with one logical service in it, enter e.g.:
```
dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop --Multiservice TeamA --Logicalservice Catalog --allow-scripts Yes
```
Expand Down Expand Up @@ -82,8 +82,8 @@ Single team solution:
- Debug [eShopTeamA.sln](https://github.com/Applicita/Orleans.Multiservice/tree/main/src/Example/eShopBySingleTeam/TeamA)
Two team solution:
- Ensure you have the [.NET OpenAPI tool](https://learn.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-7.0) installed for .NET 7:
`dotnet tool install --global Microsoft.dotnet-openapi --version 7.0.0`
- Ensure you have the latest [.NET OpenAPI tool](https://learn.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-8.0) for .NET 8 installed:<br />
`dotnet tool install --global Microsoft.dotnet-openapi`<br />
On build, this will generate the `CatalogServiceClient` from `CatalogService.json`
- Debug [eShopTeamAof2.sln](https://github.com/Applicita/Orleans.Multiservice/tree/main/src/Example/eShopByTwoTeams/TeamA) and [eShopTeamBof2.sln](https://github.com/Applicita/Orleans.Multiservice/tree/main/src/Example/eShopByTwoTeams/TeamB)
Expand All @@ -102,7 +102,7 @@ These rules ensure that the pattern remains intact:
`*Service -> Contracts`
2) These types are only allowed in specific namespaces:<br />
All API controllers must be in or under `Apis.<service-name>Api`<br />
All API endpoints must be in or under `Apis.<service-name>Api`<br />
All `public` grain contracts must be in or under `Contracts.<service-name>Contract`<br />
3) References between types in these namespaces are **not** allowed:<br />
Expand All @@ -112,10 +112,3 @@ These rules ensure that the pattern remains intact:
4) The `public` keyword in `*Service` projects is *only* used on interface member implementations, grain constructors and serializable members in a type.<br />
This ensures that the only external code access is Orleans instantiating grains. It makes it safe to reference the service implementation projects in the silo host project (Apis) to let Orleans locate the grain implementations; the types in the service implementation projects will not be available in the silo host project.
The Roslyn analyzer will allow rules to be configured in `.editorconfig`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 26 additions & 2 deletions src/Example/eShopBySingleTeam/TeamA/AddLogicalService.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
Param(
Param(
[Parameter(Mandatory, HelpMessage="The name (without 'Service' suffix) of the logical service to add to the CoreTeam multiservice solution in the current directory; used in the name of the new service project and in new namespaces + classes in the Apis and Contracts projects")]
[string]
$Name
)
dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop -M . --Logicalservice $Name --allow-scripts Yes

# Function to update the Program.cs file to add a new parameter to the RegisterEndpoints method
function Update-RegisterEndpoints {
$newParameter = "`n typeof(Applicita.eShop.Apis.${Name}Api.${Name}Endpoints)`n"
$apisDirectory = Join-Path -Path $PWD -ChildPath "Apis"
$programFile = Get-ChildItem -Path $apisDirectory -Recurse -Filter "Program.cs" -ErrorAction SilentlyContinue | Select-Object -First 1

if ($programFile -ne $null) {
$programContent = Get-Content -Path $programFile.FullName -Raw
$pattern = "(?s)(app\s*\.RegisterEndpoints\s*\(.+?\))\s*\)"
$modifiedContent = $programContent -replace $pattern, "`$1,$newParameter)"

if ($modifiedContent -ne $programContent) {
Set-Content -Path $programFile.FullName -Value $modifiedContent
Write-Output "Successfully added new parameter to RegisterEndpoints call in $($programFile.FullName):$newParameter"
return
}
}

Write-Warning "Could not automatically add below parameter to the RegisterEndpoints(...) call; please add it manually:$newParameter"
}

dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop -M . --Logicalservice $Name --allow-scripts Yes

Update-RegisterEndpoints
8 changes: 4 additions & 4 deletions src/Example/eShopBySingleTeam/TeamA/Apis/Apis.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>

Expand All @@ -19,9 +19,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Persistence.Memory" Version="7.1.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
<PackageReference Include="Microsoft.Orleans.Persistence.Memory" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,28 @@

namespace Applicita.eShop.Apis.BasketApi;

[Route("[controller]")]
[ApiController]
public class BasketsController : ControllerBase
public class BasketsEndpoints(IClusterClient orleans) : IEndpoints
{
const string Basket = "{buyerId}";

readonly IClusterClient orleans;

public BasketsController(IClusterClient orleans)
=> this.orleans = orleans;
public void Register(IEndpointRouteBuilder routeBuilder)
{
var group = routeBuilder.MapGroup("/baskets").WithTags("Baskets");
_ = group.MapGet (Basket, GetBasket);
_ = group.MapPut ("" , UpdateBasket);
_ = group.MapDelete(Basket, EmptyBasket);
}

/// <response code="200">The basket of buyerId is returned</response>
[HttpGet(Basket)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Basket>> GetBasket(int buyerId)
public async Task<Ok<Basket>> GetBasket(int buyerId)
=> Ok(await BasketGrain(buyerId).GetBasket());

/// <response code="200">The updated basket is returned, with items updated from the current products in the Catalog service</response>
[HttpPut()]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<Basket>> UpdateBasket(Basket basket)
public async Task<Ok<Basket>> UpdateBasket(Basket basket)
=> Ok(await BasketGrain(basket.BuyerId).UpdateBasket(basket));

/// <response code="200">The basket of buyerId is emptied</response>
[HttpDelete(Basket)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> EmptyBasket(int buyerId)
public async Task<Ok> EmptyBasket(int buyerId)
{ await BasketGrain(buyerId).EmptyBasket(); return Ok(); }

IBasketGrain BasketGrain(int buyerId)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Applicita.eShop.Contracts.CatalogContract;

namespace Applicita.eShop.Apis.CatalogApi;

public class CatalogEndpoints(IClusterClient orleans) : IEndpoints
{
const string Products = "/products";
const string Product = Products + "/{id}";

readonly ICatalogGrain catalog = orleans.GetGrain<ICatalogGrain>(ICatalogGrain.Key);

public void Register(IEndpointRouteBuilder routeBuilder)
{
var group = routeBuilder.MapGroup("/catalog").WithTags("Catalog");
_ = group.MapPost (Products, CreateProduct);
_ = group.MapGet (Products, GetProducts ).WithName(nameof(GetProducts));
_ = group.MapPut (Products, UpdateProduct);
_ = group.MapDelete(Product , DeleteProduct);
}

/// <response code="201">The new product is created with the returned id</response>
async Task<CreatedAtRoute<int>> CreateProduct(Product product)
{
int id = await catalog.CreateProduct(product);
return CreatedAtRoute(id, nameof(GetProducts), new { id });
}

/// <response code="200">
/// Products for all <paramref name="id"/>'s currently in the catalog are returned;
/// unknown product id's are skipped.
/// If no <paramref name="id"/>'s are specified, all products in the catalog are returned
/// </response>
async Task<Ok<ImmutableArray<Product>>> GetProducts(int[]? id) => Ok(
id?.Length > 0
? await catalog.GetCurrentProducts([.. id])
: await catalog.GetAllProducts()
);

/// <response code="200">The product is updated</response>
/// <response code="404">The product id is not found</response>
public async Task<Results<Ok, NotFound<int>>> UpdateProduct(Product product)
=> await catalog.UpdateProduct(product)
? Ok()
: NotFound(product.Id);

/// <response code="200">The product is deleted</response>
/// <response code="404">The product id is not found</response>
public async Task<Results<Ok, NotFound<int>>> DeleteProduct(int id)
=> await catalog.DeleteProduct(id)
? Ok()
: NotFound(id);
}
15 changes: 15 additions & 0 deletions src/Example/eShopBySingleTeam/TeamA/Apis/Foundation/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Applicita.eShop.Apis.Foundation;

public interface IEndpoints
{
void Register(IEndpointRouteBuilder routeBuilder);
}

public static class WebApplicationExtensions
{
public static void RegisterEndpoints(this WebApplication app, params Type[] endpointsTypes)
{
foreach (var endpointsType in endpointsTypes)
((IEndpoints)ActivatorUtilities.CreateInstance(app.Services, endpointsType)).Register(app);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Not relevant in ASP.NET Core")]
[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Public methods are only invoked by ASP.NET Core, which ensures non-null parameter values")]
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
global using Microsoft.AspNetCore.Mvc;
global using System.Collections.Immutable;
global using Microsoft.AspNetCore.Http.HttpResults;
global using static Microsoft.AspNetCore.Http.TypedResults;
global using Applicita.eShop.Apis.Foundation;
10 changes: 4 additions & 6 deletions src/Example/eShopBySingleTeam/TeamA/Apis/Foundation/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
.AddMemoryGrainStorageAsDefault()
);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => {
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"));
Expand All @@ -19,10 +18,9 @@
if (app.Environment.IsDevelopment())
_ = app.UseSwagger().UseSwaggerUI(options => options.EnableTryItOutByDefault());

app.UseAuthorization();

app.MapControllers();
app.RegisterEndpoints(
typeof(Applicita.eShop.Apis.BasketApi.BasketsEndpoints),
typeof(Applicita.eShop.Apis.CatalogApi.CatalogEndpoints)
);

app.Run();

sealed partial class Program { } // Fix CA1852
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ sealed class BasketGrain : Grain, IBasketGrain
internal sealed class State
{
[Id(0)] public bool HasBuyerId { get; set; }
[Id(1)] public Basket Basket { get; set; } = new Basket(-1, ImmutableArray<BasketItem>.Empty);
[Id(1)] public Basket Basket { get; set; } = new Basket(-1, []);
}

readonly IPersistentState<State> state;
Expand Down Expand Up @@ -46,7 +46,7 @@ public BasketGrain([PersistentState("state")] IPersistentState<State> state)

public async Task EmptyBasket()
{
Basket = Basket with { Items = ImmutableArray<BasketItem>.Empty };
Basket = Basket with { Items = [] };
await state.WriteStateAsync();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>preview-All</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<AssemblyName>Applicita.eShop.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>Applicita.eShop.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>preview-All</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Runtime" Version="7.1.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.1.0" />
</ItemGroup>
<NoWarn>$(NoWarn);EnableGenerateDocumentationFile</NoWarn>

<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>
<AssemblyName>Applicita.eShop.$(MSBuildProjectName)</AssemblyName>
<RootNamespace>Applicita.eShop.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Orleans.Runtime" Version="8.0.0" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Contracts\Contracts.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public async Task<ImmutableArray<BasketItem>> UpdateFromCurrentProducts(Immutabl
var productIds = basketItems.Select(bi => bi.ProductId).ToImmutableArray();
var products = await catalog.GetCurrentProducts(productIds);

List<BasketItem> updatedItems = new();
List<BasketItem> updatedItems = [];
foreach (var item in basketItems)
{
var product = products.SingleOrDefault(p => p.Id == item.ProductId);
Expand All @@ -32,6 +32,6 @@ public async Task<ImmutableArray<BasketItem>> UpdateFromCurrentProducts(Immutabl
UnitPrice = product.Price,
});
}
return updatedItems.ToImmutableArray();
return [.. updatedItems];
}
}
Loading

0 comments on commit 6a6f33f

Please sign in to comment.