From 563d0150da8668bed06a29ce2c40cbd7b6d8d692 Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 15:46:10 +0100 Subject: [PATCH 1/9] Backport DAT-Acquisition components + upgrade to .NET 10 / Akka 1.5 Bones.Akka: - Add InitializeActor (async init with stash + linear backoff retry) - Add BaseManager (dynamic child routing pattern) - Add DebouncedActor (timer-based message debouncing) - Add RestartException, DisableMessage, RestartMessage singleton - Creator: add covariance (out T), keep IActorContext - DependencyInjector: add dual registration in AddCreator - Upgrade Akka 1.4.27 -> 1.5.14 (min), use Akka.DependencyInjection Bones.Akka.Tests (src/ library): - Add AkkaTestClass, AkkaActorWrapper, ProxyNodeActor test helpers Bones.Converters: - Add ToFloat(), ToHalf() (IEEE 754 float16), StringConverter - Add ToInt()/ToUInt() overloads without startIndex + odd byte sizes (3,5,6,7) Bones.Flow: - Add ResponsibilityChain pattern (3 impls + 4 interfaces) - Add ServiceProviderExtensions for chain factory resolution Bones.Grpc: - Add DI/DependencyInjector, DeadlineInterceptor, StreamDeadlineInterceptor - Add AddGrpcClientWithInterceptors extension, ByteStringExtensions - Move NotFoundInterceptor to Interceptors/ subfolder Bones.Selectors (new project): - JSON dot-path selector + XML XPath selector TFM: net7.0 -> net10.0 (apps/tests), keep netstandard2.0/2.1 (libraries) Dependencies: minimum compatible versions for libraries, latest for tests Co-Authored-By: Claude Opus 4.6 (1M context) --- BACKPORT.md | 180 ++++++++++++++++++ .../Bones.Converters.Benchmarks.csproj | 4 +- .../Demo.Akka.Monitoring.Console.csproj | 10 +- .../Demo.Flow.Console.csproj | 10 +- .../app/Demo.Core/Demo.Core.csproj | 10 +- .../app/Demo.Runtime/Demo.Runtime.csproj | 10 +- .../shared/Demo.Domain/Demo.Domain.csproj | 4 +- .../Bones.Akka.Monitoring.Weaver.Fody.csproj | 2 +- .../Bones.Akka.Monitoring.Weaver.csproj | 6 +- .../Bones.Akka.Monitoring.csproj | 8 +- src/Bones.Akka.Tests/AkkaActorWrapper.cs | 64 +++++++ src/Bones.Akka.Tests/AkkaTestClass.cs | 73 +++++++ src/Bones.Akka.Tests/Bones.Akka.Tests.csproj | 25 +++ src/Bones.Akka.Tests/ProxyNodeActor.cs | 19 ++ src/Bones.Akka/BaseManager.cs | 65 +++++++ src/Bones.Akka/Bones.Akka.csproj | 9 +- src/Bones.Akka/Creator.cs | 4 +- src/Bones.Akka/DI/DependencyInjector.cs | 5 + src/Bones.Akka/DebouncedActor.cs | 36 ++++ src/Bones.Akka/Exceptions/RestartException.cs | 9 + src/Bones.Akka/InitializeActor.cs | 99 ++++++++++ src/Bones.Akka/Messages/DisableMessage.cs | 15 ++ src/Bones.Akka/Messages/RestartMessage.cs | 11 +- src/Bones.AspNetCore/Bones.AspNetCore.csproj | 2 +- src/Bones.Converters/Bones.Converters.csproj | 2 +- src/Bones.Converters/EndianBitConverter.cs | 98 ++++++++-- src/Bones.Converters/StringConverter.cs | 25 +++ src/Bones.Flow/Bones.Flow.csproj | 6 +- .../ResponsibilityChain.cs | 49 +++++ .../ResponsibilityChainFactory.cs | 52 +++++ .../ResponsibilityChainLink.cs | 56 ++++++ .../Extensions/ServiceProviderExtensions.cs | 15 ++ .../Interfaces/IResponsibilityChain.cs | 8 + .../Interfaces/IResponsibilityChainFactory.cs | 9 + .../Interfaces/IResponsibilityChainHandler.cs | 9 + .../Interfaces/IResponsibilityChainLink.cs | 12 ++ src/Bones.Grpc/Bones.Grpc.csproj | 5 +- src/Bones.Grpc/DI/DependencyInjector.cs | 16 ++ ...AddGrpcClientWithInterceptorsExtensions.cs | 20 ++ .../Extensions/ByteStringExtensions.cs | 14 ++ .../Interceptors/DeadlineInterceptor.cs | 38 ++++ .../{ => Interceptors}/NotFoundInterceptor.cs | 4 +- .../Interceptors/StreamDeadlineInterceptor.cs | 30 +++ src/Bones.Monitoring/Bones.Monitoring.csproj | 16 +- src/Bones.Selectors/Bones.Selectors.csproj | 14 ++ src/Bones.Selectors/DI/DependencyInjector.cs | 17 ++ .../Interfaces/IJsonSelector.cs | 10 + .../Interfaces/IXmlSelector.cs | 11 ++ src/Bones.Selectors/SelectorException.cs | 13 ++ src/Bones.Selectors/Selectors/JsonSelector.cs | 100 ++++++++++ src/Bones.Selectors/Selectors/XmlSelector.cs | 30 +++ src/Bones.Tests/Bones.Tests.csproj | 2 +- src/Bones.X509/Bones.X509.csproj | 2 +- src/Bones/Bones.csproj | 6 +- .../Bones.Akka.Tests/Bones.Akka.Tests.csproj | 10 +- .../Bones.Converters.Tests.csproj | 10 +- .../Bones.Flow.Tests/Bones.Flow.Tests.csproj | 10 +- tests/Bones.Tests/Bones.Tests.csproj | 12 +- .../Bones.X509.Tests/Bones.X509.Tests.csproj | 10 +- 59 files changed, 1321 insertions(+), 100 deletions(-) create mode 100644 BACKPORT.md create mode 100644 src/Bones.Akka.Tests/AkkaActorWrapper.cs create mode 100644 src/Bones.Akka.Tests/AkkaTestClass.cs create mode 100644 src/Bones.Akka.Tests/Bones.Akka.Tests.csproj create mode 100644 src/Bones.Akka.Tests/ProxyNodeActor.cs create mode 100644 src/Bones.Akka/BaseManager.cs create mode 100644 src/Bones.Akka/DebouncedActor.cs create mode 100644 src/Bones.Akka/Exceptions/RestartException.cs create mode 100644 src/Bones.Akka/InitializeActor.cs create mode 100644 src/Bones.Akka/Messages/DisableMessage.cs create mode 100644 src/Bones.Converters/StringConverter.cs create mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs create mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs create mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs create mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChain.cs create mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs create mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs create mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs create mode 100644 src/Bones.Grpc/DI/DependencyInjector.cs create mode 100644 src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs create mode 100644 src/Bones.Grpc/Extensions/ByteStringExtensions.cs create mode 100644 src/Bones.Grpc/Interceptors/DeadlineInterceptor.cs rename src/Bones.Grpc/{ => Interceptors}/NotFoundInterceptor.cs (98%) create mode 100644 src/Bones.Grpc/Interceptors/StreamDeadlineInterceptor.cs create mode 100644 src/Bones.Selectors/Bones.Selectors.csproj create mode 100644 src/Bones.Selectors/DI/DependencyInjector.cs create mode 100644 src/Bones.Selectors/Interfaces/IJsonSelector.cs create mode 100644 src/Bones.Selectors/Interfaces/IXmlSelector.cs create mode 100644 src/Bones.Selectors/SelectorException.cs create mode 100644 src/Bones.Selectors/Selectors/JsonSelector.cs create mode 100644 src/Bones.Selectors/Selectors/XmlSelector.cs diff --git a/BACKPORT.md b/BACKPORT.md new file mode 100644 index 0000000..bad209f --- /dev/null +++ b/BACKPORT.md @@ -0,0 +1,180 @@ +# Backport des composants DAT'Acquisition vers Bones NuGet + +## Contexte + +DAT'Acquisition utilise Bones via un git submodule local qui a diverge du repo GitHub/NuGet. +Ce document recense chaque ajout/modification necessaire pour pouvoir supprimer le submodule +et utiliser exclusivement les packages NuGet Bones. + +Aucun composant backporté n'est specifique a DAT'Acquisition — tous sont des utilitaires generiques. + +--- + +## 1. Bones.Akka — InitializeActor + +**Action** : AJOUTER `InitializeActor.cs` + +**Justification** : Pattern standard Akka pour les acteurs necessitant une initialisation asynchrone +avec stash des messages et retry avec backoff lineaire. Utilise par 32 acteurs dans DAT'Acquisition. +Ce pattern est documente dans la litterature Akka et n'a aucun couplage metier. + +--- + +## 2. Bones.Akka — RestartException + +**Action** : AJOUTER `Exceptions/RestartException.cs` + +**Justification** : Exception marker pour les strategies de supervision Akka +("si RestartException → restart l'acteur"). Pattern standard de supervision, 15 fichiers l'utilisent. + +--- + +## 3. Bones.Akka — DisableMessage + +**Action** : AJOUTER `Messages/DisableMessage.cs` (singleton) + +**Justification** : Message de controle lifecycle acteur complementaire a RestartMessage. +Pattern singleton pour eviter les allocations inutiles (best practice Akka pour les messages sans donnees). + +--- + +## 4. Bones.Akka — RestartMessage + +**Action** : MODIFIER `Messages/RestartMessage.cs` — ajouter le pattern singleton + +**Justification** : Best practice Akka — les messages sans donnees doivent etre des singletons +pour eviter les allocations. 7 fichiers utilisent `RestartMessage.Instance`. + +--- + +## 5. Bones.Akka — Creator delegates + +**Action** : MODIFIER `Creator.cs` — ajouter la covariance `out T` sur `Creator` + +**Justification** : La covariance permet d'utiliser un `Creator` la ou un +`Creator` est attendu, ce qui est essentiel pour le pattern `AddCreator()`. +On garde `IActorContext` (plus generique que `IUntypedActorContext`) et `RootCreator` du NuGet actuel. + +--- + +## 6. Bones.Akka — DependencyInjector + +**Action** : MODIFIER `DI/DependencyInjector.cs` — ajouter la double registration dans `AddCreator()` + +**Justification** : Quand on enregistre `AddCreator()`, il faut +pouvoir resoudre aussi bien `Creator` que `Creator` (pour les tests). +Le NuGet actuel n'enregistre que l'interface. + +--- + +## 7. Bones.Converters — ToFloat, ToHalf, ToInt/ToUInt sans startIndex, StringConverter + +**Action** : MODIFIER `EndianBitConverter.cs` + AJOUTER `StringConverter.cs` + +**Justification** : +- `ToFloat(byte[])` : dispatch automatique vers Half/Single/Double selon la taille — essentiel pour les protocoles industriels ou la taille du registre varie +- `ToHalf(byte[], int)` : decodage IEEE 754 half-precision (16 bits) — standard float16 +- `ToInt(byte[])` / `ToUInt(byte[])` sans startIndex : surcharges de commodite + support des tailles impaires (3, 5, 6, 7 octets) — les registres Modbus et protocoles IoT utilisent des tailles non standard +- `StringConverter` : conversion hex string ↔ byte array — utilitaire de base pour le debug protocole + +Tout est du pur calcul binaire sans couplage metier. + +--- + +## 8. Bones.Flow — ResponsibilityChain + Traces + +**Action** : AJOUTER `Core/ResponsibilityChain/` (3 fichiers) + interfaces (4 fichiers) + +**Justification** : Implementation du design pattern Chain of Responsibility, integre au systeme +de pipeline Bones.Flow. Utilise par 15 fichiers (handlers de contexte, publishers gateway). +Le pattern est un GoF classique, l'implementation ne contient aucun couplage metier. + +Les fichiers Traces (ITrace, ITraceFactory, Trace, TraceFactory) restent dans Bones.Flow +car ils existaient avant l'introduction de Bones.Monitoring. On pourra les deprecier plus tard +en faveur de Bones.Monitoring. + +--- + +## 9. Bones.Grpc — Interceptors et Extensions + +**Action** : AJOUTER `DI/DependencyInjector.cs`, `Extensions/AddGrpcClientWithInterceptorsExtensions.cs`, +`Extensions/ByteStringExtensions.cs`, `Interceptors/DeadlineInterceptor.cs`, +`Interceptors/StreamDeadlineInterceptor.cs` + +**Justification** : Infrastructure gRPC standard : +- `DeadlineInterceptor` : ajoute un deadline de 5s aux appels unaires — best practice gRPC pour eviter les appels pendants +- `StreamDeadlineInterceptor` : deadline de 30min pour le streaming serveur +- `AddGrpcClientWithInterceptors()` : extension DI pour enregistrer un client gRPC avec ses interceptors +- `ByteStringExtensions` : conversion `byte[] → ByteString` (commodite Protobuf) +- Le `NotFoundInterceptor` existe deja dans le NuGet + +Tout est de l'infrastructure gRPC generique. + +--- + +## 10. Bones.Selectors — Projet complet + +**Action** : AJOUTER le projet `Bones.Selectors/` (6 fichiers) + +**Justification** : Utilitaire de selection de donnees dans JSON (dot-path) et XML (XPath). +Pattern generique utilise pour l'extraction de valeurs dans des payloads de protocoles. +Aucun couplage metier — c'est l'equivalent d'un JSONPath simplifie. + +--- + +## 11. Bones.Akka.Tests — Helpers de test + +**Action** : AJOUTER `AkkaTestClass.cs`, `AkkaActorWrapper.cs`, `ProxyNodeActor.cs` +dans `src/Bones.Akka.Tests/` (librairie, pas projet de test) + +**Justification** : Infrastructure de test Akka reutilisable : +- `AkkaTestClass` : classe de base xUnit/TestKit avec helpers (`CreateProxyCreator`, `CreateLogger`, `CreateWrappedActor`) +- `AkkaActorWrapper` : wrapper pour verifier l'existence d'acteurs/enfants dans les tests +- `ProxyNodeActor` : acteur proxy qui forward vers un TestProbe — pattern standard pour mocker les enfants + +Le projet de test actuel sur NuGet est un placeholder vide (`Test1() { }`). + +--- + +## 12. Bones.Akka — BaseManager (depuis DAT-Foundation) + +**Action** : AJOUTER `BaseManager.cs` + +**Justification** : Pattern generique de routage de messages vers des acteurs enfants dynamiques. +L'acteur parent recoit un message, extrait un nom d'enfant (abstract `GetChildName`), cree +l'enfant via Creator s'il n'existe pas, puis forward le message. Ce pattern est utilise +dans DAT-Foundation (DevicesManagerActor, etc.) et correspond exactement a ce que font +DevicesManager, PollersManager, etc. dans DAT-Acquisition — sans la formalisation. + +Pattern GoF Mediator/Router, completement generique. + +--- + +## 13. Bones.Akka — DebouncedActor (depuis DAT-Foundation) + +**Action** : AJOUTER `DebouncedActor.cs` + +**Justification** : Classe de base pour les acteurs qui ont besoin de debouncer des messages +(ne traiter que le dernier message recu dans une fenetre de temps). Utilise les Timers Akka. +Pattern generique utile pour les gateways OPC-UA, les watchers de configuration, etc. + +Variante standalone (pas liee a BaseWorker) pour plus de flexibilite. + +--- + +## 14. FakeEntity — Pas de modification + +**Action** : AUCUNE — la version NuGet (generique `FakeEntity`) est superieure a la version locale (non-generique `FakeEntity`). +C'est DAT'Acquisition qui devra migrer vers `FakeEntity`. + +--- + +## Composant NON backporte + +### MicrosoftDependencyResolver.cs + +**Action** : NE PAS backporter + +**Justification** : Implementation custom de `IDependencyResolver` (Akka.DI.Core deprecated). +Le NuGet utilise deja `Akka.DependencyInjection` (API moderne). Le code local est 129 lignes +de plomberie que l'API standard rend inutile. diff --git a/benchmarks/Bones.Converters.Benchmarks/Bones.Converters.Benchmarks.csproj b/benchmarks/Bones.Converters.Benchmarks/Bones.Converters.Benchmarks.csproj index cf75546..3cc7121 100644 --- a/benchmarks/Bones.Converters.Benchmarks/Bones.Converters.Benchmarks.csproj +++ b/benchmarks/Bones.Converters.Benchmarks/Bones.Converters.Benchmarks.csproj @@ -1,6 +1,6 @@ - + @@ -8,7 +8,7 @@ Exe - net7.0 + net10.0 diff --git a/dev/demo.akka.monitoring/Demo.Akka.Monitoring.Console/Demo.Akka.Monitoring.Console.csproj b/dev/demo.akka.monitoring/Demo.Akka.Monitoring.Console/Demo.Akka.Monitoring.Console.csproj index 6321b0f..4acb6cc 100644 --- a/dev/demo.akka.monitoring/Demo.Akka.Monitoring.Console/Demo.Akka.Monitoring.Console.csproj +++ b/dev/demo.akka.monitoring/Demo.Akka.Monitoring.Console/Demo.Akka.Monitoring.Console.csproj @@ -2,14 +2,14 @@ Exe - net7.0 + net10.0 - - - - + + + + diff --git a/dev/demo.flow/Demo.Flow.Console/Demo.Flow.Console.csproj b/dev/demo.flow/Demo.Flow.Console/Demo.Flow.Console.csproj index 3c2bf88..3034a6d 100644 --- a/dev/demo.flow/Demo.Flow.Console/Demo.Flow.Console.csproj +++ b/dev/demo.flow/Demo.Flow.Console/Demo.Flow.Console.csproj @@ -2,14 +2,14 @@ Exe - net7.0 + net10.0 - - - - + + + + diff --git a/dev/demo.weaving/app/Demo.Core/Demo.Core.csproj b/dev/demo.weaving/app/Demo.Core/Demo.Core.csproj index 371f554..52c0ed8 100644 --- a/dev/demo.weaving/app/Demo.Core/Demo.Core.csproj +++ b/dev/demo.weaving/app/Demo.Core/Demo.Core.csproj @@ -1,16 +1,16 @@ - net7.0 + net10.0 enable enable - - - - + + + + diff --git a/dev/demo.weaving/app/Demo.Runtime/Demo.Runtime.csproj b/dev/demo.weaving/app/Demo.Runtime/Demo.Runtime.csproj index 14e981e..66d82e5 100644 --- a/dev/demo.weaving/app/Demo.Runtime/Demo.Runtime.csproj +++ b/dev/demo.weaving/app/Demo.Runtime/Demo.Runtime.csproj @@ -2,16 +2,16 @@ Exe - net7.0 + net10.0 enable enable - - - - + + + + diff --git a/dev/demo.weaving/shared/Demo.Domain/Demo.Domain.csproj b/dev/demo.weaving/shared/Demo.Domain/Demo.Domain.csproj index 3cf6ca1..0db5411 100644 --- a/dev/demo.weaving/shared/Demo.Domain/Demo.Domain.csproj +++ b/dev/demo.weaving/shared/Demo.Domain/Demo.Domain.csproj @@ -1,11 +1,11 @@ - net7.0 + net10.0 - + diff --git a/src/Bones.Akka.Monitoring.Weaver.Fody/Bones.Akka.Monitoring.Weaver.Fody.csproj b/src/Bones.Akka.Monitoring.Weaver.Fody/Bones.Akka.Monitoring.Weaver.Fody.csproj index 393c24c..fd6403d 100644 --- a/src/Bones.Akka.Monitoring.Weaver.Fody/Bones.Akka.Monitoring.Weaver.Fody.csproj +++ b/src/Bones.Akka.Monitoring.Weaver.Fody/Bones.Akka.Monitoring.Weaver.Fody.csproj @@ -7,6 +7,6 @@ - + diff --git a/src/Bones.Akka.Monitoring.Weaver/Bones.Akka.Monitoring.Weaver.csproj b/src/Bones.Akka.Monitoring.Weaver/Bones.Akka.Monitoring.Weaver.csproj index 061b063..87f8ea9 100644 --- a/src/Bones.Akka.Monitoring.Weaver/Bones.Akka.Monitoring.Weaver.csproj +++ b/src/Bones.Akka.Monitoring.Weaver/Bones.Akka.Monitoring.Weaver.csproj @@ -1,13 +1,13 @@ - net7.0 + net10.0 ./nugets $(VERSION) - - + + diff --git a/src/Bones.Akka.Monitoring/Bones.Akka.Monitoring.csproj b/src/Bones.Akka.Monitoring/Bones.Akka.Monitoring.csproj index 718611b..74b5d9d 100644 --- a/src/Bones.Akka.Monitoring/Bones.Akka.Monitoring.csproj +++ b/src/Bones.Akka.Monitoring/Bones.Akka.Monitoring.csproj @@ -1,15 +1,15 @@ - net7.0 + net10.0 $(VERSION) dative-gpi dative-gpi - - - + + + diff --git a/src/Bones.Akka.Tests/AkkaActorWrapper.cs b/src/Bones.Akka.Tests/AkkaActorWrapper.cs new file mode 100644 index 0000000..11a8c7a --- /dev/null +++ b/src/Bones.Akka.Tests/AkkaActorWrapper.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; +using Akka.Actor; + +namespace Bones.Akka.Tests +{ + public class AkkaActorWrapper + { + public IActorRef ActorRef; + + public AkkaActorWrapper(IActorRef actorRef) + { + ActorRef = actorRef; + } + + public async Task Exists(TimeSpan timeout) + { + try + { + var identity = await ActorRef.Ask(new Identify(null), timeout); + return identity.Subject != null; + } + catch + { + return false; + } + } + + public async Task HasChild(string name, TimeSpan timeout) + { + var selection = new ActorSelection(ActorRef, name); + + try + { + var actorRef = await selection.ResolveOne(timeout); + return true; + } + catch + { + return false; + } + } + + public async Task GetChildOrDefault(string name, TimeSpan timeout) + { + var selection = new ActorSelection(ActorRef, name); + + try + { + var actorRef = await selection.ResolveOne(timeout); + return actorRef; + } + catch + { + return default(IActorRef); + } + } + + public void Tell(object message, IActorRef sender) + { + ActorRef.Tell(message, sender); + } + } +} diff --git a/src/Bones.Akka.Tests/AkkaTestClass.cs b/src/Bones.Akka.Tests/AkkaTestClass.cs new file mode 100644 index 0000000..8096725 --- /dev/null +++ b/src/Bones.Akka.Tests/AkkaTestClass.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq.Expressions; +using Akka.Actor; +using Akka.TestKit; +using Akka.TestKit.Xunit; +using Bones.Tests; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Bones.Akka.Tests +{ + public class AkkaTestClass : TestKit + { + protected static readonly TimeSpan STANDARD_TIMEOUT = TimeSpan.FromMilliseconds(150); + + // https://gist.github.com/Havret/78409e91c9adf62aed3392574a3d0446 + protected TestScheduler Scheduler => (TestScheduler)Sys.Scheduler; + protected ITestOutputHelper _output; + + public AkkaTestClass(ITestOutputHelper output) + : base(@"akka.scheduler.implementation = ""Akka.TestKit.TestScheduler, Akka.TestKit""") + { + _output = output; + } + + public AkkaActorWrapper CreateWrappedActor( + Expression> constructor, + string name, + TestProbe parent = null + ) + where T : ActorBase + { + IActorRef actorRef; + + if (parent == null) + actorRef = Sys.ActorOf(Props.Create(constructor), name); + else + actorRef = parent.ChildActorOf(Props.Create(constructor), name); + + return new AkkaActorWrapper(actorRef); + } + + public AkkaActorWrapper CreateWrappedActor( + Expression> constructor, + string name, + IUntypedActorContext context + ) + where T : ActorBase + { + IActorRef actorRef; + + actorRef = context.ActorOf(Props.Create(constructor), name); + + return new AkkaActorWrapper(actorRef); + } + + public ILogger CreateLogger() + { + return new XunitLogger(_output); + } + + public Creator CreateProxyCreator(TestProbe probe) + { + return ctx => + Props.Create(() => new ProxyNodeActor(CreateLogger(), probe)); + } + + public IActorRefProvider CreateActorRefProvider(TestProbe probe) + { + return new ActorRefProvider(ActorSelection(probe.Ref.Path)); + } + } +} diff --git a/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj b/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj new file mode 100644 index 0000000..3be28f7 --- /dev/null +++ b/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj @@ -0,0 +1,25 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bones.Akka.Tests/ProxyNodeActor.cs b/src/Bones.Akka.Tests/ProxyNodeActor.cs new file mode 100644 index 0000000..cd2738a --- /dev/null +++ b/src/Bones.Akka.Tests/ProxyNodeActor.cs @@ -0,0 +1,19 @@ +using Akka.Actor; +using Microsoft.Extensions.Logging; + +namespace Bones.Akka.Tests +{ + public class ProxyNodeActor : ReceiveActor + { + private ILogger _logger; + private IActorRef _probe; + + public ProxyNodeActor(ILogger logger, IActorRef probe) + { + _logger = logger; + _probe = probe; + + ReceiveAny(m => _probe.Tell(m)); + } + } +} \ No newline at end of file diff --git a/src/Bones.Akka/BaseManager.cs b/src/Bones.Akka/BaseManager.cs new file mode 100644 index 0000000..36d5875 --- /dev/null +++ b/src/Bones.Akka/BaseManager.cs @@ -0,0 +1,65 @@ +using System; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Akka.Actor; + +namespace Bones.Akka +{ + public abstract class BaseManager : ReceiveActor + { + protected ILogger Logger { get; } + protected IServiceProvider ServiceProvider { get; } + + public BaseManager(IServiceProvider sp, ILogger logger) + { + Logger = logger; + ServiceProvider = sp; + + Receive(OnMessageReceived); + + Logger.LogInformation("Started at {path}", Self.Path.ToString()); + } + + private void OnMessageReceived(TMessage message) + { + var childName = GetChildName(message); + + if (string.IsNullOrWhiteSpace(childName)) + { + Logger.LogWarning("Ignoring message because no child name was provided in message : {@message}", message); + return; + } + + var child = Context.Child(childName); + + if (child.IsNobody()) + { + using (var scope = ServiceProvider.CreateScope()) + { + var creator = scope.ServiceProvider.GetRequiredService>(); + + Logger.LogInformation("Creating child with name {childName}", childName); + child = Context.ActorOf(creator(Context), childName); + } + } + + child.Forward(message); + } + + protected abstract string GetChildName(TMessage message); + + protected override void PreRestart(Exception reason, object message) + { + Logger.LogError(reason, "Restarting actor at {path}", Self.Path.ToString()); + + base.PreRestart(reason, message); + } + + protected override void PostStop() + { + Logger.LogInformation("Stopped at {path}", Self.Path.ToString()); + } + } +} diff --git a/src/Bones.Akka/Bones.Akka.csproj b/src/Bones.Akka/Bones.Akka.csproj index 577cd2e..8cd7755 100644 --- a/src/Bones.Akka/Bones.Akka.csproj +++ b/src/Bones.Akka/Bones.Akka.csproj @@ -8,10 +8,11 @@ - - - - + + + + + diff --git a/src/Bones.Akka/Creator.cs b/src/Bones.Akka/Creator.cs index 227b35e..c23ec17 100644 --- a/src/Bones.Akka/Creator.cs +++ b/src/Bones.Akka/Creator.cs @@ -4,6 +4,6 @@ namespace Bones.Akka { public delegate Props Creator(Type t, IActorContext context); - public delegate Props Creator(IActorContext context); + public delegate Props Creator(IActorContext context); public delegate Props RootCreator(ActorSystem context); -} \ No newline at end of file +} diff --git a/src/Bones.Akka/DI/DependencyInjector.cs b/src/Bones.Akka/DI/DependencyInjector.cs index 28bca44..44b5a5b 100644 --- a/src/Bones.Akka/DI/DependencyInjector.cs +++ b/src/Bones.Akka/DI/DependencyInjector.cs @@ -76,6 +76,11 @@ public static IServiceCollection AddCreator(this IServiceCol return (context) => DependencyResolver.For(context.System).Props(); }); + services.AddScoped>(sp => + { + return (context) => DependencyResolver.For(context.System).Props(); + }); + return services; } diff --git a/src/Bones.Akka/DebouncedActor.cs b/src/Bones.Akka/DebouncedActor.cs new file mode 100644 index 0000000..335d4dd --- /dev/null +++ b/src/Bones.Akka/DebouncedActor.cs @@ -0,0 +1,36 @@ +using System; + +using Akka.Actor; + +namespace Bones.Akka +{ + public abstract class DebouncedActor : ReceiveActor, IWithTimers + { + const string DEBOUNCER = "DEBOUNCER"; + + public ITimerScheduler Timers { get; set; } + + protected void Debounce(TMessage message, TimeSpan? delay = null) + { + Timers.StartSingleTimer( + DEBOUNCER, + new DebouncedMessage { Content = message }, + delay ?? TimeSpan.FromMilliseconds(500) + ); + } + + protected void Debounce(string key, TMessage message, TimeSpan? delay = null) + { + Timers.StartSingleTimer( + key, + new DebouncedMessage { Content = message }, + delay ?? TimeSpan.FromMilliseconds(500) + ); + } + + protected class DebouncedMessage + { + public T Content { get; set; } + } + } +} diff --git a/src/Bones.Akka/Exceptions/RestartException.cs b/src/Bones.Akka/Exceptions/RestartException.cs new file mode 100644 index 0000000..88f372d --- /dev/null +++ b/src/Bones.Akka/Exceptions/RestartException.cs @@ -0,0 +1,9 @@ +using System; + +namespace Bones.Akka.Exceptions +{ + public class RestartException : Exception + { + + } +} diff --git a/src/Bones.Akka/InitializeActor.cs b/src/Bones.Akka/InitializeActor.cs new file mode 100644 index 0000000..d3dbab9 --- /dev/null +++ b/src/Bones.Akka/InitializeActor.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Akka.Actor; + +namespace Bones.Akka +{ + public abstract class InitializeActor : + ReceiveActor, + IWithUnboundedStash, + IWithTimers + { + protected const string INIT = "init"; + protected const int MAX_WAIT_TIME = 30; + + int ReinitializeCount { get; set; } + + protected IActorRef _self; + protected ILogger _logger; + public IStash Stash { get; set; } + public ITimerScheduler Timers { get; set; } + + public InitializeActor( + ILogger logger + ) + { + _logger = logger; + ReinitializeCount = 0; + Initializing(); + } + + // Initialize + + protected override void PreStart() + { + base.PreStart(); + Self.Tell(INIT); + } + + private void Initializing() + { + OnInitializing(); + + ReceiveAsync(StartInitialization, s => s == INIT); + ReceiveAny(m => Stash.Stash()); + } + + /// + /// Allows us to add custom receive handlers before the basic during initialization. + /// This method can be overridden in derived classes to set up additional message handlers. + /// + protected virtual void OnInitializing() + { + } + + private async Task StartInitialization(string init) + { + _self = Self; + + try + { + await Initialize(); + } + catch (Exception ex) + { + _logger.LogError(ex, "[{path}] An error occurred during initialization. Retrying...", Self.Path); + Reinitialize(); + return; + } + } + + protected abstract Task Initialize(); + + // Reinitialize + + public void Reinitialize() + { + _logger.LogInformation("Retrying in {time} seconds", Math.Min(MAX_WAIT_TIME, 2 * ReinitializeCount)); + + Timers.StartSingleTimer( + INIT, + INIT, + TimeSpan.FromSeconds(Math.Min(MAX_WAIT_TIME, 2 * ReinitializeCount)) + ); + + ReinitializeCount++; + } + + // Lifecycle + + protected override void PostStop() + { + base.PostStop(); + Timers.CancelAll(); + } + } +} diff --git a/src/Bones.Akka/Messages/DisableMessage.cs b/src/Bones.Akka/Messages/DisableMessage.cs new file mode 100644 index 0000000..217652b --- /dev/null +++ b/src/Bones.Akka/Messages/DisableMessage.cs @@ -0,0 +1,15 @@ +namespace Bones.Akka.Messages +{ + public class DisableMessage + { + private static readonly DisableMessage _instance = new DisableMessage(); + public static DisableMessage Instance + { + get { return _instance; } + } + + static DisableMessage() { } + + private DisableMessage() { } + } +} diff --git a/src/Bones.Akka/Messages/RestartMessage.cs b/src/Bones.Akka/Messages/RestartMessage.cs index d1dd45b..b9d7d24 100644 --- a/src/Bones.Akka/Messages/RestartMessage.cs +++ b/src/Bones.Akka/Messages/RestartMessage.cs @@ -2,5 +2,14 @@ namespace Bones.Akka.Messages { public class RestartMessage { + private static readonly RestartMessage _instance = new RestartMessage(); + public static RestartMessage Instance + { + get { return _instance; } + } + + static RestartMessage() { } + + private RestartMessage() { } } -} \ No newline at end of file +} diff --git a/src/Bones.AspNetCore/Bones.AspNetCore.csproj b/src/Bones.AspNetCore/Bones.AspNetCore.csproj index b87c7f0..090af61 100644 --- a/src/Bones.AspNetCore/Bones.AspNetCore.csproj +++ b/src/Bones.AspNetCore/Bones.AspNetCore.csproj @@ -5,7 +5,7 @@ - net7.0 + net10.0 $(VERSION) dative-gpi dative-gpi diff --git a/src/Bones.Converters/Bones.Converters.csproj b/src/Bones.Converters/Bones.Converters.csproj index 87b1448..714b26a 100644 --- a/src/Bones.Converters/Bones.Converters.csproj +++ b/src/Bones.Converters/Bones.Converters.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Bones.Converters/EndianBitConverter.cs b/src/Bones.Converters/EndianBitConverter.cs index fa3f551..5ea2458 100644 --- a/src/Bones.Converters/EndianBitConverter.cs +++ b/src/Bones.Converters/EndianBitConverter.cs @@ -165,18 +165,33 @@ public float ToSingle(byte[] value, int startIndex) } + public virtual long ToInt(byte[] value) + { + return ToInt(value, 0); + } + public virtual long ToInt(byte[] value, int startIndex) { - switch (value.Length) + switch (value.Length - startIndex) { case 0: return 0; + case 1: + return (short)(ToInt16(value, startIndex) >> 8); case 2: - return ToInt16(value, 0); + return ToInt16(value, startIndex); + case 3: + return ToInt32(value, startIndex) >> 8; case 4: - return ToInt32(value, 0); + return ToInt32(value, startIndex); + case 5: + return ToInt64(value, startIndex) >> 24; + case 6: + return ToInt64(value, startIndex) >> 16; + case 7: + return ToInt64(value, startIndex) >> 8; case 8: - return ToInt64(value, 0); + return ToInt64(value, startIndex); default: throw new NotImplementedException($"Data can't be converted to int - b64: {Convert.ToBase64String(value)} \tLength: {value.Length}"); } @@ -207,29 +222,84 @@ public virtual long ToInt(byte[] value, int startIndex) public abstract long ToInt64(byte[] value, int startIndex); - public virtual ulong ToUInt(byte[] value, int startIndex) + public virtual double ToFloat(byte[] value) { switch (value.Length) + { + case 0: + return 0; + case 2: + return ToHalf(value, 0); + case 4: + return ToSingle(value, 0); + case 8: + return ToDouble(value, 0); + default: + throw new NotImplementedException($"Data can't be converted to float - b64: {Convert.ToBase64String(value)} \tLength: {value.Length}"); + } + } + + public float ToHalf(byte[] value, int startIndex) + { + var intVal = ToUInt16(value, startIndex); + int sign = (intVal >> 15) & 0x0001; + int exponent = (intVal >> 10) & 0x001F; + int mantissa = intVal & 0x03FF; + + if (exponent == 0) + { + if (mantissa == 0) + return sign == 0 ? 0f : -0f; + // Subnormal + while ((mantissa & 0x0400) == 0) + { + mantissa <<= 1; + exponent--; + } + exponent++; + mantissa &= ~0x0400; + } + else if (exponent == 31) + { + return mantissa == 0 + ? (sign == 0 ? float.PositiveInfinity : float.NegativeInfinity) + : float.NaN; + } + + exponent = exponent + (127 - 15); + mantissa = mantissa << 13; + + int floatBits = (sign << 31) | (exponent << 23) | mantissa; + return new Int32SingleUnion(floatBits).AsSingle; + } + + public virtual ulong ToUInt(byte[] value) + { + return ToUInt(value, 0); + } + + public virtual ulong ToUInt(byte[] value, int startIndex) + { + switch (value.Length - startIndex) { case 0: return 0; case 1: - return (UInt16)(ToUInt16(value, 0) >> 8); + return (UInt16)(ToUInt16(value, startIndex) >> 8); case 2: - return ToUInt16(value, 0); + return ToUInt16(value, startIndex); case 3: - return ToUInt32(value, 0) >> 8; + return ToUInt32(value, startIndex) >> 8; case 4: - return ToUInt32(value, 0); - + return ToUInt32(value, startIndex); case 5: - return ToUInt64(value, 0) >> 24; + return ToUInt64(value, startIndex) >> 24; case 6: - return ToUInt64(value, 0) >> 16; + return ToUInt64(value, startIndex) >> 16; case 7: - return ToUInt64(value, 0) >> 8; + return ToUInt64(value, startIndex) >> 8; case 8: - return ToUInt64(value, 0); + return ToUInt64(value, startIndex); default: throw new NotImplementedException($"Data can't be convert to uint - b64: {Convert.ToBase64String(value)}"); } diff --git a/src/Bones.Converters/StringConverter.cs b/src/Bones.Converters/StringConverter.cs new file mode 100644 index 0000000..e9bc0a6 --- /dev/null +++ b/src/Bones.Converters/StringConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Text; + +namespace Bones.Converters +{ + public static class StringConverter + { + public static byte[] ToBytes(string hex) + { + int NumberChars = hex.Length; + byte[] bytes = new byte[NumberChars / 2]; + for (int i = 0; i < NumberChars; i += 2) + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + return bytes; + } + + public static string FromBytes(byte[] ba) + { + StringBuilder hex = new StringBuilder(ba.Length * 2); + foreach (byte b in ba) + hex.AppendFormat("{0:x2}", b); + return hex.ToString(); + } + } +} diff --git a/src/Bones.Flow/Bones.Flow.csproj b/src/Bones.Flow/Bones.Flow.csproj index 28cf400..3c42491 100644 --- a/src/Bones.Flow/Bones.Flow.csproj +++ b/src/Bones.Flow/Bones.Flow.csproj @@ -1,15 +1,15 @@ - net7.0 + net10.0 $(VERSION) dative-gpi dative-gpi - - + + diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs new file mode 100644 index 0000000..ed27c14 --- /dev/null +++ b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.Contracts; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Bones.Flow.Core +{ + internal class ResponsibilityChain : + IResponsibilityChain + where TRequest : IRequest + { + IResponsibilityChainLink _firstLink; + ILogger> _logger; + + public ResponsibilityChain( + ILogger> logger + ) + { + _logger = logger; + } + + public void SetFirst( + IResponsibilityChainLink firstLink + ) + { + Contract.Assert(firstLink != null); + _firstLink = firstLink; + } + + public async Task HandleAsync(TRequest request, CancellationToken cancellationToken = default, bool commit = true) + { + try + { + if (_firstLink != null) + await _firstLink.HandleAsync(request, cancellationToken); + } + catch (System.Exception ex) + { + _logger.LogError(ex, "An error occured executing ResponsibilityChain<{TRequest}>", typeof(TRequest).Name); + throw; + } + } + + public override string ToString() + { + return "Chain = " + _firstLink.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs new file mode 100644 index 0000000..4a709e1 --- /dev/null +++ b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Bones.Flow.Core +{ + internal class ResponsibilityChainFactory : IResponsibilityChainFactory where TRequest : IRequest + { + List> _handlers; + IServiceProvider _provider; + + public ResponsibilityChainFactory(IServiceProvider provider) + { + _provider = provider; + _handlers = new List>(); + } + + + public IResponsibilityChainFactory Add() where THandler : IResponsibilityChainHandler + { + var handler = _provider.GetRequiredService(); + return Add(handler); + } + + public IResponsibilityChainFactory Add(THandler handler) where THandler : IResponsibilityChainHandler + { + _handlers.Add(handler); + return this; + } + + public IResponsibilityChain Build() + { + var chain = _provider.GetRequiredService>(); + + if (_handlers.Count() > 0) + { + var previous = _provider.GetResponsibilityChainLink(_handlers[0]); + chain.SetFirst(previous); + + for (var i = 1; i < _handlers.Count(); i++) + { + var wrapper = _provider.GetResponsibilityChainLink(_handlers[i]); + previous.SetNext(wrapper); + previous = wrapper; + } + } + + return chain; + } + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs new file mode 100644 index 0000000..1a8ed5d --- /dev/null +++ b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs @@ -0,0 +1,56 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Bones.Flow +{ + internal class ResponsibilityChainLink : IResponsibilityChainLink + { + private ILogger> _logger; + private IResponsibilityChainLink _nextWrapper; + private IResponsibilityChainHandler _handler; + + public ResponsibilityChainLink( + ILogger> logger + ) + { + _logger = logger; + } + + public void SetNext( + IResponsibilityChainLink nextWrapper + ) + { + _nextWrapper = nextWrapper; + } + + public void SetHandler( + IResponsibilityChainHandler handler + ) + { + _handler = handler; + } + + public async Task HandleAsync(TRequest request, CancellationToken token) + { + var handled = false; + + try + { + if (_handler != null) handled = await _handler.HandleAsync(request); + } + catch + { + throw; + } + + if (handled || _nextWrapper == null) return; + else await _nextWrapper.HandleAsync(request, token); + } + + public override string ToString() + { + return (_handler == null ? "null" : _handler.ToString()) + " -> " + (_nextWrapper == null ? "null" : _nextWrapper.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs b/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs index 3b5c2c7..6c8be3f 100644 --- a/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs +++ b/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs @@ -14,5 +14,20 @@ public static IPipelineFactory GetPipelineFactory>(); } + + public static IResponsibilityChainFactory GetResponsibilityChainFactory(this IServiceProvider provider) where TRequest : IRequest + { + return provider.GetRequiredService>(); + } + + internal static IResponsibilityChainLink GetResponsibilityChainLink( + this IServiceProvider provider, + IResponsibilityChainHandler handler + ) where TRequest : IRequest + { + var wrapper = provider.GetRequiredService>(); + wrapper.SetHandler(handler); + return wrapper; + } } } \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChain.cs b/src/Bones.Flow/Interfaces/IResponsibilityChain.cs new file mode 100644 index 0000000..a13509b --- /dev/null +++ b/src/Bones.Flow/Interfaces/IResponsibilityChain.cs @@ -0,0 +1,8 @@ +namespace Bones.Flow +{ + public interface IResponsibilityChain : ICommandHandler where TRequest : IRequest + { + void SetFirst(IResponsibilityChainLink firstWrapper); + string ToString(); + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs new file mode 100644 index 0000000..4748c82 --- /dev/null +++ b/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs @@ -0,0 +1,9 @@ +namespace Bones.Flow +{ + public interface IResponsibilityChainFactory where TRequest : IRequest + { + IResponsibilityChainFactory Add() where THandler : IResponsibilityChainHandler; + IResponsibilityChainFactory Add(THandler handler) where THandler : IResponsibilityChainHandler; + IResponsibilityChain Build(); + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs new file mode 100644 index 0000000..7444167 --- /dev/null +++ b/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Bones.Flow +{ + public interface IResponsibilityChainHandler + { + Task HandleAsync(TRequest request); + } +} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs new file mode 100644 index 0000000..8c69ed1 --- /dev/null +++ b/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs @@ -0,0 +1,12 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Bones.Flow +{ + public interface IResponsibilityChainLink + { + void SetNext(IResponsibilityChainLink nextWrapper); + void SetHandler(IResponsibilityChainHandler handler); + Task HandleAsync(TRequest request, CancellationToken token); + } +} \ No newline at end of file diff --git a/src/Bones.Grpc/Bones.Grpc.csproj b/src/Bones.Grpc/Bones.Grpc.csproj index 11da5b2..6a62cf8 100644 --- a/src/Bones.Grpc/Bones.Grpc.csproj +++ b/src/Bones.Grpc/Bones.Grpc.csproj @@ -8,7 +8,10 @@ - + + + + diff --git a/src/Bones.Grpc/DI/DependencyInjector.cs b/src/Bones.Grpc/DI/DependencyInjector.cs new file mode 100644 index 0000000..1613504 --- /dev/null +++ b/src/Bones.Grpc/DI/DependencyInjector.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Bones.Grpc.DI +{ + public static class DependencyInjector + { + public static IServiceCollection AddBonesGrpc(this IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs b/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs new file mode 100644 index 0000000..795f1ea --- /dev/null +++ b/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs @@ -0,0 +1,20 @@ +using System; +using Grpc.Net.ClientFactory; +using Microsoft.Extensions.DependencyInjection; + +namespace Bones.Grpc +{ + public static class AddGrpcClientWithInterceptorsExtensions + { + public static IHttpClientBuilder AddGrpcClientWithInterceptors( + this IServiceCollection services, + Action configureClient + ) where TClient : class + { + return services.AddGrpcClient(configureClient) + .AddInterceptor(); + // .AddInterceptor() + // .AddInterceptor(); + } + } +} \ No newline at end of file diff --git a/src/Bones.Grpc/Extensions/ByteStringExtensions.cs b/src/Bones.Grpc/Extensions/ByteStringExtensions.cs new file mode 100644 index 0000000..b312066 --- /dev/null +++ b/src/Bones.Grpc/Extensions/ByteStringExtensions.cs @@ -0,0 +1,14 @@ +using Google.Protobuf; + +namespace Bones.Grpc.Extensions { + public static class ByteStringExtensions + { + public static ByteString ToByteString(this byte[] byteArray) + { + if (byteArray == null || byteArray.Length == 0) + return ByteString.Empty; + + return ByteString.CopyFrom(byteArray); + } + } +} \ No newline at end of file diff --git a/src/Bones.Grpc/Interceptors/DeadlineInterceptor.cs b/src/Bones.Grpc/Interceptors/DeadlineInterceptor.cs new file mode 100644 index 0000000..c5d12b4 --- /dev/null +++ b/src/Bones.Grpc/Interceptors/DeadlineInterceptor.cs @@ -0,0 +1,38 @@ + +using System; +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace Bones.Grpc +{ + public class DeadlineInterceptor : Interceptor + { + private const double DEADLINE_IN_SECONDS = 5; + + public override AsyncUnaryCall AsyncUnaryCall(TRequest request, ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) + { + var newContext = context; + var deadline = DateTime.UtcNow.AddSeconds(DEADLINE_IN_SECONDS); + + if ( + context.Options.CancellationToken == default && + (!context.Options.Deadline.HasValue || + context.Options.Deadline.Value >= deadline) + ) + { + var newOptions = context.Options.WithDeadline(deadline); + newContext = new ClientInterceptorContext(context.Method, context.Host, newOptions); + } + + var call = continuation(request, newContext); + + return new AsyncUnaryCall( + call.ResponseAsync, + call.ResponseHeadersAsync, + call.GetStatus, + call.GetTrailers, + call.Dispose + ); + } + } +} \ No newline at end of file diff --git a/src/Bones.Grpc/NotFoundInterceptor.cs b/src/Bones.Grpc/Interceptors/NotFoundInterceptor.cs similarity index 98% rename from src/Bones.Grpc/NotFoundInterceptor.cs rename to src/Bones.Grpc/Interceptors/NotFoundInterceptor.cs index 4640372..634a710 100644 --- a/src/Bones.Grpc/NotFoundInterceptor.cs +++ b/src/Bones.Grpc/Interceptors/NotFoundInterceptor.cs @@ -19,7 +19,7 @@ public override AsyncUnaryCall AsyncUnaryCall(TR } catch (AggregateException ex) { - if(ex.InnerException is RpcException rpcException + if(ex.InnerException is RpcException rpcException && rpcException.StatusCode == StatusCode.NotFound){ return null; } @@ -27,7 +27,7 @@ public override AsyncUnaryCall AsyncUnaryCall(TR } } ); - + return new AsyncUnaryCall(responseWithErrorhandling, response.ResponseHeadersAsync, response.GetStatus, response.GetTrailers, response.Dispose); } } diff --git a/src/Bones.Grpc/Interceptors/StreamDeadlineInterceptor.cs b/src/Bones.Grpc/Interceptors/StreamDeadlineInterceptor.cs new file mode 100644 index 0000000..ab1fcc6 --- /dev/null +++ b/src/Bones.Grpc/Interceptors/StreamDeadlineInterceptor.cs @@ -0,0 +1,30 @@ +using System; +using Grpc.Core; +using Grpc.Core.Interceptors; + +namespace Bones.Grpc +{ + // https://stackoverflow.com/questions/63633332/grpc-what-are-the-best-practices-for-long-running-streaming + public class StreamDeadlineInterceptor : Interceptor + { + private const int STREAM_DEADLINE_IN_SECONDS = 1800; // 60s * 30min + + public override AsyncServerStreamingCall AsyncServerStreamingCall(TRequest request, ClientInterceptorContext context, AsyncServerStreamingCallContinuation continuation) + { + var newContext = context; + var deadline = DateTime.UtcNow.AddSeconds(STREAM_DEADLINE_IN_SECONDS); + + if ( + (!context.Options.Deadline.HasValue || + context.Options.Deadline.Value >= deadline) + ) + { + var newOptions = context.Options.WithDeadline(deadline); + newContext = new ClientInterceptorContext(context.Method, context.Host, newOptions); + } + + var response = base.AsyncServerStreamingCall(request, newContext, continuation); + return response; + } + } +} \ No newline at end of file diff --git a/src/Bones.Monitoring/Bones.Monitoring.csproj b/src/Bones.Monitoring/Bones.Monitoring.csproj index 9d1b85e..b641501 100644 --- a/src/Bones.Monitoring/Bones.Monitoring.csproj +++ b/src/Bones.Monitoring/Bones.Monitoring.csproj @@ -1,21 +1,21 @@ - net7.0 + net10.0 $(VERSION) dative-gpi dative-gpi - - - - + + + + - - + + - + diff --git a/src/Bones.Selectors/Bones.Selectors.csproj b/src/Bones.Selectors/Bones.Selectors.csproj new file mode 100644 index 0000000..8b57ffa --- /dev/null +++ b/src/Bones.Selectors/Bones.Selectors.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + + + + + + + + + + diff --git a/src/Bones.Selectors/DI/DependencyInjector.cs b/src/Bones.Selectors/DI/DependencyInjector.cs new file mode 100644 index 0000000..bd61ec5 --- /dev/null +++ b/src/Bones.Selectors/DI/DependencyInjector.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.DependencyInjection; +using Bones.Selectors.Interfaces; + +namespace Bones.Selectors.DI +{ + public static class DependencyInjector + { + + public static IServiceCollection AddSelectors(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Bones.Selectors/Interfaces/IJsonSelector.cs b/src/Bones.Selectors/Interfaces/IJsonSelector.cs new file mode 100644 index 0000000..715bc34 --- /dev/null +++ b/src/Bones.Selectors/Interfaces/IJsonSelector.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace Bones.Selectors.Interfaces +{ + public interface IJsonSelector + { + IEnumerable Select(JsonElement element, string selector); + } +} \ No newline at end of file diff --git a/src/Bones.Selectors/Interfaces/IXmlSelector.cs b/src/Bones.Selectors/Interfaces/IXmlSelector.cs new file mode 100644 index 0000000..5ce4e6b --- /dev/null +++ b/src/Bones.Selectors/Interfaces/IXmlSelector.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Xml; + +namespace Bones.Selectors.Interfaces +{ + public interface IXmlSelector + { + IEnumerable Select(XmlDocument document, string selector); + } +} \ No newline at end of file diff --git a/src/Bones.Selectors/SelectorException.cs b/src/Bones.Selectors/SelectorException.cs new file mode 100644 index 0000000..51672ff --- /dev/null +++ b/src/Bones.Selectors/SelectorException.cs @@ -0,0 +1,13 @@ +namespace Bones.Selectors +{ + [System.Serializable] + public class SelectorException : System.Exception + { + public SelectorException() { } + public SelectorException(string message) : base(message) { } + public SelectorException(string message, System.Exception inner) : base(message, inner) { } + protected SelectorException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } +} \ No newline at end of file diff --git a/src/Bones.Selectors/Selectors/JsonSelector.cs b/src/Bones.Selectors/Selectors/JsonSelector.cs new file mode 100644 index 0000000..093b30a --- /dev/null +++ b/src/Bones.Selectors/Selectors/JsonSelector.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Bones.Selectors.Interfaces; + +namespace Bones.Selectors +{ + public class JsonSelector : IJsonSelector + { + private static readonly char JSON_QUERY_SEPARATOR = '.'; + private static readonly string JSON_QUERY_ENUMERATOR = "*"; + + public JsonSelector() { } + + public IEnumerable Select(JsonElement rootElement, string selector) + { + string[] paths = selector.Split(new char[] { JSON_QUERY_SEPARATOR }, StringSplitOptions.RemoveEmptyEntries); + IEnumerable selectedElements = BrowseJson(new[] { rootElement }, paths); + return GetRawElements(selectedElements); + } + + + + + + private IEnumerable BrowseJson(IEnumerable currentElements, IEnumerable query) + { + if (!query.Any()) return currentElements; + + List childrenOfCurrent = new List(); + string currentPath = query.First(); + + foreach (JsonElement current in currentElements) + { + try + { + if (IsIndexSelector(currentPath, out var index)) AddOneElement(childrenOfCurrent, current, index); + else if (IsArraySelector(currentPath)) AddAllElements(childrenOfCurrent, current); + else AddPropertyElement(childrenOfCurrent, current, currentPath); + } + catch (Exception e) + { + throw new SelectorException($"An error occurred while browsing ! Current step: {currentPath}", e); + } + } + + return BrowseJson(childrenOfCurrent, query.Skip(1)); + } + + private IEnumerable GetRawElements(IEnumerable selectedElements) + { + List results = new List(); + + foreach (JsonElement el in selectedElements) + { + try + { + results.Add(el.ToString()); + } + catch (Exception e) + { + throw new SelectorException($"An error occurred while getting raw text !", e); + } + } + + return results; + } + + // Helper Methods + + private bool IsIndexSelector(string selector, out int index) + { + return int.TryParse(selector, out index); + } + + private void AddOneElement(List elements, JsonElement currentElement, int index) + { + elements.Add(currentElement[index]); + } + + private bool IsArraySelector(string selector) + { + return JSON_QUERY_ENUMERATOR.Equals(selector); + } + + private void AddAllElements(List elements, JsonElement currentElement) + { + foreach (var arrayElt in currentElement.EnumerateArray()) + { + elements.Add(arrayElt); + } + } + + private void AddPropertyElement(List elements, JsonElement currentElement, string propertyName) + { + elements.Add(currentElement.GetProperty(propertyName)); + } + } +} \ No newline at end of file diff --git a/src/Bones.Selectors/Selectors/XmlSelector.cs b/src/Bones.Selectors/Selectors/XmlSelector.cs new file mode 100644 index 0000000..0743610 --- /dev/null +++ b/src/Bones.Selectors/Selectors/XmlSelector.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Xml; +using Bones.Selectors.Interfaces; + +namespace Bones.Selectors +{ + public class XmlSelector : IXmlSelector + { + public XmlSelector() { } + + // https://docs.microsoft.com/en-us/dotnet/api/system.xml.xmldocument?view=net-6.0 + public IEnumerable Select(XmlDocument document, string selector) + { + List results = new List(); + + // Namespaces cause problems, but this would be complicated to resolve, ignored for now + // XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable); + XmlNodeList nodeList = document.DocumentElement.SelectNodes(selector); + + foreach (XmlNode node in nodeList) + { + results.Add(node.InnerXml); + } + + return results; + } + } +} \ No newline at end of file diff --git a/src/Bones.Tests/Bones.Tests.csproj b/src/Bones.Tests/Bones.Tests.csproj index 9db2736..8edd5ee 100644 --- a/src/Bones.Tests/Bones.Tests.csproj +++ b/src/Bones.Tests/Bones.Tests.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Bones.X509/Bones.X509.csproj b/src/Bones.X509/Bones.X509.csproj index f5128ca..92a7017 100644 --- a/src/Bones.X509/Bones.X509.csproj +++ b/src/Bones.X509/Bones.X509.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 $(VERSION) dative-gpi dative-gpi diff --git a/src/Bones/Bones.csproj b/src/Bones/Bones.csproj index b71c67c..fd7f07d 100644 --- a/src/Bones/Bones.csproj +++ b/src/Bones/Bones.csproj @@ -8,10 +8,10 @@ - - + + - + diff --git a/tests/Bones.Akka.Tests/Bones.Akka.Tests.csproj b/tests/Bones.Akka.Tests/Bones.Akka.Tests.csproj index 2736616..cd908db 100644 --- a/tests/Bones.Akka.Tests/Bones.Akka.Tests.csproj +++ b/tests/Bones.Akka.Tests/Bones.Akka.Tests.csproj @@ -1,16 +1,16 @@ - net7.0 + net10.0 false - - - - + + + + diff --git a/tests/Bones.Converters.Tests/Bones.Converters.Tests.csproj b/tests/Bones.Converters.Tests/Bones.Converters.Tests.csproj index b99636f..4f404eb 100644 --- a/tests/Bones.Converters.Tests/Bones.Converters.Tests.csproj +++ b/tests/Bones.Converters.Tests/Bones.Converters.Tests.csproj @@ -1,19 +1,19 @@ - net7.0 + net10.0 false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Bones.Flow.Tests/Bones.Flow.Tests.csproj b/tests/Bones.Flow.Tests/Bones.Flow.Tests.csproj index cc9b82e..898fada 100644 --- a/tests/Bones.Flow.Tests/Bones.Flow.Tests.csproj +++ b/tests/Bones.Flow.Tests/Bones.Flow.Tests.csproj @@ -1,16 +1,16 @@ - net7.0 + net10.0 false - - - - + + + + diff --git a/tests/Bones.Tests/Bones.Tests.csproj b/tests/Bones.Tests/Bones.Tests.csproj index b5017f4..1778b4d 100644 --- a/tests/Bones.Tests/Bones.Tests.csproj +++ b/tests/Bones.Tests/Bones.Tests.csproj @@ -1,17 +1,17 @@ - net7.0 + net10.0 false - - - - - + + + + + diff --git a/tests/Bones.X509.Tests/Bones.X509.Tests.csproj b/tests/Bones.X509.Tests/Bones.X509.Tests.csproj index c492b37..1510a05 100644 --- a/tests/Bones.X509.Tests/Bones.X509.Tests.csproj +++ b/tests/Bones.X509.Tests/Bones.X509.Tests.csproj @@ -1,19 +1,19 @@ - net7.0 + net10.0 false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 3228aa5d00c29093b4bb2fc11d5b31670037fde1 Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 16:47:39 +0100 Subject: [PATCH 2/9] Add Bones.Selectors and Bones.Akka.Tests to NuGet publish matrix - Add Version/Authors/Company metadata to new project csproj files - Add Bones.Selectors and Bones.Akka.Tests to publish-to-nuget.yml matrix Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/publish-to-nuget.yml | 2 +- src/Bones.Akka.Tests/Bones.Akka.Tests.csproj | 3 +++ src/Bones.Selectors/Bones.Selectors.csproj | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-to-nuget.yml b/.github/workflows/publish-to-nuget.yml index 557316d..1a02ba3 100644 --- a/.github/workflows/publish-to-nuget.yml +++ b/.github/workflows/publish-to-nuget.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - folder: [Bones, Bones.Akka, Bones.AspNetCore, Bones.Converters, Bones.Flow, Bones.Grpc, Bones.Tests, Bones.X509, Bones.Akka.Monitoring, Bones.Monitoring] + folder: [Bones, Bones.Akka, Bones.Akka.Tests, Bones.AspNetCore, Bones.Converters, Bones.Flow, Bones.Grpc, Bones.Selectors, Bones.Tests, Bones.X509, Bones.Akka.Monitoring, Bones.Monitoring] steps: # Checking out repository - name: Checkout 🛎️ diff --git a/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj b/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj index 3be28f7..1489d7a 100644 --- a/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj +++ b/src/Bones.Akka.Tests/Bones.Akka.Tests.csproj @@ -2,6 +2,9 @@ netstandard2.0 + $(VERSION) + dative-gpi + dative-gpi diff --git a/src/Bones.Selectors/Bones.Selectors.csproj b/src/Bones.Selectors/Bones.Selectors.csproj index 8b57ffa..a20e45a 100644 --- a/src/Bones.Selectors/Bones.Selectors.csproj +++ b/src/Bones.Selectors/Bones.Selectors.csproj @@ -2,6 +2,9 @@ netstandard2.0 + $(VERSION) + dative-gpi + dative-gpi From a896fa7e7448f6933d5c7ca5575c0fb64176c35a Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 17:27:28 +0100 Subject: [PATCH 3/9] Address PR review: fix bugs, improve robustness, clean up dead code - InitializeActor: auto-call Stash.UnstashAll() after successful Initialize() - EndianBitConverter: fix ToInt/ToUInt crash for odd byte lengths (1/3/5/6/7) by padding to correct width with endianness-aware sign extension - EndianBitConverter: add startIndex validation in ToInt/ToUInt - EndianBitConverter: rename ToFloat to ToFloatingPoint, keep ToFloat as [Obsolete] - StringConverter: add null/empty/odd-length validation on hex input - XmlSelector: add null guards for DocumentElement and SelectNodes - Remove AddGrpcClientWithInterceptorsExtensions (each app defines its own) - Remove unused using directives and Microsoft.Extensions.Configuration reference Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Bones.Akka/InitializeActor.cs | 3 +- src/Bones.Converters/EndianBitConverter.cs | 71 +++++++++++++++---- src/Bones.Converters/StringConverter.cs | 11 ++- ...AddGrpcClientWithInterceptorsExtensions.cs | 20 ------ src/Bones.Selectors/Bones.Selectors.csproj | 1 - .../Interfaces/IXmlSelector.cs | 1 - src/Bones.Selectors/Selectors/XmlSelector.cs | 14 ++-- 7 files changed, 75 insertions(+), 46 deletions(-) delete mode 100644 src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs diff --git a/src/Bones.Akka/InitializeActor.cs b/src/Bones.Akka/InitializeActor.cs index d3dbab9..7ab48bc 100644 --- a/src/Bones.Akka/InitializeActor.cs +++ b/src/Bones.Akka/InitializeActor.cs @@ -62,12 +62,13 @@ private async Task StartInitialization(string init) try { await Initialize(); + ReinitializeCount = 0; + Stash.UnstashAll(); } catch (Exception ex) { _logger.LogError(ex, "[{path}] An error occurred during initialization. Retrying...", Self.Path); Reinitialize(); - return; } } diff --git a/src/Bones.Converters/EndianBitConverter.cs b/src/Bones.Converters/EndianBitConverter.cs index 5ea2458..4f98060 100644 --- a/src/Bones.Converters/EndianBitConverter.cs +++ b/src/Bones.Converters/EndianBitConverter.cs @@ -172,24 +172,29 @@ public virtual long ToInt(byte[] value) public virtual long ToInt(byte[] value, int startIndex) { - switch (value.Length - startIndex) + if (startIndex < 0 || startIndex > value.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + + var remaining = value.Length - startIndex; + + switch (remaining) { case 0: return 0; case 1: - return (short)(ToInt16(value, startIndex) >> 8); + return (sbyte)value[startIndex]; case 2: return ToInt16(value, startIndex); case 3: - return ToInt32(value, startIndex) >> 8; + return ToInt32(PadBytes(value, startIndex, 3, 4, signExtend: true), 0); case 4: return ToInt32(value, startIndex); case 5: - return ToInt64(value, startIndex) >> 24; + return ToInt64(PadBytes(value, startIndex, 5, 8, signExtend: true), 0); case 6: - return ToInt64(value, startIndex) >> 16; + return ToInt64(PadBytes(value, startIndex, 6, 8, signExtend: true), 0); case 7: - return ToInt64(value, startIndex) >> 8; + return ToInt64(PadBytes(value, startIndex, 7, 8, signExtend: true), 0); case 8: return ToInt64(value, startIndex); default: @@ -197,6 +202,36 @@ public virtual long ToInt(byte[] value, int startIndex) } } + private byte[] PadBytes(byte[] value, int startIndex, int count, int targetWidth, bool signExtend) + { + var padded = new byte[targetWidth]; + var padding = targetWidth - count; + + byte fill = 0x00; + if (signExtend) + { + var msb = Endianness == Endianness.BigEndian + ? value[startIndex] + : value[startIndex + count - 1]; + fill = (byte)((msb & 0x80) != 0 ? 0xFF : 0x00); + } + + if (Endianness == Endianness.BigEndian) + { + for (var i = 0; i < padding; i++) + padded[i] = fill; + Array.Copy(value, startIndex, padded, padding, count); + } + else + { + Array.Copy(value, startIndex, padded, 0, count); + for (var i = count; i < targetWidth; i++) + padded[i] = fill; + } + + return padded; + } + /// /// Returns a 16-bit signed integer converted from two bytes at a specified position in a byte array. /// @@ -222,7 +257,7 @@ public virtual long ToInt(byte[] value, int startIndex) public abstract long ToInt64(byte[] value, int startIndex); - public virtual double ToFloat(byte[] value) + public virtual double ToFloatingPoint(byte[] value) { switch (value.Length) { @@ -235,10 +270,13 @@ public virtual double ToFloat(byte[] value) case 8: return ToDouble(value, 0); default: - throw new NotImplementedException($"Data can't be converted to float - b64: {Convert.ToBase64String(value)} \tLength: {value.Length}"); + throw new NotImplementedException($"Data can't be converted to floating point - b64: {Convert.ToBase64String(value)} \tLength: {value.Length}"); } } + [Obsolete("Use ToFloatingPoint instead.")] + public virtual double ToFloat(byte[] value) => ToFloatingPoint(value); + public float ToHalf(byte[] value, int startIndex) { var intVal = ToUInt16(value, startIndex); @@ -280,24 +318,29 @@ public virtual ulong ToUInt(byte[] value) public virtual ulong ToUInt(byte[] value, int startIndex) { - switch (value.Length - startIndex) + if (startIndex < 0 || startIndex > value.Length) + throw new ArgumentOutOfRangeException(nameof(startIndex)); + + var remaining = value.Length - startIndex; + + switch (remaining) { case 0: return 0; case 1: - return (UInt16)(ToUInt16(value, startIndex) >> 8); + return value[startIndex]; case 2: return ToUInt16(value, startIndex); case 3: - return ToUInt32(value, startIndex) >> 8; + return ToUInt32(PadBytes(value, startIndex, 3, 4, signExtend: false), 0); case 4: return ToUInt32(value, startIndex); case 5: - return ToUInt64(value, startIndex) >> 24; + return ToUInt64(PadBytes(value, startIndex, 5, 8, signExtend: false), 0); case 6: - return ToUInt64(value, startIndex) >> 16; + return ToUInt64(PadBytes(value, startIndex, 6, 8, signExtend: false), 0); case 7: - return ToUInt64(value, startIndex) >> 8; + return ToUInt64(PadBytes(value, startIndex, 7, 8, signExtend: false), 0); case 8: return ToUInt64(value, startIndex); default: diff --git a/src/Bones.Converters/StringConverter.cs b/src/Bones.Converters/StringConverter.cs index e9bc0a6..f8e9fbe 100644 --- a/src/Bones.Converters/StringConverter.cs +++ b/src/Bones.Converters/StringConverter.cs @@ -7,9 +7,14 @@ public static class StringConverter { public static byte[] ToBytes(string hex) { - int NumberChars = hex.Length; - byte[] bytes = new byte[NumberChars / 2]; - for (int i = 0; i < NumberChars; i += 2) + if (string.IsNullOrEmpty(hex)) + return Array.Empty(); + + if (hex.Length % 2 != 0) + throw new ArgumentException("Hex string must have an even number of characters.", nameof(hex)); + + var bytes = new byte[hex.Length / 2]; + for (var i = 0; i < hex.Length; i += 2) bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); return bytes; } diff --git a/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs b/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs deleted file mode 100644 index 795f1ea..0000000 --- a/src/Bones.Grpc/Extensions/AddGrpcClientWithInterceptorsExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using Grpc.Net.ClientFactory; -using Microsoft.Extensions.DependencyInjection; - -namespace Bones.Grpc -{ - public static class AddGrpcClientWithInterceptorsExtensions - { - public static IHttpClientBuilder AddGrpcClientWithInterceptors( - this IServiceCollection services, - Action configureClient - ) where TClient : class - { - return services.AddGrpcClient(configureClient) - .AddInterceptor(); - // .AddInterceptor() - // .AddInterceptor(); - } - } -} \ No newline at end of file diff --git a/src/Bones.Selectors/Bones.Selectors.csproj b/src/Bones.Selectors/Bones.Selectors.csproj index a20e45a..96c87dd 100644 --- a/src/Bones.Selectors/Bones.Selectors.csproj +++ b/src/Bones.Selectors/Bones.Selectors.csproj @@ -8,7 +8,6 @@ - diff --git a/src/Bones.Selectors/Interfaces/IXmlSelector.cs b/src/Bones.Selectors/Interfaces/IXmlSelector.cs index 5ce4e6b..7e597d1 100644 --- a/src/Bones.Selectors/Interfaces/IXmlSelector.cs +++ b/src/Bones.Selectors/Interfaces/IXmlSelector.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Text.Json; using System.Xml; namespace Bones.Selectors.Interfaces diff --git a/src/Bones.Selectors/Selectors/XmlSelector.cs b/src/Bones.Selectors/Selectors/XmlSelector.cs index 0743610..2d295fc 100644 --- a/src/Bones.Selectors/Selectors/XmlSelector.cs +++ b/src/Bones.Selectors/Selectors/XmlSelector.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.Text.Json; using System.Xml; using Bones.Selectors.Interfaces; @@ -13,11 +11,15 @@ public XmlSelector() { } // https://docs.microsoft.com/en-us/dotnet/api/system.xml.xmldocument?view=net-6.0 public IEnumerable Select(XmlDocument document, string selector) { - List results = new List(); + var results = new List(); - // Namespaces cause problems, but this would be complicated to resolve, ignored for now - // XmlNamespaceManager nsmgr = new XmlNamespaceManager(document.NameTable); - XmlNodeList nodeList = document.DocumentElement.SelectNodes(selector); + var root = document.DocumentElement; + if (root == null) + return results; + + var nodeList = root.SelectNodes(selector); + if (nodeList == null) + return results; foreach (XmlNode node in nodeList) { From 0684d35c2c23d0489ba12273dd3c844a2a23e89a Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 18:53:17 +0100 Subject: [PATCH 4/9] Rewrite ChainOfResponsibility as composable Pipeline middleware - Replace old ResponsibilityChain (7 files) with new ChainOfResponsibility - Two variants: (no result) and (with result) - Implements IMiddleware so it composes with Pipeline via .With(cor) / .Add(cor) - Also implements ICommandHandler for standalone usage - Simple list iteration instead of linked-list Link abstraction - Factory pattern with fluent .Add() builder, DI-integrated - Add instance overloads .With(instance) and .Add(instance) on IPipelineFactory - Make monitoring optional: TraceFactory tolerates missing IOptionsMonitor - 7 new tests covering standalone and pipeline-composed scenarios Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ChainOfResponsibility.cs | 67 +++++ .../ChainOfResponsibilityFactory.cs | 40 +++ .../ResultChainOfResponsibility.cs | 71 ++++++ .../ResultChainOfResponsibilityFactory.cs | 40 +++ .../RequestResultPipelineFactory.cs | 8 + .../ResponsibilityChain.cs | 49 ---- .../ResponsibilityChainFactory.cs | 52 ---- .../ResponsibilityChainLink.cs | 56 ----- src/Bones.Flow/DI/DependencyInjector.cs | 7 +- .../Extensions/ServiceProviderExtensions.cs | 15 +- .../Interfaces/IChainOfResponsibility.cs | 16 ++ .../IChainOfResponsibilityFactory.cs | 20 ++ .../IChainOfResponsibilityHandler.cs | 15 ++ src/Bones.Flow/Interfaces/IPipelineFactory.cs | 6 +- .../Interfaces/IResponsibilityChain.cs | 8 - .../Interfaces/IResponsibilityChainFactory.cs | 9 - .../Interfaces/IResponsibilityChainHandler.cs | 9 - .../Interfaces/IResponsibilityChainLink.cs | 12 - src/Bones.Monitoring/Traces/TraceFactory.cs | 5 +- .../TestChainOfResponsibilityFactory.cs | 234 ++++++++++++++++++ 20 files changed, 531 insertions(+), 208 deletions(-) create mode 100644 src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs create mode 100644 src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibilityFactory.cs create mode 100644 src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs create mode 100644 src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibilityFactory.cs delete mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs delete mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs delete mode 100644 src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs create mode 100644 src/Bones.Flow/Interfaces/IChainOfResponsibility.cs create mode 100644 src/Bones.Flow/Interfaces/IChainOfResponsibilityFactory.cs create mode 100644 src/Bones.Flow/Interfaces/IChainOfResponsibilityHandler.cs delete mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChain.cs delete mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs delete mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs delete mode 100644 src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs create mode 100644 tests/Bones.Flow.Tests/TestChainOfResponsibilityFactory.cs diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs new file mode 100644 index 0000000..de10dae --- /dev/null +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Bones.Flow.Core +{ + internal class ChainOfResponsibility : IChainOfResponsibility + where TRequest : IRequest + { + private List> _handlers; + private ILogger> _logger; + + public ChainOfResponsibility( + ILogger> logger + ) + { + _logger = logger; + } + + public void Configure(List> handlers) + { + _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); + } + + async Task ICommandHandler.HandleAsync(TRequest request, CancellationToken cancellationToken, bool commit) + { + foreach (var handler in _handlers) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + if (await handler.HandleAsync(request, cancellationToken)) + return; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in ChainOfResponsibility handler {handler}", handler.GetType().Name); + throw; + } + } + } + + async Task IMiddleware.HandleAsync(TRequest request, Func next, CancellationToken cancellationToken) + { + foreach (var handler in _handlers) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + if (await handler.HandleAsync(request, cancellationToken)) + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in ChainOfResponsibility handler {handler}", handler.GetType().Name); + throw; + } + } + + await next(); + } + } +} diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibilityFactory.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibilityFactory.cs new file mode 100644 index 0000000..66b3d6b --- /dev/null +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibilityFactory.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace Bones.Flow.Core +{ + internal class ChainOfResponsibilityFactory : IChainOfResponsibilityFactory + where TRequest : IRequest + { + private IServiceProvider _provider; + private List> _handlers; + + public ChainOfResponsibilityFactory(IServiceProvider provider) + { + _provider = provider; + _handlers = new List>(); + } + + public IChainOfResponsibilityFactory Add() + where THandler : IChainOfResponsibilityHandler + { + var handler = _provider.GetRequiredService(); + return Add(handler); + } + + public IChainOfResponsibilityFactory Add(THandler handler) + where THandler : IChainOfResponsibilityHandler + { + _handlers.Add(handler); + return this; + } + + public IChainOfResponsibility Build() + { + var chain = _provider.GetRequiredService>(); + chain.Configure(_handlers); + return chain; + } + } +} diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs new file mode 100644 index 0000000..2152933 --- /dev/null +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Bones.Flow.Core +{ + internal class ResultChainOfResponsibility : IChainOfResponsibility + where TRequest : IRequest + { + private List> _handlers; + private ILogger> _logger; + + public ResultChainOfResponsibility( + ILogger> logger + ) + { + _logger = logger; + } + + public void Configure(List> handlers) + { + _handlers = handlers ?? throw new ArgumentNullException(nameof(handlers)); + } + + async Task ICommandHandler.HandleAsync(TRequest request, CancellationToken cancellationToken, bool commit) + { + foreach (var handler in _handlers) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var (handled, result) = await handler.HandleAsync(request, cancellationToken); + if (handled) + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in ChainOfResponsibility handler {handler}", handler.GetType().Name); + throw; + } + } + + return default; + } + + async Task IMiddleware.HandleAsync(TRequest request, Func> next, CancellationToken cancellationToken) + { + foreach (var handler in _handlers) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var (handled, result) = await handler.HandleAsync(request, cancellationToken); + if (handled) + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred in ChainOfResponsibility handler {handler}", handler.GetType().Name); + throw; + } + } + + return await next(); + } + } +} diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibilityFactory.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibilityFactory.cs new file mode 100644 index 0000000..703f59e --- /dev/null +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibilityFactory.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; + +namespace Bones.Flow.Core +{ + internal class ResultChainOfResponsibilityFactory : IChainOfResponsibilityFactory + where TRequest : IRequest + { + private IServiceProvider _provider; + private List> _handlers; + + public ResultChainOfResponsibilityFactory(IServiceProvider provider) + { + _provider = provider; + _handlers = new List>(); + } + + public IChainOfResponsibilityFactory Add() + where THandler : IChainOfResponsibilityHandler + { + var handler = _provider.GetRequiredService(); + return Add(handler); + } + + public IChainOfResponsibilityFactory Add(THandler handler) + where THandler : IChainOfResponsibilityHandler + { + _handlers.Add(handler); + return this; + } + + public IChainOfResponsibility Build() + { + var chain = _provider.GetRequiredService>(); + chain.Configure(_handlers); + return chain; + } + } +} diff --git a/src/Bones.Flow/Core/RequestResultPipeline/RequestResultPipelineFactory.cs b/src/Bones.Flow/Core/RequestResultPipeline/RequestResultPipelineFactory.cs index 79966ef..e7fcc03 100644 --- a/src/Bones.Flow/Core/RequestResultPipeline/RequestResultPipelineFactory.cs +++ b/src/Bones.Flow/Core/RequestResultPipeline/RequestResultPipelineFactory.cs @@ -51,7 +51,11 @@ public IPipeline Build() public IBuildablePipelineFactory Add() where TMiddleware : IMiddleware { var middleware = _provider.GetRequiredService(); + return Add(middleware); + } + public IBuildablePipelineFactory Add(TMiddleware middleware) where TMiddleware : IMiddleware + { _requestResultMiddlewares.Add(middleware); _middlewareTypes.Add(MiddlewareType.RequestResultMiddleware); @@ -61,7 +65,11 @@ public IBuildablePipelineFactory Add() where TMi public IPipelineFactory With() where TMiddleware : IMiddleware { var middleware = _provider.GetRequiredService(); + return With(middleware); + } + public IPipelineFactory With(TMiddleware middleware) where TMiddleware : IMiddleware + { _requestMiddlewares.Add(middleware); _middlewareTypes.Add(MiddlewareType.RequestMiddleware); diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs deleted file mode 100644 index ed27c14..0000000 --- a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChain.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Diagnostics.Contracts; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Bones.Flow.Core -{ - internal class ResponsibilityChain : - IResponsibilityChain - where TRequest : IRequest - { - IResponsibilityChainLink _firstLink; - ILogger> _logger; - - public ResponsibilityChain( - ILogger> logger - ) - { - _logger = logger; - } - - public void SetFirst( - IResponsibilityChainLink firstLink - ) - { - Contract.Assert(firstLink != null); - _firstLink = firstLink; - } - - public async Task HandleAsync(TRequest request, CancellationToken cancellationToken = default, bool commit = true) - { - try - { - if (_firstLink != null) - await _firstLink.HandleAsync(request, cancellationToken); - } - catch (System.Exception ex) - { - _logger.LogError(ex, "An error occured executing ResponsibilityChain<{TRequest}>", typeof(TRequest).Name); - throw; - } - } - - public override string ToString() - { - return "Chain = " + _firstLink.ToString(); - } - } -} \ No newline at end of file diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs deleted file mode 100644 index 4a709e1..0000000 --- a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainFactory.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; - -namespace Bones.Flow.Core -{ - internal class ResponsibilityChainFactory : IResponsibilityChainFactory where TRequest : IRequest - { - List> _handlers; - IServiceProvider _provider; - - public ResponsibilityChainFactory(IServiceProvider provider) - { - _provider = provider; - _handlers = new List>(); - } - - - public IResponsibilityChainFactory Add() where THandler : IResponsibilityChainHandler - { - var handler = _provider.GetRequiredService(); - return Add(handler); - } - - public IResponsibilityChainFactory Add(THandler handler) where THandler : IResponsibilityChainHandler - { - _handlers.Add(handler); - return this; - } - - public IResponsibilityChain Build() - { - var chain = _provider.GetRequiredService>(); - - if (_handlers.Count() > 0) - { - var previous = _provider.GetResponsibilityChainLink(_handlers[0]); - chain.SetFirst(previous); - - for (var i = 1; i < _handlers.Count(); i++) - { - var wrapper = _provider.GetResponsibilityChainLink(_handlers[i]); - previous.SetNext(wrapper); - previous = wrapper; - } - } - - return chain; - } - } -} \ No newline at end of file diff --git a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs b/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs deleted file mode 100644 index 1a8ed5d..0000000 --- a/src/Bones.Flow/Core/ResponsibilityChain/ResponsibilityChainLink.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Bones.Flow -{ - internal class ResponsibilityChainLink : IResponsibilityChainLink - { - private ILogger> _logger; - private IResponsibilityChainLink _nextWrapper; - private IResponsibilityChainHandler _handler; - - public ResponsibilityChainLink( - ILogger> logger - ) - { - _logger = logger; - } - - public void SetNext( - IResponsibilityChainLink nextWrapper - ) - { - _nextWrapper = nextWrapper; - } - - public void SetHandler( - IResponsibilityChainHandler handler - ) - { - _handler = handler; - } - - public async Task HandleAsync(TRequest request, CancellationToken token) - { - var handled = false; - - try - { - if (_handler != null) handled = await _handler.HandleAsync(request); - } - catch - { - throw; - } - - if (handled || _nextWrapper == null) return; - else await _nextWrapper.HandleAsync(request, token); - } - - public override string ToString() - { - return (_handler == null ? "null" : _handler.ToString()) + " -> " + (_nextWrapper == null ? "null" : _nextWrapper.ToString()); - } - } -} \ No newline at end of file diff --git a/src/Bones.Flow/DI/DependencyInjector.cs b/src/Bones.Flow/DI/DependencyInjector.cs index 6c38fa4..7fecacc 100644 --- a/src/Bones.Flow/DI/DependencyInjector.cs +++ b/src/Bones.Flow/DI/DependencyInjector.cs @@ -19,8 +19,13 @@ public static IServiceCollection AddFlow(this IServiceCollection services, Actio services.AddScoped(typeof(IPipelineFactory<,>), typeof(RequestResultPipelineFactory<,>)); services.AddScoped(typeof(IPipeline<,>), typeof(RequestResultPipeline<,>)); + services.AddScoped(typeof(IChainOfResponsibilityFactory<>), typeof(ChainOfResponsibilityFactory<>)); + services.AddScoped(typeof(IChainOfResponsibility<>), typeof(ChainOfResponsibility<>)); + services.AddScoped(typeof(IChainOfResponsibilityFactory<,>), typeof(ResultChainOfResponsibilityFactory<,>)); + services.AddScoped(typeof(IChainOfResponsibility<,>), typeof(ResultChainOfResponsibility<,>)); + services.AddMonitoring(BONES_FLOW_INSTRUMENTATION, configureMonitoringOptions); - + return services; } diff --git a/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs b/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs index 6c8be3f..57da647 100644 --- a/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs +++ b/src/Bones.Flow/Extensions/ServiceProviderExtensions.cs @@ -15,19 +15,16 @@ public static IPipelineFactory GetPipelineFactory>(); } - public static IResponsibilityChainFactory GetResponsibilityChainFactory(this IServiceProvider provider) where TRequest : IRequest + public static IChainOfResponsibilityFactory GetChainOfResponsibilityFactory(this IServiceProvider provider) + where TRequest : IRequest { - return provider.GetRequiredService>(); + return provider.GetRequiredService>(); } - internal static IResponsibilityChainLink GetResponsibilityChainLink( - this IServiceProvider provider, - IResponsibilityChainHandler handler - ) where TRequest : IRequest + public static IChainOfResponsibilityFactory GetChainOfResponsibilityFactory(this IServiceProvider provider) + where TRequest : IRequest { - var wrapper = provider.GetRequiredService>(); - wrapper.SetHandler(handler); - return wrapper; + return provider.GetRequiredService>(); } } } \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IChainOfResponsibility.cs b/src/Bones.Flow/Interfaces/IChainOfResponsibility.cs new file mode 100644 index 0000000..b1b3b6e --- /dev/null +++ b/src/Bones.Flow/Interfaces/IChainOfResponsibility.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Bones.Flow +{ + public interface IChainOfResponsibility : ICommandHandler, IMiddleware + where TRequest : IRequest + { + void Configure(List> handlers); + } + + public interface IChainOfResponsibility : ICommandHandler, IMiddleware + where TRequest : IRequest + { + void Configure(List> handlers); + } +} diff --git a/src/Bones.Flow/Interfaces/IChainOfResponsibilityFactory.cs b/src/Bones.Flow/Interfaces/IChainOfResponsibilityFactory.cs new file mode 100644 index 0000000..9d91138 --- /dev/null +++ b/src/Bones.Flow/Interfaces/IChainOfResponsibilityFactory.cs @@ -0,0 +1,20 @@ +namespace Bones.Flow +{ + public interface IChainOfResponsibilityFactory where TRequest : IRequest + { + IChainOfResponsibilityFactory Add() + where THandler : IChainOfResponsibilityHandler; + IChainOfResponsibilityFactory Add(THandler handler) + where THandler : IChainOfResponsibilityHandler; + IChainOfResponsibility Build(); + } + + public interface IChainOfResponsibilityFactory where TRequest : IRequest + { + IChainOfResponsibilityFactory Add() + where THandler : IChainOfResponsibilityHandler; + IChainOfResponsibilityFactory Add(THandler handler) + where THandler : IChainOfResponsibilityHandler; + IChainOfResponsibility Build(); + } +} diff --git a/src/Bones.Flow/Interfaces/IChainOfResponsibilityHandler.cs b/src/Bones.Flow/Interfaces/IChainOfResponsibilityHandler.cs new file mode 100644 index 0000000..f58681a --- /dev/null +++ b/src/Bones.Flow/Interfaces/IChainOfResponsibilityHandler.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Bones.Flow +{ + public interface IChainOfResponsibilityHandler where TRequest : IRequest + { + Task HandleAsync(TRequest request, CancellationToken cancellationToken); + } + + public interface IChainOfResponsibilityHandler where TRequest : IRequest + { + Task<(bool handled, TResult result)> HandleAsync(TRequest request, CancellationToken cancellationToken); + } +} diff --git a/src/Bones.Flow/Interfaces/IPipelineFactory.cs b/src/Bones.Flow/Interfaces/IPipelineFactory.cs index f537892..e9edf5b 100644 --- a/src/Bones.Flow/Interfaces/IPipelineFactory.cs +++ b/src/Bones.Flow/Interfaces/IPipelineFactory.cs @@ -24,11 +24,15 @@ IPipelineFactory OnFailure(THandler handler) public interface IPipelineFactory where TRequest: IRequest { - IBuildablePipelineFactory Add() + IBuildablePipelineFactory Add() + where TMiddleware : IMiddleware; + IBuildablePipelineFactory Add(TMiddleware middleware) where TMiddleware : IMiddleware; IPipelineFactory With() where TMiddleware : IMiddleware; + IPipelineFactory With(TMiddleware middleware) + where TMiddleware : IMiddleware; } public interface IBuildablePipelineFactory : IPipelineFactory diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChain.cs b/src/Bones.Flow/Interfaces/IResponsibilityChain.cs deleted file mode 100644 index a13509b..0000000 --- a/src/Bones.Flow/Interfaces/IResponsibilityChain.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Bones.Flow -{ - public interface IResponsibilityChain : ICommandHandler where TRequest : IRequest - { - void SetFirst(IResponsibilityChainLink firstWrapper); - string ToString(); - } -} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs deleted file mode 100644 index 4748c82..0000000 --- a/src/Bones.Flow/Interfaces/IResponsibilityChainFactory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bones.Flow -{ - public interface IResponsibilityChainFactory where TRequest : IRequest - { - IResponsibilityChainFactory Add() where THandler : IResponsibilityChainHandler; - IResponsibilityChainFactory Add(THandler handler) where THandler : IResponsibilityChainHandler; - IResponsibilityChain Build(); - } -} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs deleted file mode 100644 index 7444167..0000000 --- a/src/Bones.Flow/Interfaces/IResponsibilityChainHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading.Tasks; - -namespace Bones.Flow -{ - public interface IResponsibilityChainHandler - { - Task HandleAsync(TRequest request); - } -} \ No newline at end of file diff --git a/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs b/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs deleted file mode 100644 index 8c69ed1..0000000 --- a/src/Bones.Flow/Interfaces/IResponsibilityChainLink.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Bones.Flow -{ - public interface IResponsibilityChainLink - { - void SetNext(IResponsibilityChainLink nextWrapper); - void SetHandler(IResponsibilityChainHandler handler); - Task HandleAsync(TRequest request, CancellationToken token); - } -} \ No newline at end of file diff --git a/src/Bones.Monitoring/Traces/TraceFactory.cs b/src/Bones.Monitoring/Traces/TraceFactory.cs index 9158c46..1b9ab04 100644 --- a/src/Bones.Monitoring/Traces/TraceFactory.cs +++ b/src/Bones.Monitoring/Traces/TraceFactory.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,10 +11,10 @@ public class TraceFactory : ITraceFactory private ILogger _logger; private IOptionsMonitor _options; - public TraceFactory(ILogger logger, IOptionsMonitor options) + public TraceFactory(ILogger logger, IServiceProvider provider) { _logger = logger; - _options = options; + _options = provider.GetService>(); } public ITrace Create(ActivitySource source, string name, ITrace parent = null) diff --git a/tests/Bones.Flow.Tests/TestChainOfResponsibilityFactory.cs b/tests/Bones.Flow.Tests/TestChainOfResponsibilityFactory.cs new file mode 100644 index 0000000..50d5192 --- /dev/null +++ b/tests/Bones.Flow.Tests/TestChainOfResponsibilityFactory.cs @@ -0,0 +1,234 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.DependencyInjection; + +using Xunit; +using Xunit.Abstractions; + +using Bones.Tests.DI; + +namespace Bones.Flow.Tests +{ + public class TestChainOfResponsibilityFactory + { + private ServiceProvider _provider; + + public TestChainOfResponsibilityFactory(ITestOutputHelper output) + { + IServiceCollection serviceCollection = new ServiceCollection(); + + serviceCollection.AddFlow(); + serviceCollection.AddDebug(output); + + serviceCollection + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(); + + _provider = serviceCollection.BuildServiceProvider(); + } + + // --- CoR standalone (sans résultat) --- + + [Fact] + public async Task ChainOfResponsibility_FirstMatchHandles() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Add() + .Build(); + + var cmd = new NumberCommand { Value = 4 }; + await cor.HandleAsync(cmd); + + Assert.Equal(nameof(EvenHandler), cmd.HandledBy); + } + + [Fact] + public async Task ChainOfResponsibility_SecondMatchHandles() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Add() + .Build(); + + var cmd = new NumberCommand { Value = 3 }; + await cor.HandleAsync(cmd); + + Assert.Equal(nameof(OddHandler), cmd.HandledBy); + } + + [Fact] + public async Task ChainOfResponsibility_NoMatch_DoesNotThrow() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Build(); + + var cmd = new NumberCommand { Value = 3 }; + await cor.HandleAsync(cmd); + + Assert.Null(cmd.HandledBy); + } + + // --- CoR standalone (avec résultat) --- + + [Fact] + public async Task ChainOfResponsibility_WithResult_FirstMatchReturns() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Add() + .Build(); + + var result = await cor.HandleAsync(new NumberQuery { Value = 4 }); + + Assert.Equal("even:4", result); + } + + [Fact] + public async Task ChainOfResponsibility_WithResult_NoMatch_ReturnsDefault() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Build(); + + var result = await cor.HandleAsync(new NumberQuery { Value = 3 }); + + Assert.Null(result); + } + + // --- CoR composée dans une Pipeline --- + + [Fact] + public async Task ChainOfResponsibility_ComposedInPipeline() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Add() + .Build(); + + var pipeline = sp.GetPipelineFactory() + .Add(cor) + .Add() + .Build(); + + var cmd = new NumberCommand { Value = 4 }; + await pipeline.HandleAsync(cmd); + + Assert.Equal(nameof(EvenHandler), cmd.HandledBy); + Assert.True(cmd.FallbackReached); + } + + [Fact] + public async Task ChainOfResponsibility_ResultComposedInPipeline() + { + using var scope = _provider.CreateScope(); + var sp = scope.ServiceProvider; + + var cor = sp.GetChainOfResponsibilityFactory() + .Add() + .Add() + .Build(); + + IQueryHandler pipeline = sp.GetPipelineFactory() + .Add(cor) + .Build(); + + var result = await pipeline.HandleAsync(new NumberQuery { Value = 7 }); + + Assert.Equal("odd:7", result); + } + + // --- Fixtures --- + + public class NumberCommand : IRequest + { + public int Value { get; set; } + public string HandledBy { get; set; } + public bool FallbackReached { get; set; } + } + + public class NumberQuery : IRequest + { + public int Value { get; set; } + } + + public class EvenHandler : IChainOfResponsibilityHandler + { + public Task HandleAsync(NumberCommand request, CancellationToken ct) + { + if (request.Value % 2 != 0) + return Task.FromResult(false); + + request.HandledBy = nameof(EvenHandler); + return Task.FromResult(true); + } + } + + public class OddHandler : IChainOfResponsibilityHandler + { + public Task HandleAsync(NumberCommand request, CancellationToken ct) + { + if (request.Value % 2 == 0) + return Task.FromResult(false); + + request.HandledBy = nameof(OddHandler); + return Task.FromResult(true); + } + } + + public class FallbackHandler : IMiddleware + { + public async Task HandleAsync(NumberCommand request, Func next, CancellationToken ct) + { + request.FallbackReached = true; + await next(); + } + } + + public class EvenResultHandler : IChainOfResponsibilityHandler + { + public Task<(bool handled, string result)> HandleAsync(NumberQuery request, CancellationToken ct) + { + if (request.Value % 2 != 0) + return Task.FromResult((false, (string)null)); + + return Task.FromResult((true, $"even:{request.Value}")); + } + } + + public class OddResultHandler : IChainOfResponsibilityHandler + { + public Task<(bool handled, string result)> HandleAsync(NumberQuery request, CancellationToken ct) + { + if (request.Value % 2 == 0) + return Task.FromResult((false, (string)null)); + + return Task.FromResult((true, $"odd:{request.Value}")); + } + } + } +} From 9ba80f595414b175af0cb7dfcc1adffd8781bb1a Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 19:39:56 +0100 Subject: [PATCH 5/9] Fix null safety: TraceFactory.Enrich guard, CoR handlers default init Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Core/ChainOfResponsibility/ChainOfResponsibility.cs | 2 +- .../Core/ChainOfResponsibility/ResultChainOfResponsibility.cs | 2 +- src/Bones.Monitoring/Traces/TraceFactory.cs | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs index de10dae..cc12fa8 100644 --- a/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ChainOfResponsibility.cs @@ -9,7 +9,7 @@ namespace Bones.Flow.Core internal class ChainOfResponsibility : IChainOfResponsibility where TRequest : IRequest { - private List> _handlers; + private List> _handlers = new List>(); private ILogger> _logger; public ChainOfResponsibility( diff --git a/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs index 2152933..fca0687 100644 --- a/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs +++ b/src/Bones.Flow/Core/ChainOfResponsibility/ResultChainOfResponsibility.cs @@ -9,7 +9,7 @@ namespace Bones.Flow.Core internal class ResultChainOfResponsibility : IChainOfResponsibility where TRequest : IRequest { - private List> _handlers; + private List> _handlers = new List>(); private ILogger> _logger; public ResultChainOfResponsibility( diff --git a/src/Bones.Monitoring/Traces/TraceFactory.cs b/src/Bones.Monitoring/Traces/TraceFactory.cs index 1b9ab04..7a41887 100644 --- a/src/Bones.Monitoring/Traces/TraceFactory.cs +++ b/src/Bones.Monitoring/Traces/TraceFactory.cs @@ -54,6 +54,9 @@ public ITrace Create(ActivitySource source, string name, ITrace parent = null) public ITrace Enrich(ITrace trace, object param, string optionsName) { + if (_options == null) + return trace; + var option = _options.Get(optionsName); if(option != null && option.SpanEnricher != null) option.SpanEnricher(trace, param); return trace; From a31400d646b6efcd45119fc347288e6d2901cb64 Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 19:49:07 +0100 Subject: [PATCH 6/9] Update CI for .NET 10, fix BACKPORT.md - Bump Docker test image from sdk:7.0 to sdk:10.0 - Add setup-dotnet step in publish-to-nuget workflow - Update BACKPORT.md: ChainOfResponsibility rewrite, removed gRPC extension Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/publish-to-nuget.yml | 5 +++++ BACKPORT.md | 17 ++++++++--------- dev/dockerfiles/tests-dotnet.dockerfile | 2 +- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish-to-nuget.yml b/.github/workflows/publish-to-nuget.yml index 1a02ba3..98b2037 100644 --- a/.github/workflows/publish-to-nuget.yml +++ b/.github/workflows/publish-to-nuget.yml @@ -17,6 +17,11 @@ jobs: - name: Checkout 🛎️ uses: actions/checkout@v2.3.1 + - name: Setup .NET 10.0 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + # pack file to publish to NuGet - name: Create a NuGet Package 🔧 run: | diff --git a/BACKPORT.md b/BACKPORT.md index bad209f..4cfeed4 100644 --- a/BACKPORT.md +++ b/BACKPORT.md @@ -81,33 +81,32 @@ Tout est du pur calcul binaire sans couplage metier. --- -## 8. Bones.Flow — ResponsibilityChain + Traces +## 8. Bones.Flow — ChainOfResponsibility -**Action** : AJOUTER `Core/ResponsibilityChain/` (3 fichiers) + interfaces (4 fichiers) +**Action** : AJOUTER `Core/ChainOfResponsibility/` (4 fichiers) + interfaces (3 fichiers) **Justification** : Implementation du design pattern Chain of Responsibility, integre au systeme -de pipeline Bones.Flow. Utilise par 15 fichiers (handlers de contexte, publishers gateway). +de pipeline Bones.Flow. Deux variantes : `` (sans resultat) et `` (avec resultat). +Implemente `IMiddleware` pour etre composable avec Pipeline via `.With(cor)` / `.Add(cor)`. +Utilise par 15 fichiers dans DAT'Acquisition (handlers de contexte, publishers gateway). Le pattern est un GoF classique, l'implementation ne contient aucun couplage metier. -Les fichiers Traces (ITrace, ITraceFactory, Trace, TraceFactory) restent dans Bones.Flow -car ils existaient avant l'introduction de Bones.Monitoring. On pourra les deprecier plus tard -en faveur de Bones.Monitoring. - --- ## 9. Bones.Grpc — Interceptors et Extensions -**Action** : AJOUTER `DI/DependencyInjector.cs`, `Extensions/AddGrpcClientWithInterceptorsExtensions.cs`, +**Action** : AJOUTER `DI/DependencyInjector.cs`, `Extensions/ByteStringExtensions.cs`, `Interceptors/DeadlineInterceptor.cs`, `Interceptors/StreamDeadlineInterceptor.cs` **Justification** : Infrastructure gRPC standard : - `DeadlineInterceptor` : ajoute un deadline de 5s aux appels unaires — best practice gRPC pour eviter les appels pendants - `StreamDeadlineInterceptor` : deadline de 30min pour le streaming serveur -- `AddGrpcClientWithInterceptors()` : extension DI pour enregistrer un client gRPC avec ses interceptors - `ByteStringExtensions` : conversion `byte[] → ByteString` (commodite Protobuf) - Le `NotFoundInterceptor` existe deja dans le NuGet +Chaque applicatif definit sa propre methode d'extension pour composer les interceptors souhaites. + Tout est de l'infrastructure gRPC generique. --- diff --git a/dev/dockerfiles/tests-dotnet.dockerfile b/dev/dockerfiles/tests-dotnet.dockerfile index f2529f3..83ccfda 100644 --- a/dev/dockerfiles/tests-dotnet.dockerfile +++ b/dev/dockerfiles/tests-dotnet.dockerfile @@ -13,7 +13,7 @@ RUN find . -type d -empty -delete # ---------------------------------------- -FROM mcr.microsoft.com/dotnet/sdk:7.0 +FROM mcr.microsoft.com/dotnet/sdk:10.0 WORKDIR /app From 226f5c6e46ead71b68d6a484750666b1f3a67f6c Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 20:03:08 +0100 Subject: [PATCH 7/9] =?UTF-8?q?Remove=20IActorRefProvider=20=E2=80=94=20re?= =?UTF-8?q?placed=20by=20Akka=201.5=20RequiredActor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Bones.Akka.Tests/AkkaTestClass.cs | 4 -- src/Bones.Akka/ActorRefProvider.cs | 21 ------- src/Bones.Akka/DI/DependencyInjector.cs | 11 ---- src/Bones.Akka/IActorRefProvider.cs | 13 ----- src/Bones.Akka/IActorRefProviderExtensions.cs | 55 ------------------- 5 files changed, 104 deletions(-) delete mode 100644 src/Bones.Akka/ActorRefProvider.cs delete mode 100644 src/Bones.Akka/IActorRefProvider.cs delete mode 100644 src/Bones.Akka/IActorRefProviderExtensions.cs diff --git a/src/Bones.Akka.Tests/AkkaTestClass.cs b/src/Bones.Akka.Tests/AkkaTestClass.cs index 8096725..3e11e6a 100644 --- a/src/Bones.Akka.Tests/AkkaTestClass.cs +++ b/src/Bones.Akka.Tests/AkkaTestClass.cs @@ -65,9 +65,5 @@ public Creator CreateProxyCreator(TestProbe probe) Props.Create(() => new ProxyNodeActor(CreateLogger(), probe)); } - public IActorRefProvider CreateActorRefProvider(TestProbe probe) - { - return new ActorRefProvider(ActorSelection(probe.Ref.Path)); - } } } diff --git a/src/Bones.Akka/ActorRefProvider.cs b/src/Bones.Akka/ActorRefProvider.cs deleted file mode 100644 index 1e4bbc0..0000000 --- a/src/Bones.Akka/ActorRefProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Akka.Actor; - -namespace Bones.Akka -{ - public class ActorRefProvider : ActorRefProvider, IActorRefProvider - { - public ActorRefProvider(ActorSelection actorRefs) - : base(actorRefs) - { - } - } - - public abstract class ActorRefProvider : IActorRefProvider - { - public ActorSelection ActorRefs { get; } - protected ActorRefProvider(ActorSelection actorRefs) - { - ActorRefs = actorRefs; - } - } -} \ No newline at end of file diff --git a/src/Bones.Akka/DI/DependencyInjector.cs b/src/Bones.Akka/DI/DependencyInjector.cs index 44b5a5b..e894964 100644 --- a/src/Bones.Akka/DI/DependencyInjector.cs +++ b/src/Bones.Akka/DI/DependencyInjector.cs @@ -84,16 +84,5 @@ public static IServiceCollection AddCreator(this IServiceCol return services; } - public static IServiceCollection AddActorRef(this IServiceCollection services, string pattern) - { - services.AddSingleton>(sp => { - var actorSystem = sp.GetRequiredService(); - return new ActorRefProvider( - actorSystem.ActorSelection(pattern) - ); - }); - - return services; - } } } \ No newline at end of file diff --git a/src/Bones.Akka/IActorRefProvider.cs b/src/Bones.Akka/IActorRefProvider.cs deleted file mode 100644 index 0a3f2e5..0000000 --- a/src/Bones.Akka/IActorRefProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Akka.Actor; - -namespace Bones.Akka -{ - public interface IActorRefProvider - { - ActorSelection ActorRefs { get; } - } - - public interface IActorRefProvider : IActorRefProvider - { - } -} \ No newline at end of file diff --git a/src/Bones.Akka/IActorRefProviderExtensions.cs b/src/Bones.Akka/IActorRefProviderExtensions.cs deleted file mode 100644 index 96a07d9..0000000 --- a/src/Bones.Akka/IActorRefProviderExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using Akka.Actor; - -namespace Bones.Akka -{ - public static class IActorRefProviderExtensions - { - public static void Tell(this IActorRefProvider provider, object message, IActorRef sender) - { - provider.ActorRefs.Tell(message, sender); - } - - public static void Tell(this IActorRefProvider provider, object message) - { - provider.ActorRefs.Tell(message, ActorRefs.NoSender); - } - - public static Task Ask(this IActorRefProvider provider, object message, TimeSpan? timeout = null) - { - return provider.ActorRefs.Ask(message, timeout); - } - - public static Task Ask(this IActorRefProvider provider, object message, CancellationToken cancellationToken) - { - return provider.ActorRefs.Ask(message, cancellationToken); - } - - public static Task Ask(this IActorRefProvider provider, object message, TimeSpan? timeout, CancellationToken cancellationToken) - { - return provider.ActorRefs.Ask(message, timeout, cancellationToken); - } - - public static Task Ask(this IActorRefProvider provider, object message, TimeSpan? timeout = null) - { - return provider.ActorRefs.Ask(message, timeout); - } - - public static Task Ask(this IActorRefProvider provider, object message, CancellationToken cancellationToken) - { - return provider.ActorRefs.Ask(message, cancellationToken); - } - - public static Task Ask(this IActorRefProvider provider, object message, TimeSpan? timeout, CancellationToken cancellationToken) - { - return provider.ActorRefs.Ask(message, timeout, cancellationToken); - } - - public static Task Ask(this IActorRefProvider provider, Func messageFactory, TimeSpan? timeout, CancellationToken cancellationToken) - { - return provider.ActorRefs.Ask(messageFactory, timeout, cancellationToken); - } - } -} \ No newline at end of file From a4fead35352db24058c047755796172ddd84414b Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 20:08:27 +0100 Subject: [PATCH 8/9] Register Creator delegates as Singleton instead of Scoped Creators are pure delegates that don't capture scope state. Aligns with RootCreator which was already Singleton. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Bones.Akka/DI/DependencyInjector.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Bones.Akka/DI/DependencyInjector.cs b/src/Bones.Akka/DI/DependencyInjector.cs index e894964..ff18bc8 100644 --- a/src/Bones.Akka/DI/DependencyInjector.cs +++ b/src/Bones.Akka/DI/DependencyInjector.cs @@ -31,7 +31,7 @@ public static IServiceCollection AddAkka(this IServiceCollection services, strin return actorSystem; }); - services.AddScoped(sp => + services.AddSingleton(sp => { return (type, context) => DependencyResolver.For(context.System).Props(type); }); @@ -44,7 +44,7 @@ public static IServiceCollection AddCreator(this IServiceCollection serv { services.AddScoped(); - services.AddScoped>(sp => + services.AddSingleton>(sp => { return (context) => DependencyResolver.For(context.System).Props(); }); @@ -71,12 +71,12 @@ public static IServiceCollection AddCreator(this IServiceCol { services.AddScoped(); - services.AddScoped>(sp => + services.AddSingleton>(sp => { return (context) => DependencyResolver.For(context.System).Props(); }); - services.AddScoped>(sp => + services.AddSingleton>(sp => { return (context) => DependencyResolver.For(context.System).Props(); }); From a168fc4224dfb63f777af08fbcd75a7f1271080d Mon Sep 17 00:00:00 2001 From: Quentin SCHROTER Date: Sun, 22 Mar 2026 20:10:40 +0100 Subject: [PATCH 9/9] =?UTF-8?q?Remove=20RootCreator=20=E2=80=94=20replaced?= =?UTF-8?q?=20by=20Akka.Hosting=20ActorRegistry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Bones.Akka/Creator.cs | 1 - src/Bones.Akka/DI/DependencyInjector.cs | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/src/Bones.Akka/Creator.cs b/src/Bones.Akka/Creator.cs index c23ec17..90edae2 100644 --- a/src/Bones.Akka/Creator.cs +++ b/src/Bones.Akka/Creator.cs @@ -5,5 +5,4 @@ namespace Bones.Akka { public delegate Props Creator(Type t, IActorContext context); public delegate Props Creator(IActorContext context); - public delegate Props RootCreator(ActorSystem context); } diff --git a/src/Bones.Akka/DI/DependencyInjector.cs b/src/Bones.Akka/DI/DependencyInjector.cs index ff18bc8..4853ba9 100644 --- a/src/Bones.Akka/DI/DependencyInjector.cs +++ b/src/Bones.Akka/DI/DependencyInjector.cs @@ -52,20 +52,6 @@ public static IServiceCollection AddCreator(this IServiceCollection serv return services; } - - public static IServiceCollection AddRootCreator(this IServiceCollection services) - where TActor : ActorBase - { - services.AddScoped(); - - services.AddSingleton>(sp => - { - return (context) => DependencyResolver.For(context).Props(); - }); - - return services; - } - public static IServiceCollection AddCreator(this IServiceCollection services) where TActor : ActorBase, TInterface {