2

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?

5
  • 1
    What hosting model are you using? I've only used server-side Blazor but I'm guessing that client hosted means all singletons are local to each user instance. Commented Feb 25, 2022 at 3:45
  • That is more accurate, although both components are already in the same user instance. I am using client side - so I'll update my post to be more specific. Commented Feb 25, 2022 at 3:51
  • 1
    You're right, each component should use the same instance. I just tested this on WASM and each component does use the same instance , but the instance changes every refresh - which makes sense. Do you have some code to share? Commented Feb 25, 2022 at 4:17
  • @bmiller I updated the code samples to contain complete files. Commented Feb 25, 2022 at 5:09
  • 1
    @ChrisPhillips AddHttpClient is meant to register Http clients, not services. You're using the same class to do far more than it's supposed to do. The docs clearly explain the registered class is transient. The underlying Http connection though is provided by the HttpClientFactory, or in BlazorWasm, by the browser itself. Split NodeService in two, the actual service and the typed client Commented Feb 25, 2022 at 9:55

3 Answers 3

2

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");
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

This was my issue. I removed the INodeService typed HttpClient for a generic HttpClient injection, and then performed the build of the HttpClient in the constructor of the nodeService. Fixed it right up.. It seems odd to me that we inject an HttpClient into the services instead of an IHttpClientFactory. Is is in the documentation, and the goal perhaps is to save us a line of code by compressing both injections into one, but it definitely took me down the wrong road.
Well bottom line is HttpClient is scoped, IHttpClientFactory is a singleton. Note I removed the 'using' on var client. Seems you shouldn't do that - not sure it really make a difference though. (aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong)
I'm sure it makes a difference. Otherwise garbage collection kind of just happens when garbage collection wants to happen.
2

Typed Clients are registered as Transient services (see docs).

You are registering your type twice - once as Singleton and (via AddHttpClient) as Transient.

When you inject your service, you will get the Transient Typed Client, not the Singleton instance.

You can see this by listing out the registrations for your service, which will show one Singleton and one Transient.

foreach (var item in builder
  .Services
  .Where(
    service => service.ServiceType.Equals( typeof( INodeService ) ) 
  ) 
 )
{
    Console.WriteLine($"Service: {item.ServiceType.Name} as {item.Lifetime}");
}

Comments

1

Not sure why you're experiencing that. I do get the same value. If you need me to push the repo to GitHub let me know.

enter image description here

2 Comments

I updated the question with more complete code examples.
Do you have a repo? Otherwise, some of the calls to endpoints are going to fail in my end

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.