diff --git a/sdk/node/Libplanet.Node.Executable/EntryPoints/DefaultEntryPoint.cs b/sdk/node/Libplanet.Node.Executable/EntryPoints/DefaultEntryPoint.cs new file mode 100644 index 00000000000..871f06b99e1 --- /dev/null +++ b/sdk/node/Libplanet.Node.Executable/EntryPoints/DefaultEntryPoint.cs @@ -0,0 +1,39 @@ +using Libplanet.Node.DependencyInjection; +using Libplanet.Node.Options.Schema; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace Libplanet.Node.API.EntryPoints; + +[Singleton] +[Singleton] +internal sealed class DefaultEntryPoint + : IApplicationEntryPoint, IApplicationBuilderEntryPoint +{ + private const string HandlerMessage = """ + Communication with gRPC endpoints must be made through a gRPC client. To learn how to + create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909 + """; + + public void Run(WebApplicationBuilder builder) + { + builder.Logging.AddConsole(); + + if (builder.Environment.IsDevelopment()) + { + builder.WebHost.ConfigureKestrel(options => + { + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(5259, o => o.Protocols = + HttpProtocols.Http1AndHttp2); + }); + } + } + + public async Task RunAsync(WebApplication app, CancellationToken cancellationToken) + { + var serviceProvider = app.Services; + var schema = await OptionsSchemaBuilder.GetSchemaAsync(cancellationToken); + app.MapGet("/", () => HandlerMessage); + app.MapGet("/schema", () => schema); + } +} diff --git a/sdk/node/Libplanet.Node.Executable/EntryPoints/GrpcServiceEntryPoint.cs b/sdk/node/Libplanet.Node.Executable/EntryPoints/GrpcServiceEntryPoint.cs new file mode 100644 index 00000000000..61970557fc1 --- /dev/null +++ b/sdk/node/Libplanet.Node.Executable/EntryPoints/GrpcServiceEntryPoint.cs @@ -0,0 +1,41 @@ +using Libplanet.Node.API.Extensions; +using Libplanet.Node.API.Options; +using Libplanet.Node.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Libplanet.Node.API.EntryPoints; + +[Singleton] +[Singleton] +internal sealed class GrpcServiceEntryPoint( + IOptions options, IWebHostEnvironment environment) + : IApplicationBuilderEntryPoint, IApplicationEntryPoint +{ + private readonly GrpcOptions _options = options.Value; + + public Task RunAsync(WebApplication app, CancellationToken cancellationToken) + { + var serviceProvider = app.Services; + var nodeBuilder = serviceProvider.GetRequiredService(); + app.MapGrpcServiceFromDomain(nodeBuilder.Scopes); + if (environment.IsDevelopment()) + { + app.MapGrpcReflectionService().AllowAnonymous(); + } + + return Task.CompletedTask; + } + + public void Run(WebApplicationBuilder builder) + { + if (_options.IsEnabled) + { + builder.Services.AddGrpc(); + + if (environment.IsDevelopment()) + { + builder.Services.AddGrpcReflection(); + } + } + } +} diff --git a/sdk/node/Libplanet.Node/Extensions/IEndpointRouteBuilderExtensions.cs b/sdk/node/Libplanet.Node.Executable/Extensions/IApplicationBuilderExtensions.cs similarity index 72% rename from sdk/node/Libplanet.Node/Extensions/IEndpointRouteBuilderExtensions.cs rename to sdk/node/Libplanet.Node.Executable/Extensions/IApplicationBuilderExtensions.cs index 5146c171ec6..6add9da3280 100644 --- a/sdk/node/Libplanet.Node/Extensions/IEndpointRouteBuilderExtensions.cs +++ b/sdk/node/Libplanet.Node.Executable/Extensions/IApplicationBuilderExtensions.cs @@ -1,18 +1,16 @@ using System.Reflection; using Libplanet.Node.DependencyInjection; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Routing; -namespace Libplanet.Node.Extensions; +namespace Libplanet.Node.API.Extensions; -public static class IEndpointRouteBuilderExtensions +internal static class IApplicationBuilderExtensions { - public static IEndpointRouteBuilder MapGrpcServiceFromDomain( - this IEndpointRouteBuilder @this) + public static IApplicationBuilder MapGrpcServiceFromDomain( + this IApplicationBuilder @this) => @this.MapGrpcServiceFromDomain(scope: string.Empty); - public static IEndpointRouteBuilder MapGrpcServiceFromDomain( - this IEndpointRouteBuilder @this, string scope) + public static IApplicationBuilder MapGrpcServiceFromDomain( + this IApplicationBuilder @this, string scope) { var types = ServiceUtility.GetTypes(typeof(GrpcAttribute), inherit: true); var methodType = typeof(GrpcEndpointRouteBuilderExtensions); @@ -38,8 +36,8 @@ static IEnumerable GetAttributes(Type type, string scope) return @this; } - public static IEndpointRouteBuilder MapGrpcServiceFromDomain( - this IEndpointRouteBuilder @this, string[] scopes) + public static IApplicationBuilder MapGrpcServiceFromDomain( + this IApplicationBuilder @this, string[] scopes) { foreach (var scope in scopes) { diff --git a/sdk/node/Libplanet.Node.Executable/GlobalUsings.cs b/sdk/node/Libplanet.Node.Executable/GlobalUsings.cs new file mode 100644 index 00000000000..de29ab2dc85 --- /dev/null +++ b/sdk/node/Libplanet.Node.Executable/GlobalUsings.cs @@ -0,0 +1 @@ +global using Libplanet.Node; diff --git a/sdk/node/Libplanet.Node.Executable/Options/GrpcOptions.cs b/sdk/node/Libplanet.Node.Executable/Options/GrpcOptions.cs new file mode 100644 index 00000000000..c9cd967fb54 --- /dev/null +++ b/sdk/node/Libplanet.Node.Executable/Options/GrpcOptions.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Libplanet.Node.DependencyInjection; +using Libplanet.Node.Options; + +namespace Libplanet.Node.API.Options; + +[Options(Position)] +public sealed class GrpcOptions : OptionsBase +{ + public const string Position = "Grpc"; + + [DefaultValue(true)] + public bool IsEnabled { get; set; } +} diff --git a/sdk/node/Libplanet.Node.Executable/Program.cs b/sdk/node/Libplanet.Node.Executable/Program.cs index e0fdbe7a147..b1027a3d8ba 100644 --- a/sdk/node/Libplanet.Node.Executable/Program.cs +++ b/sdk/node/Libplanet.Node.Executable/Program.cs @@ -1,48 +1,2 @@ -using Libplanet.Node.Extensions; -using Libplanet.Node.Options.Schema; -using Microsoft.AspNetCore.Server.Kestrel.Core; - -SynchronizationContext.SetSynchronizationContext(new()); -var builder = WebApplication.CreateBuilder(args); - -builder.Logging.AddConsole(); - -if (builder.Environment.IsDevelopment()) -{ - builder.WebHost.ConfigureKestrel(options => - { - // Setup a HTTP/2 endpoint without TLS. - options.ListenLocalhost(5259, o => o.Protocols = - HttpProtocols.Http1AndHttp2); - }); -} - -// Additional configuration is required to successfully run gRPC on macOS. -// For instructions on how to configure Kestrel and gRPC clients on macOS, -// visit https://go.microsoft.com/fwlink/?linkid=2099682 - -// Add services to the container. -builder.Services.AddGrpc(); -builder.Services.AddGrpcReflection(); -var libplanetBuilder = builder.Services.AddLibplanetNode(builder.Configuration) - .WithSeed() - .WithNode(); - -var app = builder.Build(); -var handlerMessage = """ - Communication with gRPC endpoints must be made through a gRPC client. To learn how to - create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909 - """; -var schema = await OptionsSchemaBuilder.GetSchemaAsync(default); - -// Configure the HTTP request pipeline. -app.MapGrpcServiceFromDomain(libplanetBuilder.Scopes); -app.MapGet("/", () => handlerMessage); -app.MapGet("/schema", () => schema); - -if (builder.Environment.IsDevelopment()) -{ - app.MapGrpcReflectionService().AllowAnonymous(); -} - +await using var app = new NodeApplication(args); await app.RunAsync(); diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json index 0e393589221..2b68e3ed3c3 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json @@ -1084,6 +1084,17 @@ } } }, + "Grpc": { + "title": "GrpcOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "IsEnabled": { + "type": "boolean", + "default": true + } + } + }, "Genesis": { "title": "GenesisOptions", "type": "object", @@ -1114,6 +1125,10 @@ "type": "object", "additionalProperties": false, "properties": { + "IsEnabled": { + "type": "boolean", + "default": true + }, "PrivateKey": { "type": "string", "description": "The private key of the node.", @@ -1136,6 +1151,9 @@ "type": "object", "additionalProperties": false, "properties": { + "IsEnabled": { + "type": "boolean" + }, "PrivateKey": { "type": "string", "description": "The private key of the seed node.", @@ -1174,6 +1192,9 @@ "type": "object", "additionalProperties": false, "properties": { + "IsEnabled": { + "type": "boolean" + }, "PrivateKey": { "type": "string", "description": "The private key of the seed node.", @@ -1256,6 +1277,10 @@ { "type": "object", "properties": { + "Grpc": { + "description": "Type 'GrpcOptions' does not have a description.", + "$ref": "#/definitions/Grpc" + }, "Genesis": { "description": "Options for the genesis block.", "$ref": "#/definitions/Genesis" diff --git a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json index b39b643db3b..99adfe68711 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json @@ -6,9 +6,22 @@ "Microsoft.AspNetCore": "Warning" } }, + "AllowedHosts": "*", "Kestrel": { "EndpointDefaults": { "Protocols": "Http1AndHttp2" } + }, + "Node": { + "IsEnabled": true + }, + "BlocksyncSeed": { + "IsEnabled": true + }, + "ConsensusSeed": { + "IsEnabled": true + }, + "Grpc": { + "IsEnabled": true } } diff --git a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/ILibplanetNodeBuilder.cs b/sdk/node/Libplanet.Node.Extensions/NodeBuilder/ILibplanetNodeBuilder.cs deleted file mode 100644 index 23dde235eb6..00000000000 --- a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/ILibplanetNodeBuilder.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Libplanet.Node.Extensions.NodeBuilder; - -public interface ILibplanetNodeBuilder -{ - IServiceCollection Services { get; } - - string[] Scopes { get; } - - ILibplanetNodeBuilder WithSolo(); - - ILibplanetNodeBuilder WithNode(); - - ILibplanetNodeBuilder WithValidate(); - - ILibplanetNodeBuilder WithSeed(); -} diff --git a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs b/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs deleted file mode 100644 index acc2da19a8f..00000000000 --- a/sdk/node/Libplanet.Node.Extensions/NodeBuilder/LibplanetNodeBuilder.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Libplanet.Node.Services; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace Libplanet.Node.Extensions.NodeBuilder; - -public class LibplanetNodeBuilder : ILibplanetNodeBuilder -{ - private readonly IConfiguration _configuration; - private readonly List _scopeList = [string.Empty]; - - internal LibplanetNodeBuilder(IServiceCollection services, IConfiguration configuration) - { - Services = services; - _configuration = configuration; - Services.AddSingletonsFromDomain(); - } - - public IServiceCollection Services { get; } - - public string[] Scopes => [.. _scopeList]; - - public ILibplanetNodeBuilder WithSolo() - { - Services.AddHostedService(); - return this; - } - - public ILibplanetNodeBuilder WithNode() - { - Services.AddSingletonsFromDomain(scope: "Node"); - Services.AddOptionsFromDomain(_configuration, scope: "Node"); - _scopeList.Add("Node"); - return this; - } - - public ILibplanetNodeBuilder WithValidate() => - this; - - public ILibplanetNodeBuilder WithSeed() - { - Services.AddSingletonsFromDomain(scope: "Seed"); - Services.AddOptionsFromDomain(_configuration, scope: "Seed"); - _scopeList.Add("Seed"); - return this; - } -} diff --git a/sdk/node/Libplanet.Node/DependencyInjection/IApplicationBuilderEntryPoint.cs b/sdk/node/Libplanet.Node/DependencyInjection/IApplicationBuilderEntryPoint.cs new file mode 100644 index 00000000000..877d8f8d7bc --- /dev/null +++ b/sdk/node/Libplanet.Node/DependencyInjection/IApplicationBuilderEntryPoint.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Builder; + +namespace Libplanet.Node.DependencyInjection; + +public interface IApplicationBuilderEntryPoint +{ + void Run(WebApplicationBuilder builder); +} diff --git a/sdk/node/Libplanet.Node/DependencyInjection/IApplicationEntryPoint.cs b/sdk/node/Libplanet.Node/DependencyInjection/IApplicationEntryPoint.cs new file mode 100644 index 00000000000..2592d205eb6 --- /dev/null +++ b/sdk/node/Libplanet.Node/DependencyInjection/IApplicationEntryPoint.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Builder; + +namespace Libplanet.Node.DependencyInjection; + +public interface IApplicationEntryPoint +{ + Task RunAsync(WebApplication app, CancellationToken cancellationToken); +} diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node/Extensions/LibplanetServicesExtensions.cs similarity index 73% rename from sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs rename to sdk/node/Libplanet.Node/Extensions/LibplanetServicesExtensions.cs index c6934494780..7e65ef5b822 100644 --- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs +++ b/sdk/node/Libplanet.Node/Extensions/LibplanetServicesExtensions.cs @@ -1,4 +1,3 @@ -using Libplanet.Node.Extensions.NodeBuilder; using Libplanet.Node.Options; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -7,7 +6,7 @@ namespace Libplanet.Node.Extensions; public static class LibplanetServicesExtensions { - public static ILibplanetNodeBuilder AddLibplanetNode( + public static INodeApplicationBuilder AddLibplanetNode( this IServiceCollection services, IConfiguration configuration) { @@ -16,6 +15,9 @@ public static ILibplanetNodeBuilder AddLibplanetNode( services.Configure(configuration.GetSection(SoloProposeOption.Position)); services.AddOptionsFromDomain(configuration); - return new LibplanetNodeBuilder(services, configuration); + var builder = new NodeApplicationBuilder(services, configuration); + services.AddSingleton(builder); + + return builder; } } diff --git a/sdk/node/Libplanet.Node/Extensions/ServiceCollectionExtensions.cs b/sdk/node/Libplanet.Node/Extensions/ServiceCollectionExtensions.cs index e324d350809..797cf2993e8 100644 --- a/sdk/node/Libplanet.Node/Extensions/ServiceCollectionExtensions.cs +++ b/sdk/node/Libplanet.Node/Extensions/ServiceCollectionExtensions.cs @@ -43,11 +43,13 @@ public static IServiceCollection AddOptionsFromDomain( foreach (var type in types) { var optionsNames = GetAttributes(type, scope).Select(item => item.Name); + var isSingleOption = optionsNames.Count() == 1; foreach (var optionsName in optionsNames) { - @this.AddOptions() - .Bind(configuration.GetSection(optionsName)) - .ValidateDataAnnotations(); + var name = isSingleOption ? string.Empty : optionsName; + var optionsBuilder1 = AddOptions(@this, type, name); + var optionsBuilder2 = Bind(optionsBuilder1, configuration.GetSection(optionsName)); + ValidateDataAnnotations(optionsBuilder2); } static IEnumerable GetAttributes(Type type, string scope) @@ -74,4 +76,67 @@ public static IServiceCollection AddOptionsValidator(this IServiceCollec var validatorType = typeof(IValidateOptions<>).MakeGenericType(typeof(TO)); return @this.AddSingleton(validatorType, typeof(TV)); } + + // Microsoft.Extensions.Options.OptionsBuilder`1[TOptions] AddOptions[TOptions]( + // Microsoft.Extensions.DependencyInjection.IServiceCollection, + // System.String) + private static object AddOptions( + IServiceCollection services, Type optionsType, string optionsName) + { + var type = typeof(OptionsServiceCollectionExtensions); + var methodName = nameof(OptionsServiceCollectionExtensions.AddOptions); + var args = new Type[] { typeof(IServiceCollection), typeof(string) }; + var methodInfo = type.GetMethod( + methodName, + genericParameterCount: 1, + types: args, + modifiers: null) + ?? throw new InvalidOperationException($"Method '{methodName}' not found."); + var genericMethodInfo = methodInfo.MakeGenericMethod(optionsType) + ?? throw new InvalidOperationException( + $"Method '{methodName}' failed to make generic method."); + + return genericMethodInfo.Invoke(null, parameters: [services, optionsName]) + ?? throw new InvalidOperationException($"Method '{methodName}' must not return null."); + } + + // Microsoft.Extensions.Options.OptionsBuilder`1[TOptions] Bind[TOptions]( + // Microsoft.Extensions.Options.OptionsBuilder`1[TOptions], + // Microsoft.Extensions.Configuration.IConfiguration) + private static object Bind(object optionsBuilder, IConfiguration configuration) + { + var optionsType = optionsBuilder.GetType().GenericTypeArguments[0]; + var type = typeof(OptionsBuilderConfigurationExtensions); + var methodName = nameof(OptionsBuilderConfigurationExtensions.Bind); + var args = new Type[] { typeof(OptionsBuilder<>), typeof(IConfiguration) }; + var methodInfo = type.GetMethods().Single( + method => method.Name == methodName + && method.GetParameters().Length == args.Length + && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == args[0]); + var genericMethodInfo = methodInfo.MakeGenericMethod(optionsType) + ?? throw new InvalidOperationException( + $"Method '{methodName}' failed to make generic method."); + + return genericMethodInfo.Invoke(null, parameters: [optionsBuilder, configuration]) + ?? throw new InvalidOperationException($"Method '{methodName}' must not return null."); + } + + // Microsoft.Extensions.Options.OptionsBuilder`1[TOptions] ValidateDataAnnotations[TOptions]( + // Microsoft.Extensions.Options.OptionsBuilder`1[TOptions]) + private static void ValidateDataAnnotations(object optionsBuilder) + { + var optionsType = optionsBuilder.GetType().GenericTypeArguments[0]; + var type = typeof(OptionsBuilderDataAnnotationsExtensions); + var methodName = nameof(OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations); + var args = new Type[] { typeof(OptionsBuilder<>) }; + var methodInfo = type.GetMethods().Single( + method => method.Name == methodName + && method.GetParameters().Length == args.Length + && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == args[0]); + var genericMethodInfo = methodInfo.MakeGenericMethod(optionsType) + ?? throw new InvalidOperationException( + $"Method '{methodName}' failed to make generic method."); + + genericMethodInfo.Invoke(null, parameters: [optionsBuilder]); + } } diff --git a/sdk/node/Libplanet.Node/INodeApplicationBuilder.cs b/sdk/node/Libplanet.Node/INodeApplicationBuilder.cs new file mode 100644 index 00000000000..d6e981e1053 --- /dev/null +++ b/sdk/node/Libplanet.Node/INodeApplicationBuilder.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Libplanet.Node; + +public interface INodeApplicationBuilder +{ + IServiceCollection Services { get; } + + string[] Scopes { get; } + + INodeApplicationBuilder WithSolo(); + + INodeApplicationBuilder WithNode(); + + INodeApplicationBuilder WithValidate(); + + INodeApplicationBuilder WithBlocksyncSeed(); + + INodeApplicationBuilder WithConsensusSeed(); +} diff --git a/sdk/node/Libplanet.Node/NodeApplication.cs b/sdk/node/Libplanet.Node/NodeApplication.cs new file mode 100644 index 00000000000..a837a941e60 --- /dev/null +++ b/sdk/node/Libplanet.Node/NodeApplication.cs @@ -0,0 +1,77 @@ +using Libplanet.Node.DependencyInjection; +using Libplanet.Node.Extensions; +using Libplanet.Node.Options; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Libplanet.Node; + +public sealed class NodeApplication : IAsyncDisposable +{ + private readonly WebApplicationBuilder _builder; + private WebApplication? _app; + + public NodeApplication(string[] args) + { + SynchronizationContext.SetSynchronizationContext(new()); + var builder = WebApplication.CreateBuilder(args); + var nodeBuilder = builder.Services.AddLibplanetNode(builder.Configuration); + var serviceProvider = builder.Services.BuildServiceProvider(); + var serviceEntryPoints + = serviceProvider.GetRequiredService>(); + var nodeOptions = serviceProvider.GetRequiredService>(); + var seedOptionsMonitor = serviceProvider.GetRequiredService>(); + + foreach (var serviceEntryPoint in serviceEntryPoints) + { + serviceEntryPoint.Run(builder); + } + + if (nodeOptions.Value.IsEnabled) + { + nodeBuilder.WithNode(); + } + + if (seedOptionsMonitor.Get(SeedOptions.BlocksyncSeed).IsEnabled) + { + nodeBuilder.WithBlocksyncSeed(); + } + + if (seedOptionsMonitor.Get(SeedOptions.ConsensusSeed).IsEnabled) + { + nodeBuilder.WithConsensusSeed(); + } + + _builder = builder; + } + + public async ValueTask DisposeAsync() + { + if (_app is not null) + { + await _app.DisposeAsync(); + _app = null; + } + } + + public async Task RunAsync() + { + _app = _builder.Build(); + await RunEntryPointAsync(_app, default); + await _app.RunAsync(); + } + + private static async Task RunEntryPointAsync( + WebApplication app, CancellationToken cancellationToken) + { + var serviceProvider = app.Services; + var applicationEntryPoints + = serviceProvider.GetRequiredService>(); + + foreach (var entryPoint in applicationEntryPoints) + { + await entryPoint.RunAsync(app, cancellationToken); + } + } +} diff --git a/sdk/node/Libplanet.Node/NodeApplicationBuilder.cs b/sdk/node/Libplanet.Node/NodeApplicationBuilder.cs new file mode 100644 index 00000000000..dc09ec66bd4 --- /dev/null +++ b/sdk/node/Libplanet.Node/NodeApplicationBuilder.cs @@ -0,0 +1,80 @@ +using System.Reflection; +using Libplanet.Node.Extensions; +using Libplanet.Node.Options; +using Libplanet.Node.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Libplanet.Node; + +public sealed class NodeApplicationBuilder : INodeApplicationBuilder +{ + private readonly IConfiguration _configuration; + private readonly List _scopeList = [string.Empty]; + + internal NodeApplicationBuilder(IServiceCollection services, IConfiguration configuration) + { + Services = services; + _configuration = configuration; + LoadPlugins(); + Services.AddSingletonsFromDomain(); + } + + public IServiceCollection Services { get; } + + public string[] Scopes => [.. _scopeList]; + + public INodeApplicationBuilder WithSolo() + { + Services.AddHostedService(); + return this; + } + + public INodeApplicationBuilder WithNode() + { + Services.AddSingletonsFromDomain(scope: "Node"); + Services.AddOptionsFromDomain(_configuration, scope: "Node"); + _scopeList.Add("Node"); + return this; + } + + public INodeApplicationBuilder WithValidate() => + this; + + public INodeApplicationBuilder WithBlocksyncSeed() + { + Services.AddSingletonsFromDomain(scope: SeedOptions.BlocksyncSeed); + Services.AddOptionsFromDomain(_configuration, scope: SeedOptions.BlocksyncSeed); + _scopeList.Add(SeedOptions.BlocksyncSeed); + return this; + } + + public INodeApplicationBuilder WithConsensusSeed() + { + Services.AddSingletonsFromDomain(scope: SeedOptions.ConsensusSeed); + Services.AddOptionsFromDomain(_configuration, scope: SeedOptions.ConsensusSeed); + _scopeList.Add(SeedOptions.ConsensusSeed); + return this; + } + + private static void LoadPlugins() + { + var directory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + ?? throw new InvalidOperationException( + "Failed to get the directory of the current process."); + var files = Directory.GetFiles(directory, "Libplanet.Node.*.dll"); + + foreach (var file in files) + { + var filename = Path.GetFileNameWithoutExtension(file); + try + { + var assembly = Assembly.Load(filename); + } + catch + { + // Ignore the exception. + } + } + } +} diff --git a/sdk/node/Libplanet.Node/Options/NodeOptions.cs b/sdk/node/Libplanet.Node/Options/NodeOptions.cs index 95df88e067f..6cdaf2668b2 100644 --- a/sdk/node/Libplanet.Node/Options/NodeOptions.cs +++ b/sdk/node/Libplanet.Node/Options/NodeOptions.cs @@ -4,11 +4,14 @@ namespace Libplanet.Node.Options; -[Options(Position, Scope = "Node")] +[Options(Position)] public sealed class NodeOptions : OptionsBase { public const string Position = "Node"; + [DefaultValue(true)] + public bool IsEnabled { get; set; } + [PrivateKey] [Description("The private key of the node.")] public string PrivateKey { get; set; } = string.Empty; diff --git a/sdk/node/Libplanet.Node/Options/NodeOptionsConfigurator.cs b/sdk/node/Libplanet.Node/Options/NodeOptionsConfigurator.cs index b0b98ccb6d4..8988c7ec1a0 100644 --- a/sdk/node/Libplanet.Node/Options/NodeOptionsConfigurator.cs +++ b/sdk/node/Libplanet.Node/Options/NodeOptionsConfigurator.cs @@ -6,7 +6,7 @@ namespace Libplanet.Node.Options; -[Singleton>(Scope = "Node")] +[Singleton>] internal sealed class NodeOptionsConfigurator( ILogger logger) : OptionsConfiguratorBase diff --git a/sdk/node/Libplanet.Node/Options/SeedOptions.cs b/sdk/node/Libplanet.Node/Options/SeedOptions.cs index 6a36d577a5f..67468456408 100644 --- a/sdk/node/Libplanet.Node/Options/SeedOptions.cs +++ b/sdk/node/Libplanet.Node/Options/SeedOptions.cs @@ -5,13 +5,15 @@ namespace Libplanet.Node.Options; -[Options(BlocksyncSeed, Scope = "Seed")] -[Options(ConsensusSeed, Scope = "Seed")] +[Options(BlocksyncSeed)] +[Options(ConsensusSeed)] public sealed class SeedOptions : OptionsBase { public const string BlocksyncSeed = nameof(BlocksyncSeed); public const string ConsensusSeed = nameof(ConsensusSeed); + public bool IsEnabled { get; set; } + [PrivateKey] [Description("The private key of the seed node.")] public string PrivateKey { get; set; } = string.Empty; diff --git a/sdk/node/Libplanet.Node/Options/SeedOptionsConfigurator.cs b/sdk/node/Libplanet.Node/Options/SeedOptionsConfigurator.cs index 10854a8289d..53b66b508bc 100644 --- a/sdk/node/Libplanet.Node/Options/SeedOptionsConfigurator.cs +++ b/sdk/node/Libplanet.Node/Options/SeedOptionsConfigurator.cs @@ -6,8 +6,8 @@ namespace Libplanet.Node.Options; -[Singleton>(Scope = "Seed")] -[Singleton>(Scope = "Seed")] +[Singleton>] +[Singleton>] public sealed class SeedOptionsConfigurator( ILogger logger) : ConfigureNamedOptionsBase diff --git a/sdk/node/Libplanet.Node/Services/BlocksyncSeedService.cs b/sdk/node/Libplanet.Node/Services/BlocksyncSeedService.cs index 0f3759be6af..4468ffc732b 100644 --- a/sdk/node/Libplanet.Node/Services/BlocksyncSeedService.cs +++ b/sdk/node/Libplanet.Node/Services/BlocksyncSeedService.cs @@ -6,8 +6,8 @@ namespace Libplanet.Node.Services; -[Singleton(Scope = "Seed")] -[Singleton(Scope = "Seed")] +[Singleton(Scope = SeedOptions.BlocksyncSeed)] +[Singleton(Scope = SeedOptions.BlocksyncSeed)] internal sealed class BlocksyncSeedService( IOptionsMonitor seedOptionsMonitor, ILogger logger) diff --git a/sdk/node/Libplanet.Node/Services/ConsensusSeedService.cs b/sdk/node/Libplanet.Node/Services/ConsensusSeedService.cs index ea5134e884f..53f46ecec3f 100644 --- a/sdk/node/Libplanet.Node/Services/ConsensusSeedService.cs +++ b/sdk/node/Libplanet.Node/Services/ConsensusSeedService.cs @@ -6,8 +6,8 @@ namespace Libplanet.Node.Services; -[Singleton(Scope = "Seed")] -[Singleton(Scope = "Seed")] +[Singleton(Scope = SeedOptions.ConsensusSeed)] +[Singleton(Scope = SeedOptions.ConsensusSeed)] internal sealed class ConsensusSeedService( IOptionsMonitor seedOptionsMonitor, ILogger logger)