How to return azure blob storage fileas FileStreamResult?

933 views Asked by At

I am developing web API for work with files. I am trying to download file from azure blob storage and return it as response. For development API I using FastEndpoints.

On Stack Overflow I found out that the best option is to use FileStreamResult, but I am getting error:

System.NotSupportedException: Deserialization of types without a parameterless constructor

How can I resolve it and why I am facing it?

Endpoint

public class Endpoint : Endpoint<DownloadFileRequest, FileStreamResult>
{
    private readonly DatabaseContext _context;
    private readonly ILogger _logger;
    private readonly IConfiguration _configuration;

    public Endpoint(DatabaseContext context, ILogger<Endpoint> logger, IConfiguration configuration)
    {
        _context = context;
        _logger = logger;
        _configuration = configuration;
    }

    public override void Configure()
    {
        Get("/file/{id}/download");
        Roles(Role.Administrator, Role.ProjectManager, Role.GraphicDesigner, Role.Accountant);
        Description(b => b.WithName("DownloadFile"));
    }

    public override async Task HandleAsync(DownloadFileRequest r, CancellationToken c)
    {
        var file = await Data.GetTriadaFileAsync(_context, r.Id, c); // gets file to 
                                                                     // download

        if (file == null)
        {
            await SendNotFoundAsync(c);
            return;
        }
           
        var result = await Data.DownloadBlobAsync(file.AzureName, r, c, _configuration);

        if (result != null)
        {
            Response.FileStream = result.FileStream;
        }
    }
}

Data

public static class Data
{
    public static async Task<TriadaFile?> GetTriadaFileAsync(DatabaseContext context, Guid id, CancellationToken c)
    {
        return await context.Files
            .FirstOrDefaultAsync(x => x.Id == id, c);
    }
    public static async Task<FileStreamResult?> DownloadBlobAsync(string blobFileName, DownloadFileRequest r, CancellationToken c, IConfiguration _configuration)
    {
        string connectionString = _configuration.GetSection("Azure").GetValue<string>("BlobConnectionString")!;
        string containerName = _configuration.GetSection("Azure").GetValue<string>("BlobContainerName")!;

        BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString);
        BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName);

        BlobClient blobClient = containerClient.GetBlobClient(blobFileName);

        using (var stream = new MemoryStream())
        {
            await blobClient.DownloadToAsync(stream);
            stream.Position = 0;
            var contentType = (await blobClient.GetPropertiesAsync()).Value.ContentType;

            var fileStreamResult = new FileStreamResult(stream, "application/octet-stream");

            fileStreamResult.FileDownloadName = blobFileName;

            return fileStreamResult;
        }
    }


}

Models

public class DownloadFileRequest
{
    public Guid Id{ get; set; }
}

public class Validator : Validator<DownloadFileRequest>
{
    public Validator()
    {
        RuleFor(x => x.Id)
            .NotEmpty();
    }
}

public class DownloadFileResponse
{
   

}

Question that helped me with me

3

There are 3 answers

0
Dĵ ΝιΓΞΗΛψΚ On

FileStreamResult is a relic from MVC framework days. we do things a bit diferrently now. your requirement can be simplified like this:

public class Endpoint : Endpoint<DownloadFileRequest>
{
    public override void Configure()
    {
        Get("/file/{id}/download");
        ...
    }

    public override async Task HandleAsync(DownloadFileRequest r, CancellationToken ct)
    {
        var fileName = "some-file-name.bin";
        var blob = await Data.DownloadBlobAsync(fileName, Config);

        if (blob is not null)
            await SendStreamAsync(blob?.fileStream, fileName, contentType: blob?.fileContentType);
        else
            await SendNotFoundAsync();
    }
}

public static class Data
{
    public static async Task<(Stream fileStream, string fileContentType)?> DownloadBlobAsync(string blobFileName, IConfiguration configuration)
    {
        ...

        BlobDownloadInfo download = await blobClient.DownloadAsync();
        Stream stream = download.Content;

        var contentType = download.ContentType;

        return new(stream, contentType);
    }
}
1
NaveenBaliga On

I was able to test it with my below API application and download the blob content as a stream. Could you please check if that helps ?

using Azure.Storage.Blobs;
using System.IO;
using System.Web.Mvc;
using System.Web;
using System.Threading.Tasks;
using Azure.Storage.Blobs.Models;
using System.Web.Http;
using System.Net.Http;
using System.Net;

namespace StorageFileStream.Controllers
{
    public class HomeController : ApiController
    {

        private readonly string _connectionString = "XXXXXXXXX";
        private readonly string _containerName = "MyContainerName";
        private readonly string _blobName = "MyBlobName";

       
        [System.Web.Http.HttpGet]
        public async Task<HttpResponseMessage> DownloadFile()
        {
            BlobServiceClient blobServiceClient = new BlobServiceClient(_connectionString);
            BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(_containerName);
            BlobClient blobClient = containerClient.GetBlobClient(_blobName);

           // Download the blob content
            BlobDownloadInfo download = await blobClient.DownloadAsync();
            Stream stream = download.Content;

           // Create an HttpResponseMessage
            HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
            response.Content = new StreamContent(stream);
           // Set the content type of the response to the blob's content type
            response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(download.ContentType);
            response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
            {
                FileName = _blobName
            };

            // Set the content length header
            if (stream.CanSeek)
            {
                response.Content.Headers.ContentLength = stream.Length;
            }

            return response;
        }
    }
}

I was able to invoke the above GET API operation and download the blob response stream. Hope this helps.

0
Petr Barabáš On

The problem was whatever the content type was, it always serialize object to JSON. So I have fixed it like this in Program.cs:

app.UseFastEndpoints(c =>
{
    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);

    c.Serializer.ResponseSerializer = (rsp, dto, cType, jCtx, ct) =>
    {
        // this condition allows any content type
        if (dto is FileStreamResult fsr)
        {
            rsp.ContentType = fsr.ContentType;
            rsp.Body = fsr.FileStream;

            return rsp.SendStreamAsync(rsp.Body, fsr.FileDownloadName, null, fsr.ContentType); ;
        }

        rsp.ContentType = cType;
        return rsp.WriteAsync(JsonSerializer.Serialize(dto, options), ct);
    };
    c.Serializer.RequestDeserializer = async (req, tDto, jCtx, ct) =>
    {       
        using var reader = new StreamReader(req.Body);
        return JsonSerializer.Deserialize(await reader.ReadToEndAsync(), tDto, options);              
    };
});

Data

public static async Task<FileStreamResult?> DownloadBlobAsync(string blobFileName , IConfiguration configuration)
    {
        string connectionString = configuration.GetSection("Azure").GetValue<string>("BlobConnectionString")!;
        string containerName = configuration.GetSection("Azure").GetValue<string>("BlobContainerName")!;

        BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString);
        BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName);

        BlobClient blobClient = containerClient.GetBlobClient(blobFileName);

        BlobDownloadInfo download = await blobClient.DownloadAsync();
        Stream stream = download.Content;

        var contentType = download.ContentType;
      
        return new FileStreamResult(stream, contentType)
        {
            FileDownloadName = blobFileName
        };
    }
}