From 485b1a1ebb2991d2096ee4b9c0040daac895f8b4 Mon Sep 17 00:00:00 2001 From: smdn Date: Wed, 28 May 2025 21:54:17 +0900 Subject: [PATCH 1/6] expose MuninNodeBuilder --- .../IMuninNodeBuilderExtensions.cs | 16 ++++++++-------- .../IMuninServiceBuilderExtensions.cs | 2 +- ...ltMuninNodeBuilder.cs => MuninNodeBuilder.cs} | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) rename src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/{DefaultMuninNodeBuilder.cs => MuninNodeBuilder.cs} (89%) diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs index 07c8689..bde44c5 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs @@ -48,10 +48,10 @@ Func buildPlugin if (buildPlugin is null) throw new ArgumentNullException(nameof(buildPlugin)); - if (builder is not DefaultMuninNodeBuilder defaultMuninNodeBuilder) + if (builder is not MuninNodeBuilder muninNodeBuilder) throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - defaultMuninNodeBuilder.AddPluginFactory(buildPlugin); + muninNodeBuilder.AddPluginFactory(buildPlugin); return builder; } @@ -94,10 +94,10 @@ Func buildPluginProvider if (buildPluginProvider is null) throw new ArgumentNullException(nameof(buildPluginProvider)); - if (builder is not DefaultMuninNodeBuilder defaultMuninNodeBuilder) + if (builder is not MuninNodeBuilder muninNodeBuilder) throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - defaultMuninNodeBuilder.SetPluginProviderFactory(buildPluginProvider); + muninNodeBuilder.SetPluginProviderFactory(buildPluginProvider); return builder; } @@ -171,10 +171,10 @@ Func buildSessionCallback if (buildSessionCallback is null) throw new ArgumentNullException(nameof(buildSessionCallback)); - if (builder is not DefaultMuninNodeBuilder defaultMuninNodeBuilder) + if (builder is not MuninNodeBuilder muninNodeBuilder) throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - defaultMuninNodeBuilder.SetSessionCallbackFactory(buildSessionCallback); + muninNodeBuilder.SetSessionCallbackFactory(buildSessionCallback); return builder; } @@ -232,10 +232,10 @@ Func buildListenerFactory if (buildListenerFactory is null) throw new ArgumentNullException(nameof(buildListenerFactory)); - if (builder is not DefaultMuninNodeBuilder defaultMuninNodeBuilder) + if (builder is not MuninNodeBuilder muninNodeBuilder) throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - defaultMuninNodeBuilder.SetListenerFactory(buildListenerFactory); + muninNodeBuilder.SetListenerFactory(buildListenerFactory); return builder; } diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index e89bf75..e43e77b 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -46,7 +46,7 @@ Action configure configure(options); - var nodeBuilder = new DefaultMuninNodeBuilder( + var nodeBuilder = new MuninNodeBuilder( serviceBuilder: builder, serviceKey: options.HostName // use configured hostname as a service key and option name ); diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/DefaultMuninNodeBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs similarity index 89% rename from src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/DefaultMuninNodeBuilder.cs rename to src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs index 9b43d69..0107943 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/DefaultMuninNodeBuilder.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs @@ -14,7 +14,7 @@ namespace Smdn.Net.MuninNode.DependencyInjection; -internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder { +public class MuninNodeBuilder : IMuninNodeBuilder { private readonly List> pluginFactories = new(capacity: 4); private Func? buildPluginProvider; private Func? buildSessionCallback; @@ -23,13 +23,13 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder { public IServiceCollection Services { get; } public string ServiceKey { get; } - public DefaultMuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) + internal MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) { Services = (serviceBuilder ?? throw new ArgumentNullException(nameof(serviceBuilder))).Services; ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); } - public void AddPluginFactory(Func buildPlugin) + internal void AddPluginFactory(Func buildPlugin) { if (buildPlugin is null) throw new ArgumentNullException(nameof(buildPlugin)); @@ -37,7 +37,7 @@ public void AddPluginFactory(Func buildPlugin) pluginFactories.Add(serviceProvider => buildPlugin(serviceProvider)); } - public void SetPluginProviderFactory( + internal void SetPluginProviderFactory( Func buildPluginProvider ) { @@ -47,7 +47,7 @@ Func buildPluginProvider this.buildPluginProvider = buildPluginProvider; } - public void SetSessionCallbackFactory( + internal void SetSessionCallbackFactory( Func buildSessionCallback ) { @@ -57,7 +57,7 @@ Func buildSessionCallback this.buildSessionCallback = buildSessionCallback; } - public void SetListenerFactory( + internal void SetListenerFactory( Func buildListenerFactory ) { From 2d0cd1db6bd463c12955d8d98f28d2ee8bd566af Mon Sep 17 00:00:00 2001 From: smdn Date: Wed, 28 May 2025 22:21:11 +0900 Subject: [PATCH 2/6] add support for configuring with custom MuninNodeOptions types --- .../IMuninServiceBuilderExtensions.cs | 45 ++++++++++++++----- .../MuninNodeBuilder.cs | 13 +++++- .../Smdn.Net.MuninNode/MuninNodeOptions.cs | 13 +++++- .../IMuninServiceBuilderExtensions.cs | 40 +++++++++++++++++ 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index e43e77b..f2facc0 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -11,7 +11,9 @@ public static class IMuninServiceBuilderExtensions { /// /// Adds a Munin-Node to the with default configurations. /// - /// An to build the Munin-Node to be added. + /// + /// An that the built Munin-Node will be added to. + /// /// The current so that additional calls can be chained. public static IMuninNodeBuilder AddNode( this IMuninServiceBuilder builder @@ -25,7 +27,7 @@ this IMuninServiceBuilder builder /// Adds a Munin-Node to the with specified configurations. /// /// - /// An to build the Munin-Node to be added. + /// An that the built Munin-Node will be added to. /// /// /// An to setup to @@ -36,29 +38,48 @@ public static IMuninNodeBuilder AddNode( this IMuninServiceBuilder builder, Action configure ) + => AddNode( + builder: builder ?? throw new ArgumentNullException(nameof(builder)), + configure: configure ?? throw new ArgumentNullException(nameof(configure)) + ); + + /// + /// Adds a Munin-Node to the with specified configurations. + /// + /// + /// The extended type of to configure the Munin-Node. + /// + /// + /// An that the built Munin-Node will be added to. + /// + /// + /// An to setup to + /// configure the Munin-Node to be built. + /// + /// The current so that additional calls can be chained. + public static IMuninNodeBuilder AddNode( + this IMuninServiceBuilder builder, + Action configure + ) + where TMuninNodeOptions : MuninNodeOptions, new() { if (builder is null) throw new ArgumentNullException(nameof(builder)); if (configure is null) throw new ArgumentNullException(nameof(configure)); - var options = new MuninNodeOptions(); + var configuredOptions = new TMuninNodeOptions(); - configure(options); + configure(configuredOptions); var nodeBuilder = new MuninNodeBuilder( serviceBuilder: builder, - serviceKey: options.HostName // use configured hostname as a service key and option name + serviceKey: configuredOptions.HostName // use configured hostname as a service key and option name ); - _ = builder.Services.Configure( + _ = builder.Services.Configure( name: nodeBuilder.ServiceKey, // configure MuninNodeOptions for this builder - opts => { - opts.Address = options.Address; - opts.Port = options.Port; - opts.HostName = options.HostName; - opts.AccessRule = options.AccessRule; - } + options => options.Configure(configuredOptions) ); builder.Services.Add( diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs index 0107943..d19b93b 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs @@ -73,7 +73,7 @@ public IMuninNode Build(IServiceProvider serviceProvider) throw new ArgumentNullException(nameof(serviceProvider)); return new DefaultMuninNode( - options: serviceProvider.GetRequiredService>().Get(name: ServiceKey), + options: GetConfiguredOptions(serviceProvider), pluginProvider: buildPluginProvider is null ? new PluginProvider( plugins: pluginFactories.Select(factory => factory(serviceProvider)).ToList(), @@ -98,4 +98,15 @@ public PluginProvider( SessionCallback = sessionCallback; } } + + protected TMuninNodeOptions GetConfiguredOptions(IServiceProvider serviceProvider) + where TMuninNodeOptions : MuninNodeOptions + { + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + return serviceProvider + .GetRequiredService>() + .Get(name: ServiceKey); + } } diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/MuninNodeOptions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/MuninNodeOptions.cs index 2b4a062..3a77f2b 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/MuninNodeOptions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/MuninNodeOptions.cs @@ -14,7 +14,7 @@ namespace Smdn.Net.MuninNode; /// Options to configure the Munin-Node. /// /// -public sealed class MuninNodeOptions { +public class MuninNodeOptions { private static IPAddress LoopbackAddress => Socket.OSSupportsIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback; private static IPAddress AnyAddress => Socket.OSSupportsIPv6 ? IPAddress.IPv6Any : IPAddress.Any; @@ -124,6 +124,17 @@ public MuninNodeOptions Clone() => (MuninNodeOptions)MemberwiseClone(); #endif + protected internal virtual void Configure(MuninNodeOptions baseOptions) + { + if (baseOptions is null) + throw new ArgumentNullException(nameof(baseOptions)); + + Address = baseOptions.Address; + Port = baseOptions.Port; + HostName = baseOptions.HostName; + AccessRule = baseOptions.AccessRule; + } + /// /// Set the value of the property to use the address of /// or . diff --git a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index 8582ed3..18ae8bd 100644 --- a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -4,6 +4,7 @@ using System.Linq; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using NUnit.Framework; @@ -161,4 +162,43 @@ public void AddNode_Multiple_GetKeyedService(string hostName) Assert.That(node.HostName, Is.EqualTo(hostName)); Assert.That(node, Is.Not.SameAs(anotherNode)); } + + private class CustomMuninNodeOptions : MuninNodeOptions { + public string? ExtraOption { get; set; } + + protected override void Configure(MuninNodeOptions baseOptions) + { + base.Configure(baseOptions ?? throw new ArgumentNullException(nameof(baseOptions))); + + if (baseOptions is CustomMuninNodeOptions options) + ExtraOption = options.ExtraOption; + } + } + + [Test] + public void AddNode_CustomOptionsType() + { + const string HostName = "munin-node.localhost"; + const string ExtraOptionValue = "foo"; + + var services = new ServiceCollection(); + + services.AddMunin(configure: builder => { + builder.AddNode( + configure: options => { + options.HostName = HostName; + options.ExtraOption = ExtraOptionValue; + } + ); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider + .GetRequiredService>() + .Get(name: HostName); + + Assert.That(options.HostName, Is.EqualTo(HostName)); + Assert.That(options.ExtraOption, Is.EqualTo(ExtraOptionValue)); + } } From 0e785ed9fc1d1a4fbd67c6a8645dcf3ace38067b Mon Sep 17 00:00:00 2001 From: smdn Date: Wed, 28 May 2025 23:07:22 +0900 Subject: [PATCH 3/6] add support for building with custom MuninNodeBuilder types --- .../IMuninServiceBuilderExtensions.cs | 38 +++++-- .../MuninNodeBuilder.cs | 24 ++++- .../IMuninServiceBuilderExtensions.cs | 101 +++++++++++++++++- 3 files changed, 150 insertions(+), 13 deletions(-) diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index f2facc0..1ee01e7 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -38,9 +38,13 @@ public static IMuninNodeBuilder AddNode( this IMuninServiceBuilder builder, Action configure ) - => AddNode( + => AddNode< + MuninNodeOptions, + MuninNodeBuilder + >( builder: builder ?? throw new ArgumentNullException(nameof(builder)), - configure: configure ?? throw new ArgumentNullException(nameof(configure)) + configure: configure ?? throw new ArgumentNullException(nameof(configure)), + createBuilder: static (serviceBuilder, serviceKey) => new(serviceBuilder, serviceKey) ); /// @@ -49,6 +53,9 @@ Action configure /// /// The extended type of to configure the Munin-Node. /// + /// + /// The extended type of to build the Munin-Node. + /// /// /// An that the built Munin-Node will be added to. /// @@ -56,25 +63,42 @@ Action configure /// An to setup to /// configure the Munin-Node to be built. /// + /// + /// An to create to build + /// the Munin-Node. + /// /// The current so that additional calls can be chained. - public static IMuninNodeBuilder AddNode( + /// + /// is , or + /// is , or + /// is . + /// + public static + IMuninNodeBuilder AddNode< + TMuninNodeOptions, + TMuninNodeBuilder + >( this IMuninServiceBuilder builder, - Action configure + Action configure, + Func createBuilder ) where TMuninNodeOptions : MuninNodeOptions, new() + where TMuninNodeBuilder : MuninNodeBuilder { if (builder is null) throw new ArgumentNullException(nameof(builder)); if (configure is null) throw new ArgumentNullException(nameof(configure)); + if (createBuilder is null) + throw new ArgumentNullException(nameof(createBuilder)); var configuredOptions = new TMuninNodeOptions(); configure(configuredOptions); - var nodeBuilder = new MuninNodeBuilder( - serviceBuilder: builder, - serviceKey: configuredOptions.HostName // use configured hostname as a service key and option name + var nodeBuilder = createBuilder( + /* serviceBuilder: */ builder, + /* serviceKey: */ configuredOptions.HostName // use configured hostname as a service key and option name ); _ = builder.Services.Configure( diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs index d19b93b..f8d7d21 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs @@ -23,7 +23,7 @@ public class MuninNodeBuilder : IMuninNodeBuilder { public IServiceCollection Services { get; } public string ServiceKey { get; } - internal MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) + protected internal MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) { Services = (serviceBuilder ?? throw new ArgumentNullException(nameof(serviceBuilder))).Services; ServiceKey = serviceKey ?? throw new ArgumentNullException(nameof(serviceKey)); @@ -72,8 +72,7 @@ public IMuninNode Build(IServiceProvider serviceProvider) if (serviceProvider is null) throw new ArgumentNullException(nameof(serviceProvider)); - return new DefaultMuninNode( - options: GetConfiguredOptions(serviceProvider), + return Build( pluginProvider: buildPluginProvider is null ? new PluginProvider( plugins: pluginFactories.Select(factory => factory(serviceProvider)).ToList(), @@ -81,7 +80,7 @@ public IMuninNode Build(IServiceProvider serviceProvider) ) : buildPluginProvider.Invoke(serviceProvider), listenerFactory: buildListenerFactory?.Invoke(serviceProvider), - logger: serviceProvider.GetService()?.CreateLogger() + serviceProvider: serviceProvider ); } @@ -99,6 +98,23 @@ public PluginProvider( } } + protected virtual IMuninNode Build( + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory, + IServiceProvider serviceProvider + ) + { + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + return new DefaultMuninNode( + options: GetConfiguredOptions(serviceProvider), + pluginProvider: pluginProvider, + listenerFactory: listenerFactory, + logger: serviceProvider.GetService()?.CreateLogger() + ); + } + protected TMuninNodeOptions GetConfiguredOptions(IServiceProvider serviceProvider) where TMuninNodeOptions : MuninNodeOptions { diff --git a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index 18ae8bd..2a112bb 100644 --- a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -8,6 +8,9 @@ using NUnit.Framework; +using Smdn.Net.MuninNode.Transport; +using Smdn.Net.MuninPlugin; + namespace Smdn.Net.MuninNode.DependencyInjection; [TestFixture] @@ -163,6 +166,91 @@ public void AddNode_Multiple_GetKeyedService(string hostName) Assert.That(node, Is.Not.SameAs(anotherNode)); } + private class CustomMuninNodeBuilder : MuninNodeBuilder + where TMuninNodeOptions : MuninNodeOptions + { + private readonly Func nodeFactory; + + public CustomMuninNodeBuilder( + IMuninServiceBuilder serviceBuilder, + string serviceKey, + Func nodeFactory + ) + : base(serviceBuilder, serviceKey) + { + this.nodeFactory = nodeFactory; + } + + protected override IMuninNode Build( + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory, + IServiceProvider serviceProvider + ) + => nodeFactory( + GetConfiguredOptions(serviceProvider), + pluginProvider, + listenerFactory, + serviceProvider + ); + } + + private class CustomMuninNode : LocalNode { + public MuninNodeOptions Options { get; } + public override string HostName => Options.HostName; + public override IPluginProvider PluginProvider { get; } + + public CustomMuninNode( + MuninNodeOptions options, + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory + ) + : base( + listenerFactory: listenerFactory, + accessRule: null, + logger: null + ) + { + Options = options; + PluginProvider = pluginProvider; + } + } + + [Test] + public void AddNode_CustomBuilderType() + { + const string HostName = "munin-node.localhost"; + var services = new ServiceCollection(); + + services.AddMunin(configure: builder => { + builder.AddNode>( + configure: options => options.HostName = HostName, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) + ); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var node = serviceProvider.GetService(); + + Assert.That(node, Is.Not.Null); + Assert.That(node, Is.TypeOf()); + Assert.That(node.HostName, Is.EqualTo(HostName)); + + var keyedNode = serviceProvider.GetKeyedService(HostName); + + Assert.That(keyedNode, Is.Not.Null); + Assert.That(keyedNode, Is.TypeOf()); + Assert.That(keyedNode.HostName, Is.EqualTo(HostName)); + } + private class CustomMuninNodeOptions : MuninNodeOptions { public string? ExtraOption { get; set; } @@ -184,11 +272,20 @@ public void AddNode_CustomOptionsType() var services = new ServiceCollection(); services.AddMunin(configure: builder => { - builder.AddNode( + builder.AddNode>( configure: options => { options.HostName = HostName; options.ExtraOption = ExtraOptionValue; - } + }, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) ); }); From a7a8c3973c7706c4f6df1974ac719988021ce36f Mon Sep 17 00:00:00 2001 From: smdn Date: Wed, 28 May 2025 23:27:57 +0900 Subject: [PATCH 4/6] add support for specifying the service type and implementation type of registering nodes --- .../IMuninNodeBuilderExtensions.cs | 18 +++ .../IMuninServiceBuilderExtensions.cs | 129 ++++++++++++++- .../IMuninServiceBuilderExtensions.cs | 149 ++++++++++++++++++ 3 files changed, 290 insertions(+), 6 deletions(-) diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs index bde44c5..ae73c9b 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs @@ -239,4 +239,22 @@ Func buildListenerFactory return builder; } + + internal static TMuninNode Build( + this IMuninNodeBuilder builder, + IServiceProvider serviceProvider + ) where TMuninNode : IMuninNode + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + var n = builder.Build(serviceProvider); + + if (n is not TMuninNode node) + throw new InvalidOperationException($"The type '{n.GetType()}' of the constructed instance did not match the requested type '{typeof(TMuninNode)}'."); + + return node; + } } diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index 1ee01e7..716cd66 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -39,6 +39,8 @@ public static IMuninNodeBuilder AddNode( Action configure ) => AddNode< + IMuninNode, + IMuninNode, MuninNodeOptions, MuninNodeBuilder >( @@ -67,6 +69,113 @@ Action configure /// An to create to build /// the Munin-Node. /// + /// The current so that additional calls can be chained. + /// + /// is , or + /// is , or + /// is . + /// + public static + TMuninNodeBuilder AddNode< + TMuninNodeOptions, + TMuninNodeBuilder + >( + this IMuninServiceBuilder builder, + Action configure, + Func createBuilder + ) + where TMuninNodeOptions : MuninNodeOptions, new() + where TMuninNodeBuilder : MuninNodeBuilder + => AddNode< + IMuninNode, + IMuninNode, + TMuninNodeOptions, + TMuninNodeBuilder + >( + builder: builder ?? throw new ArgumentNullException(nameof(builder)), + configure: configure ?? throw new ArgumentNullException(nameof(configure)), + createBuilder: createBuilder ?? throw new ArgumentNullException(nameof(createBuilder)) + ); + + /// + /// Adds a to the with specified configurations. + /// + /// + /// The type of service to add to the . + /// + /// + /// The extended type of to configure the . + /// + /// + /// The extended type of to build the . + /// + /// + /// An that the built will be added to. + /// + /// + /// An to setup to + /// configure the to be built. + /// + /// + /// An to create to build + /// the . + /// + /// The current so that additional calls can be chained. + /// + /// is , or + /// is , or + /// is . + /// + public static + TMuninNodeBuilder AddNode< + TMuninNode, + TMuninNodeOptions, + TMuninNodeBuilder + >( + this IMuninServiceBuilder builder, + Action configure, + Func createBuilder + ) + where TMuninNode : class, IMuninNode + where TMuninNodeOptions : MuninNodeOptions, new() + where TMuninNodeBuilder : MuninNodeBuilder + => AddNode< + TMuninNode, + TMuninNode, + TMuninNodeOptions, + TMuninNodeBuilder + >( + builder: builder ?? throw new ArgumentNullException(nameof(builder)), + configure: configure ?? throw new ArgumentNullException(nameof(configure)), + createBuilder: createBuilder ?? throw new ArgumentNullException(nameof(createBuilder)) + ); + + /// + /// Adds a to the with specified configurations. + /// + /// + /// The type of service to add to the . + /// + /// + /// The type of implementation. + /// + /// + /// The extended type of to configure the . + /// + /// + /// The extended type of to build the . + /// + /// + /// An that the built will be added to. + /// + /// + /// An to setup to + /// configure the to be built. + /// + /// + /// An to create to build + /// the . + /// /// The current so that additional calls can be chained. /// /// is , or @@ -74,7 +183,9 @@ Action configure /// is . /// public static - IMuninNodeBuilder AddNode< + TMuninNodeBuilder AddNode< + TMuninNodeService, + TMuninNodeImplementation, TMuninNodeOptions, TMuninNodeBuilder >( @@ -82,6 +193,8 @@ IMuninNodeBuilder AddNode< Action configure, Func createBuilder ) + where TMuninNodeService : class, IMuninNode + where TMuninNodeImplementation : class, TMuninNodeService where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder { @@ -107,7 +220,7 @@ Func createBuilder ); builder.Services.Add( - ServiceDescriptor.KeyedSingleton( + ServiceDescriptor.KeyedSingleton( serviceKey: nodeBuilder.ServiceKey, implementationFactory: (_, _) => nodeBuilder ) @@ -115,19 +228,23 @@ Func createBuilder // add keyed/singleton IMuninNode builder.Services.Add( - ServiceDescriptor.KeyedSingleton( + ServiceDescriptor.KeyedSingleton( serviceKey: nodeBuilder.ServiceKey, static (serviceProvider, serviceKey) - => serviceProvider.GetRequiredKeyedService(serviceKey).Build(serviceProvider) + => serviceProvider + .GetRequiredKeyedService(serviceKey) + .Build(serviceProvider) ) ); // add keyless/multiple IMuninNode +#pragma warning disable IDE0200 builder.Services.Add( - ServiceDescriptor.Transient( - nodeBuilder.Build + ServiceDescriptor.Transient( + serviceProvider => nodeBuilder.Build(serviceProvider) ) ); +#pragma warning restore IDE0200 return nodeBuilder; } diff --git a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index 2a112bb..ff83b2a 100644 --- a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -251,6 +251,133 @@ public void AddNode_CustomBuilderType() Assert.That(keyedNode.HostName, Is.EqualTo(HostName)); } + [Test] + public void AddNode_CustomBuilderType_AbstractServiceType() + { + const string HostName = "munin-node.localhost"; + var services = new ServiceCollection(); + + services.AddMunin(configure: builder => { + builder.AddNode>( + configure: options => options.HostName = HostName, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) + ); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var node = serviceProvider.GetService(); + + Assert.That(node, Is.Not.Null); + Assert.That(node, Is.TypeOf()); + Assert.That(node.HostName, Is.EqualTo(HostName)); + + var keyedNode = serviceProvider.GetKeyedService(HostName); + + Assert.That(keyedNode, Is.Not.Null); + Assert.That(keyedNode, Is.TypeOf()); + Assert.That(keyedNode.HostName, Is.EqualTo(HostName)); + } + + [Test] + public void AddNode_CustomBuilderType_ConcreteServiceType() + { + const string HostName = "munin-node.localhost"; + var services = new ServiceCollection(); + + services.AddMunin(configure: builder => { + builder.AddNode>( + configure: options => options.HostName = HostName, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) + ); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var node = serviceProvider.GetService(); + + Assert.That(node, Is.Not.Null); + Assert.That(node.HostName, Is.EqualTo(HostName)); + + var keyedNode = serviceProvider.GetKeyedService(HostName); + + Assert.That(keyedNode, Is.Not.Null); + Assert.That(keyedNode.HostName, Is.EqualTo(HostName)); + } + + private class ExtendedCustomMuninNode : CustomMuninNode { + public ExtendedCustomMuninNode( + MuninNodeOptions options, + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory + ) + : base( + options: options, + pluginProvider: pluginProvider, + listenerFactory: listenerFactory + ) + { + } + } + + [Test] + public void AddNode_CustomBuilderType_ConcreteServiceType_ImplementationTypeMismatch() + { + const string HostName = "munin-node.localhost"; + var services = new ServiceCollection(); + + services.AddMunin(configure: builder => { + builder.AddNode>( + configure: options => options.HostName = HostName, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) + ); + }); + + var serviceProvider = services.BuildServiceProvider(); + + Assert.That( + () => serviceProvider.GetService(), + Throws.InvalidOperationException + ); + Assert.That( + () => serviceProvider.GetService(), + Is.Null + ); + + Assert.That( + () => serviceProvider.GetKeyedService(HostName), + Throws.InvalidOperationException + ); + Assert.That( + () => serviceProvider.GetKeyedService(HostName), + Is.Null + ); + } + private class CustomMuninNodeOptions : MuninNodeOptions { public string? ExtraOption { get; set; } @@ -297,5 +424,27 @@ public void AddNode_CustomOptionsType() Assert.That(options.HostName, Is.EqualTo(HostName)); Assert.That(options.ExtraOption, Is.EqualTo(ExtraOptionValue)); + + var node = serviceProvider.GetService(); + + Assert.That(node, Is.Not.Null); + Assert.That(node, Is.TypeOf()); + + var customNode = (CustomMuninNode)node; + + Assert.That(customNode.Options, Is.TypeOf()); + Assert.That((customNode.Options as CustomMuninNodeOptions)!.ExtraOption, Is.EqualTo(ExtraOptionValue)); + Assert.That(customNode.HostName, Is.EqualTo(HostName)); + + var keyedNode = serviceProvider.GetKeyedService(HostName); + + Assert.That(keyedNode, Is.Not.Null); + Assert.That(keyedNode, Is.TypeOf()); + + var customKeyedNode = (CustomMuninNode)keyedNode; + + Assert.That(customKeyedNode.Options, Is.TypeOf()); + Assert.That((customKeyedNode.Options as CustomMuninNodeOptions)!.ExtraOption, Is.EqualTo(ExtraOptionValue)); + Assert.That(customKeyedNode.HostName, Is.EqualTo(HostName)); } } From b4fd88506ff203c74025ac1933cc49e436024cbc Mon Sep 17 00:00:00 2001 From: smdn Date: Sat, 31 May 2025 12:19:17 +0900 Subject: [PATCH 5/6] mark IMuninNodeBuilder obsolete --- .../IServiceCollectionExtensions.cs | 2 + .../IMuninNodeBuilder.cs | 1 + .../IMuninNodeBuilderExtensions.cs | 196 +++---------- .../IMuninServiceBuilderExtensions.cs | 4 + .../MuninNodeBuilder.cs | 24 ++ .../MuninNodeBuilderExtensions.cs | 258 ++++++++++++++++++ .../IMuninNodeBuilderExtensions.cs | 10 +- 7 files changed, 338 insertions(+), 157 deletions(-) create mode 100644 src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilderExtensions.cs diff --git a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs index 4a3b85a..92b2e3e 100644 --- a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs +++ b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs @@ -29,11 +29,13 @@ public static class IServiceCollectionExtensions { /// is , or /// is . /// +#pragma warning disable CS0618 // accept MuninNodeBuilder instead of IMuninNodeBuilder public static IServiceCollection AddHostedMuninNodeService( this IServiceCollection services, Action configureNode, Action buildNode ) +#pragma warning restore CS0618 { if (services is null) throw new ArgumentNullException(nameof(services)); diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilder.cs index b17d19c..cc82372 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilder.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilder.cs @@ -14,6 +14,7 @@ namespace Smdn.Net.MuninNode.DependencyInjection; /// /// /// +[Obsolete($"Use or inherit {nameof(MuninNodeBuilder)} instead.")] public interface IMuninNodeBuilder { /// /// Gets the where the Munin-Node services are configured. diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs index ae73c9b..6098230 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs @@ -11,7 +11,16 @@ namespace Smdn.Net.MuninNode.DependencyInjection; +[Obsolete($"Use {nameof(MuninNodeBuilderExtensions)} instead.")] public static class IMuninNodeBuilderExtensions { + private static MuninNodeBuilder ThrowIfBuilderTypeIsNotSupported(IMuninNodeBuilder builder) + { + if (builder is not MuninNodeBuilder muninNodeBuilder) + throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); + + return muninNodeBuilder; + } + #pragma warning disable CS0419 /// /// If is called, the configurations made by this method will be overridden. @@ -21,17 +30,10 @@ public static IMuninNodeBuilder AddPlugin( IPlugin plugin ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (plugin is null) - throw new ArgumentNullException(nameof(plugin)); - - return AddPlugin( - builder: builder, - buildPlugin: _ => plugin + => MuninNodeBuilderExtensions.AddPlugin( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + plugin: plugin ?? throw new ArgumentNullException(nameof(plugin)) ); - } #pragma warning disable CS0419 /// @@ -42,19 +44,10 @@ public static IMuninNodeBuilder AddPlugin( Func buildPlugin ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (buildPlugin is null) - throw new ArgumentNullException(nameof(buildPlugin)); - - if (builder is not MuninNodeBuilder muninNodeBuilder) - throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - - muninNodeBuilder.AddPluginFactory(buildPlugin); - - return builder; - } + => MuninNodeBuilderExtensions.AddPlugin( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + buildPlugin: buildPlugin ?? throw new ArgumentNullException(nameof(buildPlugin)) + ); #pragma warning disable CS0419 /// @@ -66,17 +59,10 @@ public static IMuninNodeBuilder UsePluginProvider( IPluginProvider pluginProvider ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (pluginProvider is null) - throw new ArgumentNullException(nameof(pluginProvider)); - - return UsePluginProvider( - builder: builder, - buildPluginProvider: _ => pluginProvider + => MuninNodeBuilderExtensions.UsePluginProvider( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + pluginProvider: pluginProvider ?? throw new ArgumentNullException(nameof(pluginProvider)) ); - } #pragma warning disable CS0419 /// @@ -88,19 +74,10 @@ public static IMuninNodeBuilder UsePluginProvider( Func buildPluginProvider ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (buildPluginProvider is null) - throw new ArgumentNullException(nameof(buildPluginProvider)); - - if (builder is not MuninNodeBuilder muninNodeBuilder) - throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - - muninNodeBuilder.SetPluginProviderFactory(buildPluginProvider); - - return builder; - } + => MuninNodeBuilderExtensions.UsePluginProvider( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + buildPluginProvider: buildPluginProvider ?? throw new ArgumentNullException(nameof(buildPluginProvider)) + ); #pragma warning disable CS0419 /// @@ -111,17 +88,10 @@ public static IMuninNodeBuilder UseSessionCallback( INodeSessionCallback sessionCallback ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (sessionCallback is null) - throw new ArgumentNullException(nameof(sessionCallback)); - - return UseSessionCallback( - builder: builder, - buildSessionCallback: _ => sessionCallback + => MuninNodeBuilderExtensions.UseSessionCallback( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + sessionCallback: sessionCallback ?? throw new ArgumentNullException(nameof(sessionCallback)) ); - } #pragma warning disable CS0419 /// @@ -133,29 +103,12 @@ public static IMuninNodeBuilder UseSessionCallback( Func? reportSessionClosedAsyncFunc ) #pragma warning restore CS0419 - => UseSessionCallback( - builder: builder, - buildSessionCallback: _ => new SessionCallbackFuncWrapper( - reportSessionStartedAsyncFunc, - reportSessionClosedAsyncFunc - ) + => MuninNodeBuilderExtensions.UseSessionCallback( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + reportSessionStartedAsyncFunc: reportSessionStartedAsyncFunc, + reportSessionClosedAsyncFunc: reportSessionClosedAsyncFunc ); - private sealed class SessionCallbackFuncWrapper( - Func? reportSessionStartedAsyncFunc, - Func? reportSessionClosedAsyncFunc - ) : INodeSessionCallback { - public ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) - => reportSessionStartedAsyncFunc is null - ? default - : reportSessionStartedAsyncFunc(sessionId, cancellationToken); - - public ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) - => reportSessionClosedAsyncFunc is null - ? default - : reportSessionClosedAsyncFunc(sessionId, cancellationToken); - } - #pragma warning disable CS0419 /// /// If is called, the configurations made by this method will be overridden. @@ -165,96 +118,35 @@ public static IMuninNodeBuilder UseSessionCallback( Func buildSessionCallback ) #pragma warning restore CS0419 - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (buildSessionCallback is null) - throw new ArgumentNullException(nameof(buildSessionCallback)); - - if (builder is not MuninNodeBuilder muninNodeBuilder) - throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - - muninNodeBuilder.SetSessionCallbackFactory(buildSessionCallback); - - return builder; - } + => MuninNodeBuilderExtensions.UseSessionCallback( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + buildSessionCallback: buildSessionCallback ?? throw new ArgumentNullException(nameof(buildSessionCallback)) + ); public static IMuninNodeBuilder UseListenerFactory( this IMuninNodeBuilder builder, IMuninNodeListenerFactory listenerFactory ) - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (listenerFactory is null) - throw new ArgumentNullException(nameof(listenerFactory)); - - return UseListenerFactory( - builder: builder, - buildListenerFactory: _ => listenerFactory + => MuninNodeBuilderExtensions.UseListenerFactory( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + listenerFactory: listenerFactory ?? throw new ArgumentNullException(nameof(listenerFactory)) ); - } public static IMuninNodeBuilder UseListenerFactory( this IMuninNodeBuilder builder, Func> createListenerAsyncFunc ) - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (createListenerAsyncFunc is null) - throw new ArgumentNullException(nameof(createListenerAsyncFunc)); - - return UseListenerFactory( - builder: builder, - buildListenerFactory: serviceProvider => new CreateListenerAsyncFuncWrapper( - serviceProvider, - createListenerAsyncFunc - ) + => MuninNodeBuilderExtensions.UseListenerFactory( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + createListenerAsyncFunc: createListenerAsyncFunc ?? throw new ArgumentNullException(nameof(createListenerAsyncFunc)) ); - } - - private sealed class CreateListenerAsyncFuncWrapper( - IServiceProvider serviceProvider, - Func> createListenerAsyncFunc - ) : IMuninNodeListenerFactory { - public ValueTask CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken) - => createListenerAsyncFunc(serviceProvider, endPoint, node, cancellationToken); - } public static IMuninNodeBuilder UseListenerFactory( this IMuninNodeBuilder builder, Func buildListenerFactory ) - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (buildListenerFactory is null) - throw new ArgumentNullException(nameof(buildListenerFactory)); - - if (builder is not MuninNodeBuilder muninNodeBuilder) - throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration."); - - muninNodeBuilder.SetListenerFactory(buildListenerFactory); - - return builder; - } - - internal static TMuninNode Build( - this IMuninNodeBuilder builder, - IServiceProvider serviceProvider - ) where TMuninNode : IMuninNode - { - if (builder is null) - throw new ArgumentNullException(nameof(builder)); - if (serviceProvider is null) - throw new ArgumentNullException(nameof(serviceProvider)); - - var n = builder.Build(serviceProvider); - - if (n is not TMuninNode node) - throw new InvalidOperationException($"The type '{n.GetType()}' of the constructed instance did not match the requested type '{typeof(TMuninNode)}'."); - - return node; - } + => MuninNodeBuilderExtensions.UseListenerFactory( + builder: ThrowIfBuilderTypeIsNotSupported(builder ?? throw new ArgumentNullException(nameof(builder))), + buildListenerFactory: buildListenerFactory ?? throw new ArgumentNullException(nameof(buildListenerFactory)) + ); } diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs index 716cd66..8579ac9 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs @@ -15,9 +15,11 @@ public static class IMuninServiceBuilderExtensions { /// An that the built Munin-Node will be added to. /// /// The current so that additional calls can be chained. +#pragma warning disable CS0618 // TODO: return IMuninNodeBuilder instead of MuninNodeBuilder public static IMuninNodeBuilder AddNode( this IMuninServiceBuilder builder ) +#pragma warning restore CS0618 => AddNode( builder: builder, configure: _ => { } @@ -34,10 +36,12 @@ this IMuninServiceBuilder builder /// configure the Munin-Node to be built. /// /// The current so that additional calls can be chained. +#pragma warning disable CS0618 // TODO: return IMuninNodeBuilder instead of MuninNodeBuilder public static IMuninNodeBuilder AddNode( this IMuninServiceBuilder builder, Action configure ) +#pragma warning restore CS0618 => AddNode< IMuninNode, IMuninNode, diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs index f8d7d21..b7505a0 100644 --- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs @@ -14,13 +14,30 @@ namespace Smdn.Net.MuninNode.DependencyInjection; +/// +/// Provides builder pattern for configuring and building the Munin-Node. +/// +/// +#pragma warning disable CS0618 // TODO: remove IMuninNodeBuilder public class MuninNodeBuilder : IMuninNodeBuilder { +#pragma warning restore CS0618 private readonly List> pluginFactories = new(capacity: 4); private Func? buildPluginProvider; private Func? buildSessionCallback; private Func? buildListenerFactory; + /// + /// Gets the where the Munin-Node services are configured. + /// public IServiceCollection Services { get; } + + /// + /// Gets the key of Munin-Node service. + /// + /// + /// The value set as the hostname of the Munin-Node (see ) is used as the service key. + /// + /// public string ServiceKey { get; } protected internal MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) @@ -67,6 +84,13 @@ Func buildListenerFactory this.buildListenerFactory = buildListenerFactory; } + /// + /// Builds the Munin-Node with current configurations. + /// + /// + /// An that provides the services to be used by the being built. + /// + /// An initialized . public IMuninNode Build(IServiceProvider serviceProvider) { if (serviceProvider is null) diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilderExtensions.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilderExtensions.cs new file mode 100644 index 0000000..f500ae1 --- /dev/null +++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilderExtensions.cs @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2025 smdn +// SPDX-License-Identifier: MIT + +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +using Smdn.Net.MuninNode.Transport; +using Smdn.Net.MuninPlugin; + +namespace Smdn.Net.MuninNode.DependencyInjection; + +public static class MuninNodeBuilderExtensions { +#pragma warning disable CS0419 + /// + /// If is called, the configurations made by this method will be overridden. + /// + public static TMuninNodeBuilder AddPlugin( + this TMuninNodeBuilder builder, + IPlugin plugin + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (plugin is null) + throw new ArgumentNullException(nameof(plugin)); + + return AddPlugin( + builder: builder, + buildPlugin: _ => plugin + ); + } + +#pragma warning disable CS0419 + /// + /// If is called, the configurations made by this method will be overridden. + /// + public static TMuninNodeBuilder AddPlugin( + this TMuninNodeBuilder builder, + Func buildPlugin + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (buildPlugin is null) + throw new ArgumentNullException(nameof(buildPlugin)); + + builder.AddPluginFactory(buildPlugin); + + return builder; + } + +#pragma warning disable CS0419 + /// + /// Calling this method will override the configurations made by + /// and . + /// + public static TMuninNodeBuilder UsePluginProvider( + this TMuninNodeBuilder builder, + IPluginProvider pluginProvider + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (pluginProvider is null) + throw new ArgumentNullException(nameof(pluginProvider)); + + return UsePluginProvider( + builder: builder, + buildPluginProvider: _ => pluginProvider + ); + } + +#pragma warning disable CS0419 + /// + /// Calling this method will override the configurations made by + /// and . + /// + public static TMuninNodeBuilder UsePluginProvider( + this TMuninNodeBuilder builder, + Func buildPluginProvider + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (buildPluginProvider is null) + throw new ArgumentNullException(nameof(buildPluginProvider)); + + builder.SetPluginProviderFactory(buildPluginProvider); + + return builder; + } + +#pragma warning disable CS0419 + /// + /// If is called, the configurations made by this method will be overridden. + /// + public static TMuninNodeBuilder UseSessionCallback( + this TMuninNodeBuilder builder, + INodeSessionCallback sessionCallback + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (sessionCallback is null) + throw new ArgumentNullException(nameof(sessionCallback)); + + return UseSessionCallback( + builder: builder, + buildSessionCallback: _ => sessionCallback + ); + } + +#pragma warning disable CS0419 + /// + /// If is called, the configurations made by this method will be overridden. + /// + public static TMuninNodeBuilder UseSessionCallback( + this TMuninNodeBuilder builder, + Func? reportSessionStartedAsyncFunc, + Func? reportSessionClosedAsyncFunc + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + => UseSessionCallback( + builder: builder, + buildSessionCallback: _ => new SessionCallbackFuncWrapper( + reportSessionStartedAsyncFunc, + reportSessionClosedAsyncFunc + ) + ); + + private sealed class SessionCallbackFuncWrapper( + Func? reportSessionStartedAsyncFunc, + Func? reportSessionClosedAsyncFunc + ) : INodeSessionCallback { + public ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) + => reportSessionStartedAsyncFunc is null + ? default + : reportSessionStartedAsyncFunc(sessionId, cancellationToken); + + public ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) + => reportSessionClosedAsyncFunc is null + ? default + : reportSessionClosedAsyncFunc(sessionId, cancellationToken); + } + +#pragma warning disable CS0419 + /// + /// If is called, the configurations made by this method will be overridden. + /// + public static TMuninNodeBuilder UseSessionCallback( + this TMuninNodeBuilder builder, + Func buildSessionCallback + ) + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore CS0419 + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (buildSessionCallback is null) + throw new ArgumentNullException(nameof(buildSessionCallback)); + + builder.SetSessionCallbackFactory(buildSessionCallback); + + return builder; + } + + public static TMuninNodeBuilder UseListenerFactory( + this TMuninNodeBuilder builder, + IMuninNodeListenerFactory listenerFactory + ) + where TMuninNodeBuilder : MuninNodeBuilder + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (listenerFactory is null) + throw new ArgumentNullException(nameof(listenerFactory)); + + return UseListenerFactory( + builder: builder, + buildListenerFactory: _ => listenerFactory + ); + } + + public static TMuninNodeBuilder UseListenerFactory( + this TMuninNodeBuilder builder, + Func> createListenerAsyncFunc + ) + where TMuninNodeBuilder : MuninNodeBuilder + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (createListenerAsyncFunc is null) + throw new ArgumentNullException(nameof(createListenerAsyncFunc)); + + return UseListenerFactory( + builder: builder, + buildListenerFactory: serviceProvider => new CreateListenerAsyncFuncWrapper( + serviceProvider, + createListenerAsyncFunc + ) + ); + } + + private sealed class CreateListenerAsyncFuncWrapper( + IServiceProvider serviceProvider, + Func> createListenerAsyncFunc + ) : IMuninNodeListenerFactory { + public ValueTask CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken) + => createListenerAsyncFunc(serviceProvider, endPoint, node, cancellationToken); + } + + public static TMuninNodeBuilder UseListenerFactory( + this TMuninNodeBuilder builder, + Func buildListenerFactory + ) + where TMuninNodeBuilder : MuninNodeBuilder + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (buildListenerFactory is null) + throw new ArgumentNullException(nameof(buildListenerFactory)); + + builder.SetListenerFactory(buildListenerFactory); + + return builder; + } + + public static TMuninNode Build( + this MuninNodeBuilder builder, + IServiceProvider serviceProvider + ) where TMuninNode : IMuninNode + { + if (builder is null) + throw new ArgumentNullException(nameof(builder)); + if (serviceProvider is null) + throw new ArgumentNullException(nameof(serviceProvider)); + + var n = builder.Build(serviceProvider); + + if (n is not TMuninNode node) + throw new InvalidOperationException($"The type '{n.GetType()}' of the constructed instance did not match the requested type '{typeof(TMuninNode)}'."); + + return node; + } +} diff --git a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs index 45c3bff..1c2e009 100644 --- a/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs +++ b/tests/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninNodeBuilderExtensions.cs @@ -306,7 +306,7 @@ public void UseSessionCallback_INodeSessionCallback() private static System.Collections.IEnumerable YieldTestCases_UseSessionCallback_FuncSessionCallback() { yield return new object[] { - (IMuninNodeBuilder builder) => { + (MuninNodeBuilder builder) => { builder.UseSessionCallback( reportSessionStartedAsyncFunc: null, reportSessionClosedAsyncFunc: null @@ -326,7 +326,7 @@ private static System.Collections.IEnumerable YieldTestCases_UseSessionCallback_ }; yield return new object[] { - (IMuninNodeBuilder builder) => { + (MuninNodeBuilder builder) => { builder.UseSessionCallback( reportSessionStartedAsyncFunc: (sessionId, ct) => throw new NotImplementedException($"sessionId={sessionId}"), reportSessionClosedAsyncFunc: null @@ -350,7 +350,7 @@ private static System.Collections.IEnumerable YieldTestCases_UseSessionCallback_ }; yield return new object[] { - (IMuninNodeBuilder builder) => { + (MuninNodeBuilder builder) => { builder.UseSessionCallback( reportSessionStartedAsyncFunc: null, reportSessionClosedAsyncFunc: (sessionId, ct) => throw new NotImplementedException($"sessionId={sessionId}") @@ -376,14 +376,14 @@ private static System.Collections.IEnumerable YieldTestCases_UseSessionCallback_ [TestCaseSource(nameof(YieldTestCases_UseSessionCallback_FuncSessionCallback))] public void UseSessionCallback_FuncSessionCallback( - Action callUseSessionCallback, + Action callUseSessionCallback, Action assertBuiltNode ) { var services = new ServiceCollection(); services.AddMunin( - builder => callUseSessionCallback(builder.AddNode(option => { })) + builder => callUseSessionCallback((MuninNodeBuilder)builder.AddNode(option => { })) ); var serviceProvider = services.BuildServiceProvider(); From 253dfda90defc960d8242dad78b079c62ccd5ddc Mon Sep 17 00:00:00 2001 From: smdn Date: Sat, 31 May 2025 13:47:27 +0900 Subject: [PATCH 6/6] add support for registering custom MuninNodeBackgroundService type --- .../Smdn.Net.MuninNode.Hosting.csproj | 2 +- .../IServiceCollectionExtensions.cs | 248 +++++++++++++++++- .../IServiceCollectionExtensions.cs | 171 ++++++++++++ 3 files changed, 408 insertions(+), 13 deletions(-) diff --git a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting.csproj b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting.csproj index 6ad094f..d62c293 100644 --- a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting.csproj +++ b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting.csproj @@ -34,7 +34,7 @@ This library uses [Smdn.Net.MuninNode](https://www.nuget.org/packages/Smdn.Net.M - + diff --git a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs index 92b2e3e..fc1893e 100644 --- a/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs +++ b/src/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs @@ -2,6 +2,9 @@ // SPDX-License-Identifier: MIT using System; +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_DYNAMICALLYACCESSEDMEMBERSATTRIBUTE +using System.Diagnostics.CodeAnalysis; +#endif using Microsoft.Extensions.DependencyInjection; @@ -20,8 +23,8 @@ public static class IServiceCollectionExtensions { /// configure the Munin-Node to be built. /// /// - /// An to build Munin-Node using with - /// the . + /// An to build Munin-Node using with + /// the . /// /// The current so that additional calls can be chained. /// @@ -36,36 +39,257 @@ public static IServiceCollection AddHostedMuninNodeService( Action buildNode ) #pragma warning restore CS0618 + => AddHostedMuninNodeService< + MuninNodeBackgroundService, + IMuninNode, + IMuninNode, + MuninNodeOptions, + DefaultMuninNodeBuilder + >( + services: services ?? throw new ArgumentNullException(nameof(services)), + configureNode: configureNode ?? throw new ArgumentNullException(nameof(configureNode)), + createNodeBuilder: static (serviceBuilder, serviceKey) => new(serviceBuilder, serviceKey), + buildNode: builder => (buildNode ?? throw new ArgumentNullException(nameof(buildNode)))(builder) + ); + + private class DefaultMuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) + : MuninNodeBuilder(serviceBuilder, serviceKey) { + } + + /// + /// Add , which runs as an + /// , to . + /// + /// + /// The type of service to add to the . + /// + /// + /// The type of service to add to the . + /// + /// + /// The extended type of to configure the . + /// + /// + /// The extended type of to build the . + /// + /// + /// An that the built and + /// will be added to. + /// + /// + /// An to setup to + /// configure the to be built. + /// + /// + /// An to create to build + /// the . + /// + /// + /// An to build using with + /// the . + /// + /// The current so that additional calls can be chained. + /// + /// is , or + /// is , or + /// is , or + /// is . + /// +#pragma warning disable IDE0055 + public static + IServiceCollection AddHostedMuninNodeService< +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_DYNAMICALLYACCESSEDMEMBERSATTRIBUTE + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + TMuninNodeBackgroundService, + TMuninNode, + TMuninNodeOptions, + TMuninNodeBuilder + >( + this IServiceCollection services, + Action configureNode, + Func createNodeBuilder, + Action buildNode + ) + where TMuninNodeBackgroundService : MuninNodeBackgroundService + where TMuninNode : class, IMuninNode + where TMuninNodeOptions : MuninNodeOptions, new() + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore IDE0055 + => AddHostedMuninNodeService< + TMuninNodeBackgroundService, + TMuninNode, + TMuninNode, + TMuninNodeOptions, + TMuninNodeBuilder + >( + services: services ?? throw new ArgumentNullException(nameof(services)), + configureNode: configureNode ?? throw new ArgumentNullException(nameof(configureNode)), + createNodeBuilder: createNodeBuilder ?? throw new ArgumentNullException(nameof(configureNode)), + buildNode: buildNode ?? throw new ArgumentNullException(nameof(buildNode)) + ); + + /// + /// Add , which runs as an + /// , to . + /// + /// + /// The type of service to add to the . + /// + /// + /// The type of service to add to the . + /// + /// + /// The type of implementation. + /// + /// + /// The extended type of to configure the . + /// + /// + /// The extended type of to build the . + /// + /// + /// An that the built and + /// will be added to. + /// + /// + /// An to setup to + /// configure the to be built. + /// + /// + /// An to create to build + /// the . + /// + /// + /// An to build using with + /// the . + /// + /// The current so that additional calls can be chained. + /// + /// is , or + /// is , or + /// is , or + /// is . + /// +#pragma warning disable IDE0055 + public static + IServiceCollection AddHostedMuninNodeService< +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_DYNAMICALLYACCESSEDMEMBERSATTRIBUTE + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + TMuninNodeBackgroundService, + TMuninNodeService, + TMuninNodeImplementation, + TMuninNodeOptions, + TMuninNodeBuilder + >( + this IServiceCollection services, + Action configureNode, + Func createNodeBuilder, + Action buildNode + ) + where TMuninNodeBackgroundService : MuninNodeBackgroundService + where TMuninNodeService : class, IMuninNode + where TMuninNodeImplementation : class, TMuninNodeService + where TMuninNodeOptions : MuninNodeOptions, new() + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore IDE0055 { if (services is null) throw new ArgumentNullException(nameof(services)); if (configureNode is null) throw new ArgumentNullException(nameof(configureNode)); + if (createNodeBuilder is null) + throw new ArgumentNullException(nameof(createNodeBuilder)); if (buildNode is null) throw new ArgumentNullException(nameof(buildNode)); - return services.AddMunin( - muninBuilder => { - var muninNodeBuilder = muninBuilder.AddNode(configureNode); + return AddHostedMuninNodeService( + services: services, + buildMunin: muninBuilder => { + var muninNodeBuilder = muninBuilder.AddNode< + TMuninNodeService, + TMuninNodeImplementation, + TMuninNodeOptions, + TMuninNodeBuilder + >( + configureNode, + createNodeBuilder + ); buildNode(muninNodeBuilder); - muninNodeBuilder.Services.AddHostedService(); + return muninNodeBuilder; + } + ); + } - // TODO: support keyed service -#if false - var muninNodeBuilder = muninBuilder.AddKeyedNode(configureNode); + /// + /// Add , which runs Munin-Node as an + /// , to . + /// + /// + /// The type of service to add to the . + /// + /// + /// The extended type of to build the Munin-Node. + /// + /// + /// An that the built and + /// Munin-Node will be added to. + /// + /// + /// A that registers at least one to + /// and returns , which builds the + /// to be registered. + /// + /// The current so that additional calls can be chained. + /// + /// is , or + /// is . + /// + /// + /// In future implementations, to be registered by + /// this method will use the same key as the of the + /// returned by the . + /// +#pragma warning disable IDE0055 + public static + IServiceCollection AddHostedMuninNodeService< +#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_DYNAMICALLYACCESSEDMEMBERSATTRIBUTE + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] +#endif + TMuninNodeBackgroundService, + TMuninNodeBuilder + >( + this IServiceCollection services, + Func buildMunin + ) + where TMuninNodeBackgroundService : MuninNodeBackgroundService + where TMuninNodeBuilder : MuninNodeBuilder +#pragma warning restore IDE0055 + { + if (services is null) + throw new ArgumentNullException(nameof(services)); + if (buildMunin is null) + throw new ArgumentNullException(nameof(buildMunin)); - buildNode(muninNodeBuilder); + return services.AddMunin( + muninBuilder => { + var muninNodeBuilder = buildMunin(muninBuilder); + + muninNodeBuilder.Services.AddHostedService(); + // TODO: support keyed service +#if false // these code does not work currently // https://github.com/dotnet/runtime/issues/99085 - muninNodeBuilder.Services.AddHostedService( + muninNodeBuilder.Services.AddHostedService( serviceKey: muninNodeBuilder.ServiceKey ); muninNodeBuilder.Services.TryAddEnumerable( - ServiceDescriptor.KeyedSingleton( + ServiceDescriptor.KeyedSingleton( serviceKey: muninNodeBuilder.ServiceKey ) ); diff --git a/tests/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs b/tests/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs index 8cd7078..1baacde 100644 --- a/tests/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs +++ b/tests/Smdn.Net.MuninNode.Hosting/Smdn.Net.MuninNode.Hosting/IServiceCollectionExtensions.cs @@ -4,9 +4,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using NUnit.Framework; +using Smdn.Net.MuninNode.DependencyInjection; +using Smdn.Net.MuninNode.Transport; +using Smdn.Net.MuninPlugin; + namespace Smdn.Net.MuninNode.Hosting; [TestFixture] @@ -56,4 +61,170 @@ public void AddHostedMuninNodeService() Assert.That(muninNodeService, Is.TypeOf()); } + + private class CustomMuninNodeBuilder : MuninNodeBuilder + where TMuninNodeOptions : MuninNodeOptions { + private readonly Func nodeFactory; + + public CustomMuninNodeBuilder( + IMuninServiceBuilder serviceBuilder, + string serviceKey, + Func nodeFactory + ) + : base(serviceBuilder, serviceKey) + { + this.nodeFactory = nodeFactory; + } + + protected override IMuninNode Build( + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory, + IServiceProvider serviceProvider + ) + => nodeFactory( + GetConfiguredOptions(serviceProvider), + pluginProvider, + listenerFactory, + serviceProvider + ); + } + + private class CustomMuninNode : LocalNode { + public CustomMuninNodeOptions Options { get; } + public override string HostName => Options.HostName; + public override IPluginProvider PluginProvider { get; } + + public string? ExtraOption => Options.ExtraOption; + + public CustomMuninNode( + CustomMuninNodeOptions options, + IPluginProvider pluginProvider, + IMuninNodeListenerFactory? listenerFactory + ) + : base( + listenerFactory: listenerFactory, + accessRule: null, + logger: null + ) + { + Options = options; + PluginProvider = pluginProvider; + } + } + + private class CustomMuninNodeOptions : MuninNodeOptions { + public string? ExtraOption { get; set; } + + protected override void Configure(MuninNodeOptions baseOptions) + { + base.Configure(baseOptions ?? throw new ArgumentNullException(nameof(baseOptions))); + + if (baseOptions is CustomMuninNodeOptions options) + ExtraOption = options.ExtraOption; + } + } + + private class CustomMuninNodeBackgroundService(CustomMuninNode node) : MuninNodeBackgroundService(node) { + public string? ExtraOption => node.ExtraOption; + } + + [Test] + public void AddHostedMuninNodeService_CustomBackgroundServiceType() + { + const string HostName = "munin-node.localhost"; + const string ExtraOptionValue = "foo"; + + var services = new ServiceCollection(); + + services.AddHostedMuninNodeService< + CustomMuninNodeBackgroundService, + CustomMuninNode, + CustomMuninNode, + CustomMuninNodeOptions, + CustomMuninNodeBuilder + >( + configureNode: options => { + options.HostName = HostName; + options.ExtraOption = ExtraOptionValue; + }, + createNodeBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ), + buildNode: nodeBuilder => { } + ); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider + .GetRequiredService>() + .Get(name: HostName); + + Assert.That(options.HostName, Is.EqualTo(HostName)); + Assert.That(options.ExtraOption, Is.EqualTo(ExtraOptionValue)); + + var muninNodeBackgroundService = serviceProvider.GetRequiredService(); + + Assert.That(muninNodeBackgroundService, Is.TypeOf()); + Assert.That( + (muninNodeBackgroundService as CustomMuninNodeBackgroundService)!.ExtraOption, + Is.EqualTo(ExtraOptionValue) + ); + } + + [Test] + public void AddHostedMuninNodeService_CustomBackgroundServiceType_WithIMuninServiceBuilder() + { + const string HostName = "munin-node.localhost"; + const string ExtraOptionValue = "foo"; + + var services = new ServiceCollection(); + + services.AddHostedMuninNodeService< + CustomMuninNodeBackgroundService, + CustomMuninNodeBuilder + >( + buildMunin: muninServiceBuilder => muninServiceBuilder.AddNode< + CustomMuninNode, + CustomMuninNodeOptions, + CustomMuninNodeBuilder + >( + configure: options => { + options.HostName = HostName; + options.ExtraOption = ExtraOptionValue; + }, + createBuilder: static (serviceBuilder, serviceKey) => new CustomMuninNodeBuilder( + serviceBuilder: serviceBuilder, + serviceKey: serviceKey, + nodeFactory: static (options, pluginProvider, listenerFactory, serviceProvider) => new CustomMuninNode( + options, + pluginProvider, + listenerFactory + ) + ) + ) + ); + + var serviceProvider = services.BuildServiceProvider(); + + var options = serviceProvider + .GetRequiredService>() + .Get(name: HostName); + + Assert.That(options.HostName, Is.EqualTo(HostName)); + Assert.That(options.ExtraOption, Is.EqualTo(ExtraOptionValue)); + + var muninNodeBackgroundService = serviceProvider.GetRequiredService(); + + Assert.That(muninNodeBackgroundService, Is.TypeOf()); + Assert.That( + (muninNodeBackgroundService as CustomMuninNodeBackgroundService)!.ExtraOption, + Is.EqualTo(ExtraOptionValue) + ); + } }