diff --git a/SlipeServer.Console/Program.cs b/SlipeServer.Console/Program.cs index 45a43690..a19f5d0b 100644 --- a/SlipeServer.Console/Program.cs +++ b/SlipeServer.Console/Program.cs @@ -22,6 +22,7 @@ using SlipeServer.Example; using SlipeServer.Example.Services; using SlipeServer.Scripting.Luau; +using SlipeServer.Server.Resources; namespace SlipeServer.Console; @@ -103,6 +104,7 @@ public Program(string[] args) services.AddScoped(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.AddHttpClient(); diff --git a/SlipeServer.Example/Resources/ResourceA/client.lua b/SlipeServer.Example/Resources/ResourceA/client.lua new file mode 100644 index 00000000..b6fad1b9 --- /dev/null +++ b/SlipeServer.Example/Resources/ResourceA/client.lua @@ -0,0 +1,3 @@ +addCommandHandler("sampltest", function() + outputChatBox("ResourceA is running!") +end) \ No newline at end of file diff --git a/SlipeServer.Example/Resources/ResourceB/client.lua b/SlipeServer.Example/Resources/ResourceB/client.lua new file mode 100644 index 00000000..bd8d1c6b --- /dev/null +++ b/SlipeServer.Example/Resources/ResourceB/client.lua @@ -0,0 +1,3 @@ +addCommandHandler("sampltest", function() + outputChatBox("ResourceB is running!") +end) \ No newline at end of file diff --git a/SlipeServer.Example/ResourcesLogic.cs b/SlipeServer.Example/ResourcesLogic.cs new file mode 100644 index 00000000..2de6c0e3 --- /dev/null +++ b/SlipeServer.Example/ResourcesLogic.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using SlipeServer.Server; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.Services; + +namespace SlipeServer.Example; + +public class ResourcesLogic +{ + private readonly ChatBox chatBox; + private readonly ResourceService resourceService; + private readonly ILogger logger; + + public ResourcesLogic(MtaServer mtaServer, ChatBox chatBox, IResourceProvider resourceProvider, ResourceService resourceService, ILogger logger) + { + this.chatBox = chatBox; + this.resourceService = resourceService; + this.logger = logger; + this.resourceService.AddStartResource("ResourceA"); + this.resourceService.AddStartResource("ResourceB"); + this.resourceService.AllStarted += HandleAllStarted; + mtaServer.PlayerJoined += HandlePlayerJoin; + } + + private async void HandlePlayerJoin(Player player) + { + try + { + await this.resourceService.StartResourcesForPlayer(player); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Failed to start resources for {playerName}", player.Name); + } + } + + private void HandleAllStarted(Player player) + { + Console.WriteLine("All resources started for: {0}", player.Name); + this.chatBox.Output($"All resources started for: {player.Name}"); + } +} diff --git a/SlipeServer.Example/ServerBuilderExtensions.cs b/SlipeServer.Example/ServerBuilderExtensions.cs index eb7407ed..6ada59d4 100644 --- a/SlipeServer.Example/ServerBuilderExtensions.cs +++ b/SlipeServer.Example/ServerBuilderExtensions.cs @@ -9,6 +9,7 @@ public static ServerBuilder AddExampleLogic(this ServerBuilder builder) { builder.AddLogic(); builder.AddLuaControllers(); + builder.AddLogic(); return builder; } diff --git a/SlipeServer.Example/SlipeServer.Example.csproj b/SlipeServer.Example/SlipeServer.Example.csproj index 8476689c..5fa054d1 100644 --- a/SlipeServer.Example/SlipeServer.Example.csproj +++ b/SlipeServer.Example/SlipeServer.Example.csproj @@ -21,4 +21,18 @@ + + + + + + + + + Always + + + Always + + diff --git a/SlipeServer.HostBuilderExample/Program.cs b/SlipeServer.HostBuilderExample/Program.cs index a90c73f1..55d56bbe 100644 --- a/SlipeServer.HostBuilderExample/Program.cs +++ b/SlipeServer.HostBuilderExample/Program.cs @@ -6,6 +6,7 @@ using SlipeServer.Example; using SlipeServer.Example.Services; using SlipeServer.Example.Elements; +using SlipeServer.Server.Resources; Configuration? configuration = null; @@ -28,6 +29,7 @@ services.AddHttpClient(); services.AddSingleton(); services.AddScoped(); + services.AddSingleton(); services.AddHostedService(); // Use instead of logics services.TryAddSingleton(x => x.GetRequiredService>()); diff --git a/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs b/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs index 3aef4655..daa2121c 100644 --- a/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs +++ b/SlipeServer.Packets/Definitions/Resources/ResourceStartPacket.cs @@ -1,7 +1,9 @@ using SlipeServer.Packets.Builder; using SlipeServer.Packets.Enums; +using SlipeServer.Packets.Reader; using SlipeServer.Packets.Structs; using System.Collections.Generic; +using System.Text; namespace SlipeServer.Packets.Definitions.Resources; @@ -13,7 +15,7 @@ public sealed class ResourceStartPacket : Packet public override PacketPriority Priority => PacketPriority.High; public string Name { get; set; } - public ushort NetId { get; } + public ushort NetId { get; set; } public ElementId ResourceDynamicElementId { get; set; } public ushort UncachedScriptCount { get; } public string MinServerVersion { get; } @@ -24,6 +26,11 @@ public sealed class ResourceStartPacket : Packet public IEnumerable ExportedFunctions { get; } public ElementId ResourceElementId { get; } + public ResourceStartPacket() + { + + } + public ResourceStartPacket( string name, ushort netId, @@ -53,6 +60,10 @@ IEnumerable exportedFunctions public override void Read(byte[] bytes) { + var reader = new PacketReader(bytes); + var len = reader.GetByte(); + this.Name = Encoding.UTF8.GetString(reader.GetBytes(len)); + this.NetId = reader.GetUInt16(); } public override byte[] Write() diff --git a/SlipeServer.Packets/Reader/PacketReader.cs b/SlipeServer.Packets/Reader/PacketReader.cs index b61710e0..e8d5aa0c 100644 --- a/SlipeServer.Packets/Reader/PacketReader.cs +++ b/SlipeServer.Packets/Reader/PacketReader.cs @@ -96,6 +96,7 @@ public bool[] GetBits(int count) public uint GetUint32() => BitConverter.ToUInt32(GetBytes(4), 0); public ulong GetUint64() => BitConverter.ToUInt64(GetBytes(8), 0); public short GetInt16() => BitConverter.ToInt16(GetBytes(2), 0); + public ushort GetUInt16() => BitConverter.ToUInt16(GetBytes(2), 0); public int GetInt32() => BitConverter.ToInt32(GetBytes(4), 0); public long GetInt64() => BitConverter.ToInt64(GetBytes(8), 0); public float GetFloat() => BitConverter.ToSingle(GetBytes(4), 0); diff --git a/SlipeServer.Server.TestTools/TestResourceProvider.cs b/SlipeServer.Server.TestTools/TestResourceProvider.cs new file mode 100644 index 00000000..08baf656 --- /dev/null +++ b/SlipeServer.Server.TestTools/TestResourceProvider.cs @@ -0,0 +1,44 @@ +using SlipeServer.Server.Resources.Interpreters; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.Resources; +using System.Collections.Generic; + +namespace SlipeServer.Server.TestTools; + +public class TestResourceProvider : IResourceProvider +{ + private readonly Dictionary resources = []; + private readonly object netIdLock = new(); + private ushort netId = 0; + + public void AddResource(Resource resource) + { + this.resources[resource.Name] = resource; + } + + public Resource GetResource(string name) + { + return this.resources[name]; + } + + public IEnumerable GetResources() + { + return this.resources.Values; + } + + public void Refresh() { } + + private IEnumerable IndexResourceDirectory(string directory) => []; + + public IEnumerable GetFilesForResource(string name) => []; + + public byte[] GetFileContent(string resource, string file) => []; + + public ushort ReserveNetId() + { + lock (this.netIdLock) + return this.netId++; + } + + public void AddResourceInterpreter(IResourceInterpreter resourceInterpreter) { } +} diff --git a/SlipeServer.Server.TestTools/TestingServer.cs b/SlipeServer.Server.TestTools/TestingServer.cs index 8aaa4dc1..f5118851 100644 --- a/SlipeServer.Server.TestTools/TestingServer.cs +++ b/SlipeServer.Server.TestTools/TestingServer.cs @@ -1,5 +1,6 @@ using Castle.Core.Logging; using FluentAssertions; +using FluentAssertions.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; @@ -7,15 +8,19 @@ using SlipeServer.Packets; using SlipeServer.Packets.Definitions.Lua; using SlipeServer.Packets.Definitions.Lua.ElementRpc; +using SlipeServer.Packets.Definitions.Resources; using SlipeServer.Packets.Enums; using SlipeServer.Packets.Lua.Event; using SlipeServer.Server.Clients; using SlipeServer.Server.Elements; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; using SlipeServer.Server.Resources.Serving; using SlipeServer.Server.ServerBuilders; using System; using System.Collections.Generic; using System.Linq; +using static System.Runtime.InteropServices.JavaScript.JSType; using ILogger = Microsoft.Extensions.Logging.ILogger; namespace SlipeServer.Server.TestTools; @@ -76,12 +81,14 @@ private void SetupSendPacketMocks() this.NetWrapperMock.Setup(x => x.SendPacket(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((ulong address, ushort version, Packet packet) => { + var data = packet.Write(); + this.packetReducer.EnqueuePacket(this.clients[this.NetWrapperMock.Object][address], packet.PacketId, data); this.sendPacketCalls.Add(new SendPacketCall() { Address = address, Version = version, PacketId = packet.PacketId, - Data = packet.Write(), + Data = data, Priority = packet.Priority, Reliability = packet.Reliability }); @@ -153,6 +160,26 @@ public void VerifyPacketSent(PacketId packetId, TPlayer to, byte[] data = null, ).Should().Be(count); } + public void VerifyResourceStartedPacketSent(TPlayer player, Resource resource, int count = 1) + { + var sendPacketCalls = this.sendPacketCalls + .Where(x => x.PacketId == PacketId.PACKET_ID_RESOURCE_START && x.Address == player.GetAddress()); + + int startedCount = 0; + + var packet = new ResourceStartPacket(); + foreach (var sendPacketCall in sendPacketCalls) + { + packet.Read(sendPacketCall.Data); + if(packet.NetId == resource.NetId) + { + startedCount++; + } + } + + startedCount.Should().Be(count); + } + public void VerifyLuaElementRpcPacketSent(ElementRpcFunction packetId, TPlayer to, byte[] data = null, int count = 1) { this.sendPacketCalls.Count(x => @@ -192,6 +219,11 @@ public void VerifyLuaEventTriggered(string eventName, TPlayer to, Element source count.Should().Be(expectedCount); } + public T CreateInstance(params object[] parameters) + { + return ActivatorUtilities.CreateInstance(this.serviceProvider, parameters); + } + public void ResetPacketCountVerification() => this.sendPacketCalls.Clear(); public uint GenerateBinaryAddress() => ++this.binaryAddressCounter; diff --git a/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs b/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs new file mode 100644 index 00000000..72badfb3 --- /dev/null +++ b/SlipeServer.Server.Tests/Integration/Services/ResourceServiceTests.cs @@ -0,0 +1,185 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SlipeServer.Packets.Definitions.Resources; +using SlipeServer.Packets.Enums; +using SlipeServer.Server.Clients; +using SlipeServer.Server.Elements; +using SlipeServer.Server.PacketHandling.Handlers; +using SlipeServer.Server.Resources; +using SlipeServer.Server.Resources.Providers; +using SlipeServer.Server.TestTools; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace SlipeServer.Server.Tests.Integration.Services; + +public class TestResourceStartPacketHandler : IPacketHandler +{ + public PacketId PacketId => PacketId.PACKET_ID_RESOURCE_START; + + public TestResourceStartPacketHandler() + { + } + + public void HandlePacket(IClient client, ResourceStartPacket packet) + { + client.Player.TriggerResourceStarted(packet.NetId); + } +} + +public class ResourceA : Resource +{ + public ResourceA(MtaServer server) : base(server, server.RootElement, "ResourceA") { } +} + +public class ResourceB : Resource +{ + public ResourceB(MtaServer server) : base(server, server.RootElement, "ResourceB") { } +} + +public class TestingServerResourceService +{ + public TestingServer Server { get; } + public ResourceA ResourceA { get; } + public ResourceB ResourceB { get; } + + public TestingServerResourceService() + { + this.Server = new TestingServer((Configuration?)null, builder => + { + builder.ConfigureServices(services => + { + services.RemoveAll(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + }); + }); + + this.Server.RegisterPacketHandler(); + + var resourceProvider = this.Server.GetRequiredService(); + this.ResourceA = new ResourceA(this.Server); + this.Server.AddAdditionalResource(this.ResourceA, []); + resourceProvider.AddResource(this.ResourceA); + this.ResourceB = new ResourceB(this.Server); + this.Server.AddAdditionalResource(this.ResourceB, []); + resourceProvider.AddResource(this.ResourceB); + } +} + +public class ResourceServiceTests : IClassFixture +{ + private readonly TestingServer server; + private readonly ResourceService resourceService; + private readonly ResourceA resourceA; + private readonly ResourceB resourceB; + + public ResourceServiceTests(TestingServerResourceService testingServerResourceService) + { + this.server = testingServerResourceService.Server; + + this.resourceService = this.server.CreateInstance(); + this.resourceService.AddStartResource("ResourceA"); + this.resourceService.AddStartResource("ResourceB"); + + this.resourceA = testingServerResourceService.ResourceA; + this.resourceB = testingServerResourceService.ResourceB; + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ResourceServiceShouldWork(bool parallel) + { + var player = this.server.AddFakePlayer(); + using var monitor = this.resourceService.Monitor(); + + await this.resourceService.StartResourcesForPlayer(player, parallel); + + using var _ = new AssertionScope(); + monitor.OccurredEvents.Select(x => x.EventName).Should().BeEquivalentTo(["Started", "Started", "AllStarted"]); + this.server.VerifyResourceStartedPacketSent(player, this.resourceA); + this.server.VerifyResourceStartedPacketSent(player, this.resourceB); + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task DisconnectingPlayerShouldStopResourceStarting(bool parallel) + { + using var monitor = this.resourceService.Monitor(); + + var player = this.server.AddFakePlayer(); + + void handleStarted(Resource arg1, Player arg2) + { + player.TriggerDisconnected(Enums.QuitReason.Quit); + } + + this.resourceService.Started += handleStarted; + + var act = async () => await this.resourceService.StartResourcesForPlayer(player, parallel); + + using var _ = new AssertionScope(); + (await act.Should().ThrowAsync()) + .Which.InnerExceptions.Should() + .SatisfyRespectively( + first => + { + first.Should().BeOfType(); + }); + + monitor.OccurredEvents.Select(x => x.EventName).Should().BeEquivalentTo(["Started"]); + + if (parallel) + { + this.server.VerifyResourceStartedPacketSent(player, this.resourceA); + this.server.VerifyResourceStartedPacketSent(player, this.resourceB); + } + else + { + this.server.VerifyResourceStartedPacketSent(player, this.resourceA); + this.server.VerifyResourceStartedPacketSent(player, this.resourceB, 0); + } + } + + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ExceptionThrownInStartedEventShouldBeHandledProperly(bool parallel) + { + using var monitor = this.resourceService.Monitor(); + + var player = this.server.AddFakePlayer(); + + void handleStarted(Resource resource, Player player) + { + throw new Exception("oops"); + } + + void handleAllStarted(Player player) + { + throw new Exception("oops"); + } + + this.resourceService.Started += handleStarted; + this.resourceService.AllStarted += handleAllStarted; + + var act = async () => await this.resourceService.StartResourcesForPlayer(player, parallel); + + using var _ = new AssertionScope(); + (await act.Should().ThrowAsync()) + .Which.InnerExceptions.Should().HaveCount(3); + + monitor.OccurredEvents.Select(x => x.EventName).Should().BeEquivalentTo(["Started", "Started", "AllStarted"]); + + this.server.VerifyResourceStartedPacketSent(player, this.resourceA); + this.server.VerifyResourceStartedPacketSent(player, this.resourceB); + } + + +} diff --git a/SlipeServer.Server/Elements/Player.cs b/SlipeServer.Server/Elements/Player.cs index 98f47db3..0b31f62f 100644 --- a/SlipeServer.Server/Elements/Player.cs +++ b/SlipeServer.Server/Elements/Player.cs @@ -18,6 +18,7 @@ using SlipeServer.Server.Clients; using System.Net; using SlipeServer.Packets.Definitions.Lua.ElementRpc.Player; +using System.Threading; namespace SlipeServer.Server.Elements; @@ -363,7 +364,11 @@ public void VoiceDataEnd() public void TriggerDisconnected(QuitReason reason) { if (this.Destroy()) + { + this.Client.IsConnected = false; + this.Client.SetDisconnected(); this.Disconnected?.Invoke(this, new PlayerQuitEventArgs(reason)); + } } public void TakeScreenshot(ushort width, ushort height, string tag = "", byte quality = 30, uint maxBandwith = 5000, ushort maxPacketSize = 500) @@ -559,6 +564,27 @@ internal bool ShouldSendReturnSyncPacket() return this.pureSyncPacketsCount++ % 4 == 0; } + /// + /// Returns a CancellationToken that is valid until the player leaves the server or is destroyed + /// + public CancellationToken GetCancellationToken() + { + var cts = new CancellationTokenSource(); + + void handleDisconnected(Player sender, PlayerQuitEventArgs e) + { + cts.Cancel(); + }; + + cts.Token.Register(() => this.Disconnected -= handleDisconnected); + this.Disconnected += handleDisconnected; + + if (this.IsDestroyed) + cts.Cancel(); + + return cts.Token; + } + public event ElementChangedEventHandler? WantedLevelChanged; public event ElementChangedEventHandler? NametagTextChanged; public event ElementChangedEventHandler? IsNametagShowingChanged; diff --git a/SlipeServer.Server/Resources/ResourceService.cs b/SlipeServer.Server/Resources/ResourceService.cs index 9a55c07e..01f4c95b 100644 --- a/SlipeServer.Server/Resources/ResourceService.cs +++ b/SlipeServer.Server/Resources/ResourceService.cs @@ -1,7 +1,13 @@ -using SlipeServer.Server.Elements; +using Microsoft.Extensions.Logging; +using SlipeServer.Server.ElementCollections; +using SlipeServer.Server.Elements; +using SlipeServer.Server.Elements.Events; using SlipeServer.Server.Resources.Providers; +using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace SlipeServer.Server.Resources; @@ -10,56 +16,138 @@ namespace SlipeServer.Server.Resources; /// public class ResourceService { - private readonly MtaServer server; - private readonly RootElement root; private readonly IResourceProvider resourceProvider; - private readonly List startedResources; + private readonly List startedResources = []; public IReadOnlyCollection StartedResources => this.startedResources.AsReadOnly(); - public ResourceService(MtaServer server, RootElement root, IResourceProvider resourceProvider) + public event Action? AllStarted; + public event Action? Stopped; + public event Action? Started; + + public ResourceService(IResourceProvider resourceProvider) { - this.server = server; - this.root = root; this.resourceProvider = resourceProvider; - - this.startedResources = new List(); - - this.server.PlayerJoined += HandlePlayerJoin; } - private void HandlePlayerJoin(Player player) + public async Task StartResourcesForPlayer(Player player, bool parallel = true, CancellationToken cancellationToken = default) { - foreach (var resource in this.startedResources) + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, player.GetCancellationToken()); + + cts.Token.ThrowIfCancellationRequested(); + + List exceptions = []; + + if (parallel) { - resource.StartFor(player); - } - } + var resourcesNetIds = this.startedResources.Select(x => x.NetId).ToList(); + var tcs = new TaskCompletionSource(); - public Resource? StartResource(string name) - { - if (!this.startedResources.Any(r => r.Name == name)) + void handleResourceStart(Player sender, PlayerResourceStartedEventArgs e) + { + if (sender == player && resourcesNetIds.Remove(e.NetId)) + { + try + { + this.Started?.Invoke(this.startedResources.Where(x => x.NetId == e.NetId).First(), sender); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + finally + { + if (resourcesNetIds.Count == 0) + tcs.SetResult(); + } + } + } + + void handlePlayerDisconnected(Player disconnectingPlayer, PlayerQuitEventArgs e) + { + if (player != disconnectingPlayer) + return; + + player.ResourceStarted -= handleResourceStart; + player.Disconnected -= handlePlayerDisconnected; + + tcs.SetException(new Exception("Player disconnected.")); + } + + player.ResourceStarted += handleResourceStart; + player.Disconnected += handlePlayerDisconnected; + cts.Token.Register(() => + { + tcs.TrySetException(new OperationCanceledException()); + }); + + foreach (var resource in this.startedResources) + resource.StartFor(player); + + try + { + await tcs.Task; + } + catch(Exception ex) + { + exceptions.Add(ex); + } + finally + { + player.ResourceStarted -= handleResourceStart; + player.Disconnected -= handlePlayerDisconnected; + } + } + else { - var resource = this.resourceProvider.GetResource(name); - resource.Start(); - this.startedResources.Add(resource); + foreach (var resource in this.startedResources) + { + try + { + await resource.StartForAsync(player, cts.Token); + this.Started?.Invoke(resource, player); + } + catch (OperationCanceledException ex) + { + exceptions.Add(ex); + break; + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } - return resource; + if (!cts.IsCancellationRequested) + { + try + { + this.AllStarted?.Invoke(player); + } + catch(Exception ex) + { + exceptions.Add(ex); + } } - return null; - } - public void StopResource(string name) - { - var resource = this.startedResources.Single(r => r.Name == name); - this.startedResources.Remove(resource); - resource.Stop(); + if (exceptions.Count > 0) + throw new AggregateException(exceptions); } - public void StopResource(Resource resource) + public bool AddStartResource(string name) { - this.startedResources.Remove(resource); - resource.Stop(); + if (this.startedResources.Any(r => r.Name == name)) + return false; + + var resource = this.resourceProvider.GetResource(name); + if (resource == null) + return false; + + resource.Start(); + this.startedResources.Add(resource); + + return true; } } diff --git a/SlipeServer.WebHostBuilderExample/Program.cs b/SlipeServer.WebHostBuilderExample/Program.cs index e3158116..d4eb356a 100644 --- a/SlipeServer.WebHostBuilderExample/Program.cs +++ b/SlipeServer.WebHostBuilderExample/Program.cs @@ -10,6 +10,7 @@ using SlipeServer.Example; using SlipeServer.Example.Services; using SlipeServer.Example.Elements; +using SlipeServer.Server.Resources; Directory.SetCurrentDirectory(Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly()!.Location)!); @@ -37,6 +38,7 @@ builder.Services.AddSingleton(); builder.Services.AddScoped(); +builder.Services.AddSingleton(); builder.Services.AddHostedService(); // Use instead of logics builder.Services.TryAddSingleton(x => x.GetRequiredService>());