diff --git a/src/console/LibplanetConsole.Console/BlockChain.cs b/src/console/LibplanetConsole.Console/BlockChain.cs deleted file mode 100644 index 05af28b5..00000000 --- a/src/console/LibplanetConsole.Console/BlockChain.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System.Diagnostics; -using System.Security.Cryptography; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace LibplanetConsole.Console; - -internal sealed class BlockChain : IBlockChain, IDisposable, IConsole -{ - private readonly NodeCollection _nodes; - private readonly PrivateKey _privateKey; - private readonly BlockHash _genesisHash; - private readonly ILogger _logger; - private IBlockChain? _blockChain; - private Node? _node; - private bool _isDisposed; - - public BlockChain(NodeCollection nodes, IApplicationOptions options, ILogger logger) - { - _nodes = nodes; - _privateKey = options.PrivateKey; - _genesisHash = options.GenesisBlock.Hash; - _logger = logger; - UpdateCurrent(_nodes.Current); - _nodes.CurrentChanged += Nodes_CurrentChanged; - } - - public event EventHandler? BlockAppended; - - public event EventHandler? Started; - - public event EventHandler? Stopped; - - public BlockInfo Tip { get; private set; } = BlockInfo.Empty; - - public bool IsRunning { get; private set; } - - void IDisposable.Dispose() - { - if (_isDisposed is false) - { - _nodes.CurrentChanged -= Nodes_CurrentChanged; - UpdateCurrent(null); - - _isDisposed = true; - } - } - - public async Task SendTransactionAsync( - IAction[] actions, CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null || _node is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - var privateKey = _privateKey; - var genesisHash = _genesisHash; - var nonce = await _blockChain.GetNextNonceAsync(privateKey.Address, cancellationToken); - var values = actions.Select(item => item.PlainValue).ToArray(); - var transaction = Transaction.Create( - nonce: nonce, - privateKey: privateKey, - genesisHash: genesisHash, - actions: new TxActionList(values)); - - await _node.SendTransactionAsync(transaction, cancellationToken); - return transaction.Id; - } - - Task IBlockChain.GetNextNonceAsync( - Address address, CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetNextNonceAsync(address, cancellationToken); - } - - Task IBlockChain.GetStateAsync( - long height, - Address accountAddress, - Address address, - CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetStateAsync(height, accountAddress, address, cancellationToken); - } - - Task IBlockChain.GetStateAsync( - BlockHash blockHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetStateAsync(blockHash, accountAddress, address, cancellationToken); - } - - Task IBlockChain.GetStateAsync( - HashDigest stateRootHash, - Address accountAddress, - Address address, - CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetStateAsync( - stateRootHash, accountAddress, address, cancellationToken); - } - - Task IBlockChain.GetBlockHashAsync( - long height, CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetBlockHashAsync(height, cancellationToken); - } - - Task IBlockChain.GetActionAsync( - TxId txId, int actionIndex, CancellationToken cancellationToken) - { - if (IsRunning is false || _blockChain is null) - { - throw new InvalidOperationException("BlockChain is not running."); - } - - return _blockChain.GetActionAsync(txId, actionIndex, cancellationToken); - } - - private void UpdateCurrent(Node? node) - { - if (_blockChain is not null) - { - _blockChain.Started -= BlockChain_Started; - _blockChain.Stopped -= BlockChain_Stopped; - _blockChain.BlockAppended -= BlockChain_BlockAppended; - if (_blockChain.IsRunning is false) - { - Tip = BlockInfo.Empty; - IsRunning = false; - _logger.LogDebug("BlockChain is stopped."); - Stopped?.Invoke(this, EventArgs.Empty); - } - } - - _node = node; - _blockChain = node?.GetKeyedService(INode.Key); - - if (_blockChain is not null) - { - if (_blockChain.IsRunning is true) - { - Tip = _blockChain.Tip; - IsRunning = true; - _logger.LogDebug("BlockChain is started."); - Started?.Invoke(this, EventArgs.Empty); - } - - _blockChain.Started += BlockChain_Started; - _blockChain.Stopped += BlockChain_Stopped; - _blockChain.BlockAppended += BlockChain_BlockAppended; - } - } - - private void Nodes_CurrentChanged(object? sender, EventArgs e) - => UpdateCurrent(_nodes.Current); - - private void BlockChain_BlockAppended(object? sender, BlockEventArgs e) - { - Tip = e.BlockInfo; - BlockAppended?.Invoke(sender, e); - } - - private void BlockChain_Started(object? sender, EventArgs e) - { - if (sender is IBlockChain blockChain && blockChain == _blockChain) - { - Tip = _blockChain.Tip; - IsRunning = true; - _logger.LogDebug("BlockChain is started."); - Started?.Invoke(this, EventArgs.Empty); - } - else - { - throw new UnreachableException("The sender is not an instance of IBlockChain."); - } - } - - private void BlockChain_Stopped(object? sender, EventArgs e) - { - Tip = BlockInfo.Empty; - IsRunning = false; - _logger.LogDebug("BlockChain is stopped."); - Stopped?.Invoke(this, EventArgs.Empty); - } -} diff --git a/src/console/LibplanetConsole.Console/ClientCollection.cs b/src/console/LibplanetConsole.Console/ClientCollection.cs index 753d7a2b..edf4c426 100644 --- a/src/console/LibplanetConsole.Console/ClientCollection.cs +++ b/src/console/LibplanetConsole.Console/ClientCollection.cs @@ -8,7 +8,7 @@ namespace LibplanetConsole.Console; internal sealed class ClientCollection( IServiceProvider serviceProvider, IApplicationOptions options) - : IEnumerable, IClientCollection + : ConsoleContentBase("clients"), IEnumerable, IClientCollection { private static readonly object LockObject = new(); private readonly List _clientList = new(options.Clients.Length); @@ -119,37 +119,6 @@ public async Task AttachAsync( await client.AttachAsync(cancellationToken); } - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - for (var i = 0; i < _clientList.Capacity; i++) - { - var client = ClientFactory.CreateNew(serviceProvider, options.Clients[i]); - InsertClient(client); - } - - Current = _clientList.FirstOrDefault(); - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while starting clients."); - } - - await Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - for (var i = _clientList.Count - 1; i >= 0; i--) - { - var client = _clientList[i]!; - client.Disposed -= Client_Disposed; - await ClientFactory.DisposeScopeAsync(client); - _logger.LogDebug("Disposed a client: {Address}", client.Address); - } - } - public async Task InitializeAsync(CancellationToken cancellationToken) { for (var i = 0; i < _clientList.Count; i++) @@ -194,6 +163,37 @@ IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator() => _clientList.GetEnumerator(); + protected override async Task OnStartAsync(CancellationToken cancellationToken) + { + try + { + for (var i = 0; i < _clientList.Capacity; i++) + { + var client = ClientFactory.CreateNew(serviceProvider, options.Clients[i]); + InsertClient(client); + } + + Current = _clientList.FirstOrDefault(); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while starting clients."); + } + + await Task.CompletedTask; + } + + protected override async Task OnStopAsync(CancellationToken cancellationToken) + { + for (var i = _clientList.Count - 1; i >= 0; i--) + { + var client = _clientList[i]!; + client.Disposed -= Client_Disposed; + await ClientFactory.DisposeScopeAsync(client); + _logger.LogDebug("Disposed a client: {Address}", client.Address); + } + } + private void Client_Disposed(object? sender, EventArgs e) { if (sender is Client client) diff --git a/src/console/LibplanetConsole.Console/ConsoleContentBase.cs b/src/console/LibplanetConsole.Console/ConsoleContentBase.cs new file mode 100644 index 00000000..cbb8f997 --- /dev/null +++ b/src/console/LibplanetConsole.Console/ConsoleContentBase.cs @@ -0,0 +1,40 @@ +namespace LibplanetConsole.Console; + +public abstract class ConsoleContentBase(string name) : IConsoleContent, IDisposable +{ + private readonly string _name = name; + private bool _isDisposed; + + public string Name => _name != string.Empty ? _name : GetType().Name; + + public virtual IEnumerable Dependencies => []; + + void IDisposable.Dispose() + { + OnDispose(disposing: true); + GC.SuppressFinalize(this); + } + + Task IConsoleContent.StartAsync(CancellationToken cancellationToken) + => OnStartAsync(cancellationToken); + + Task IConsoleContent.StopAsync(CancellationToken cancellationToken) + => OnStopAsync(cancellationToken); + + protected abstract Task OnStartAsync(CancellationToken cancellationToken); + + protected abstract Task OnStopAsync(CancellationToken cancellationToken); + + protected virtual void OnDispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // do nothing + } + + _isDisposed = true; + } + } +} diff --git a/src/console/LibplanetConsole.Console/ConsoleHost.BlockChain.cs b/src/console/LibplanetConsole.Console/ConsoleHost.BlockChain.cs new file mode 100644 index 00000000..ca363c10 --- /dev/null +++ b/src/console/LibplanetConsole.Console/ConsoleHost.BlockChain.cs @@ -0,0 +1,84 @@ +using System.Security.Cryptography; + +namespace LibplanetConsole.Console; + +internal sealed partial class ConsoleHost : IBlockChain +{ + public event EventHandler? BlockAppended; + + Task IBlockChain.GetNextNonceAsync( + Address address, CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetNextNonceAsync(address, cancellationToken); + } + + Task IBlockChain.GetStateAsync( + long height, + Address accountAddress, + Address address, + CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetStateAsync(height, accountAddress, address, cancellationToken); + } + + Task IBlockChain.GetStateAsync( + BlockHash blockHash, + Address accountAddress, + Address address, + CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetStateAsync(blockHash, accountAddress, address, cancellationToken); + } + + Task IBlockChain.GetStateAsync( + HashDigest stateRootHash, + Address accountAddress, + Address address, + CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetStateAsync( + stateRootHash, accountAddress, address, cancellationToken); + } + + Task IBlockChain.GetBlockHashAsync( + long height, CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetBlockHashAsync(height, cancellationToken); + } + + Task IBlockChain.GetActionAsync( + TxId txId, int actionIndex, CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + return _node.GetActionAsync(txId, actionIndex, cancellationToken); + } +} diff --git a/src/console/LibplanetConsole.Console/ConsoleHost.cs b/src/console/LibplanetConsole.Console/ConsoleHost.cs new file mode 100644 index 00000000..fce8c200 --- /dev/null +++ b/src/console/LibplanetConsole.Console/ConsoleHost.cs @@ -0,0 +1,207 @@ +using System.Collections.Specialized; +using LibplanetConsole.Common.Exceptions; +using Microsoft.Extensions.Logging; + +namespace LibplanetConsole.Console; + +internal sealed partial class ConsoleHost : IConsole, IDisposable +{ + private readonly NodeCollection _nodes; + private readonly ClientCollection _clients; + private readonly PrivateKey _privateKey; + private readonly BlockHash _genesisHash; + private readonly ILogger _logger; + private Node? _node; + private IConsoleContent[]? _contents; + private bool _isDisposed; + private CancellationTokenSource? _cancellationTokenSource; + + public ConsoleHost( + NodeCollection nodes, + ClientCollection clients, + IApplicationOptions options, + ILogger logger) + { + _nodes = nodes; + _clients = clients; + _privateKey = options.PrivateKey; + _genesisHash = options.GenesisBlock.Hash; + _logger = logger; + _nodes.CollectionChanged += Nodes_CollectionChanged; + } + + public event EventHandler? Started; + + public event EventHandler? Stopped; + + public BlockInfo Tip { get; private set; } = BlockInfo.Empty; + + public bool IsRunning { get; private set; } + + public Address Address => _privateKey.Address; + + public IConsoleContent[] Contents + { + get => _contents ?? throw new InvalidOperationException("Contents is not initialized."); + set => _contents = value; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + ObjectDisposedExceptionUtility.ThrowIf(_isDisposed, this); + if (IsRunning is true) + { + throw new InvalidOperationException("Node is already running."); + } + + _cancellationTokenSource = new(); + IsRunning = true; + _logger.LogDebug("Console is started: {Address}", Address); + await Task.WhenAll(Contents.Select(item => item.StartAsync(cancellationToken))); + _logger.LogDebug("Console Contents are started: {Address}", Address); + Started?.Invoke(this, EventArgs.Empty); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + ObjectDisposedExceptionUtility.ThrowIf(_isDisposed, this); + if (IsRunning is false) + { + throw new InvalidOperationException("Node is not running."); + } + + await Task.WhenAll(Contents.Select(item => item.StopAsync(cancellationToken))); + _logger.LogDebug("Console Contents are stopped: {Address}", Address); + + if (_cancellationTokenSource is not null) + { + await _cancellationTokenSource.CancelAsync(); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + + IsRunning = false; + _logger.LogDebug("Console is stopped: {Address}", Address); + Stopped?.Invoke(this, EventArgs.Empty); + } + + public async Task InitializeAsync() + { + _cancellationTokenSource = new(); + try + { + await _nodes.InitializeAsync(_cancellationTokenSource.Token); + await _clients.InitializeAsync(_cancellationTokenSource.Token); + } + catch (OperationCanceledException e) + { + _logger.LogDebug(e, "The console was canceled."); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while starting the console."); + } + } + + public async Task SendTransactionAsync( + IAction[] actions, CancellationToken cancellationToken) + { + if (IsRunning is false || _node is null) + { + throw new InvalidOperationException("BlockChain is not running."); + } + + var privateKey = _privateKey; + var genesisHash = _genesisHash; + var nonce = await _node.GetNextNonceAsync(privateKey.Address, cancellationToken); + var values = actions.Select(item => item.PlainValue).ToArray(); + var transaction = Transaction.Create( + nonce: nonce, + privateKey: privateKey, + genesisHash: genesisHash, + actions: new TxActionList(values)); + + await _node.SendTransactionAsync(transaction, cancellationToken); + return transaction.Id; + } + + void IDisposable.Dispose() + { + if (_isDisposed is false) + { + _nodes.CollectionChanged -= Nodes_CollectionChanged; + _isDisposed = true; + } + } + + private void Nodes_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + foreach (Node node in e.NewItems) + { + node.BlockAppended += Node_BlockAppended; + node.Started += Node_Started; + node.Stopped += Node_Stopped; + } + } + + break; + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + foreach (Node node in e.OldItems) + { + node.BlockAppended -= Node_BlockAppended; + node.Started -= Node_Started; + node.Stopped -= Node_Stopped; + } + } + + break; + } + } + + private void Node_BlockAppended(object? sender, BlockEventArgs e) + { + if (e.BlockInfo.Height > Tip.Height) + { + Tip = e.BlockInfo; + BlockAppended?.Invoke(sender, e); + } + } + + private void Node_Started(object? sender, EventArgs e) + { + _node = GetRandomNode(); + } + + private void Node_Stopped(object? sender, EventArgs e) + { + _node = GetRandomNode(); + } + + private Node? GetRandomNode() + { + var nodeList = new List(_nodes.Count); + for (var i = 0; i < _nodes.Count; i++) + { + var node = _nodes[i]; + if (node.IsRunning is true) + { + nodeList.Add(node); + } + } + + if (nodeList.Count is 0) + { + return null; + } + + var index = Random.Shared.Next(nodeList.Count); + return nodeList[index]; + } +} diff --git a/src/console/LibplanetConsole.Console/ConsoleHostedService.cs b/src/console/LibplanetConsole.Console/ConsoleHostedService.cs index 1ee954c4..d4da6543 100644 --- a/src/console/LibplanetConsole.Console/ConsoleHostedService.cs +++ b/src/console/LibplanetConsole.Console/ConsoleHostedService.cs @@ -1,28 +1,25 @@ using LibplanetConsole.Common; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace LibplanetConsole.Console; internal sealed class ConsoleHostedService( + IServiceProvider serviceProvider, SeedService seedService, - NodeCollection nodes, - ClientCollection clients, - IHostApplicationLifetime applicationLifetime, - ApplicationInfoProvider applicationInfoProvider, - ILogger logger) + ConsoleHost console, + IHostApplicationLifetime applicationLifetime) : IHostedService { private CancellationTokenSource? _cancellationTokenSource; public async Task StartAsync(CancellationToken cancellationToken) { - applicationLifetime.ApplicationStarted.Register(Initialize); + console.Contents = GetConsoleContents(serviceProvider); + applicationLifetime.ApplicationStarted.Register( + async () => await console.InitializeAsync()); await seedService.StartAsync(cancellationToken); - await nodes.StartAsync(cancellationToken); - await clients.StartAsync(cancellationToken); - - await Task.CompletedTask; + await console.StartAsync(cancellationToken); } public async Task StopAsync(CancellationToken cancellationToken) @@ -34,27 +31,13 @@ public async Task StopAsync(CancellationToken cancellationToken) _cancellationTokenSource = null; } - await clients.StopAsync(cancellationToken); - await nodes.StopAsync(cancellationToken); + await console.StopAsync(cancellationToken); await seedService.StopAsync(cancellationToken); } - private async void Initialize() + private static IConsoleContent[] GetConsoleContents(IServiceProvider serviceProvider) { - _cancellationTokenSource = new(); - try - { - logger.LogDebug(JsonUtility.Serialize(applicationInfoProvider.Info)); - await nodes.InitializeAsync(_cancellationTokenSource.Token); - await clients.InitializeAsync(_cancellationTokenSource.Token); - } - catch (OperationCanceledException e) - { - logger.LogDebug(e, "The console was canceled."); - } - catch (Exception e) - { - logger.LogError(e, "An error occurred while starting the console."); - } + var contents = serviceProvider.GetServices(); + return [.. DependencyUtility.TopologicalSort(contents, content => content.Dependencies)]; } } diff --git a/src/console/LibplanetConsole.Console/IConsole.cs b/src/console/LibplanetConsole.Console/IConsole.cs index 9417a624..01b92114 100644 --- a/src/console/LibplanetConsole.Console/IConsole.cs +++ b/src/console/LibplanetConsole.Console/IConsole.cs @@ -1,9 +1,8 @@ -using LibplanetConsole.Common; -using Microsoft.Extensions.DependencyInjection; - namespace LibplanetConsole.Console; public interface IConsole { + Address Address { get; } + Task SendTransactionAsync(IAction[] actions, CancellationToken cancellationToken); } diff --git a/src/console/LibplanetConsole.Console/IConsoleContent.cs b/src/console/LibplanetConsole.Console/IConsoleContent.cs new file mode 100644 index 00000000..715064dd --- /dev/null +++ b/src/console/LibplanetConsole.Console/IConsoleContent.cs @@ -0,0 +1,12 @@ +namespace LibplanetConsole.Console; + +public interface IConsoleContent +{ + string Name { get; } + + IEnumerable Dependencies { get; } + + Task StartAsync(CancellationToken cancellationToken); + + Task StopAsync(CancellationToken cancellationToken); +} diff --git a/src/console/LibplanetConsole.Console/NodeCollection.cs b/src/console/LibplanetConsole.Console/NodeCollection.cs index ea46a279..63f249cc 100644 --- a/src/console/LibplanetConsole.Console/NodeCollection.cs +++ b/src/console/LibplanetConsole.Console/NodeCollection.cs @@ -8,7 +8,7 @@ namespace LibplanetConsole.Console; internal sealed class NodeCollection( IServiceProvider serviceProvider, IApplicationOptions options) - : IEnumerable, INodeCollection + : ConsoleContentBase("nodes"), IEnumerable, INodeCollection { private static readonly object LockObject = new(); private readonly List _nodeList = new(options.Nodes.Length); @@ -118,37 +118,6 @@ public async Task AttachAsync( await node.AttachAsync(cancellationToken); } - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - for (var i = 0; i < _nodeList.Capacity; i++) - { - var node = NodeFactory.CreateNew(serviceProvider, options.Nodes[i]); - InsertNode(node); - } - - Current = _nodeList.FirstOrDefault(); - } - catch (Exception e) - { - _logger.LogError(e, "An error occurred while starting nodes."); - } - - await Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - for (var i = _nodeList.Count - 1; i >= 0; i--) - { - var node = _nodeList[i]!; - node.Disposed -= Node_Disposed; - await NodeFactory.DisposeScopeAsync(node); - _logger.LogDebug("Disposed a client: {Address}", node.Address); - } - } - public async Task InitializeAsync(CancellationToken cancellationToken) { for (var i = 0; i < _nodeList.Count; i++) @@ -204,6 +173,37 @@ internal Node RandomNode() return _nodeList[nodeIndex]; } + protected override async Task OnStartAsync(CancellationToken cancellationToken) + { + try + { + for (var i = 0; i < _nodeList.Capacity; i++) + { + var node = NodeFactory.CreateNew(serviceProvider, options.Nodes[i]); + InsertNode(node); + } + + Current = _nodeList.FirstOrDefault(); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred while starting nodes."); + } + + await Task.CompletedTask; + } + + protected override async Task OnStopAsync(CancellationToken cancellationToken) + { + for (var i = _nodeList.Count - 1; i >= 0; i--) + { + var node = _nodeList[i]!; + node.Disposed -= Node_Disposed; + await NodeFactory.DisposeScopeAsync(node); + _logger.LogDebug("Disposed a client: {Address}", node.Address); + } + } + private void Node_Disposed(object? sender, EventArgs e) { if (sender is Node node) diff --git a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs index 043faeb1..5907215b 100644 --- a/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs +++ b/src/console/LibplanetConsole.Console/ServiceCollectionExtensions.cs @@ -28,12 +28,14 @@ public static IServiceCollection AddConsole( @this.AddSingleton() .AddSingleton(s => s.GetRequiredService()); @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()) .AddSingleton(s => s.GetRequiredService()); @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()) .AddSingleton(s => s.GetRequiredService()); - @this.AddSingleton() - .AddSingleton(s => s.GetRequiredService()) - .AddSingleton(s => s.GetRequiredService()); + @this.AddSingleton() + .AddSingleton(s => s.GetRequiredService()) + .AddSingleton(s => s.GetRequiredService()); @this.AddSingleton() .AddSingleton(s => s.GetRequiredService());