Skip to content

Commit 6a6f33f

Browse files
authored
Merge pull request #1 from Applicita/v2
V2
2 parents cb6244b + 6e9563e commit 6a6f33f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+716
-568
lines changed

.editorconfig

Lines changed: 221 additions & 248 deletions
Large diffs are not rendered by default.

README.md

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

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

@@ -37,18 +37,18 @@ Orleans.Multiservice consists of:
3737
> 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)
3838
3939
## Template usage
40-
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):
40+
1) On the command line, ensure that the [mcs-orleans-multiservice template](https://github.com/Applicita/Modern.CSharp.Templates#readme) is installed:
4141
```
4242
dotnet new install Modern.CSharp.Templates
4343
```
4444
**Note** that the `dotnet new mcs-orleans-multiservice` template requires **PowerShell** to be installed
4545
46-
2) Type this command to read the documentation for the template parameters:
46+
2) Enter this command to read the documentation for the template parameters:
4747
```
4848
dotnet new mcs-orleans-multiservice -h
4949
```
5050
51-
3) To create a new multiservice with one logical service in it, type e.g.:
51+
3) To create a new multiservice with one logical service in it, enter e.g.:
5252
```
5353
dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop --Multiservice TeamA --Logicalservice Catalog --allow-scripts Yes
5454
```
@@ -82,8 +82,8 @@ Single team solution:
8282
- Debug [eShopTeamA.sln](https://github.com/Applicita/Orleans.Multiservice/tree/main/src/Example/eShopBySingleTeam/TeamA)
8383
8484
Two team solution:
85-
- 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:
86-
`dotnet tool install --global Microsoft.dotnet-openapi --version 7.0.0`
85+
- 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 />
86+
`dotnet tool install --global Microsoft.dotnet-openapi`<br />
8787
On build, this will generate the `CatalogServiceClient` from `CatalogService.json`
8888
- 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)
8989
@@ -102,7 +102,7 @@ These rules ensure that the pattern remains intact:
102102
`*Service -> Contracts`
103103
104104
2) These types are only allowed in specific namespaces:<br />
105-
All API controllers must be in or under `Apis.<service-name>Api`<br />
105+
All API endpoints must be in or under `Apis.<service-name>Api`<br />
106106
All `public` grain contracts must be in or under `Contracts.<service-name>Contract`<br />
107107
108108
3) References between types in these namespaces are **not** allowed:<br />
@@ -112,10 +112,3 @@ These rules ensure that the pattern remains intact:
112112
113113
4) The `public` keyword in `*Service` projects is *only* used on interface member implementations, grain constructors and serializable members in a type.<br />
114114
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.
115-
116-
117-
The Roslyn analyzer will allow rules to be configured in `.editorconfig`
118-
119-
120-
121-
-2.5 KB
Loading
Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,30 @@
1-
Param(
1+
Param(
22
[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")]
33
[string]
44
$Name
55
)
6-
dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop -M . --Logicalservice $Name --allow-scripts Yes
6+
7+
# Function to update the Program.cs file to add a new parameter to the RegisterEndpoints method
8+
function Update-RegisterEndpoints {
9+
$newParameter = "`n typeof(Applicita.eShop.Apis.${Name}Api.${Name}Endpoints)`n"
10+
$apisDirectory = Join-Path -Path $PWD -ChildPath "Apis"
11+
$programFile = Get-ChildItem -Path $apisDirectory -Recurse -Filter "Program.cs" -ErrorAction SilentlyContinue | Select-Object -First 1
12+
13+
if ($programFile -ne $null) {
14+
$programContent = Get-Content -Path $programFile.FullName -Raw
15+
$pattern = "(?s)(app\s*\.RegisterEndpoints\s*\(.+?\))\s*\)"
16+
$modifiedContent = $programContent -replace $pattern, "`$1,$newParameter)"
17+
18+
if ($modifiedContent -ne $programContent) {
19+
Set-Content -Path $programFile.FullName -Value $modifiedContent
20+
Write-Output "Successfully added new parameter to RegisterEndpoints call in $($programFile.FullName):$newParameter"
21+
return
22+
}
23+
}
24+
25+
Write-Warning "Could not automatically add below parameter to the RegisterEndpoints(...) call; please add it manually:$newParameter"
26+
}
27+
28+
dotnet new mcs-orleans-multiservice --RootNamespace Applicita.eShop -M . --Logicalservice $Name --allow-scripts Yes
29+
30+
Update-RegisterEndpoints

src/Example/eShopBySingleTeam/TeamA/Apis/Apis.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
4-
<TargetFramework>net7.0</TargetFramework>
4+
<TargetFramework>net8.0</TargetFramework>
55
<Nullable>enable</Nullable>
66
<ImplicitUsings>enable</ImplicitUsings>
77

@@ -19,9 +19,9 @@
1919
</PropertyGroup>
2020

2121
<ItemGroup>
22-
<PackageReference Include="Microsoft.Orleans.Persistence.Memory" Version="7.1.0" />
23-
<PackageReference Include="Microsoft.Orleans.Sdk" Version="7.1.0" />
24-
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.3" />
22+
<PackageReference Include="Microsoft.Orleans.Persistence.Memory" Version="8.0.0" />
23+
<PackageReference Include="Microsoft.Orleans.Sdk" Version="8.0.0" />
24+
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.3" />
2525
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
2626
</ItemGroup>
2727

src/Example/eShopBySingleTeam/TeamA/Apis/BasketApi/BasketsController.cs renamed to src/Example/eShopBySingleTeam/TeamA/Apis/BasketApi/BasketsEndpoints.cs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,28 @@
22

33
namespace Applicita.eShop.Apis.BasketApi;
44

5-
[Route("[controller]")]
6-
[ApiController]
7-
public class BasketsController : ControllerBase
5+
public class BasketsEndpoints(IClusterClient orleans) : IEndpoints
86
{
97
const string Basket = "{buyerId}";
108

11-
readonly IClusterClient orleans;
12-
13-
public BasketsController(IClusterClient orleans)
14-
=> this.orleans = orleans;
9+
public void Register(IEndpointRouteBuilder routeBuilder)
10+
{
11+
var group = routeBuilder.MapGroup("/baskets").WithTags("Baskets");
12+
_ = group.MapGet (Basket, GetBasket);
13+
_ = group.MapPut ("" , UpdateBasket);
14+
_ = group.MapDelete(Basket, EmptyBasket);
15+
}
1516

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

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

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

3429
IBasketGrain BasketGrain(int buyerId)

src/Example/eShopBySingleTeam/TeamA/Apis/CatalogApi/CatalogController.cs

Lines changed: 0 additions & 48 deletions
This file was deleted.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Applicita.eShop.Contracts.CatalogContract;
2+
3+
namespace Applicita.eShop.Apis.CatalogApi;
4+
5+
public class CatalogEndpoints(IClusterClient orleans) : IEndpoints
6+
{
7+
const string Products = "/products";
8+
const string Product = Products + "/{id}";
9+
10+
readonly ICatalogGrain catalog = orleans.GetGrain<ICatalogGrain>(ICatalogGrain.Key);
11+
12+
public void Register(IEndpointRouteBuilder routeBuilder)
13+
{
14+
var group = routeBuilder.MapGroup("/catalog").WithTags("Catalog");
15+
_ = group.MapPost (Products, CreateProduct);
16+
_ = group.MapGet (Products, GetProducts ).WithName(nameof(GetProducts));
17+
_ = group.MapPut (Products, UpdateProduct);
18+
_ = group.MapDelete(Product , DeleteProduct);
19+
}
20+
21+
/// <response code="201">The new product is created with the returned id</response>
22+
async Task<CreatedAtRoute<int>> CreateProduct(Product product)
23+
{
24+
int id = await catalog.CreateProduct(product);
25+
return CreatedAtRoute(id, nameof(GetProducts), new { id });
26+
}
27+
28+
/// <response code="200">
29+
/// Products for all <paramref name="id"/>'s currently in the catalog are returned;
30+
/// unknown product id's are skipped.
31+
/// If no <paramref name="id"/>'s are specified, all products in the catalog are returned
32+
/// </response>
33+
async Task<Ok<ImmutableArray<Product>>> GetProducts(int[]? id) => Ok(
34+
id?.Length > 0
35+
? await catalog.GetCurrentProducts([.. id])
36+
: await catalog.GetAllProducts()
37+
);
38+
39+
/// <response code="200">The product is updated</response>
40+
/// <response code="404">The product id is not found</response>
41+
public async Task<Results<Ok, NotFound<int>>> UpdateProduct(Product product)
42+
=> await catalog.UpdateProduct(product)
43+
? Ok()
44+
: NotFound(product.Id);
45+
46+
/// <response code="200">The product is deleted</response>
47+
/// <response code="404">The product id is not found</response>
48+
public async Task<Results<Ok, NotFound<int>>> DeleteProduct(int id)
49+
=> await catalog.DeleteProduct(id)
50+
? Ok()
51+
: NotFound(id);
52+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Applicita.eShop.Apis.Foundation;
2+
3+
public interface IEndpoints
4+
{
5+
void Register(IEndpointRouteBuilder routeBuilder);
6+
}
7+
8+
public static class WebApplicationExtensions
9+
{
10+
public static void RegisterEndpoints(this WebApplication app, params Type[] endpointsTypes)
11+
{
12+
foreach (var endpointsType in endpointsTypes)
13+
((IEndpoints)ActivatorUtilities.CreateInstance(app.Services, endpointsType)).Register(app);
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// This file is used by Code Analysis to maintain SuppressMessage
2+
// attributes that are applied to this project.
3+
// Project-level suppressions either have no target or are given
4+
// a specific target and scoped to a namespace, type, member, etc.
5+
6+
using System.Diagnostics.CodeAnalysis;
7+
8+
[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "Not relevant in ASP.NET Core")]
9+
[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")]

0 commit comments

Comments
 (0)