2

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?

Brian
  • 1,726
  • 2
  • 24
  • 62
  • Is this WASM or server side? – Brian Parker Sep 24 '21 at 19:02
  • That's the thing, currently it is server side, but I believe the powers that be want to move it to WASM. – Brian Sep 24 '21 at 19:06
  • Where are you assigning the value for theFile in the second case? – Mayur Ekbote Sep 26 '21 at 05:13
  • @MayurEkbote I am trying to access it from the download controller. I mean, I have the byte array **somewhere** in the code, it shouldn't be too hard to access it from everywhere. – Brian Sep 27 '21 at 12:19
  • Please note that your Title has very little to do with the rest of the question. Also, your tags are not getting you the best possible audience. – H H Sep 27 '21 at 12:56
  • @HenkHolterman I changed the title. Better? – Brian Sep 27 '21 at 13:14
  • Can you set the byte array (.theFile) value before returning File(...) and check if it is working? – Mayur Ekbote Sep 27 '21 at 13:52
  • You could have dependency injection scope issues. You have mentioned below that theFile property is set somewhere else in the controller and you are using it here again. If update the question to share the complete example I can attempt a permanent solution. But in absence of that, you can set the theFile value at each request. That would be your temp fix. – Mayur Ekbote Sep 27 '21 at 14:03
  • @MayurEkbote Yes, theFile is set in my .RAZOR page, but it is not there in my download controller. I suspect it is because I am instantiating a new instance of it in the download controller and it is not the same instance that I injected into my .RAZOR page. What I need is to "inject" the same DownloadingFile instance into my download controller as is in my .RAZOR file. – Brian Sep 27 '21 at 14:34
  • OH. That's not the correct usage. You either inject it in the controller OR in the razor file. Not both. There is no good reason for doing both. – Mayur Ekbote Sep 27 '21 at 15:19
  • @MayurEkbote So, how can I set it in my .RAZOR file, then pick it up in the download controller .CS file? – Brian Sep 27 '21 at 15:31
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237558/discussion-between-mayur-ekbote-and-brian). – Mayur Ekbote Sep 27 '21 at 15:36
  • 3
    But your razor page runs on client while controller is on server, so DownloadingFile instances are of course different, you can't magically pass byte array like that. – Evk Sep 27 '21 at 20:59
  • As @Evk says, this question appears to seriously misunderstand how client/server architecture works (in particular for WASM). Also, it seems like a classic X/Y problem as you're asking how to accomplish something impossible in order to achieve some ends that aren't obvious to me. You appear to want to download a file _on the client_ and somehow consume that on the server without actually doing the work of passing that data between client and server. While not particularly difficult, that is the hard part, and you don't get to magically have that data appear for free on the server. – Kirk Woll Sep 28 '21 at 00:16
  • @KirkWoll *"You appear to want to download a file on the client and somehow consume that on the server"* ... That is not it at all. I want to download the file **to** the client **from** the API from a .RAZOR file **on** the client. I then want to use the downloaded byte array (still on the client) in my download controller (still on the client). Everything is in the same codebase; once the client gets the byte array from the API, the byte array stays on the client, until downloaded to the **client's** file system. – Brian Sep 28 '21 at 12:00
  • @Brian, the controller is not "still on the client". A controller is by definition operating on the server. – Kirk Woll Sep 28 '21 at 14:00
  • @KirkWoll the path for my controller is: `MyApp -> Controllers -> DownloadController.cs` and I am hitting the API with code in: `MyApp -> Shared -> Searcher.razor`. I am not trying to be difficult here, but I cannot make it any more plain. All of this code is within my app's codebase. I believe there may be a huge communication breakdown somewhere, but I cannot figure out where. – Brian Sep 28 '21 at 14:47
  • That APIResponse.getResponse() - is that some thing party api? Why not just call it from DownloadController.DownloadFile, why this extra step with calling it on razor page? – Evk Oct 14 '21 at 21:12
  • When you step through with your dubugger, is `downloadingFile` null? Before `return File(downloadingFile.theFile, "application/zip", filename);` – mxmissile Oct 15 '21 at 16:49

4 Answers4

2

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.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • I don't think that is a problem. He is claiming that the DownloadingFile is null in the controller itself. I find that strange. – Mayur Ekbote Sep 25 '21 at 18:14
  • @MayurEkbote - having re-read the question, I've changed my answer. I think `downloadingFile.theFile` is null, not the actual service. – MrC aka Shaun Curtis Sep 25 '21 at 20:51
  • @MrCakaShaunCurtis What I am trying to do is to get the file, in the form of a byte array, from the API in one part of my code, and then access the byte array from another part of my code so the local user can download the file to their computer. – Brian Sep 27 '21 at 12:21
  • 1
    Why don't you just point the user to the API to download the file directly? – MrC aka Shaun Curtis Sep 27 '21 at 17:33
  • @MrCakaShaunCurtis I cannot, for security reasons. – Brian Sep 28 '21 at 12:01
  • Is your Blazor Application Blazor Server or Blazor WASM? I believe it's Server as you are registering your service in `Startup`, but can you confirm please. – MrC aka Shaun Curtis Sep 28 '21 at 15:04
  • @MrCakaShaunCurtis Currently it is "Server" but I believe the higher-ups are wanting to switch it over to "WASM". – Brian Sep 28 '21 at 15:47
  • In Server mode all your code is running on the server - only the UI is running on the browser. The only way to download the file is to point the user to API to get the file as you did in MyFile.razor. In WASM mode, I'm not sure there's a way to get the byte array in the browser and then save it locally. It's basically the same as downloading it directly to the browser. – MrC aka Shaun Curtis Sep 28 '21 at 19:07
0

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>
Bertramp
  • 376
  • 1
  • 15
  • This is not two different pieces of software ... it is all one piece of software. There is no client **AND** server, it is just the software. Yes, I make an API call and it sends me the data via REST, but there is only the one piece of software. – Brian Oct 14 '21 at 19:09
  • There is always a client and a server. That's how the internet works - a server that sends content to a client upon request. Just because you write the code in one solution doesn't mean there is no client or server. Blazor server uses SignalR to communicate between the client and the server. – Bertramp Oct 14 '21 at 19:21
0

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.

Zach Ioannou
  • 656
  • 2
  • 8
  • 19
0

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?

lrpe
  • 730
  • 1
  • 5
  • 13