Skip to content

Smdn.Net.MuninNode version 2.5.0

Compare
Choose a tag to compare
@smdn smdn released this 01 Jun 09:40
· 33 commits to main since this release
d9b9375

Released package

Release notes

The full release notes are available at gist.

Change log

Change log in this release:

API changes

API changes in this release:
diff --git a/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-net8.0.apilist.cs b/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-net8.0.apilist.cs
index 3caafc0..a379024 100644
--- a/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-net8.0.apilist.cs
+++ b/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-net8.0.apilist.cs
@@ -1,509 +1,546 @@
-// Smdn.Net.MuninNode.dll (Smdn.Net.MuninNode-2.4.0)
+// Smdn.Net.MuninNode.dll (Smdn.Net.MuninNode-2.5.0)
 //   Name: Smdn.Net.MuninNode
-//   AssemblyVersion: 2.4.0.0
-//   InformationalVersion: 2.4.0+6578cec572157dafbc9518cc746aae28f7f1ce6d
+//   AssemblyVersion: 2.5.0.0
+//   InformationalVersion: 2.5.0+41ff114bf69b864033a05a389896010d3eefe4d5
 //   TargetFramework: .NETCoreApp,Version=v8.0
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Options, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     System.Collections, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Collections.Concurrent, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.ComponentModel, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.IO.Pipelines, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     System.Linq, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Memory, Version=8.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     System.Net.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Net.Sockets, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Runtime, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Security.Cryptography, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Text.RegularExpressions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 //     System.Threading, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 #nullable enable annotations
 
 using System;
 using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Net;
 using System.Net.Sockets;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Smdn.Net.MuninNode;
 using Smdn.Net.MuninNode.DependencyInjection;
 using Smdn.Net.MuninNode.Protocol;
 using Smdn.Net.MuninNode.Transport;
 using Smdn.Net.MuninPlugin;
 
 namespace Smdn.Net.MuninNode {
   public interface IAccessRule {
     bool IsAcceptable(IPEndPoint remoteEndPoint);
   }
 
   public interface IMuninNode {
     EndPoint EndPoint { get; }
     string HostName { get; }
 
     Task RunAsync(CancellationToken cancellationToken);
   }
 
   public static class IAccessRuleServiceCollectionExtensions {
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IAccessRule accessRule) {}
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IReadOnlyList<IPAddress> addressListAllowFrom) {}
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IReadOnlyList<IPAddress> addressListAllowFrom, bool shouldConsiderIPv4MappedIPv6Address) {}
     public static IServiceCollection AddMuninNodeLoopbackOnlyAccessRule(this IServiceCollection services) {}
   }
 
   public abstract class LocalNode : NodeBase {
     public static LocalNode Create(IPluginProvider pluginProvider, int port, string? hostName = null, IReadOnlyList<IPAddress>? addressListAllowFrom = null, IServiceProvider? serviceProvider = null) {}
     public static LocalNode Create(IReadOnlyCollection<IPlugin> plugins, int port, string? hostName = null, IReadOnlyList<IPAddress>? addressListAllowFrom = null, IServiceProvider? serviceProvider = null) {}
 
     [Obsolete("Use a constructor overload that takes IMuninNodeListenerFactory as an argument.")]
     protected LocalNode(IAccessRule? accessRule, ILogger? logger = null) {}
     protected LocalNode(IMuninNodeListenerFactory? listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
 
     [Obsolete("Use IMuninNodeListenerFactory and StartAsync instead.")]
     protected override Socket CreateServerSocket() {}
   }
 
-  public sealed class MuninNodeOptions {
+  public class MuninNodeOptions {
     public const string DefaultHostName = "munin-node.localhost";
     public const int DefaultPort = 4949;
 
     public static IPAddress DefaultAddress { get; }
 
     public MuninNodeOptions() {}
 
     public IAccessRule? AccessRule { get; set; }
     public IPAddress Address { get; set; }
     public string HostName { get; set; }
     public int Port { get; set; }
 
     public MuninNodeOptions AllowFrom(IReadOnlyList<IPAddress> addresses, bool shouldConsiderIPv4MappedIPv6Address = true) {}
     public MuninNodeOptions AllowFromLoopbackOnly() {}
+    internal protected virtual void Configure(MuninNodeOptions baseOptions) {}
     public MuninNodeOptions UseAnyAddress() {}
     public MuninNodeOptions UseAnyAddress(int port) {}
     public MuninNodeOptions UseLoopbackAddress() {}
     public MuninNodeOptions UseLoopbackAddress(int port) {}
   }
 
   public abstract class NodeBase :
     IAsyncDisposable,
     IDisposable,
     IMuninNode,
     IMuninNodeProfile
   {
     [Obsolete("Use a constructor overload that takes IMuninNodeListenerFactory as an argument.")]
     protected NodeBase(IAccessRule? accessRule, ILogger? logger) {}
     protected NodeBase(IMuninNodeListenerFactory listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
     protected NodeBase(IMuninProtocolHandlerFactory protocolHandlerFactory, IMuninNodeListenerFactory listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
 
     public virtual Encoding Encoding { get; }
     public EndPoint EndPoint { get; }
     public abstract string HostName { get; }
     protected IMuninNodeListener? Listener { get; }
     [Obsolete("Use EndPoint instead.")]
     public EndPoint LocalEndPoint { get; }
     protected ILogger? Logger { get; }
     public virtual Version NodeVersion { get; }
     public abstract IPluginProvider PluginProvider { get; }
     string IMuninNodeProfile.Version { get; }
 
     public async ValueTask AcceptAsync(bool throwIfCancellationRequested, CancellationToken cancellationToken) {}
     public async ValueTask AcceptSingleSessionAsync(CancellationToken cancellationToken = default) {}
     [Obsolete("Use IMuninNodeListenerFactory and StartAsync instead.")]
     protected virtual Socket CreateServerSocket() {}
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
     public async ValueTask DisposeAsync() {}
     protected virtual async ValueTask DisposeAsyncCore() {}
     protected virtual EndPoint GetLocalEndPointToBind() {}
     protected virtual IMuninNodeProfile GetNodeProfile() {}
     public Task RunAsync(CancellationToken cancellationToken) {}
     [Obsolete("This method will be deprecated in the future.Use IMuninNodeListenerFactory and StartAsync instead.Make sure to override CreateServerSocket if you need to use this method.")]
     public void Start() {}
     public ValueTask StartAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask StartedAsync(CancellationToken cancellationToken) {}
+    protected virtual ValueTask StartingAsync(CancellationToken cancellationToken) {}
     public ValueTask StopAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask StoppedAsync(CancellationToken cancellationToken) {}
+    protected virtual ValueTask StoppingAsync(CancellationToken cancellationToken) {}
     protected void ThrowIfDisposed() {}
     protected void ThrowIfPluginProviderIsNull() {}
   }
 }
 
 namespace Smdn.Net.MuninNode.DependencyInjection {
+  [Obsolete("Use or inherit MuninNodeBuilder instead.")]
   public interface IMuninNodeBuilder {
     string ServiceKey { get; }
     IServiceCollection Services { get; }
 
     IMuninNode Build(IServiceProvider serviceProvider);
   }
 
   public interface IMuninServiceBuilder {
     IServiceCollection Services { get; }
   }
 
+  [Obsolete("Use MuninNodeBuilderExtensions instead.")]
   public static class IMuninNodeBuilderExtensions {
     public static IMuninNodeBuilder AddPlugin(this IMuninNodeBuilder builder, Func<IServiceProvider, IPlugin> buildPlugin) {}
     public static IMuninNodeBuilder AddPlugin(this IMuninNodeBuilder builder, IPlugin plugin) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, Func<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, IMuninNodeListenerFactory listenerFactory) {}
     public static IMuninNodeBuilder UsePluginProvider(this IMuninNodeBuilder builder, Func<IServiceProvider, IPluginProvider> buildPluginProvider) {}
     public static IMuninNodeBuilder UsePluginProvider(this IMuninNodeBuilder builder, IPluginProvider pluginProvider) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, Func<IServiceProvider, INodeSessionCallback> buildSessionCallback) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc, Func<string, CancellationToken, ValueTask>? reportSessionClosedAsyncFunc) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, INodeSessionCallback sessionCallback) {}
   }
 
   public static class IMuninServiceBuilderExtensions {
     public static IMuninNodeBuilder AddNode(this IMuninServiceBuilder builder) {}
     public static IMuninNodeBuilder AddNode(this IMuninServiceBuilder builder, Action<MuninNodeOptions> configure) {}
+    public static TMuninNodeBuilder AddNode<TMuninNode, TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNode : class, IMuninNode where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddNode<TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddNode<TMuninNodeService, TMuninNodeImplementation, TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNodeService : class, IMuninNode where TMuninNodeImplementation : class, TMuninNodeService where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
   }
 
   public static class IServiceCollectionExtensions {
     public static IServiceCollection AddMunin(this IServiceCollection services, Action<IMuninServiceBuilder> configure) {}
   }
+
+  public class MuninNodeBuilder : IMuninNodeBuilder {
+    internal protected MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) {}
+
+    public string ServiceKey { get; }
+    public IServiceCollection Services { get; }
+
+    protected virtual IMuninNode Build(IPluginProvider pluginProvider, IMuninNodeListenerFactory? listenerFactory, IServiceProvider serviceProvider) {}
+    public IMuninNode Build(IServiceProvider serviceProvider) {}
+    protected TMuninNodeOptions GetConfiguredOptions<TMuninNodeOptions>(IServiceProvider serviceProvider) where TMuninNodeOptions : MuninNodeOptions {}
+  }
+
+  public static class MuninNodeBuilderExtensions {
+    public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IPlugin> buildPlugin) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IPlugin plugin) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNode Build<TMuninNode>(this MuninNodeBuilder builder, IServiceProvider serviceProvider) where TMuninNode : IMuninNode {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IMuninNodeListenerFactory listenerFactory) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IPluginProvider> buildPluginProvider) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IPluginProvider pluginProvider) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, INodeSessionCallback> buildSessionCallback) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc, Func<string, CancellationToken, ValueTask>? reportSessionClosedAsyncFunc) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, INodeSessionCallback sessionCallback) where TMuninNodeBuilder : MuninNodeBuilder {}
+  }
 }
 
 namespace Smdn.Net.MuninNode.Protocol {
   public interface IMuninNodeProfile {
     Encoding Encoding { get; }
     string HostName { get; }
     IPluginProvider PluginProvider { get; }
     string Version { get; }
   }
 
   public interface IMuninProtocolHandler {
     ValueTask HandleCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken);
     ValueTask HandleTransactionEndAsync(IMuninNodeClient client, CancellationToken cancellationToken);
     ValueTask HandleTransactionStartAsync(IMuninNodeClient client, CancellationToken cancellationToken);
   }
 
   public interface IMuninProtocolHandlerFactory {
     ValueTask<IMuninProtocolHandler> CreateAsync(IMuninNodeProfile profile, CancellationToken cancellationToken);
   }
 
   public class MuninProtocolHandler : IMuninProtocolHandler {
     public MuninProtocolHandler(IMuninNodeProfile profile) {}
 
     protected bool IsDirtyConfigEnabled { get; }
     protected bool IsMultigraphEnabled { get; }
 
     protected virtual ValueTask HandleCapCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     public ValueTask HandleCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleCommandAsyncCore(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleConfigCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual async ValueTask HandleFetchCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleListCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleNodesCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleQuitCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     public ValueTask HandleTransactionEndAsync(IMuninNodeClient client, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleTransactionEndAsyncCore(IMuninNodeClient client, CancellationToken cancellationToken) {}
     public ValueTask HandleTransactionStartAsync(IMuninNodeClient client, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleTransactionStartAsyncCore(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleVersionCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected ValueTask SendResponseAsync(IMuninNodeClient client, IEnumerable<string> responseLines, CancellationToken cancellationToken) {}
   }
 
   public static class MuninProtocolHandlerFactory {
     public static IMuninProtocolHandlerFactory Default { get; }
   }
 }
 
 namespace Smdn.Net.MuninNode.Transport {
   public interface IMuninNodeClient :
     IAsyncDisposable,
     IDisposable
   {
     EndPoint? EndPoint { get; }
 
     ValueTask DisconnectAsync(CancellationToken cancellationToken);
     ValueTask<int> ReceiveAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken);
     ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken);
   }
 
   public interface IMuninNodeListener :
     IAsyncDisposable,
     IDisposable
   {
     EndPoint? EndPoint { get; }
 
     ValueTask<IMuninNodeClient> AcceptAsync(CancellationToken cancellationToken);
     ValueTask StartAsync(CancellationToken cancellationToken);
   }
 
   public interface IMuninNodeListenerFactory {
     ValueTask<IMuninNodeListener> CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken);
   }
 
   public sealed class MuninNodeClientDisconnectedException : InvalidOperationException {
     public MuninNodeClientDisconnectedException() {}
     public MuninNodeClientDisconnectedException(string message) {}
     public MuninNodeClientDisconnectedException(string message, Exception innerException) {}
   }
 }
 
 namespace Smdn.Net.MuninPlugin {
   public interface IMultigraphPlugin : IPlugin {
     IReadOnlyCollection<IPlugin> Plugins { get; }
   }
 
   public interface INodeSessionCallback {
     ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken);
     ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken);
   }
 
   public interface IPlugin {
     IPluginDataSource DataSource { get; }
     IPluginGraphAttributes GraphAttributes { get; }
     string Name { get; }
     INodeSessionCallback? SessionCallback { get; }
   }
 
   public interface IPluginDataSource {
     IReadOnlyCollection<IPluginField> Fields { get; }
   }
 
   public interface IPluginField {
     PluginFieldAttributes Attributes { get; }
     string Name { get; }
 
     ValueTask<string> GetFormattedValueStringAsync(CancellationToken cancellationToken);
   }
 
   public interface IPluginGraphAttributes {
     IEnumerable<string> EnumerateAttributes();
   }
 
   public interface IPluginProvider {
     IReadOnlyCollection<IPlugin> Plugins { get; }
     INodeSessionCallback? SessionCallback { get; }
   }
 
   public enum PluginFieldGraphStyle : int {
     Area = 1,
     AreaStack = 3,
     Default = 0,
     Line = 100,
     LineStack = 200,
     LineStackWidth1 = 201,
     LineStackWidth2 = 202,
     LineStackWidth3 = 203,
     LineWidth1 = 101,
     LineWidth2 = 102,
     LineWidth3 = 103,
     Stack = 2,
   }
 
   public enum WellKnownCategory : int {
     AntiVirus = 2,
     ApplicationServer = 3,
     AuthenticationServer = 4,
     Backup = 5,
     Cloud = 7,
     ContentManagementSystem = 8,
     Cpu = 9,
     DatabaseServer = 10,
     DevelopmentTool = 11,
     Disk = 12,
     Dns = 13,
     FileSystem = 16,
     FileTransfer = 14,
     Forum = 15,
     GameServer = 18,
     HighThroughputComputing = 19,
     LoadBalancer = 20,
     Mail = 21,
     MailingList = 22,
     Memory = 23,
     MessagingServer = 6,
     Munin = 24,
     Network = 25,
     NetworkFiltering = 17,
     OneSec = 1,
     Other = 0,
     Printing = 26,
     Process = 27,
     Radio = 28,
     Search = 30,
     Security = 31,
     Sensor = 32,
     SpamFilter = 33,
     StorageAreaNetwork = 29,
     Streaming = 34,
     System = 35,
     TimeSynchronization = 36,
     Video = 37,
     Virtualization = 38,
     VoIP = 39,
     WebServer = 40,
     Wiki = 41,
     Wireless = 42,
   }
 
   public sealed class AggregatePluginProvider :
     ReadOnlyCollection<IPluginProvider>,
     INodeSessionCallback,
     IPluginProvider
   {
     public AggregatePluginProvider(IList<IPluginProvider> pluginProviders) {}
 
     public IReadOnlyCollection<IPlugin> Plugins { get; }
     INodeSessionCallback? IPluginProvider.SessionCallback { get; }
 
     async ValueTask INodeSessionCallback.ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     async ValueTask INodeSessionCallback.ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
   }
 
   public class MultigraphPlugin : IMultigraphPlugin {
     public MultigraphPlugin(string name, IReadOnlyCollection<IPlugin> plugins) {}
 
     public IPluginDataSource DataSource { get; }
     public IPluginGraphAttributes GraphAttributes { get; }
     public string Name { get; }
     public IReadOnlyCollection<IPlugin> Plugins { get; }
     public INodeSessionCallback? SessionCallback { get; }
   }
 
   public class Plugin :
     INodeSessionCallback,
     IPlugin,
     IPluginDataSource
   {
     public Plugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
 
     public IReadOnlyCollection<IPluginField> Fields { get; }
     public PluginGraphAttributes GraphAttributes { get; }
     public string Name { get; }
     IPluginDataSource IPlugin.DataSource { get; }
     IPluginGraphAttributes IPlugin.GraphAttributes { get; }
     INodeSessionCallback? IPlugin.SessionCallback { get; }
     IReadOnlyCollection<IPluginField> IPluginDataSource.Fields { get; }
 
     protected virtual ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     protected virtual ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
     ValueTask INodeSessionCallback.ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     ValueTask INodeSessionCallback.ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
   }
 
   public static class PluginFactory {
     public static IPluginField CreateField(string label, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string label, PluginFieldGraphStyle graphStyle, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string name, string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string name, string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName, Func<double?> fetchValue) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, IReadOnlyCollection<PluginFieldBase> fields) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, PluginFieldBase field) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<PluginFieldBase> fields) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, PluginFieldBase field) {}
     public static IPlugin CreatePlugin(string name, string fieldLabel, Func<double?> fetchFieldValue, IPluginGraphAttributes graphAttributes) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, string fieldLabel, Func<double?> fetchFieldValue, PluginGraphAttributes graphAttributes) {}
     public static IPlugin CreatePlugin(string name, string fieldLabel, PluginFieldGraphStyle fieldGraphStyle, Func<double?> fetchFieldValue, IPluginGraphAttributes graphAttributes) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, string fieldLabel, PluginFieldGraphStyle fieldGraphStyle, Func<double?> fetchFieldValue, PluginGraphAttributes graphAttributes) {}
   }
 
   public abstract class PluginFieldBase : IPluginField {
     protected PluginFieldBase(string label, string? name, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default, PluginFieldNormalValueRange normalRangeForWarning = default, PluginFieldNormalValueRange normalRangeForCritical = default) {}
     protected PluginFieldBase(string label, string? name, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName) {}
 
     public PluginFieldGraphStyle GraphStyle { get; }
     public string Label { get; }
     public string Name { get; }
     public string? NegativeFieldName { get; }
     public PluginFieldNormalValueRange NormalRangeForCritical { get; }
     public PluginFieldNormalValueRange NormalRangeForWarning { get; }
     PluginFieldAttributes IPluginField.Attributes { get; }
 
     protected abstract ValueTask<double?> FetchValueAsync(CancellationToken cancellationToken);
     async ValueTask<string> IPluginField.GetFormattedValueStringAsync(CancellationToken cancellationToken) {}
   }
 
   public sealed class PluginGraphAttributes : IPluginGraphAttributes {
     public PluginGraphAttributes(string title, string category, string verticalLabel, bool scale, string arguments) {}
     public PluginGraphAttributes(string title, string category, string verticalLabel, bool scale, string arguments, TimeSpan? updateRate, int? width, int? height, IEnumerable<string>? order, string? totalValueLabel) {}
 
     public string Arguments { get; }
     public string Category { get; }
     public int? Height { get; }
     public string? Order { get; }
     public bool Scale { get; }
     public string Title { get; }
     public string? TotalValueLabel { get; }
     public TimeSpan? UpdateRate { get; }
     public string VerticalLabel { get; }
     public int? Width { get; }
 
     public IEnumerable<string> EnumerateAttributes() {}
   }
 
   public class PluginGraphAttributesBuilder {
     public static Regex RegexCategory { get; }
     public static Regex RegexTitle { get; }
 
     public PluginGraphAttributesBuilder(string title) {}
     public PluginGraphAttributesBuilder(string title, PluginGraphAttributesBuilder baseBuilder) {}
 
     public PluginGraphAttributesBuilder AddGraphArgument(string argument) {}
     public IPluginGraphAttributes Build() {}
     public PluginGraphAttributesBuilder ClearGraphArguments() {}
     public PluginGraphAttributesBuilder DisableUnitScaling() {}
     public PluginGraphAttributesBuilder EnableUnitScaling() {}
     public PluginGraphAttributesBuilder HideGraph() {}
     public PluginGraphAttributesBuilder ShowGraph() {}
     public PluginGraphAttributesBuilder WithCategory(WellKnownCategory category) {}
     public PluginGraphAttributesBuilder WithCategory(string category) {}
     public PluginGraphAttributesBuilder WithCategoryOther() {}
     public PluginGraphAttributesBuilder WithFieldOrder(IEnumerable<string> order) {}
     public PluginGraphAttributesBuilder WithFormatString(string printf) {}
     public PluginGraphAttributesBuilder WithGraphBinaryBase() {}
     public PluginGraphAttributesBuilder WithGraphDecimalBase() {}
     public PluginGraphAttributesBuilder WithGraphLimit(double lowerLimitValue, double upperLimitValue) {}
     public PluginGraphAttributesBuilder WithGraphLogarithmic() {}
     public PluginGraphAttributesBuilder WithGraphLowerLimit(double @value) {}
     public PluginGraphAttributesBuilder WithGraphRigid() {}
     public PluginGraphAttributesBuilder WithGraphUpperLimit(double @value) {}
     public PluginGraphAttributesBuilder WithHeight(int height) {}
     public PluginGraphAttributesBuilder WithSize(int width, int height) {}
+    [MemberNotNull("title")]
+    public PluginGraphAttributesBuilder WithTitle(string title) {}
     public PluginGraphAttributesBuilder WithTotal(string labelForTotal) {}
     public PluginGraphAttributesBuilder WithUpdateRate(TimeSpan updateRate) {}
     public PluginGraphAttributesBuilder WithVerticalLabel(string verticalLabel) {}
     public PluginGraphAttributesBuilder WithWidth(int width) {}
   }
 
   public readonly struct PluginFieldAttributes {
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default) {}
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default, PluginFieldNormalValueRange normalRangeForWarning = default, PluginFieldNormalValueRange normalRangeForCritical = default) {}
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName) {}
 
     public PluginFieldGraphStyle GraphStyle { get; }
     public string Label { get; }
     public string? NegativeFieldName { get; }
     public PluginFieldNormalValueRange NormalRangeForCritical { get; }
     public PluginFieldNormalValueRange NormalRangeForWarning { get; }
   }
 
   public readonly struct PluginFieldNormalValueRange {
     public static readonly PluginFieldNormalValueRange None; // = "Smdn.Net.MuninPlugin.PluginFieldNormalValueRange"
 
     public static PluginFieldNormalValueRange CreateMax(double max) {}
     public static PluginFieldNormalValueRange CreateMin(double min) {}
     public static PluginFieldNormalValueRange CreateRange(double min, double max) {}
 
     public bool HasValue { get; }
     public double? Max { get; }
     public double? Min { get; }
   }
 }
 // API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.5.0.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.1.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)
diff --git a/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-netstandard2.1.apilist.cs b/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-netstandard2.1.apilist.cs
index 640a17f..1cf4e93 100644
--- a/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-netstandard2.1.apilist.cs
+++ b/doc/api-list/Smdn.Net.MuninNode/Smdn.Net.MuninNode-netstandard2.1.apilist.cs
@@ -1,501 +1,537 @@
-// Smdn.Net.MuninNode.dll (Smdn.Net.MuninNode-2.4.0)
+// Smdn.Net.MuninNode.dll (Smdn.Net.MuninNode-2.5.0)
 //   Name: Smdn.Net.MuninNode
-//   AssemblyVersion: 2.4.0.0
-//   InformationalVersion: 2.4.0+6578cec572157dafbc9518cc746aae28f7f1ce6d
+//   AssemblyVersion: 2.5.0.0
+//   InformationalVersion: 2.5.0+41ff114bf69b864033a05a389896010d3eefe4d5
 //   TargetFramework: .NETStandard,Version=v2.1
 //   Configuration: Release
 //   Referenced assemblies:
 //     Microsoft.Extensions.DependencyInjection.Abstractions, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Logging.Abstractions, Version=6.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Microsoft.Extensions.Options, Version=8.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60
 //     Smdn.Fundamental.Encoding.Buffer, Version=3.0.0.0, Culture=neutral
 //     Smdn.Fundamental.Exception, Version=3.0.0.0, Culture=neutral
 //     System.IO.Pipelines, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 //     netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51
 #nullable enable annotations
 
 using System;
 using System.Buffers;
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Net;
 using System.Net.Sockets;
 using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Logging;
 using Smdn.Net.MuninNode;
 using Smdn.Net.MuninNode.DependencyInjection;
 using Smdn.Net.MuninNode.Protocol;
 using Smdn.Net.MuninNode.Transport;
 using Smdn.Net.MuninPlugin;
 
 namespace Smdn.Net.MuninNode {
   public interface IAccessRule {
     bool IsAcceptable(IPEndPoint remoteEndPoint);
   }
 
   public interface IMuninNode {
     EndPoint EndPoint { get; }
     string HostName { get; }
 
     Task RunAsync(CancellationToken cancellationToken);
   }
 
   public static class IAccessRuleServiceCollectionExtensions {
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IAccessRule accessRule) {}
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IReadOnlyList<IPAddress> addressListAllowFrom) {}
     public static IServiceCollection AddMuninNodeAccessRule(this IServiceCollection services, IReadOnlyList<IPAddress> addressListAllowFrom, bool shouldConsiderIPv4MappedIPv6Address) {}
     public static IServiceCollection AddMuninNodeLoopbackOnlyAccessRule(this IServiceCollection services) {}
   }
 
   public abstract class LocalNode : NodeBase {
     public static LocalNode Create(IPluginProvider pluginProvider, int port, string? hostName = null, IReadOnlyList<IPAddress>? addressListAllowFrom = null, IServiceProvider? serviceProvider = null) {}
     public static LocalNode Create(IReadOnlyCollection<IPlugin> plugins, int port, string? hostName = null, IReadOnlyList<IPAddress>? addressListAllowFrom = null, IServiceProvider? serviceProvider = null) {}
 
     [Obsolete("Use a constructor overload that takes IMuninNodeListenerFactory as an argument.")]
     protected LocalNode(IAccessRule? accessRule, ILogger? logger = null) {}
     protected LocalNode(IMuninNodeListenerFactory? listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
 
     [Obsolete("Use IMuninNodeListenerFactory and StartAsync instead.")]
     protected override Socket CreateServerSocket() {}
   }
 
-  public sealed class MuninNodeOptions {
+  public class MuninNodeOptions {
     public const string DefaultHostName = "munin-node.localhost";
     public const int DefaultPort = 4949;
 
     public static IPAddress DefaultAddress { get; }
 
     public MuninNodeOptions() {}
 
     public IAccessRule? AccessRule { get; set; }
     public IPAddress Address { get; set; }
     public string HostName { get; set; }
     public int Port { get; set; }
 
     public MuninNodeOptions AllowFrom(IReadOnlyList<IPAddress> addresses, bool shouldConsiderIPv4MappedIPv6Address = true) {}
     public MuninNodeOptions AllowFromLoopbackOnly() {}
+    internal protected virtual void Configure(MuninNodeOptions baseOptions) {}
     public MuninNodeOptions UseAnyAddress() {}
     public MuninNodeOptions UseAnyAddress(int port) {}
     public MuninNodeOptions UseLoopbackAddress() {}
     public MuninNodeOptions UseLoopbackAddress(int port) {}
   }
 
   public abstract class NodeBase :
     IAsyncDisposable,
     IDisposable,
     IMuninNode,
     IMuninNodeProfile
   {
     [Obsolete("Use a constructor overload that takes IMuninNodeListenerFactory as an argument.")]
     protected NodeBase(IAccessRule? accessRule, ILogger? logger) {}
     protected NodeBase(IMuninNodeListenerFactory listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
     protected NodeBase(IMuninProtocolHandlerFactory protocolHandlerFactory, IMuninNodeListenerFactory listenerFactory, IAccessRule? accessRule, ILogger? logger) {}
 
     public virtual Encoding Encoding { get; }
     public EndPoint EndPoint { get; }
     public abstract string HostName { get; }
     protected IMuninNodeListener? Listener { get; }
     [Obsolete("Use EndPoint instead.")]
     public EndPoint LocalEndPoint { get; }
     protected ILogger? Logger { get; }
     public virtual Version NodeVersion { get; }
     public abstract IPluginProvider PluginProvider { get; }
     string IMuninNodeProfile.Version { get; }
 
     public async ValueTask AcceptAsync(bool throwIfCancellationRequested, CancellationToken cancellationToken) {}
     public async ValueTask AcceptSingleSessionAsync(CancellationToken cancellationToken = default) {}
     [Obsolete("Use IMuninNodeListenerFactory and StartAsync instead.")]
     protected virtual Socket CreateServerSocket() {}
     protected virtual void Dispose(bool disposing) {}
     public void Dispose() {}
     public async ValueTask DisposeAsync() {}
     protected virtual async ValueTask DisposeAsyncCore() {}
     protected virtual EndPoint GetLocalEndPointToBind() {}
     protected virtual IMuninNodeProfile GetNodeProfile() {}
     public Task RunAsync(CancellationToken cancellationToken) {}
     [Obsolete("This method will be deprecated in the future.Use IMuninNodeListenerFactory and StartAsync instead.Make sure to override CreateServerSocket if you need to use this method.")]
     public void Start() {}
     public ValueTask StartAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask StartedAsync(CancellationToken cancellationToken) {}
+    protected virtual ValueTask StartingAsync(CancellationToken cancellationToken) {}
     public ValueTask StopAsync(CancellationToken cancellationToken = default) {}
+    protected virtual ValueTask StoppedAsync(CancellationToken cancellationToken) {}
+    protected virtual ValueTask StoppingAsync(CancellationToken cancellationToken) {}
     protected void ThrowIfDisposed() {}
     protected void ThrowIfPluginProviderIsNull() {}
   }
 }
 
 namespace Smdn.Net.MuninNode.DependencyInjection {
+  [Obsolete("Use or inherit MuninNodeBuilder instead.")]
   public interface IMuninNodeBuilder {
     string ServiceKey { get; }
     IServiceCollection Services { get; }
 
     IMuninNode Build(IServiceProvider serviceProvider);
   }
 
   public interface IMuninServiceBuilder {
     IServiceCollection Services { get; }
   }
 
+  [Obsolete("Use MuninNodeBuilderExtensions instead.")]
   public static class IMuninNodeBuilderExtensions {
     public static IMuninNodeBuilder AddPlugin(this IMuninNodeBuilder builder, Func<IServiceProvider, IPlugin> buildPlugin) {}
     public static IMuninNodeBuilder AddPlugin(this IMuninNodeBuilder builder, IPlugin plugin) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, Func<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory) {}
     public static IMuninNodeBuilder UseListenerFactory(this IMuninNodeBuilder builder, IMuninNodeListenerFactory listenerFactory) {}
     public static IMuninNodeBuilder UsePluginProvider(this IMuninNodeBuilder builder, Func<IServiceProvider, IPluginProvider> buildPluginProvider) {}
     public static IMuninNodeBuilder UsePluginProvider(this IMuninNodeBuilder builder, IPluginProvider pluginProvider) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, Func<IServiceProvider, INodeSessionCallback> buildSessionCallback) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc, Func<string, CancellationToken, ValueTask>? reportSessionClosedAsyncFunc) {}
     public static IMuninNodeBuilder UseSessionCallback(this IMuninNodeBuilder builder, INodeSessionCallback sessionCallback) {}
   }
 
   public static class IMuninServiceBuilderExtensions {
     public static IMuninNodeBuilder AddNode(this IMuninServiceBuilder builder) {}
     public static IMuninNodeBuilder AddNode(this IMuninServiceBuilder builder, Action<MuninNodeOptions> configure) {}
+    public static TMuninNodeBuilder AddNode<TMuninNode, TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNode : class, IMuninNode where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddNode<TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddNode<TMuninNodeService, TMuninNodeImplementation, TMuninNodeOptions, TMuninNodeBuilder>(this IMuninServiceBuilder builder, Action<TMuninNodeOptions> configure, Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder) where TMuninNodeService : class, IMuninNode where TMuninNodeImplementation : class, TMuninNodeService where TMuninNodeOptions : MuninNodeOptions, new() where TMuninNodeBuilder : MuninNodeBuilder {}
   }
 
   public static class IServiceCollectionExtensions {
     public static IServiceCollection AddMunin(this IServiceCollection services, Action<IMuninServiceBuilder> configure) {}
   }
+
+  public class MuninNodeBuilder : IMuninNodeBuilder {
+    internal protected MuninNodeBuilder(IMuninServiceBuilder serviceBuilder, string serviceKey) {}
+
+    public string ServiceKey { get; }
+    public IServiceCollection Services { get; }
+
+    protected virtual IMuninNode Build(IPluginProvider pluginProvider, IMuninNodeListenerFactory? listenerFactory, IServiceProvider serviceProvider) {}
+    public IMuninNode Build(IServiceProvider serviceProvider) {}
+    protected TMuninNodeOptions GetConfiguredOptions<TMuninNodeOptions>(IServiceProvider serviceProvider) where TMuninNodeOptions : MuninNodeOptions {}
+  }
+
+  public static class MuninNodeBuilderExtensions {
+    public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IPlugin> buildPlugin) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IPlugin plugin) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNode Build<TMuninNode>(this MuninNodeBuilder builder, IServiceProvider serviceProvider) where TMuninNode : IMuninNode {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IMuninNodeListenerFactory listenerFactory) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, IPluginProvider> buildPluginProvider) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(this TMuninNodeBuilder builder, IPluginProvider pluginProvider) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<IServiceProvider, INodeSessionCallback> buildSessionCallback) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc, Func<string, CancellationToken, ValueTask>? reportSessionClosedAsyncFunc) where TMuninNodeBuilder : MuninNodeBuilder {}
+    public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(this TMuninNodeBuilder builder, INodeSessionCallback sessionCallback) where TMuninNodeBuilder : MuninNodeBuilder {}
+  }
 }
 
 namespace Smdn.Net.MuninNode.Protocol {
   public interface IMuninNodeProfile {
     Encoding Encoding { get; }
     string HostName { get; }
     IPluginProvider PluginProvider { get; }
     string Version { get; }
   }
 
   public interface IMuninProtocolHandler {
     ValueTask HandleCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken);
     ValueTask HandleTransactionEndAsync(IMuninNodeClient client, CancellationToken cancellationToken);
     ValueTask HandleTransactionStartAsync(IMuninNodeClient client, CancellationToken cancellationToken);
   }
 
   public interface IMuninProtocolHandlerFactory {
     ValueTask<IMuninProtocolHandler> CreateAsync(IMuninNodeProfile profile, CancellationToken cancellationToken);
   }
 
   public class MuninProtocolHandler : IMuninProtocolHandler {
     public MuninProtocolHandler(IMuninNodeProfile profile) {}
 
     protected bool IsDirtyConfigEnabled { get; }
     protected bool IsMultigraphEnabled { get; }
 
     protected virtual ValueTask HandleCapCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     public ValueTask HandleCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleCommandAsyncCore(IMuninNodeClient client, ReadOnlySequence<byte> commandLine, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleConfigCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual async ValueTask HandleFetchCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleListCommandAsync(IMuninNodeClient client, ReadOnlySequence<byte> arguments, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleNodesCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleQuitCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     public ValueTask HandleTransactionEndAsync(IMuninNodeClient client, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleTransactionEndAsyncCore(IMuninNodeClient client, CancellationToken cancellationToken) {}
     public ValueTask HandleTransactionStartAsync(IMuninNodeClient client, CancellationToken cancellationToken = default) {}
     protected virtual ValueTask HandleTransactionStartAsyncCore(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected virtual ValueTask HandleVersionCommandAsync(IMuninNodeClient client, CancellationToken cancellationToken) {}
     protected ValueTask SendResponseAsync(IMuninNodeClient client, IEnumerable<string> responseLines, CancellationToken cancellationToken) {}
   }
 
   public static class MuninProtocolHandlerFactory {
     public static IMuninProtocolHandlerFactory Default { get; }
   }
 }
 
 namespace Smdn.Net.MuninNode.Transport {
   public interface IMuninNodeClient :
     IAsyncDisposable,
     IDisposable
   {
     EndPoint? EndPoint { get; }
 
     ValueTask DisconnectAsync(CancellationToken cancellationToken);
     ValueTask<int> ReceiveAsync(IBufferWriter<byte> buffer, CancellationToken cancellationToken);
     ValueTask SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken);
   }
 
   public interface IMuninNodeListener :
     IAsyncDisposable,
     IDisposable
   {
     EndPoint? EndPoint { get; }
 
     ValueTask<IMuninNodeClient> AcceptAsync(CancellationToken cancellationToken);
     ValueTask StartAsync(CancellationToken cancellationToken);
   }
 
   public interface IMuninNodeListenerFactory {
     ValueTask<IMuninNodeListener> CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken);
   }
 
   public sealed class MuninNodeClientDisconnectedException : InvalidOperationException {
     public MuninNodeClientDisconnectedException() {}
     public MuninNodeClientDisconnectedException(string message) {}
     public MuninNodeClientDisconnectedException(string message, Exception innerException) {}
   }
 }
 
 namespace Smdn.Net.MuninPlugin {
   public interface IMultigraphPlugin : IPlugin {
     IReadOnlyCollection<IPlugin> Plugins { get; }
   }
 
   public interface INodeSessionCallback {
     ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken);
     ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken);
   }
 
   public interface IPlugin {
     IPluginDataSource DataSource { get; }
     IPluginGraphAttributes GraphAttributes { get; }
     string Name { get; }
     INodeSessionCallback? SessionCallback { get; }
   }
 
   public interface IPluginDataSource {
     IReadOnlyCollection<IPluginField> Fields { get; }
   }
 
   public interface IPluginField {
     PluginFieldAttributes Attributes { get; }
     string Name { get; }
 
     ValueTask<string> GetFormattedValueStringAsync(CancellationToken cancellationToken);
   }
 
   public interface IPluginGraphAttributes {
     IEnumerable<string> EnumerateAttributes();
   }
 
   public interface IPluginProvider {
     IReadOnlyCollection<IPlugin> Plugins { get; }
     INodeSessionCallback? SessionCallback { get; }
   }
 
   public enum PluginFieldGraphStyle : int {
     Area = 1,
     AreaStack = 3,
     Default = 0,
     Line = 100,
     LineStack = 200,
     LineStackWidth1 = 201,
     LineStackWidth2 = 202,
     LineStackWidth3 = 203,
     LineWidth1 = 101,
     LineWidth2 = 102,
     LineWidth3 = 103,
     Stack = 2,
   }
 
   public enum WellKnownCategory : int {
     AntiVirus = 2,
     ApplicationServer = 3,
     AuthenticationServer = 4,
     Backup = 5,
     Cloud = 7,
     ContentManagementSystem = 8,
     Cpu = 9,
     DatabaseServer = 10,
     DevelopmentTool = 11,
     Disk = 12,
     Dns = 13,
     FileSystem = 16,
     FileTransfer = 14,
     Forum = 15,
     GameServer = 18,
     HighThroughputComputing = 19,
     LoadBalancer = 20,
     Mail = 21,
     MailingList = 22,
     Memory = 23,
     MessagingServer = 6,
     Munin = 24,
     Network = 25,
     NetworkFiltering = 17,
     OneSec = 1,
     Other = 0,
     Printing = 26,
     Process = 27,
     Radio = 28,
     Search = 30,
     Security = 31,
     Sensor = 32,
     SpamFilter = 33,
     StorageAreaNetwork = 29,
     Streaming = 34,
     System = 35,
     TimeSynchronization = 36,
     Video = 37,
     Virtualization = 38,
     VoIP = 39,
     WebServer = 40,
     Wiki = 41,
     Wireless = 42,
   }
 
   public sealed class AggregatePluginProvider :
     ReadOnlyCollection<IPluginProvider>,
     INodeSessionCallback,
     IPluginProvider
   {
     public AggregatePluginProvider(IList<IPluginProvider> pluginProviders) {}
 
     public IReadOnlyCollection<IPlugin> Plugins { get; }
     INodeSessionCallback? IPluginProvider.SessionCallback { get; }
 
     async ValueTask INodeSessionCallback.ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     async ValueTask INodeSessionCallback.ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
   }
 
   public class MultigraphPlugin : IMultigraphPlugin {
     public MultigraphPlugin(string name, IReadOnlyCollection<IPlugin> plugins) {}
 
     public IPluginDataSource DataSource { get; }
     public IPluginGraphAttributes GraphAttributes { get; }
     public string Name { get; }
     public IReadOnlyCollection<IPlugin> Plugins { get; }
     public INodeSessionCallback? SessionCallback { get; }
   }
 
   public class Plugin :
     INodeSessionCallback,
     IPlugin,
     IPluginDataSource
   {
     public Plugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
 
     public IReadOnlyCollection<IPluginField> Fields { get; }
     public PluginGraphAttributes GraphAttributes { get; }
     public string Name { get; }
     IPluginDataSource IPlugin.DataSource { get; }
     IPluginGraphAttributes IPlugin.GraphAttributes { get; }
     INodeSessionCallback? IPlugin.SessionCallback { get; }
     IReadOnlyCollection<IPluginField> IPluginDataSource.Fields { get; }
 
     protected virtual ValueTask ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     protected virtual ValueTask ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
     ValueTask INodeSessionCallback.ReportSessionClosedAsync(string sessionId, CancellationToken cancellationToken) {}
     ValueTask INodeSessionCallback.ReportSessionStartedAsync(string sessionId, CancellationToken cancellationToken) {}
   }
 
   public static class PluginFactory {
     public static IPluginField CreateField(string label, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string label, PluginFieldGraphStyle graphStyle, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string name, string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, Func<double?> fetchValue) {}
     public static IPluginField CreateField(string name, string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName, Func<double?> fetchValue) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, IReadOnlyCollection<PluginFieldBase> fields) {}
     public static IPlugin CreatePlugin(string name, IPluginGraphAttributes graphAttributes, PluginFieldBase field) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<IPluginField> fields) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, IReadOnlyCollection<PluginFieldBase> fields) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, PluginGraphAttributes graphAttributes, PluginFieldBase field) {}
     public static IPlugin CreatePlugin(string name, string fieldLabel, Func<double?> fetchFieldValue, IPluginGraphAttributes graphAttributes) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, string fieldLabel, Func<double?> fetchFieldValue, PluginGraphAttributes graphAttributes) {}
     public static IPlugin CreatePlugin(string name, string fieldLabel, PluginFieldGraphStyle fieldGraphStyle, Func<double?> fetchFieldValue, IPluginGraphAttributes graphAttributes) {}
     [Obsolete("Use overloads that accept IPluginGraphAttributes instead.")]
     public static IPlugin CreatePlugin(string name, string fieldLabel, PluginFieldGraphStyle fieldGraphStyle, Func<double?> fetchFieldValue, PluginGraphAttributes graphAttributes) {}
   }
 
   public abstract class PluginFieldBase : IPluginField {
     protected PluginFieldBase(string label, string? name, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default, PluginFieldNormalValueRange normalRangeForWarning = default, PluginFieldNormalValueRange normalRangeForCritical = default) {}
     protected PluginFieldBase(string label, string? name, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName) {}
 
     public PluginFieldGraphStyle GraphStyle { get; }
     public string Label { get; }
     public string Name { get; }
     public string? NegativeFieldName { get; }
     public PluginFieldNormalValueRange NormalRangeForCritical { get; }
     public PluginFieldNormalValueRange NormalRangeForWarning { get; }
     PluginFieldAttributes IPluginField.Attributes { get; }
 
     protected abstract ValueTask<double?> FetchValueAsync(CancellationToken cancellationToken);
     async ValueTask<string> IPluginField.GetFormattedValueStringAsync(CancellationToken cancellationToken) {}
   }
 
   public sealed class PluginGraphAttributes : IPluginGraphAttributes {
     public PluginGraphAttributes(string title, string category, string verticalLabel, bool scale, string arguments) {}
     public PluginGraphAttributes(string title, string category, string verticalLabel, bool scale, string arguments, TimeSpan? updateRate, int? width, int? height, IEnumerable<string>? order, string? totalValueLabel) {}
 
     public string Arguments { get; }
     public string Category { get; }
     public int? Height { get; }
     public string? Order { get; }
     public bool Scale { get; }
     public string Title { get; }
     public string? TotalValueLabel { get; }
     public TimeSpan? UpdateRate { get; }
     public string VerticalLabel { get; }
     public int? Width { get; }
 
     public IEnumerable<string> EnumerateAttributes() {}
   }
 
   public class PluginGraphAttributesBuilder {
     public static Regex RegexCategory { get; }
     public static Regex RegexTitle { get; }
 
     public PluginGraphAttributesBuilder(string title) {}
     public PluginGraphAttributesBuilder(string title, PluginGraphAttributesBuilder baseBuilder) {}
 
     public PluginGraphAttributesBuilder AddGraphArgument(string argument) {}
     public IPluginGraphAttributes Build() {}
     public PluginGraphAttributesBuilder ClearGraphArguments() {}
     public PluginGraphAttributesBuilder DisableUnitScaling() {}
     public PluginGraphAttributesBuilder EnableUnitScaling() {}
     public PluginGraphAttributesBuilder HideGraph() {}
     public PluginGraphAttributesBuilder ShowGraph() {}
     public PluginGraphAttributesBuilder WithCategory(WellKnownCategory category) {}
     public PluginGraphAttributesBuilder WithCategory(string category) {}
     public PluginGraphAttributesBuilder WithCategoryOther() {}
     public PluginGraphAttributesBuilder WithFieldOrder(IEnumerable<string> order) {}
     public PluginGraphAttributesBuilder WithFormatString(string printf) {}
     public PluginGraphAttributesBuilder WithGraphBinaryBase() {}
     public PluginGraphAttributesBuilder WithGraphDecimalBase() {}
     public PluginGraphAttributesBuilder WithGraphLimit(double lowerLimitValue, double upperLimitValue) {}
     public PluginGraphAttributesBuilder WithGraphLogarithmic() {}
     public PluginGraphAttributesBuilder WithGraphLowerLimit(double @value) {}
     public PluginGraphAttributesBuilder WithGraphRigid() {}
     public PluginGraphAttributesBuilder WithGraphUpperLimit(double @value) {}
     public PluginGraphAttributesBuilder WithHeight(int height) {}
     public PluginGraphAttributesBuilder WithSize(int width, int height) {}
+    public PluginGraphAttributesBuilder WithTitle(string title) {}
     public PluginGraphAttributesBuilder WithTotal(string labelForTotal) {}
     public PluginGraphAttributesBuilder WithUpdateRate(TimeSpan updateRate) {}
     public PluginGraphAttributesBuilder WithVerticalLabel(string verticalLabel) {}
     public PluginGraphAttributesBuilder WithWidth(int width) {}
   }
 
   public readonly struct PluginFieldAttributes {
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default) {}
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle = PluginFieldGraphStyle.Default, PluginFieldNormalValueRange normalRangeForWarning = default, PluginFieldNormalValueRange normalRangeForCritical = default) {}
     public PluginFieldAttributes(string label, PluginFieldGraphStyle graphStyle, PluginFieldNormalValueRange normalRangeForWarning, PluginFieldNormalValueRange normalRangeForCritical, string? negativeFieldName) {}
 
     public PluginFieldGraphStyle GraphStyle { get; }
     public string Label { get; }
     public string? NegativeFieldName { get; }
     public PluginFieldNormalValueRange NormalRangeForCritical { get; }
     public PluginFieldNormalValueRange NormalRangeForWarning { get; }
   }
 
   public readonly struct PluginFieldNormalValueRange {
     public static readonly PluginFieldNormalValueRange None; // = "Smdn.Net.MuninPlugin.PluginFieldNormalValueRange"
 
     public static PluginFieldNormalValueRange CreateMax(double max) {}
     public static PluginFieldNormalValueRange CreateMin(double min) {}
     public static PluginFieldNormalValueRange CreateRange(double min, double max) {}
 
     public bool HasValue { get; }
     public double? Max { get; }
     public double? Min { get; }
   }
 }
 // API list generated by Smdn.Reflection.ReverseGenerating.ListApi.MSBuild.Tasks v1.5.0.0.
 // Smdn.Reflection.ReverseGenerating.ListApi.Core v1.3.1.0 (https://github.com/smdn/Smdn.Reflection.ReverseGenerating)

Full changes

Full changes in this release:
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;
 /// <see cref="IMuninNodeBuilderExtensions.AddPlugin"/>
 /// <see cref="IMuninNodeBuilderExtensions.UseListenerFactory"/>
 /// <see cref="IMuninNodeBuilderExtensions.UseSessionCallback"/>
+[Obsolete($"Use or inherit {nameof(MuninNodeBuilder)} instead.")]
 public interface IMuninNodeBuilder {
   /// <summary>
   /// Gets the <see cref="IServiceCollection"/> where the <c>Munin-Node</c> 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 07c8689..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 @@ using Smdn.Net.MuninPlugin;
 
 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
   /// <remarks>
   /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
@@ -21,17 +30,10 @@ public static class IMuninNodeBuilderExtensions {
     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
   /// <remarks>
@@ -42,19 +44,10 @@ public static class IMuninNodeBuilderExtensions {
     Func<IServiceProvider, IPlugin> 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 DefaultMuninNodeBuilder defaultMuninNodeBuilder)
-      throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration.");
-
-    defaultMuninNodeBuilder.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
   /// <remarks>
@@ -66,17 +59,10 @@ public static class IMuninNodeBuilderExtensions {
     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
   /// <remarks>
@@ -88,19 +74,10 @@ public static class IMuninNodeBuilderExtensions {
     Func<IServiceProvider, IPluginProvider> 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 DefaultMuninNodeBuilder defaultMuninNodeBuilder)
-      throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration.");
-
-    defaultMuninNodeBuilder.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
   /// <remarks>
@@ -111,17 +88,10 @@ public static class IMuninNodeBuilderExtensions {
     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
   /// <remarks>
@@ -133,29 +103,12 @@ public static class IMuninNodeBuilderExtensions {
     Func<string, CancellationToken, ValueTask>? 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<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc,
-    Func<string, CancellationToken, ValueTask>? 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
   /// <remarks>
   /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
@@ -165,78 +118,35 @@ public static class IMuninNodeBuilderExtensions {
     Func<IServiceProvider, INodeSessionCallback> 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 DefaultMuninNodeBuilder defaultMuninNodeBuilder)
-      throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration.");
-
-    defaultMuninNodeBuilder.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<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> 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<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc
-  ) : IMuninNodeListenerFactory {
-    public ValueTask<IMuninNodeListener> CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken)
-      => createListenerAsyncFunc(serviceProvider, endPoint, node, cancellationToken);
-  }
 
   public static IMuninNodeBuilder UseListenerFactory(
     this IMuninNodeBuilder builder,
     Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory
   )
-  {
-    if (builder is null)
-      throw new ArgumentNullException(nameof(builder));
-    if (buildListenerFactory is null)
-      throw new ArgumentNullException(nameof(buildListenerFactory));
-
-    if (builder is not DefaultMuninNodeBuilder defaultMuninNodeBuilder)
-      throw new NotSupportedException($"The builder implementation of type `{builder.GetType().FullName}` does not support service key configuration.");
-
-    defaultMuninNodeBuilder.SetListenerFactory(buildListenerFactory);
-
-    return builder;
-  }
+    => 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 e89bf75..8579ac9 100644
--- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs
+++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/IMuninServiceBuilderExtensions.cs
@@ -11,11 +11,15 @@ public static class IMuninServiceBuilderExtensions {
   /// <summary>
   /// Adds a <c>Munin-Node</c> to the <see cref="IMuninServiceBuilder"/> with default configurations.
   /// </summary>
-  /// <param name="builder">An <see cref="IMuninNodeBuilder"/> to build the <c>Munin-Node</c> to be added.</param>
+  /// <param name="builder">
+  /// An <see cref="IMuninServiceBuilder"/> that the built <c>Munin-Node</c> will be added to.
+  /// </param>
   /// <returns>The current <see cref="IMuninNodeBuilder"/> so that additional calls can be chained.</returns>
+#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: _ => { }
@@ -25,44 +29,202 @@ public static class IMuninServiceBuilderExtensions {
   /// Adds a <c>Munin-Node</c> to the <see cref="IMuninServiceBuilder"/> with specified configurations.
   /// </summary>
   /// <param name="builder">
-  /// An <see cref="IMuninNodeBuilder"/> to build the <c>Munin-Node</c> to be added.
+  /// An <see cref="IMuninServiceBuilder"/> that the built <c>Munin-Node</c> will be added to.
   /// </param>
   /// <param name="configure">
   /// An <see cref="Action{MuninNodeOptions}"/> to setup <see cref="MuninNodeOptions"/> to
   /// configure the <c>Munin-Node</c> to be built.
   /// </param>
   /// <returns>The current <see cref="IMuninNodeBuilder"/> so that additional calls can be chained.</returns>
+#pragma warning disable CS0618 // TODO: return IMuninNodeBuilder instead of MuninNodeBuilder
   public static IMuninNodeBuilder AddNode(
     this IMuninServiceBuilder builder,
     Action<MuninNodeOptions> configure
   )
+#pragma warning restore CS0618
+    => AddNode<
+      IMuninNode,
+      IMuninNode,
+      MuninNodeOptions,
+      MuninNodeBuilder
+    >(
+      builder: builder ?? throw new ArgumentNullException(nameof(builder)),
+      configure: configure ?? throw new ArgumentNullException(nameof(configure)),
+      createBuilder: static (serviceBuilder, serviceKey) => new(serviceBuilder, serviceKey)
+    );
+
+  /// <summary>
+  /// Adds a <c>Munin-Node</c> to the <see cref="IMuninServiceBuilder"/> with specified configurations.
+  /// </summary>
+  /// <typeparam name="TMuninNodeOptions">
+  /// The extended type of <see cref="MuninNodeOptions"/> to configure the <c>Munin-Node</c>.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeBuilder">
+  /// The extended type of <see cref="MuninNodeBuilder"/> to build the <c>Munin-Node</c>.
+  /// </typeparam>
+  /// <param name="builder">
+  /// An <see cref="IMuninServiceBuilder"/> that the built <c>Munin-Node</c> will be added to.
+  /// </param>
+  /// <param name="configure">
+  /// An <see cref="Action{TMuninNodeOptions}"/> to setup <typeparamref name="TMuninNodeOptions"/> to
+  /// configure the <c>Munin-Node</c> to be built.
+  /// </param>
+  /// <param name="createBuilder">
+  /// An <see cref="Func{TMuninNodeBuilder}"/> to create <typeparamref name="TMuninNodeBuilder"/> to build
+  /// the <c>Munin-Node</c>.
+  /// </param>
+  /// <returns>The current <typeparamref name="TMuninNodeBuilder"/> so that additional calls can be chained.</returns>
+  /// <exception cref="ArgumentNullException">
+  /// <paramref name="builder"/> is <see langword="null"/>, or
+  /// <paramref name="configure"/> is <see langword="null"/>, or
+  /// <paramref name="createBuilder"/> is <see langword="null"/>.
+  /// </exception>
+  public static
+  TMuninNodeBuilder AddNode<
+    TMuninNodeOptions,
+    TMuninNodeBuilder
+  >(
+    this IMuninServiceBuilder builder,
+    Action<TMuninNodeOptions> configure,
+    Func<IMuninServiceBuilder, string, TMuninNodeBuilder> 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))
+    );
+
+  /// <summary>
+  /// Adds a <typeparamref name="TMuninNode"/> to the <see cref="IMuninServiceBuilder"/> with specified configurations.
+  /// </summary>
+  /// <typeparam name="TMuninNode">
+  /// The type of <see cref="IMuninNode"/> service to add to the <seealso cref="IServiceCollection"/>.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeOptions">
+  /// The extended type of <see cref="MuninNodeOptions"/> to configure the <typeparamref name="TMuninNode"/>.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeBuilder">
+  /// The extended type of <see cref="MuninNodeBuilder"/> to build the <typeparamref name="TMuninNode"/>.
+  /// </typeparam>
+  /// <param name="builder">
+  /// An <see cref="IMuninServiceBuilder"/> that the built <typeparamref name="TMuninNode"/> will be added to.
+  /// </param>
+  /// <param name="configure">
+  /// An <see cref="Action{TMuninNodeOptions}"/> to setup <typeparamref name="TMuninNodeOptions"/> to
+  /// configure the <typeparamref name="TMuninNode"/> to be built.
+  /// </param>
+  /// <param name="createBuilder">
+  /// An <see cref="Func{TMuninNodeBuilder}"/> to create <typeparamref name="TMuninNodeBuilder"/> to build
+  /// the <typeparamref name="TMuninNode"/>.
+  /// </param>
+  /// <returns>The current <typeparamref name="TMuninNodeBuilder"/> so that additional calls can be chained.</returns>
+  /// <exception cref="ArgumentNullException">
+  /// <paramref name="builder"/> is <see langword="null"/>, or
+  /// <paramref name="configure"/> is <see langword="null"/>, or
+  /// <paramref name="createBuilder"/> is <see langword="null"/>.
+  /// </exception>
+  public static
+  TMuninNodeBuilder AddNode<
+    TMuninNode,
+    TMuninNodeOptions,
+    TMuninNodeBuilder
+  >(
+    this IMuninServiceBuilder builder,
+    Action<TMuninNodeOptions> configure,
+    Func<IMuninServiceBuilder, string, TMuninNodeBuilder> 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))
+    );
+
+  /// <summary>
+  /// Adds a <typeparamref name="TMuninNodeImplementation"/> to the <see cref="IMuninServiceBuilder"/> with specified configurations.
+  /// </summary>
+  /// <typeparam name="TMuninNodeService">
+  /// The type of <see cref="IMuninNode"/> service to add to the <seealso cref="IServiceCollection"/>.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeImplementation">
+  /// The type of <typeparamref name="TMuninNodeService"/> implementation.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeOptions">
+  /// The extended type of <see cref="MuninNodeOptions"/> to configure the <typeparamref name="TMuninNodeImplementation"/>.
+  /// </typeparam>
+  /// <typeparam name="TMuninNodeBuilder">
+  /// The extended type of <see cref="MuninNodeBuilder"/> to build the <typeparamref name="TMuninNodeImplementation"/>.
+  /// </typeparam>
+  /// <param name="builder">
+  /// An <see cref="IMuninServiceBuilder"/> that the built <typeparamref name="TMuninNodeImplementation"/> will be added to.
+  /// </param>
+  /// <param name="configure">
+  /// An <see cref="Action{TMuninNodeOptions}"/> to setup <typeparamref name="TMuninNodeOptions"/> to
+  /// configure the <typeparamref name="TMuninNodeImplementation"/> to be built.
+  /// </param>
+  /// <param name="createBuilder">
+  /// An <see cref="Func{TMuninNodeBuilder}"/> to create <typeparamref name="TMuninNodeBuilder"/> to build
+  /// the <typeparamref name="TMuninNodeImplementation"/>.
+  /// </param>
+  /// <returns>The current <see cref="IMuninNodeBuilder"/> so that additional calls can be chained.</returns>
+  /// <exception cref="ArgumentNullException">
+  /// <paramref name="builder"/> is <see langword="null"/>, or
+  /// <paramref name="configure"/> is <see langword="null"/>, or
+  /// <paramref name="createBuilder"/> is <see langword="null"/>.
+  /// </exception>
+  public static
+  TMuninNodeBuilder AddNode<
+    TMuninNodeService,
+    TMuninNodeImplementation,
+    TMuninNodeOptions,
+    TMuninNodeBuilder
+  >(
+    this IMuninServiceBuilder builder,
+    Action<TMuninNodeOptions> configure,
+    Func<IMuninServiceBuilder, string, TMuninNodeBuilder> createBuilder
+  )
+    where TMuninNodeService : class, IMuninNode
+    where TMuninNodeImplementation : class, TMuninNodeService
+    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 options = new MuninNodeOptions();
+    var configuredOptions = new TMuninNodeOptions();
 
-    configure(options);
+    configure(configuredOptions);
 
-    var nodeBuilder = new DefaultMuninNodeBuilder(
-      serviceBuilder: builder,
-      serviceKey: options.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<MuninNodeOptions>(
+    _ = builder.Services.Configure<TMuninNodeOptions>(
       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(
-      ServiceDescriptor.KeyedSingleton<IMuninNodeBuilder>(
+      ServiceDescriptor.KeyedSingleton<TMuninNodeBuilder>(
         serviceKey: nodeBuilder.ServiceKey,
         implementationFactory: (_, _) => nodeBuilder
       )
@@ -70,19 +232,23 @@ public static class IMuninServiceBuilderExtensions {
 
     // add keyed/singleton IMuninNode
     builder.Services.Add(
-      ServiceDescriptor.KeyedSingleton<IMuninNode>(
+      ServiceDescriptor.KeyedSingleton<TMuninNodeService, TMuninNodeImplementation>(
         serviceKey: nodeBuilder.ServiceKey,
         static (serviceProvider, serviceKey)
-            => serviceProvider.GetRequiredKeyedService<IMuninNodeBuilder>(serviceKey).Build(serviceProvider)
+          => serviceProvider
+            .GetRequiredKeyedService<TMuninNodeBuilder>(serviceKey)
+            .Build<TMuninNodeImplementation>(serviceProvider)
       )
     );
 
     // add keyless/multiple IMuninNode
+#pragma warning disable IDE0200
     builder.Services.Add(
-      ServiceDescriptor.Transient<IMuninNode>(
-        nodeBuilder.Build
+      ServiceDescriptor.Transient<TMuninNodeService, TMuninNodeImplementation>(
+        serviceProvider => nodeBuilder.Build<TMuninNodeImplementation>(serviceProvider)
       )
     );
+#pragma warning restore IDE0200
 
     return nodeBuilder;
   }
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 56%
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..b7505a0 100644
--- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/DefaultMuninNodeBuilder.cs
+++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.DependencyInjection/MuninNodeBuilder.cs
@@ -14,22 +14,39 @@ using Smdn.Net.MuninPlugin;
 
 namespace Smdn.Net.MuninNode.DependencyInjection;
 
-internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
+/// <summary>
+/// Provides builder pattern for configuring and building the <c>Munin-Node</c>.
+/// </summary>
+/// <seealso cref="MuninNodeBuilderExtensions"/>
+#pragma warning disable CS0618 // TODO: remove IMuninNodeBuilder
+public class MuninNodeBuilder : IMuninNodeBuilder {
+#pragma warning restore CS0618
   private readonly List<Func<IServiceProvider, IPlugin>> pluginFactories = new(capacity: 4);
   private Func<IServiceProvider, IPluginProvider>? buildPluginProvider;
   private Func<IServiceProvider, INodeSessionCallback>? buildSessionCallback;
   private Func<IServiceProvider, IMuninNodeListenerFactory>? buildListenerFactory;
 
+  /// <summary>
+  /// Gets the <see cref="IServiceCollection"/> where the <c>Munin-Node</c> services are configured.
+  /// </summary>
   public IServiceCollection Services { get; }
+
+  /// <summary>
+  /// Gets the <see cref="string"/> key of <c>Munin-Node</c> service.
+  /// </summary>
+  /// <remarks>
+  /// The value set as the hostname of the <c>Munin-Node</c> (see <see cref="MuninNodeOptions.HostName"/>) is used as the service key.
+  /// </remarks>
+  /// <see cref="IMuninServiceBuilderExtensions.AddNode(IMuninServiceBuilder, Action{MuninNodeOptions})"/>
   public string ServiceKey { get; }
 
-  public DefaultMuninNodeBuilder(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));
   }
 
-  public void AddPluginFactory(Func<IServiceProvider, IPlugin> buildPlugin)
+  internal void AddPluginFactory(Func<IServiceProvider, IPlugin> buildPlugin)
   {
     if (buildPlugin is null)
       throw new ArgumentNullException(nameof(buildPlugin));
@@ -37,7 +54,7 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
     pluginFactories.Add(serviceProvider => buildPlugin(serviceProvider));
   }
 
-  public void SetPluginProviderFactory(
+  internal void SetPluginProviderFactory(
     Func<IServiceProvider, IPluginProvider> buildPluginProvider
   )
   {
@@ -47,7 +64,7 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
     this.buildPluginProvider = buildPluginProvider;
   }
 
-  public void SetSessionCallbackFactory(
+  internal void SetSessionCallbackFactory(
     Func<IServiceProvider, INodeSessionCallback> buildSessionCallback
   )
   {
@@ -57,7 +74,7 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
     this.buildSessionCallback = buildSessionCallback;
   }
 
-  public void SetListenerFactory(
+  internal void SetListenerFactory(
     Func<IServiceProvider, IMuninNodeListenerFactory> buildListenerFactory
   )
   {
@@ -67,13 +84,19 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
     this.buildListenerFactory = buildListenerFactory;
   }
 
+  /// <summary>
+  /// Builds the <c>Munin-Node</c> with current configurations.
+  /// </summary>
+  /// <param name="serviceProvider">
+  /// An <see cref="IServiceProvider"/> that provides the services to be used by the <see cref="IMuninNode"/> being built.
+  /// </param>
+  /// <returns>An initialized <see cref="IMuninNode"/>.</returns>
   public IMuninNode Build(IServiceProvider serviceProvider)
   {
     if (serviceProvider is null)
       throw new ArgumentNullException(nameof(serviceProvider));
 
-    return new DefaultMuninNode(
-      options: serviceProvider.GetRequiredService<IOptionsMonitor<MuninNodeOptions>>().Get(name: ServiceKey),
+    return Build(
       pluginProvider: buildPluginProvider is null
         ? new PluginProvider(
             plugins: pluginFactories.Select(factory => factory(serviceProvider)).ToList(),
@@ -81,7 +104,7 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
           )
         : buildPluginProvider.Invoke(serviceProvider),
       listenerFactory: buildListenerFactory?.Invoke(serviceProvider),
-      logger: serviceProvider.GetService<ILoggerFactory>()?.CreateLogger<DefaultMuninNode>()
+      serviceProvider: serviceProvider
     );
   }
 
@@ -98,4 +121,32 @@ internal sealed class DefaultMuninNodeBuilder : IMuninNodeBuilder {
       SessionCallback = sessionCallback;
     }
   }
+
+  protected virtual IMuninNode Build(
+    IPluginProvider pluginProvider,
+    IMuninNodeListenerFactory? listenerFactory,
+    IServiceProvider serviceProvider
+  )
+  {
+    if (serviceProvider is null)
+      throw new ArgumentNullException(nameof(serviceProvider));
+
+    return new DefaultMuninNode(
+      options: GetConfiguredOptions<MuninNodeOptions>(serviceProvider),
+      pluginProvider: pluginProvider,
+      listenerFactory: listenerFactory,
+      logger: serviceProvider.GetService<ILoggerFactory>()?.CreateLogger<DefaultMuninNode>()
+    );
+  }
+
+  protected TMuninNodeOptions GetConfiguredOptions<TMuninNodeOptions>(IServiceProvider serviceProvider)
+    where TMuninNodeOptions : MuninNodeOptions
+  {
+    if (serviceProvider is null)
+      throw new ArgumentNullException(nameof(serviceProvider));
+
+    return serviceProvider
+      .GetRequiredService<IOptionsMonitor<TMuninNodeOptions>>()
+      .Get(name: ServiceKey);
+  }
 }
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 <smdn@smdn.jp>
+// 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
+  /// <remarks>
+  /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
+  /// </remarks>
+  public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(
+    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
+  /// <remarks>
+  /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
+  /// </remarks>
+  public static TMuninNodeBuilder AddPlugin<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<IServiceProvider, IPlugin> 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
+  /// <remarks>
+  /// Calling this method will override the configurations made by
+  /// <see cref="AddPlugin"/> and <see cref="UseSessionCallback"/>.
+  /// </remarks>
+  public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(
+    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
+  /// <remarks>
+  /// Calling this method will override the configurations made by
+  /// <see cref="AddPlugin"/> and <see cref="UseSessionCallback"/>.
+  /// </remarks>
+  public static TMuninNodeBuilder UsePluginProvider<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<IServiceProvider, IPluginProvider> 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
+  /// <remarks>
+  /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
+  /// </remarks>
+  public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(
+    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
+  /// <remarks>
+  /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
+  /// </remarks>
+  public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc,
+    Func<string, CancellationToken, ValueTask>? reportSessionClosedAsyncFunc
+  )
+    where TMuninNodeBuilder : MuninNodeBuilder
+#pragma warning restore CS0419
+    => UseSessionCallback(
+      builder: builder,
+      buildSessionCallback: _ => new SessionCallbackFuncWrapper(
+        reportSessionStartedAsyncFunc,
+        reportSessionClosedAsyncFunc
+      )
+    );
+
+  private sealed class SessionCallbackFuncWrapper(
+    Func<string, CancellationToken, ValueTask>? reportSessionStartedAsyncFunc,
+    Func<string, CancellationToken, ValueTask>? 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
+  /// <remarks>
+  /// If <see cref="UsePluginProvider"/> is called, the configurations made by this method will be overridden.
+  /// </remarks>
+  public static TMuninNodeBuilder UseSessionCallback<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<IServiceProvider, INodeSessionCallback> 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<TMuninNodeBuilder>(
+    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<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> 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<IServiceProvider, EndPoint, IMuninNode, CancellationToken, ValueTask<IMuninNodeListener>> createListenerAsyncFunc
+  ) : IMuninNodeListenerFactory {
+    public ValueTask<IMuninNodeListener> CreateAsync(EndPoint endPoint, IMuninNode node, CancellationToken cancellationToken)
+      => createListenerAsyncFunc(serviceProvider, endPoint, node, cancellationToken);
+  }
+
+  public static TMuninNodeBuilder UseListenerFactory<TMuninNodeBuilder>(
+    this TMuninNodeBuilder builder,
+    Func<IServiceProvider, IMuninNodeListenerFactory> 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<TMuninNode>(
+    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/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.csproj b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.csproj
index 6dd4084..dc0365e 100644
--- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.csproj
+++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode.csproj
@@ -5,7 +5,7 @@ SPDX-License-Identifier: MIT
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <TargetFrameworks>netstandard2.1;net8.0</TargetFrameworks>
-    <VersionPrefix>2.4.0</VersionPrefix>
+    <VersionPrefix>2.5.0</VersionPrefix>
     <VersionSuffix></VersionSuffix>
     <PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion>
     <RootNamespace/> <!-- empty the root namespace so that the namespace is determined only by the directory name, for code style rule IDE0130 -->
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 <c>Munin-Node</c>.
 /// </summary>
 /// <see cref="DependencyInjection.IMuninServiceBuilderExtensions.AddNode(DependencyInjection.IMuninServiceBuilder, Action{MuninNodeOptions})"/>
-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 sealed class MuninNodeOptions {
     => (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;
+  }
+
   /// <summary>
   /// Set the value of the <see cref="Address"/> property to use the address of
   /// <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/>.
diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs
index d8dd80c..57cc104 100644
--- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs
+++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninNode/NodeBase.cs
@@ -228,6 +228,10 @@ public abstract partial class NodeBase : IMuninNode, IMuninNodeProfile, IDisposa
       if (Logger is not null)
         LogStartingNode(Logger, null);
 
+      await StartingAsync(cancellationToken).ConfigureAwait(false);
+
+      cancellationToken.ThrowIfCancellationRequested();
+
       listener = await listenerFactory.CreateAsync(
         endPoint: GetLocalEndPointToBind(),
         node: this,
@@ -246,13 +250,47 @@ public abstract partial class NodeBase : IMuninNode, IMuninNodeProfile, IDisposa
         cancellationToken: cancellationToken
       ).ConfigureAwait(false);
 
+      sessionCountdownEvent.Reset();
+
+      await StartedAsync(cancellationToken).ConfigureAwait(false);
+
       if (Logger is not null)
         LogStartedNode(Logger, HostName, listener.EndPoint, null);
-
-      sessionCountdownEvent.Reset();
     }
   }
 
+  /// <summary>
+  /// Provides an extension point for starting the <c>Munin-Node</c> instance.
+  /// If overridden in a derived class, it is invoked before the <c>Munin-Node</c> is started
+  /// by a call to the <see cref="StartAsync"/> method.
+  /// </summary>
+  /// <param name="cancellationToken">
+  /// The <see cref="CancellationToken"/> to monitor for cancellation requests.
+  /// </param>
+  /// <returns>
+  /// The <see cref="ValueTask"/> that represents the asynchronous operation.
+  /// </returns>
+  /// <seealso cref="StartAsync"/>
+  /// <seealso cref="StartedAsync"/>
+  protected virtual ValueTask StartingAsync(CancellationToken cancellationToken)
+    => default; // nothing to do in this class
+
+  /// <summary>
+  /// Provides an extension point for starting the <c>Munin-Node</c> instance.
+  /// If overridden in a derived class, it is invoked after the <c>Munin-Node</c> is started
+  /// by a call to the <see cref="StartAsync"/> method.
+  /// </summary>
+  /// <param name="cancellationToken">
+  /// The <see cref="CancellationToken"/> to monitor for cancellation requests.
+  /// </param>
+  /// <returns>
+  /// The <see cref="ValueTask"/> that represents the asynchronous operation.
+  /// </returns>
+  /// <seealso cref="StartAsync"/>
+  /// <seealso cref="StartingAsync"/>
+  protected virtual ValueTask StartedAsync(CancellationToken cancellationToken)
+    => default; // nothing to do in this class
+
   /// <summary>
   /// Stops accepting connections from clients at the <c>Munin-Node</c> currently running.
   /// </summary>
@@ -277,9 +315,15 @@ public abstract partial class NodeBase : IMuninNode, IMuninNodeProfile, IDisposa
 
     async ValueTask StopAsyncCore()
     {
+      cancellationToken.ThrowIfCancellationRequested();
+
       if (Logger is not null)
         LogStoppingNode(Logger, HostName, null);
 
+      await StoppingAsync(cancellationToken).ConfigureAwait(false);
+
+      cancellationToken.ThrowIfCancellationRequested();
+
       // decrement by the initial value of 1 (re)set in Start()/StartAsync()
       sessionCountdownEvent.Signal();
 
@@ -307,11 +351,45 @@ public abstract partial class NodeBase : IMuninNode, IMuninNodeProfile, IDisposa
         protocolHandler = null;
       }
 
+      await StoppedAsync(cancellationToken).ConfigureAwait(false);
+
       if (Logger is not null)
         LogStoppedNode(Logger, HostName, null);
     }
   }
 
+  /// <summary>
+  /// Provides an extension point for stopping the <c>Munin-Node</c> instance.
+  /// If overridden in a derived class, it is invoked before the <c>Munin-Node</c> is stopped
+  /// by a call to the <see cref="StopAsync"/> method.
+  /// </summary>
+  /// <param name="cancellationToken">
+  /// The <see cref="CancellationToken"/> to monitor for cancellation requests.
+  /// </param>
+  /// <returns>
+  /// The <see cref="ValueTask"/> that represents the asynchronous operation.
+  /// </returns>
+  /// <seealso cref="StopAsync"/>
+  /// <seealso cref="StoppedAsync"/>
+  protected virtual ValueTask StoppingAsync(CancellationToken cancellationToken)
+    => default; // nothing to do in this class
+
+  /// <summary>
+  /// Provides an extension point for stopping the <c>Munin-Node</c> instance.
+  /// If overridden in a derived class, it is invoked after the <c>Munin-Node</c> is stopped
+  /// by a call to the <see cref="StopAsync"/> method.
+  /// </summary>
+  /// <param name="cancellationToken">
+  /// The <see cref="CancellationToken"/> to monitor for cancellation requests.
+  /// </param>
+  /// <returns>
+  /// The <see cref="ValueTask"/> that represents the asynchronous operation.
+  /// </returns>
+  /// <seealso cref="StopAsync"/>
+  /// <seealso cref="StoppingAsync"/>
+  protected virtual ValueTask StoppedAsync(CancellationToken cancellationToken)
+    => default; // nothing to do in this class
+
   /// <inheritdoc cref="IMuninNode.RunAsync(CancellationToken)"/>
   /// <seealso cref="IMuninNode.RunAsync(CancellationToken)"/>
   /// <seealso cref="StartAsync(CancellationToken)"/>
diff --git a/src/Smdn.Net.MuninNode/Smdn.Net.MuninPlugin/PluginGraphAttributesBuilder.cs b/src/Smdn.Net.MuninNode/Smdn.Net.MuninPlugin/PluginGraphAttributesBuilder.cs
index fc69ec9..deaf417 100644
--- a/src/Smdn.Net.MuninNode/Smdn.Net.MuninPlugin/PluginGraphAttributesBuilder.cs
+++ b/src/Smdn.Net.MuninNode/Smdn.Net.MuninPlugin/PluginGraphAttributesBuilder.cs
@@ -2,6 +2,9 @@
 // SPDX-License-Identifier: MIT
 using System;
 using System.Collections.Generic;
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+using System.Diagnostics.CodeAnalysis;
+#endif
 using System.Text.RegularExpressions;
 
 namespace Smdn.Net.MuninPlugin;
@@ -42,8 +45,7 @@ public partial class PluginGraphAttributesBuilder {
     }
   }
 
-  private readonly string title;
-
+  private string title;
   private bool? showGraph;
   private string? category;
   private int? height;
@@ -106,14 +108,14 @@ public partial class PluginGraphAttributesBuilder {
   /// <paramref name="title"/> contains invalid characters.
   /// </exception>
   /// <seealso href="https://guide.munin-monitoring.org/en/latest/reference/plugin.html#graph-title">Plugin reference - Global attributes - graph_title</seealso>
+#if !SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+#pragma warning disable CS8618
+#endif
   public PluginGraphAttributesBuilder(string title)
   {
-    ArgumentExceptionShim.ThrowIfNullOrWhiteSpace(title, nameof(title));
-
-    ThrowIfNotMatch(RegexTitle, title, nameof(title), "graph_title");
-
-    this.title = title;
+    WithTitle(title);
   }
+#pragma warning restore CS8618
 
   /// <summary>Sets a value for the <c>graph</c> to <c>yes</c>.</summary>
   /// <seealso href="https://guide.munin-monitoring.org/en/latest/reference/plugin.html#graph">Plugin reference - Global attributes - graph</seealso>
@@ -231,6 +233,27 @@ public partial class PluginGraphAttributesBuilder {
     return this;
   }
 
+  /// <summary>Overwrites a value for the <c>graph_title</c>.</summary>
+  /// <seealso href="https://guide.munin-monitoring.org/en/latest/reference/plugin.html#graph-title">Plugin reference - Global attributes - graph_title</seealso>
+  /// <exception cref="ArgumentNullException"><paramref name="title"/> is <see langword="null"/>.</exception>
+  /// <exception cref="ArgumentException">
+  /// <paramref name="title"/> is empty, or
+  /// <paramref name="title"/> contains invalid characters.
+  /// </exception>
+#if SYSTEM_DIAGNOSTICS_CODEANALYSIS_MEMBERNOTNULLATTRIBUTE
+  [MemberNotNull(nameof(title))]
+#endif
+  public PluginGraphAttributesBuilder WithTitle(string title)
+  {
+    ArgumentExceptionShim.ThrowIfNullOrWhiteSpace(title, nameof(title));
+
+    ThrowIfNotMatch(RegexTitle, title, nameof(title), "graph_title");
+
+    this.title = title;
+
+    return this;
+  }
+
   /// <summary>Sets a value for the <c>graph_total</c>.</summary>
   /// <seealso href="https://guide.munin-monitoring.org/en/latest/reference/plugin.html#graph-total">Plugin reference - Global attributes - graph_total</seealso>
   /// <exception cref="ArgumentNullException"><paramref name="labelForTotal"/> is <see langword="null"/>.</exception>

Notes

What's Changed

Notable changes

  • Improve DI-related APIs to support custom types by @smdn in #27

Full Changelog: releases/Smdn.Net.MuninNode-2.4.0...releases/Smdn.Net.MuninNode-2.5.0