In this article, we are going to learn how to download files from Azure using ASP.NET Core Web API as the server-side project and Blazor WebAssembly as the client-side. This article is a continuation of a previous one where we learned about uploading files to Azure. In that article, we also created Azure storage, so if you don’t have one created, you can do it following the instructions from the mentioned article.

To download the source code for this article, you can visit our Download Files from Azure repository.

If you want to learn more about Blazor WebAssembly, we strongly suggest visiting our Blazor WebAssembly series of articles, where you can read about Blazor WebAssembly development, authentication, authorization, JSInterop, and other topics as well.

So, let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Connecting ASP.NET Core Web API Project with Azure

In this article, we are going to create new projects for both the server and the client. That said, let’s create a new ASP.NET Core Web API project named DownloadAzure.Server.

If we inspect our Azure storage created in the previous article, we are going to find a connection string that we require in our server app:

Access keys connection string for Azure storage

We are going to use it in our appsettings.json file:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "AzureConnectionString": "DefaultEndpointsProtocol=https;AccountName=blazoruploadazure;AccountKey=S7/svnosTfuXxGIJ20fl6Y1N3YcdHefHH...;EndpointSuffix=core.windows.net"
  },
  "AllowedHosts": "*"
}

Next, we have to install the Azure.Storage.Blobs library:

Azure Storage Blob library to Upload Files to Azure

After these actions, we have everything we need to connect to our Azure storage.

Retrieving all the Files from the Azure Storage

Since we are going to have both the client-side and server-side applications, we will need a dto class to help us with the data transfer. So, let’s create a new class named BlobDto with the required properties:

public class BlobDto
{
    public string Name { get; set; }
    public string Uri { get; set; }
    public string ContentType { get; set; }
}

Of course, you can always expand the list of properties if you need more of them, but for our example, these three are quite enough.

Now, in the Controllers folder, we are going to create a new BlobsController and implement an action to retrieve all the blobs from the Azure blob storage:

[Route("api/blobs")]
[ApiController]
public class BlobsController : ControllerBase
{
    private readonly string _azureConnectionString;
    private readonly string _azureContainerName;

    public BlobsController(IConfiguration configuration)
    {
        _azureConnectionString = configuration.GetConnectionString("AzureConnectionString");
        _azureContainerName = "upload-container";
    }

    public async Task<IActionResult> Get()
    {
        var blobs = new List<BlobDto>();
        var container = new BlobContainerClient(_azureConnectionString, _azureContainerName);

        await foreach (var blobItem in container.GetBlobsAsync())
        {
            var uri = container.Uri.AbsoluteUri;
            var name = blobItem.Name;
            var fullUri = uri + "/" + name;

            blobs.Add(new BlobDto { Name = name, Uri = fullUri, ContentType = blobItem.Properties.ContentType });
        }

        return Ok(blobs);
    }
}

In the constructor, we assign values to our private fields. We extract the connection string from the appsettings.json file and hardcode the container name since we know its name from the previous article. Of course, if you want, you can move that value to the appsettings.json file as well. We would even suggest that.

Then, in the Get action, we create a new BlobDto list and create a reference to the Azure container using the connection string and the container name. Next, we iterate through all the blobs inside that container and extract required values into a new BlobDto object, which we store in a list.

Finally, we return that list as a response to the client app.

Testing

In the previous article, we have uploaded one file to the storage. But while preparing for this article, we have used the project from a previous one to upload two more files to our storage:

files in the Azure storage

So, you can do the same. Just visit the previous article and you will find the link to the source code of the project you can use to upload files.

Now we can open Postman and send a single Get request to test our action:

GetAllBlobls Postman Request

We can see our result returns three files with their names, URI, and ContentType properties. So, we have everything we need to display these files in our client application.

Listing all the Files in the Blazor WebAssembly Application

First, let’s create a new Blazor WebAssembly application (don’t use the ASP.NET Core hosted option) named DownloadAzure.Client.

After the creation, let’s modify the launchSettings.json file:

{
  "profiles": {
    "DownloadAzure.Client": {
      "commandName": "Project",
      "dotnetRunMessages": "true",
      "launchBrowser": true,
      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
      "applicationUrl": "https://localhost:5011;http://localhost:5012",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Then, we have to modify the HttpClient configuration in the Program class:

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");

    builder.Services.AddScoped(sp => new HttpClient
    { 
        BaseAddress = new Uri("https://localhost:5001/api/") 
    });

    await builder.Build().RunAsync();
}

Lastly, we are going to modify the FetchData component to list all our files from Azure Storage:

@page "/fetchdata"
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (blobs == null)
{
    <p><em>Loading...</em></p>
}
else
{
    foreach (var blob in blobs)
    {
        <div style="border:1px solid gray; margin-bottom:10px; width: 202px;">
            <img src="@blob.Uri" alt="@blob.Name" width="200"/>
        </div>
    }
}

@code {
    private BlobDto[] blobs;

    protected override async Task OnInitializedAsync()
    {
        blobs = await Http.GetFromJsonAsync<BlobDto[]>("blobs");
    }

    public class BlobDto
    {
        public string Name { get; set; }
        public string Uri { get; set; }
        public string ContentType { get; set; }
    }
}

There is nothing complicated in this code. We just send the HTTP request as soon as our component initializes and once we get the response, we iterate through the list and display images on the page.

One thing to note – for the sake of simplicity, we create a BlobDto class inside the component file. Since we have only one DTO object, this is just fine for the example. Of course, in real-world applications, you should create a shared project and place your shared files in it. That way, you don’t have to duplicate the same code in both projects.

Before we test this, we have to enable CORS on the server-side project. So, let’s modify the ConfigureServices method in the Startup class:

public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(policy =>
    {
        policy.AddPolicy("CorsPolicy", opt => opt
        .AllowAnyOrigin()
        .AllowAnyHeader()
        .AllowAnyMethod());
    });

    services.AddControllers();
}

And also, add a single code line to the Configure method:

app.UseHttpsRedirection();
app.UseCors("CorsPolicy");

Excellent.

Testing

Now, let’s start both apps, and navigate to the FetchData component:

Listed all the files from Azure storage in Blazor WebAssembly app

There we go, we can see all our files from the Azure storage. Now, we can implement the logic to download files from Azure with both Web API and Blazor apps.

Download Files from Azure – Web API Part

Let’s start with a new DownloadController creation in the Controllers folder:

[Route("api/download")]
[ApiController]
public class DownloadController : ControllerBase
{
    private readonly string _azureConnectionString;
    private readonly string _azureContainerName;

    public DownloadController(IConfiguration configuration)
    {
        _azureConnectionString = configuration.GetConnectionString("AzureConnectionString");
        _azureContainerName = "upload-container";
    }
}

After the initial logic, we can create our Download action:

[HttpGet("{name}")]
public async Task<IActionResult> Download(string name)
{
    var container = new BlobContainerClient(_azureConnectionString, _azureContainerName);

    var blob = container.GetBlobClient(name);

    if (await blob.ExistsAsync())
    {
        var a = await blob.DownloadAsync();

        return File(a.Value.Content, a.Value.ContentType, name);
    }

    return BadRequest();
}

Here we start with the container reference. Then, we extract our blob by the name from that container. If the blob exists, we call the DownloadAsync method, which downloads the blob from the service including its metadata and properties. Next, we return the File to the client with the Content, ContentType, and the Name of the blob. If the blob doesn’t exist, we just return a bad request.

That’s all it takes on the server-side. We can move on to the client app.

Download Files from Azure – Blazor WebAssembly Logic

The first thing we are going to do is to modify the wwwroot folder by creating a new folder scripts and inside a new file downloadScript.js:

export function download(options) {
    var url = "data:" + options.contentType + ";base64," + options.byteArray

    var anchorElement = document.createElement('a');
    anchorElement.href = url;
    anchorElement.download = options.fileName;
    anchorElement.click();
    anchorElement.remove();
}

We create a JavaScript function that accepts a single options object parameter. The options object will contain three properties: returned byte array from the server, blob’s name, and the content type. We are going to use this function to start the download on the client-side app, once it receives a response from a server. One more thing, we use the export keyword for this function since we are going to isolate this JS file as a separate module. You can learn more about reading the JavaScript functions in Blazor WebAssembly and the advantage of this approach in our How to Call JavaScript Functions with C# in Blazor WebAssembly article.

Also, if you are not that familiar with JSInterop in Blazor WebAssembly, feel free to read all the articles from the series. You can find all the articles on our Blazor WebAssembly series page.

Then, we have to modify the FetchData.razor file:

@page "/fetchdata"
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (blobs == null)
{
    <p><em>Loading...</em></p>
}
else
{
    foreach (var blob in blobs)
    {
        <div style="border:1px solid gray; margin-bottom:10px; width: 202px;">
            <img src="@blob.Uri" alt="@blob.Name" @onclick="() => Download(blob.Name, blob.ContentType)"
                 class="imgClass"/>
        </div>
    }
}

@code {
    [Inject]
    public IJSRuntime JSRuntime { get; set; }

    private BlobDto[] blobs;
    private IJSObjectReference  _jsModule;

    protected override async Task OnInitializedAsync()
    {
        _jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./scripts/downloadScript.js");
        blobs = await Http.GetFromJsonAsync<BlobDto[]>("blobs");
    }

    private async Task Download(string name, string contentType)
    {
        var response = await Http.GetByteArrayAsync("download/" + name);

        await _jsModule.InvokeVoidAsync("download", new 
        { 
            ByteArray = response,
            FileName = name,
            ContentType = contentType
        });
    }

    public class BlobDto
    {
        public string Name { get; set; }
        public string Uri { get; set; }
        public string ContentType { get; set; }
    }
}

In the HTML part, we just modify our img element by adding the onclick event and adding a new class to extract additional styling.

The @code part contains the main changes.

Since we have JavaScript logic, which we want to import as a module, we have to inject the IJSRuntime interface and create a private IJSObjectReference variable. Then in the OnInitializedAsync method, we import our file and store it as a module in our private variable. Again, you can learn much more about this in our How to Call JavaScript Functions with C# in Blazor WebAssembly article.

Then in the Download method, we send an HTTP request that returns a byte array from the server. After getting the response, we call our download function from the js file and pass the required parameters as an object.

Finally, we have to create an additional FetchData.razor.css file for our image styles:

.imgClass {
    width: 200px;
}

    .imgClass:hover {
        cursor: pointer;
        opacity: 0.8;
    }

Testing

Let’s start both apps and navigate to the FetchData page. As soon as we hover over any picture, we are going to see the result of applied styles. Once we click on the picture, we are going to see the downloaded file:

Download file from Azure in Blazor WebAssembly

Excellent.

Everything works as expected.

Conclusion

So, as you can see, with a few simple steps, we have achieved our goal for this article.

To sum up, we have learned:

  • How to list all the files from the Azure storage on the Blazor WebAssembly application
  • The way to download files from Azure using both ASP.NET Core Web API and Blazor WebAssembly applications
  • How to use JavaScript functions to enable download action on the client-side

Until the next article.

All the best.

 

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!