How do I access data within a scoped service, annotated in Startup.cs, from a .CS file NOT a .RAZOR file using Blazor?

Viewed 433

In my Blazor app I have a scoped service:

DownloadingFile.cs

namespace MyNamespace {
    public class DownloadingFile
    {
        public byte[] theFile { get; set; }
    }
}

I have it annotated in Startup.cs:

Startup.cs

services.AddScoped<DownloadingFile>();

I can successfully access it from a .RAZOR file:

MyFile.razor

<div @onclick="MyTask">Click Me</div>

@code {
    [Inject]
    private NavigationManager navigationManager { get; set; }

    [Inject]
    DownloadingFile downloadingFile { get; set; }

    private async Task MyTask(){
        APIResponse apiResponse = await APIResponse.getResponse();
        downloadingFile = apiResponse.data;
        navigationManager.NavigateTo("/api/Download/DownloadFile?filename=" + apiResponse.filename, true);
    }
}

I have a download controller in:

MyNamespace
`- Controllers
   `- DownloadController.cs

DownloadController.cs

namespace MyNamespace.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class DownloadController : ControllerBase
    {
        [HttpGet("[action]")]
        public IActionResult DownloadFile(string filename)
        {
            byte[] theByteArray = new byte[] {1, 2, 3, 4, ... };
            return File(theByteArray, "application/zip", filename);
        }
    }
}

If I run my Blazor app as is, everything works perfectly.

However, as you expect, I cannot hard-code in theByteArray, it needs to be dynamic. If I change my download controller:

namespace MyNamespace.Controllers {
    [Route("api/[controller]")]
    [ApiController]
    public class DownloadController : ControllerBase
    {
        // TO THIS
        private DownloadingFile downloadingFile;
        public DownloadController(DownloadingFile downloadingFile)
        {
            this.downloadingFile = downloadingFile;
        }

        // OR THIS
        private DownloadingFile downloadingFile;
        public DownloadController() {
            downloadingFile = new DownloadingFile();
        }

        [HttpGet("[action]")]
        public IActionResult DownloadFile(string filename)
        {
            return File(downloadingFile.theFile, "application/zip", filename);
        }
    }
}

The DownloadingFile always comes up null in the DownloadController.


EDIT: To clarify, the object I create from instantiating DownloadingFile in my controller is not null, it is the byte array I was expecting to be there that is null.


If I can just get the byte array from the .RAZOR file to the .CS file, everything will be fine. How do I go about doing this?

4 Answers

Lets look at your code:

        APIResponse apiResponse = await APIResponse.getResponse();
        downloadingFile = apiResponse.data;
        navigationManager.NavigateTo("/api/Download/DownloadFile?filename=" + apiResponse.filename, true);

Why are you setting downloadingFile to APIResponse.data? It's an injected service. You don't reassign it.

In your second version of the controller

        private DownloadingFile downloadingFile;
        public DownloadController(DownloadingFile downloadingFile)
        {
            this.downloadingFile = downloadingFile;
        }

you are injecting DownloadingFile, but this isn't the same instance as in the Blazor Session. It's a new instance created for the server side call to the controller, and downloadingFile.theFile is null.

You can do something like this to trach instances of services:

    public class ScopedService
    {
        public Guid ID => Guid.NewGuid();

        public ScopedService()
        {
            Debug.WriteLine($"New ScopedService ID:{ID}");
        }
    }

"How do I go about doing this?" It isn't obvious from your code what you are actually downloading and from where, so it's difficult to answer.

As stated by MrC aka Shaun Curtis: Since the client and server are different scopes, and you inject the DownloadingFile service as a scoped service, the instance of DownloadingFile where you set downloadingFile = apiResponse.data is different from the one where you try to retrieve it which is why the file is NULL.

It seems to me that your goal is to provide a file download to the user. Right now (assuming your code worked) you're downloading a file on the frontend and trying to set it into a backend service, then return it to the user in an HTTP response. This is obviously impossible without actually sending an HTTP request first.

Instead you should simply navigate to the URL where the file you want resides. To do this, you need to exchange your button for an tag for the browser to allow it.

<a class="btn" href="https://myapiurl.com/filename" role="button">Click Me</a>

Try by installing this extension BlazorDownloadFile

Then register it services

builder.Services.AddBlazorDownloadFile(ServiceLifetime.Scoped);

If at this stage you already have the file bytes:

APIResponse apiResponse = await APIResponse.getResponse();
downloadingFile = apiResponse.data;

assuming you file is a pdf, you could:

@inject IBlazorDownloadFileService _blazorDownloadFile

APIResponse apiResponse = await APIResponse.getResponse();

_blazorDownloadFile.DownloadFile("myFile.pdf", apiResponse.data, "application/pdf");

You could handle more MIME types if you need to.

At the risk of adding another skip to this broken record: your razor component and your controller use different scopes. You may have everything in one codebase, but the different modules of the framework treats scopes differently.

Controllers are scoped to a single HTTP request. Services are instantiated and disposed of every time a request is made. In Blazor Server, services are scoped to the SignalR connection/circuit between the client and server.

These scopes are not shared. Quote from the Blazor documentation on dependency injection (bold emphasis mine):

The Razor Pages or MVC portion of the app treats scoped services normally and recreates the services on each HTTP request when navigating among pages or views or from a page or view to a component. Scoped services aren't reconstructed when navigating among components on the client, where the communication to the server takes place over the SignalR connection of the user's circuit, not via HTTP requests.

I'm still not exactly sure why you want to do things this way, but I don't think what you're asking is possible in the context of the ASP.NET Core DI infrastructure unless you make your service a singleton.

I would follow the advice of other posters and point the user to the API to get the file the same way as you do in your component. If there are security concerns, make it a protected endpoint?

Related