From bb304aec81bc50f5db219d9f2691ea8e5f0e7102 Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 17 May 2026 00:22:45 +0200 Subject: [PATCH 1/3] Split Z21Client into multiple classes --- .../Z21AutofacExtensions.cs | 16 +- src/Z21.Autofac/Z21AutofacExtensions.cs | 46 ++---- src/Z21.Client/Core/Helper/DelayedAction.cs | 28 ++++ src/Z21.Client/Core/Model/Z21Configuration.cs | 57 +++++--- src/Z21.Client/Core/Z21Client.cs | 138 +++--------------- src/Z21.Client/Core/Z21ResponseHandler.cs | 84 +++++++++++ src/Z21.Client/Core/Z21Watchdog.cs | 54 +++++++ src/Z21.Client/Transport/Z21Transport.cs | 36 +++-- src/Z21.Console/Program.cs | 49 +++---- src/Z21.Console/Z21.Console.csproj | 4 +- .../Z21DependencyInjectionExtensionTest.cs | 6 +- .../Z21.DependencyInjection.csproj | 3 + .../Z21DependencyInjectionExtension.cs | 43 ++---- 13 files changed, 306 insertions(+), 258 deletions(-) create mode 100644 src/Z21.Client/Core/Helper/DelayedAction.cs create mode 100644 src/Z21.Client/Core/Z21ResponseHandler.cs create mode 100644 src/Z21.Client/Core/Z21Watchdog.cs diff --git a/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs b/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs index b43b7ce..03c9b57 100644 --- a/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs +++ b/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs @@ -21,8 +21,7 @@ private IContainer BuildContainer(Action configure) [Test] public void AddZ21ResponseHandler_RegistersTypesCorrectly() { - var endpoint = new IPEndPoint(IPAddress.Loopback, 21105); - using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(endpoint)); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); var handler = container.Resolve(); Assert.That(handler, Is.InstanceOf()); @@ -35,8 +34,7 @@ public void AddZ21ResponseHandler_RegistersTypesCorrectly() [Test] public void AddZ21ResponseParser_Registers_All_Parser_Types() { - var endpoint = new IPEndPoint(IPAddress.Loopback, 21105); - using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(endpoint)); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); var baseInterface = typeof(IZ21ResponseParser); var parserTypes = baseInterface.Assembly @@ -64,8 +62,7 @@ public void AddZ21ResponseParser_Registers_All_Parser_Types() [Test] public void AddZ21Transport_Registers_Transport_As_Singleton() { - var endpoint = new IPEndPoint(IPAddress.Loopback, 21105); - using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(endpoint)); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); var t1 = container.Resolve(); var t2 = container.Resolve(); @@ -78,8 +75,7 @@ public void AddZ21Transport_Registers_Transport_As_Singleton() [Test] public void AddZ21Client_Registers_Client_As_Singleton() { - var endpoint = new IPEndPoint(IPAddress.Loopback, 21105); - using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(endpoint)); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); var c1 = container.Resolve(); var c2 = container.Resolve(); @@ -92,13 +88,11 @@ public void AddZ21Client_Registers_Client_As_Singleton() [Test] public void ConfigureZ21Client_Registers_Configuration_Instance() { - var endpoint = new IPEndPoint(IPAddress.Loopback, 21105); - using var container = BuildContainer(containerBuilder => containerBuilder.ConfigureZ21Client(endpoint, cfg => cfg.ResponseTime = TimeSpan.FromSeconds(5))); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(cfg => cfg.ResponseTime = TimeSpan.FromSeconds(5))); var config = container.Resolve(); Assert.NotNull(config); - Assert.That(config.ClientIPEndPoint, Is.EqualTo(endpoint)); Assert.That(config.ResponseTime, Is.EqualTo(TimeSpan.FromSeconds(5))); } } diff --git a/src/Z21.Autofac/Z21AutofacExtensions.cs b/src/Z21.Autofac/Z21AutofacExtensions.cs index 9a00cf8..0ccf002 100644 --- a/src/Z21.Autofac/Z21AutofacExtensions.cs +++ b/src/Z21.Autofac/Z21AutofacExtensions.cs @@ -1,5 +1,4 @@ -using System.Net; -using Autofac; +using Autofac; using Z21.Core; using Z21.Core.Model; using Z21.Core.ResponseHandler; @@ -11,20 +10,22 @@ namespace Z21.Autofac public static class Z21AutofacExtensions { - public static ContainerBuilder AddZ21(this ContainerBuilder builder, IPEndPoint z21EndPoint, Action? configurationAction = null) + public static ContainerBuilder AddZ21(this ContainerBuilder builder, Action? configurationAction = null) { - builder.ConfigureZ21Client(z21EndPoint, configurationAction); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance().AutoActivate(); + + builder.ConfigureZ21Client(configurationAction); builder.AddZ21ResponseParser(); builder.AddZ21ResponseHandler(); - builder.AddZ21Transport(); - builder.AddZ21Client(); return builder; } /// /// Discovers all Z21 response handlers and registers them in the container. /// - public static ContainerBuilder AddZ21ResponseHandler(this ContainerBuilder builder) + private static ContainerBuilder AddZ21ResponseHandler(this ContainerBuilder builder) { ArgumentNullException.ThrowIfNull(builder); @@ -57,7 +58,7 @@ public static ContainerBuilder AddZ21ResponseHandler(this ContainerBuilder build return builder; } - public static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder builder) + private static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder builder) { ArgumentNullException.ThrowIfNull(builder); @@ -90,36 +91,11 @@ public static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder builde return builder; } - public static ContainerBuilder AddZ21Transport(this ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.RegisterType() - .As() - .SingleInstance(); - - return builder; - } - - public static ContainerBuilder AddZ21Client(this ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.RegisterType() - .As() - .SingleInstance(); - - return builder; - } - - public static ContainerBuilder ConfigureZ21Client(this ContainerBuilder builder, - IPEndPoint z21EndPoint, - Action? configurationAction = null) + private static ContainerBuilder ConfigureZ21Client(this ContainerBuilder builder, Action? configurationAction = null) { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(z21EndPoint); - var config = new Z21Configuration(z21EndPoint); + var config = new Z21Configuration(); configurationAction?.Invoke(config); builder.RegisterInstance(config) diff --git a/src/Z21.Client/Core/Helper/DelayedAction.cs b/src/Z21.Client/Core/Helper/DelayedAction.cs new file mode 100644 index 0000000..39af772 --- /dev/null +++ b/src/Z21.Client/Core/Helper/DelayedAction.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using System.Timers; + +namespace Z21.Core.Helper +{ + public class DelayedAction + { + private readonly Timer _connectionKeepAlive; + + public DelayedAction(TimeSpan delayTime, Func action) + { + _connectionKeepAlive = new(delayTime) + { + AutoReset = true, + Enabled = false + }; + + _connectionKeepAlive.Elapsed += async (_, _) => await action(); + } + + public void Delay() + { + _connectionKeepAlive.Stop(); + _connectionKeepAlive.Start(); + } + } +} \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/Z21Configuration.cs b/src/Z21.Client/Core/Model/Z21Configuration.cs index f292120..c9e254d 100644 --- a/src/Z21.Client/Core/Model/Z21Configuration.cs +++ b/src/Z21.Client/Core/Model/Z21Configuration.cs @@ -7,47 +7,62 @@ namespace Z21.Core.Model { public class Z21Configuration { - - public Z21Configuration(IPEndPoint clientIpEndPoint) - { - ArgumentNullException.ThrowIfNull(clientIpEndPoint, nameof(clientIpEndPoint)); - - ClientIPEndPoint = clientIpEndPoint; - } + private IPEndPoint _clientIpEndPoint = Defaults.IpEndPoint; + private bool _allowNatTraversal = true; /// /// IPEndPoint of the Z21. /// - public IPEndPoint ClientIPEndPoint { get; } + public IPEndPoint ClientIPEndPoint + { + get => _clientIpEndPoint; + set + { + ArgumentNullException.ThrowIfNull(value); + if (_clientIpEndPoint.Equals(value)) + return; + + _clientIpEndPoint = value; + ConfigurationUpdated?.Invoke(this, System.EventArgs.Empty); + } + } /// /// Enables or disables Network Address Translation (NAT) traversal on a UdpClient instance. /// - public bool AllowNatTraversal { get; set; } = true; - - /// - /// Configures the interval in witch the client will send a keep alive command to the z21. This Setting should not need changing! - /// - /// The specification states that the client must communicate at least once per minute with the z21 or else the z21 assumes that the client has disconnected. - public TimeSpan ConnectionKeepAliveCommandInterval { get; set; } = TimeSpan.FromSeconds(20); + public bool AllowNatTraversal + { + get => _allowNatTraversal; + set + { + if (_allowNatTraversal.Equals(value)) + return; + _allowNatTraversal = value; + ConfigurationUpdated?.Invoke(this, System.EventArgs.Empty); + } + } /// /// Time it takes between a command being sent and a response being received. This Setting should not need changing! /// public TimeSpan ResponseTime { get; set; } = TimeSpan.FromSeconds(2); - + /// /// Configures the default broadcast flags that should be sent to the Z21 /// - public uint[] BroadcastFlags { get; set; } = - [ - Z21BroadcastFlags.DriveAndSwitchingMessages, - Z21BroadcastFlags.LocoInfoChangedMessages - ]; + public uint[] BroadcastFlags { get; set; } = Defaults.BroadcastFlags; + public event EventHandler? ConfigurationUpdated; + public static class Defaults { public readonly static IPEndPoint IpEndPoint = new(IPAddress.Parse("192.168.0.111"), 21105); + + public readonly static uint[] BroadcastFlags = + [ + Z21BroadcastFlags.DriveAndSwitchingMessages, + Z21BroadcastFlags.LocoInfoChangedMessages + ]; } } } \ No newline at end of file diff --git a/src/Z21.Client/Core/Z21Client.cs b/src/Z21.Client/Core/Z21Client.cs index fea51b3..34f27c3 100644 --- a/src/Z21.Client/Core/Z21Client.cs +++ b/src/Z21.Client/Core/Z21Client.cs @@ -1,164 +1,75 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using System.Timers; using Microsoft.Extensions.Logging; using Z21.Core.Command; using Z21.Core.Command.SystemState; using Z21.Core.Exception; +using Z21.Core.Helper; using Z21.Core.Model; using Z21.Core.Model.EventArgs; -using Z21.Core.ResponseHandler; using Z21.Transport; // ReSharper disable ClassWithVirtualMembersNeverInherited.Global namespace Z21.Core { + public class Z21Client : IZ21Client { - private bool _previousIsConnected; - private DateTime _lastCommunication = DateTime.MinValue; - private readonly Timer _connectionKeepAlive; private readonly ILogger? _logger; private readonly Z21Configuration _z21Configuration; private readonly IZ21Transport _transport; - private readonly List _handlers; - + private readonly DelayedAction _delayedKeepAliveAction; + private readonly Z21Watchdog _z21Watchdog; + /// /// IPv4 safe MTU for payload according to specification. /// public const int MaxUdpPayload = 1472; /// Thrown when system architecture is not little-endian. - public Z21Client(Z21Configuration z21Configuration, IZ21Transport z21Transport, IEnumerable z21ResponseHandlers, ILogger? logger = null) + public Z21Client(Z21Configuration z21Configuration, IZ21Transport z21Transport, ILogger? logger = null) { ArgumentNullException.ThrowIfNull(z21Configuration); ArgumentNullException.ThrowIfNull(z21Transport); - ArgumentNullException.ThrowIfNull(z21ResponseHandlers); if (!BitConverter.IsLittleEndian) throw new PlatformNotSupportedException("Z21Client requires little-endian architecture."); _z21Configuration = z21Configuration; _transport = z21Transport; - _handlers = z21ResponseHandlers.ToList(); _logger = logger; - _transport.OnResponseReceived += Transport_OnResponseReceived; - - _connectionKeepAlive = new(z21Configuration.ConnectionKeepAliveCommandInterval) - { - AutoReset = true, - Enabled = false - }; - _connectionKeepAlive.Elapsed += ConnectionKeepAlive_OnElapsed; + _z21Watchdog = new (z21Configuration); + _z21Watchdog.OnReachabilityChanged += async (_, args ) => await Watchdog_OnOnReachabilityChanged(args); + _delayedKeepAliveAction = new (TimeSpan.FromSeconds(45), async () => await SendCommandsAsync(new GetFirmwareVersionCommand())); } public event EventHandler? OnConnectionChanged; - public bool IsConnected => DateTime.UtcNow - _lastCommunication < _z21Configuration.ConnectionKeepAliveCommandInterval + _z21Configuration.ResponseTime; - + public bool IsConnected { get; private set; } + public async Task ConnectAsync() { _logger?.LogInformation("Z21Client trying to connect with {ClientIPEndPoint}.", _transport.Z21Configuration.ClientIPEndPoint); _transport.Connect(); - _connectionKeepAlive.Enabled = true; - await SendCommandsAsync(new GetFirmwareVersionCommand()); + await LogOnAsync(); } public async Task SendCommandsAsync(params IZ21Command[] z21Commands) { - ArgumentNullException.ThrowIfNull(z21Commands, nameof(z21Commands)); + ArgumentNullException.ThrowIfNull(z21Commands); if (!_transport.IsConnected) - throw new ClientNotConnectedException(); + await ConnectAsync(); - foreach (IZ21Command z21Command in z21Commands) + foreach (var z21Command in z21Commands) _logger?.LogDebug("{commandName} sending {datagram} to Z21.", z21Command.Name, BitConverter.ToString(z21Command.Data)); - byte[] combinedPayload = z21Commands.SelectMany(z21Command => z21Command.Data).ToArray(); - + var combinedPayload = z21Commands.SelectMany(z21Command => z21Command.Data).ToArray(); MtuPayloadLengthExceededException.ThrowIfExceeded(combinedPayload); - _connectionKeepAlive.Stop(); - _connectionKeepAlive.Start(); - await VerifyConnectionOnDemandAsync(); - await _transport.SendAsync(combinedPayload); - } - - public List CutDatagram(byte[] datagram) - { - List cutDatagrams = []; - int offset = 0; - while (offset < datagram.Length) - { - try - { - if (offset + 2 > datagram.Length) - { - _logger?.LogError("Incomplete DataLen field — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; - } - - ushort dataLen = (ushort)(datagram[offset] | (datagram[offset + 1] << 8)); - - if (offset + dataLen > datagram.Length) - { - _logger?.LogError("Incomplete packet — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; - } - - byte[] cutDatagram = new byte[dataLen]; - Buffer.BlockCopy(datagram, offset, cutDatagram, 0, dataLen); - _logger?.LogDebug("Received cut datagram: {cutDatagram}", BitConverter.ToString(cutDatagram)); - offset += dataLen; - cutDatagrams.Add(cutDatagram); - } - catch (System.Exception exception) - { - _logger?.LogError(exception, "Failed to cut datagram — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; - } - } - - return cutDatagrams; - } - - public void HandleDatagram(byte[] data) - { - foreach (IZ21ResponseHandler handler in _handlers.Where(handler => handler.CanHandle(data))) - { - try - { - _logger?.LogDebug("{handlerName} handling datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); - handler.Handle(data); - } - catch (System.Exception exception) - { - _logger?.LogError(exception, "{handlerName} failed to handle datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); - } - } - } - - private async Task VerifyConnectionOnDemandAsync() - { - bool previousIsConnected = _previousIsConnected; - _previousIsConnected = IsConnected; - - if (previousIsConnected != IsConnected) - { - if (IsConnected) - { - _logger?.LogInformation("Z21Client connecting with {ClientIPEndPoint}.", _transport.Z21Configuration.ClientIPEndPoint); - await LogOnAsync(); - } - else - { - _logger?.LogInformation("Z21Client lost connection with {ClientIPEndPoint}.", _transport.Z21Configuration.ClientIPEndPoint); - } - OnConnectionChanged?.Invoke(this, new(IsConnected)); - } + _delayedKeepAliveAction.Delay(); } protected async virtual Task LogOnAsync() @@ -166,17 +77,12 @@ protected async virtual Task LogOnAsync() await SendCommandsAsync(new SetBroadcastFlagsCommand(_z21Configuration.BroadcastFlags), new GetFirmwareVersionCommand()); } - private async void Transport_OnResponseReceived(object? sender, ResponseReceivedEventArgs bytes) - { - _lastCommunication = DateTime.UtcNow; - await VerifyConnectionOnDemandAsync(); - - CutDatagram(bytes.Response).ForEach(HandleDatagram); - } - - private async void ConnectionKeepAlive_OnElapsed(object? sender, ElapsedEventArgs e) + private async Task Watchdog_OnOnReachabilityChanged(ConnectionChangedEventArgs args) { - await SendCommandsAsync(new GetFirmwareVersionCommand()); + if (args.IsConnected) + await LogOnAsync(); + IsConnected = args.IsConnected; + OnConnectionChanged?.Invoke(this, args); } } } \ No newline at end of file diff --git a/src/Z21.Client/Core/Z21ResponseHandler.cs b/src/Z21.Client/Core/Z21ResponseHandler.cs new file mode 100644 index 0000000..cd9bf82 --- /dev/null +++ b/src/Z21.Client/Core/Z21ResponseHandler.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler; +using Z21.Transport; + +namespace Z21.Core +{ + public class Z21ResponseHandler + { + private readonly IZ21Transport _transport; + private readonly IEnumerable _z21ResponseHandlers; + private readonly ILogger? _logger; + + public Z21ResponseHandler(IZ21Transport z21Transport, IEnumerable z21ResponseHandlers, ILogger? logger = null) + { + _transport = z21Transport; + _z21ResponseHandlers = z21ResponseHandlers; + _logger = logger; + _transport.OnResponseReceived += Transport_OnResponseReceived; + } + + protected virtual void Transport_OnResponseReceived(object? sender, ResponseReceivedEventArgs bytes) + { + CutDatagram(bytes.Response).ForEach(HandleDatagram); + } + + protected virtual void HandleDatagram(byte[] data) + { + foreach (IZ21ResponseHandler handler in _z21ResponseHandlers.Where(handler => handler.CanHandle(data))) + { + try + { + _logger?.LogDebug("{handlerName} handling datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); + handler.Handle(data); + } + catch (System.Exception exception) + { + _logger?.LogError(exception, "{handlerName} failed to handle datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); + } + } + } + + protected virtual List CutDatagram(byte[] datagram) + { + List cutDatagrams = []; + int offset = 0; + while (offset < datagram.Length) + { + try + { + if (offset + 2 > datagram.Length) + { + _logger?.LogError("Incomplete DataLen field — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); + return cutDatagrams; + } + + ushort dataLen = (ushort)(datagram[offset] | (datagram[offset + 1] << 8)); + + if (offset + dataLen > datagram.Length) + { + _logger?.LogError("Incomplete packet — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); + return cutDatagrams; + } + + byte[] cutDatagram = new byte[dataLen]; + Buffer.BlockCopy(datagram, offset, cutDatagram, 0, dataLen); + _logger?.LogDebug("Received cut datagram: {cutDatagram}", BitConverter.ToString(cutDatagram)); + offset += dataLen; + cutDatagrams.Add(cutDatagram); + } + catch (System.Exception exception) + { + _logger?.LogError(exception, "Failed to cut datagram — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); + return cutDatagrams; + } + } + + return cutDatagrams; + } + } +} \ No newline at end of file diff --git a/src/Z21.Client/Core/Z21Watchdog.cs b/src/Z21.Client/Core/Z21Watchdog.cs new file mode 100644 index 0000000..1b61689 --- /dev/null +++ b/src/Z21.Client/Core/Z21Watchdog.cs @@ -0,0 +1,54 @@ +using System; +using System.Net.NetworkInformation; +using System.Timers; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core +{ + public sealed class Z21Watchdog + { + private readonly Z21Configuration _configuration; + private readonly Timer _timer; + private bool? _lastReachable; + + public event EventHandler? OnReachabilityChanged; + + public Z21Watchdog(Z21Configuration configuration) + { + _configuration = configuration; + + _timer = new(TimeSpan.FromSeconds(1)) + { + AutoReset = true, + Enabled = true + }; + _timer.Elapsed += (_, _) => CheckState(); + } + + private void CheckState() + { + var reachable = IsReachable(); + + if (_lastReachable == reachable) + return; + + _lastReachable = reachable; + OnReachabilityChanged?.Invoke(this, new (reachable)); + } + + private bool IsReachable() + { + try + { + using var ping = new Ping(); + var reply = ping.Send(_configuration.ClientIPEndPoint.Address, 1000); + return reply.Status == IPStatus.Success; + } + catch + { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Z21.Client/Transport/Z21Transport.cs b/src/Z21.Client/Transport/Z21Transport.cs index 13ead1f..fca0f0d 100644 --- a/src/Z21.Client/Transport/Z21Transport.cs +++ b/src/Z21.Client/Transport/Z21Transport.cs @@ -9,37 +9,46 @@ namespace Z21.Transport { public class Z21Transport : IZ21Transport, IDisposable { + private Lazy _udpClient; + public Z21Transport(Z21Configuration z21Configuration) { - ArgumentNullException.ThrowIfNull(z21Configuration, nameof(z21Configuration)); + ArgumentNullException.ThrowIfNull(z21Configuration); Z21Configuration = z21Configuration; + Z21Configuration.ConfigurationUpdated += (_, _) => _udpClient = new(UdpClientFactory()); + _udpClient = new(UdpClientFactory()); + } + + private UdpClient UdpClientFactory() + { + if (_udpClient?.IsValueCreated == true) + _udpClient.Value.Dispose(); - UdpClient = new(Z21Configuration.ClientIPEndPoint.Port); + var udpClient = new UdpClient(Z21Configuration.ClientIPEndPoint.Port); if (OperatingSystem.IsWindows()) - UdpClient.AllowNatTraversal(Z21Configuration.AllowNatTraversal); + udpClient.AllowNatTraversal(Z21Configuration.AllowNatTraversal); + return udpClient; } - public bool IsConnected { get; private set; } = false; - public event EventHandler? OnResponseReceived; - public UdpClient UdpClient { get; } + public bool IsConnected { get; private set; } = false; public Z21Configuration Z21Configuration { get; } public void Connect() { - UdpClient.Connect(Z21Configuration.ClientIPEndPoint); - UdpClient.BeginReceive(Receiving, null); + _udpClient.Value.Connect(Z21Configuration.ClientIPEndPoint); + _udpClient.Value.BeginReceive(Receiving, null); IsConnected = true; } private void Receiving(IAsyncResult res) { IPEndPoint? remoteIpEndPoint = null!; - byte[] received = UdpClient.EndReceive(res, ref remoteIpEndPoint); - UdpClient.BeginReceive(Receiving, null); + byte[] received = _udpClient.Value.EndReceive(res, ref remoteIpEndPoint); + _udpClient.Value.BeginReceive(Receiving, null); if (remoteIpEndPoint is not null && remoteIpEndPoint.Equals(Z21Configuration.ClientIPEndPoint)) @@ -48,13 +57,14 @@ private void Receiving(IAsyncResult res) public async Task SendAsync(byte[] datagram) { - ArgumentNullException.ThrowIfNull(datagram, nameof(datagram)); - await UdpClient.SendAsync(datagram, datagram.GetLength(0)); + ArgumentNullException.ThrowIfNull(datagram); + await _udpClient.Value.SendAsync(datagram, datagram.GetLength(0)); } public void Dispose() { - UdpClient.Dispose(); + if (_udpClient.IsValueCreated) + _udpClient.Value.Dispose(); } } } \ No newline at end of file diff --git a/src/Z21.Console/Program.cs b/src/Z21.Console/Program.cs index fa3deb0..b7e5a78 100644 --- a/src/Z21.Console/Program.cs +++ b/src/Z21.Console/Program.cs @@ -1,13 +1,12 @@ -using Microsoft.Extensions.DependencyInjection; +using Autofac; using Serilog; using Serilog.Events; +using Serilog.Extensions.Autofac.DependencyInjection; using Serilog.Sinks.SystemConsole.Themes; using Spectre.Console.Cli; -using Spectre.Console.Cli.Extensions.DependencyInjection; +using Z21.Autofac; using Z21.Console.Command; using Z21.Core; -using Z21.Core.Model; -using Z21.DependencyInjection; namespace Z21.Console { @@ -17,39 +16,33 @@ abstract internal class Program public static void Main(string[] args) { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .Enrich.FromLogContext() - .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug, theme: AnsiConsoleTheme.Sixteen) - .CreateLogger(); - - ServiceCollection services = new(); - services.AddLogging(loggingBuilder => loggingBuilder.AddSerilog(dispose: true)); - services.ConfigureZ21Client(Z21Configuration.Defaults.IpEndPoint); - services.AddZ21Client(); - services.AddZ21Transport(); - services.AddZ21ResponseParser(); - services.AddZ21ResponseHandler(); - ServiceProvider serviceProvider = services.BuildServiceProvider(); - - Z21Client = serviceProvider.GetRequiredService(); - + var log = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.FromLogContext() + .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug, theme: AnsiConsoleTheme.Sixteen); + + var builder = new ContainerBuilder(); + builder.AddZ21(); + builder.RegisterSerilog(log); + var container = builder.Build(); + + Z21Client = container.Resolve(); + Z21Client.ConnectAsync(); - - using DependencyInjectionRegistrar registrar = new(services); - CommandApp app = new(registrar); - + + CommandApp app = new(); + app.Configure( config => { config.AddCommand("SetTrackPower") .WithDescription("Turn track power on or off."); - + config.AddCommand("GetFirmwareVersion") .WithDescription("Retrieve the firmware version."); }); - - + + app.Run(new List()); while (true) { diff --git a/src/Z21.Console/Z21.Console.csproj b/src/Z21.Console/Z21.Console.csproj index 0531a19..f12f24d 100644 --- a/src/Z21.Console/Z21.Console.csproj +++ b/src/Z21.Console/Z21.Console.csproj @@ -10,8 +10,8 @@ - + @@ -21,7 +21,7 @@ - + diff --git a/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs b/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs index 8e9334d..d94bc47 100644 --- a/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs +++ b/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs @@ -7,14 +7,12 @@ namespace Z21.DependencyInjection.UnitTest public class Z21DependencyInjectionExtensionTest { [Test] - public void AddZ21ResponseHandler_SameInstanceIsRegisteredForAllInterfaces() + public void AddZ21_WithZ21ResponseHandlers_SameInstanceIsRegisteredForAllInterfaces() { ServiceCollection services = new(); - services.AddZ21ResponseParser(); - services.AddZ21ResponseHandler(); + services.AddZ21(); ServiceProvider serviceProvider = services.BuildServiceProvider(); - ISerialNumberResponseHandler implementation = serviceProvider.GetRequiredService(); ISerialNumberResponseHandler implementationSpecificInterface = diff --git a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj index d9e6351..66af20f 100644 --- a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj +++ b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj @@ -14,6 +14,9 @@ + + + diff --git a/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs b/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs index 7a9556c..43661cc 100644 --- a/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs +++ b/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs @@ -1,5 +1,4 @@ -using System.Net; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Z21.Core; using Z21.Core.Model; using Z21.Core.ResponseHandler; @@ -8,24 +7,27 @@ namespace Z21.DependencyInjection { + public static class Z21DependencyInjectionExtension { - public static ServiceCollection AddZ21(this ServiceCollection services, IPEndPoint z21EndPoint, Action? configurationAction = null) + public static IServiceCollection AddZ21(this IServiceCollection services, Action? configurationAction = null) { - services.ConfigureZ21Client(z21EndPoint, configurationAction); + services.AddSingleton(); + services.AddSingleton(); + services.AddActivatedSingleton(); + + services.ConfigureZ21Client(configurationAction); services.AddZ21ResponseParser(); services.AddZ21ResponseHandler(); - services.AddZ21Transport(); - services.AddZ21Client(); return services; } /// /// Discovers all Z21 response handlers and registers them in the collection. /// - public static ServiceCollection AddZ21ResponseHandler(this ServiceCollection services) + private static IServiceCollection AddZ21ResponseHandler(this IServiceCollection services) { - ArgumentNullException.ThrowIfNull(services, nameof(services)); + ArgumentNullException.ThrowIfNull(services); Type baseInterface = typeof(IZ21ResponseHandler); @@ -45,9 +47,9 @@ public static ServiceCollection AddZ21ResponseHandler(this ServiceCollection ser return services; } - public static ServiceCollection AddZ21ResponseParser(this ServiceCollection services) + private static IServiceCollection AddZ21ResponseParser(this IServiceCollection services) { - ArgumentNullException.ThrowIfNull(services, nameof(services)); + ArgumentNullException.ThrowIfNull(services); Type baseInterface = typeof(IZ21ResponseParser); @@ -67,26 +69,11 @@ public static ServiceCollection AddZ21ResponseParser(this ServiceCollection serv return services; } - public static ServiceCollection AddZ21Transport(this ServiceCollection services) // TODO: Test - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - services.AddSingleton(); - return services; - } - - public static ServiceCollection AddZ21Client(this ServiceCollection services) // TODO: Test - { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - services.AddSingleton(); - return services; - } - - public static ServiceCollection ConfigureZ21Client(this ServiceCollection services, IPEndPoint z21EndPoint, Action? configurationAction = null) // TODO: Test + private static IServiceCollection ConfigureZ21Client(this IServiceCollection services, Action? configurationAction = null) // TODO: Test { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - ArgumentNullException.ThrowIfNull(z21EndPoint, nameof(z21EndPoint)); + ArgumentNullException.ThrowIfNull(services); - Z21Configuration configuration = new(z21EndPoint); + Z21Configuration configuration = new(); configurationAction?.Invoke(configuration); services.AddSingleton(configuration); From 268839f89e41c778cfc8ff9bd08ca5a14feab015 Mon Sep 17 00:00:00 2001 From: Jakob <53713395+jaak0b@users.noreply.github.com> Date: Sun, 17 May 2026 00:33:34 +0200 Subject: [PATCH 2/3] Revise dependency injection section in README Updated README to reflect changes in dependency injection section, replacing references to 'Dependency Injection' with 'Autofac' and providing a new setup example. --- README.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a098252..706b259 100644 --- a/README.md +++ b/README.md @@ -46,18 +46,33 @@ This is important if certain actions should happen at the same time (i.e. contro ### -### Dependency Injection +### Autofac > [!IMPORTANT] -> While this library works without dependency injection, DI is still recommend as it makes usage of this library much easier. -[Z21.DependencyInjection](https://www.nuget.org/packages/Z21.DependencyInjection/) provides extension methods to register all required classes directly in the container. +> While this library works without Autofac, Autofac is still recommend as it makes usage of this library much easier. +[Z21.Autofac](https://www.nuget.org/packages/Z21.Autofac/) provides extension methods to register all required classes directly in the container. + +```csharp + var builder = new ContainerBuilder(); + builder.AddZ21(); + var container = builder.Build(); +``` + +### Dependency Injection +Dependency Injection is supported natively via [Z21.DependencyInjection](https://www.nuget.org/packages/Z21.DependencyInjection/) and requires the use of hosted services. +Z21 registers background components that must run inside the .NET Generic Host lifecycle. +A minimal setup looks like this: ```csharp - services.ConfigureZ21Client(Z21Configuration.Defaults.IpEndPoint); - services.AddZ21Client(); - services.AddZ21Transport(); - services.AddZ21ResponseParser(); - services.AddZ21ResponseHandler(); + var host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddZ21(); + }) + .Build(); + + await host.RunAsync(); ``` +The host is responsible for starting all Z21‑related hosted services and managing their lifetime. ## Z21 Commands From 794b219b776e602cf0177f9d784d054a668b7b5b Mon Sep 17 00:00:00 2001 From: Jakob Date: Sun, 17 May 2026 00:34:49 +0200 Subject: [PATCH 3/3] Upgrade Nuget version --- src/Z21.Autofac/Z21.Autofac.csproj | 2 +- src/Z21.Client/Z21.Client.csproj | 2 +- src/Z21.DependencyInjection/Z21.DependencyInjection.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Z21.Autofac/Z21.Autofac.csproj b/src/Z21.Autofac/Z21.Autofac.csproj index 23f4420..0dde02b 100644 --- a/src/Z21.Autofac/Z21.Autofac.csproj +++ b/src/Z21.Autofac/Z21.Autofac.csproj @@ -7,7 +7,7 @@ enable Jakob Eichberger https://github.com/Jakob-Eichberger/z21Client - 5.0.0 + 6.0.0 LICENSE true diff --git a/src/Z21.Client/Z21.Client.csproj b/src/Z21.Client/Z21.Client.csproj index fcb4229..42b901f 100644 --- a/src/Z21.Client/Z21.Client.csproj +++ b/src/Z21.Client/Z21.Client.csproj @@ -14,7 +14,7 @@ x64 enable net8.0;net8.0-windows - 5.0.0 + 6.0.0 true LICENSE Z21 diff --git a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj index 66af20f..74e3789 100644 --- a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj +++ b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj @@ -7,7 +7,7 @@ true Jakob Eichberger https://github.com/Jakob-Eichberger/z21Client - 5.0.0-alpha + 6.0.0 LICENSE true