Search code examples
c#blazorwebassembly.net-6.0

Injected singleton service is different across Blazor components [.NET 6]


On Blazor 6.0 WASM Webclient

It appears that the IOC container is returning different instances of my singleton service, NodeService. I have come to this conclusion by generating a random number in the NodeService constructor, and then checking the value of that random number from different classes that use the service.

Program.cs

using BlazorDraggableDemo;
using BlazorDraggableDemo.Factories;
using BlazorDraggableDemo.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");



builder.Services.AddSingleton<MouseService>();
builder.Services.AddSingleton<IMouseService>(ff => ff.GetRequiredService<MouseService>());
builder.Services.AddSingleton<INodeService, NodeService>();
builder.Services.AddSingleton<INodeServiceRequestMessageFactory, NodeServiceRequestMessageFactory>();
builder.Services.AddHttpClient<INodeService, NodeService>(client =>
{
    client.BaseAddress = new Uri("http://localhost:7071");
});



await builder.Build().RunAsync();

Playground.razor

@inject MouseService mouseSrv;
@inject INodeService nodeService;

<div class="row mt-2">
    <div class="col">
        <button @onclick="AddNode">Add Node</button>
        <button @onclick="SaveNodes">Save</button>
        <button @onclick="AddConnector">Add Connector</button>
        <svg class="bg-light" width="100%" height="500" xmlns="http://www.w3.org/2000/svg"
            @onmousemove=@(e => mouseSrv.FireMove(this, e))
            @onmouseup=@(e => mouseSrv.FireUp(this, e))
            @onmouseleave=@(e => mouseSrv.FireLeave(this, e))>

            @foreach(var node in nodes)
            {
                <Draggable Circle=@node>
                <circle r="15" fill="#04dcff" stroke="#fff" />
                </Draggable>
            }
            @foreach(var connector in connectors)
            {
                <ConnectorComponent Line=connector />
            }
        </svg>
    </div>
</div>

@code {
    public List<Node>? nodes;
    public List<Connector>? connectors;
    int serviceIntance = 0;
    protected override async Task OnInitializedAsync()
    {
        nodes = new List<Node>();
        connectors = new List<Connector>();
        
        try
        {
            await nodeService.LoadNodes();
            nodes = nodeService.GetNodes();
            connectors = nodeService.GetConnectors();
            serviceIntance = nodeService.getInstance();
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine(ex.Message);
        }

        Console.WriteLine("Got Stuff?");
    }

    public async Task SaveNodes()
    {
        await nodeService.SaveNodes();
    }

    private async Task AddNode()
    {
        var lastShape = nodes.LastOrDefault();
        double x = lastShape != null ? lastShape.XCoord + 15 : 0;
        double y = lastShape != null ? lastShape.YCoord : 0;
        await nodeService.CreateNode(x, y, "nodes");
    }

    private async Task AddConnector()
    {
        var startnode = nodes[0];
        var endNode = nodes[1];
        await nodeService.AddConnector(startnode, endNode);
        Console.WriteLine("We Here");
    }
}

ConnectorComponent.razor

@inject INodeService nodeService;

<path d="M @startNode.XCoord @startNode.XCoord C @Line.StartBezierXCoord @Line.StartBezierYCoord, @Line.EndBezierXCoord @Line.EndBezierYCoord, @endNode.XCoord @endNode.YCoord" stroke="rgb(108, 117, 125)" stroke-width="1.5" fill="transparent" style="pointer-events:none !important;" />

@code {
    [Parameter] public Connector Line  { get; set; }
    public Node startNode;
    public Node endNode;
    int serviceInstance;

    protected override void OnParametersSet() {
        var nodes = nodeService.GetNodes();
        serviceInstance = nodeService.getInstance();
        startNode = nodes.First(node => node.Id.Equals(Line.StartNodeId));
        endNode = nodes.First(node => node.Id.Equals(Line.EndNodeId));
        base.OnParametersSet();
    }
}

NodeService.cs

using BlazorDraggableDemo.Models;
using Microsoft.AspNetCore.Components.Web;
using System.Net.Http.Json;
using System.Net.Http;
using BlazorDraggableDemo.Factories;
using BlazorDraggableDemo.DTOs;
using System.Text.Json;

namespace BlazorDraggableDemo.Services
{
    public interface INodeService
    {
        public Task LoadNodes();
        public List<Node> GetNodes();
        public Task SaveNodes();
        public Task AddConnector(Node startNode, Node endNode);
        public void SaveConnectors();
        public Task CreateNode(double xCoord, double yCoord, string solutionId);
        public List<Connector> GetConnectors();
        public int getInstance();
    }

    public class NodeService : INodeService
    {
        private readonly HttpClient _httpClient;
        private readonly INodeServiceRequestMessageFactory _nodeServiceRequestMessageFactory;
        private readonly int instance;
        public NodeService(HttpClient httpClient, INodeServiceRequestMessageFactory nodeServiceRequestMessageFactory)
        {
            _httpClient = httpClient;
            _nodeServiceRequestMessageFactory = nodeServiceRequestMessageFactory;
            var rand = new Random();
            instance = rand.Next(0, 100);
        }
        public List<Node> Nodes = new List<Node>();
        public List<Connector> Connectors = new List<Connector>();

        public async Task LoadNodes()
        {
            try
            {
                var nodes = await _httpClient.GetFromJsonAsync<List<Node>>("api/getnodes");
                if (nodes != null)
                {
                    Nodes = nodes;
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
        }

        public List<Node> GetNodes()
        {
            return Nodes;
        }

        public async Task SaveNodes()
        {
            try
            {
                var response = await _httpClient.PostAsJsonAsync<UpsertNodesRequestMessage>("api/upsertNodes", new UpsertNodesRequestMessage()
                {
                    Nodes = Nodes.ToList()
                });
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
        }

        public async Task AddConnector(Node startNode, Node endNode)
        {
            try
            {
                var response = await _httpClient.PostAsJsonAsync("api/AddConnector", new AddConnectorRequestMessage()
                {
                    StartNode = startNode,
                    EndNode = endNode
                });
                var responseMessage = await response.Content.ReadAsStringAsync();
                var connector = JsonSerializer.Deserialize<Connector>(responseMessage);
                Connectors.Add(connector);
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
        }
        public void SaveConnectors()
        {

        }

        public List<Connector> GetConnectors()
        {
            return Connectors;
        }
        public async Task CreateNode(double xCoord, double yCoord, string solutionId)
        {
            try
            {
                var response = await _httpClient.PostAsJsonAsync<CreateNodeRequestMessage>("api/CreateNode", new CreateNodeRequestMessage()
                {
                    XCoord = xCoord,
                    YCoord = yCoord,
                    SolutionId = solutionId
                });
                var responseMessage = await response.Content.ReadAsStringAsync();
                var node = JsonSerializer.Deserialize<Node>(responseMessage);
                Nodes.Add(node);
                
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex.Message);
            }
        }

        public int getInstance()
        {
            return instance;
        }
    }
}

When I check the value of nodeService.instance from ComponentA, it comes in at 84 When I check the value from ComponentB, it comes in at 12. My understanding of singletons is that a single instance of singleton service should be across the user's instance of the application. Shouldn't the value of nodeService.instance be the same when referenced from either component?


Solution

  • To add to @Mister Magoo's answer about injecting transient/scoped services into singletons. I just tried injecting HttpClient into a singleton and got a runtime error when I then tried to inject the singleton.

    Unhandled exception rendering component: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'IComp'.
    

    builder.Services.AddHttpClient(), actually registers IHttpClientFactory as a service. So you should be injecting that, then create clients when you need them. This may be your issue, but I couldn't actually reproduce your outcome.

    public interface IComp
    {
        int num { get; set; }
    }
    public class myComp : IComp
    {
        public int num { get; set; }
    
        private readonly IHttpClientFactory _clientFactory;
        public myComp(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
            var rand = new Random();
            num = rand.Next(0, 100);
        }
    
        public async Task<string> GetSomething()
        {
            // edit, removed the using on client
            var client = _clientFactory.CreateClient();
            return await client.GetStringAsync("http://some-url");
        }
    }