5

I've been searching for a while and while it should be simple, I just can't get it to work. Based on examples I've seen, this is where I got so far:

SomeAppService.cs

public async Task<FileStream> Download(long? id)
{
    var attachment = await _repository.FirstOrDefaultAsync(x => x.Id == id);

    var fileStream = new FileStream($"{attachment.FileName}.{attachment.FileExtension}", 
        FileMode.Create, FileAccess.Write);
    fileStream.Write(attachment.File, 0, attachment.File.Length);

    return fileStream;
}

As it can be noticed, "FileName", "FileExtension" and "File" (which is the forementioned byte array) are stored in a database. The attachment can be any kind of file, save for banned extensions in the Upload method (not shown). Then in my controller I have:

SomeController.cs

[AllowAnonymous]
[HttpGet("Download/{id}")]
public async Task<IActionResult> Download(long? id)
{
    var fileStream = await appService.Download(id);
    return new FileStreamResult(fileStream, "application/octet-stream");
}

However, when I hit the download endpoint, I end up with a file named "response", no extension, with 0 bytes.

Resources:

Return file in ASP.Net Core Web API

Return a FileResult from a byte[]

Save and load MemoryStream to/from a file (Response with 255 upvotes gave me de idea of how to turn a byte array into a filestream, but I don't know if that works)

6
  • 3
    You probably need to reset the position of the stream to the beginning. However it seems you could just wrap attachment.File in a MemoryStream and pass that to the FileStreamResult constructor directly. There's also a FileContentResult you could pass attachment.File to instead of creating a stream. Commented Jan 30, 2020 at 21:54
  • The FileStream returned by Download() will be positioned immediately after the data you've just written. Does Seek()ing back to the beginning of the Stream before returning it produce a different result? Commented Jan 30, 2020 at 21:54
  • With a File you offer for download, stuff like the (prefered) filenmane and lenght (edit: and position too) have to be explicitly specified. I do not know of that pattern myself, I only ever used HTTP handlers: c-sharpcorner.com/UploadFile/dacca2/… and even that a while ago. Commented Jan 30, 2020 at 21:54
  • @BACON Just tried it. No dice. Commented Jan 30, 2020 at 22:10
  • something like this, return File(byteArray, "contentType"); content type is for example application/pdf Commented Jan 30, 2020 at 22:14

2 Answers 2

8

Thanks everyone, in the end FileContentResult did the trick.

This is how it looks:

Service

public async Task<Attachment> Download(long? id)
{
    return await _repository.FirstOrDefaultAsync(x => x.Id == id);
}

Controller

[AllowAnonymous]
[HttpGet("Download/{id}")]
public async Task<IActionResult> Download(long? id)
{
    var attachment = await appService.Download(id);
    return new FileContentResult(attachment.File, 
        MimeTypeMap.GetMimeType(attachment.FileExtension))
    {
        FileDownloadName = $"{attachment.NomeFile}.{attachment.FileExtension}"
    };
}

MimeTypeMap can be found here

Sign up to request clarification or add additional context in comments.

Comments

3

If you want to stream a file from a database blob from entity framework without loading it into memory. First split the data model into two parts;

public class Attachment{
    public int Id { get; set; }
    public string Filename { get; set; }
    public string ContentType { get; set; }
    public virtual AttachmentBlob Blob { get; set; }
    //...
}

public class AttachmentBlob{
    public int Id { get; set; }
    public byte[] File { get; set; }
}

Map them to the same table, but not as an owned type;

   modelBuilder.Entity<Attachment>(e => {
       e.HasOne(a => a.Blob)
        .WithOne()
        .HasForeignKey<AttachmentBlob>(b => b.Id);
   });
   modelBuilder.Entity<AttachmentBlob>(e => {
       e.ToTable("Attachment");
   });

Then you can read and write them either as byte arrays, or as streams;

   public static async Task Read(DbContext db, Attachment attachment, Func<Stream,Task> callback)
   {
      await db.Database.OpenConnectionAsync();
      try {
         var conn = db.Database.GetDbConnection();
         var cmd = conn.CreateCommand();
         var parm = cmd.CreateParameter();
         cmd.Parameters.Add(parm);
         parm.ParameterName = "@id";
         parm.Value = attachment.Id;
         cmd.CommandText = "select File from Attachment where Id = @id";
         using (var reader = await cmd.ExecuteReaderAsync()){
            if (await reader.ReadAsync())
               await callback(reader.GetStream(0));
         }
      } finally {
         await db.Database.CloseConnectionAsync();
      }
   }

   public class AttachmentResult : FileStreamResult
   {
      private readonly DbContext db;
      private readonly Attachment attachment;

      public AttachmentResult(DbContext db, Attachment attachment) : base(new MemoryStream(), attachment.ContentType)
      {
         this.db = db;
         this.attachment = attachment;
      }

      public override async Task ExecuteResultAsync(ActionContext context)
      {
         await Read(db, attachment, async s => {
            FileStream = s;
            await base.ExecuteResultAsync(context);
         });
      }
   }

   public static async Task Write(DbContext db, Attachment attachment, Stream content)
   {
      await db.Database.OpenConnectionAsync();
      try {
         var conn = db.Database.GetDbConnection();
         var cmd = conn.CreateCommand();
         cmd.Transaction = db.Database.CurrentTransaction?.GetDbTransaction();
         var parm = cmd.CreateParameter();
         cmd.Parameters.Add(parm);
         parm.ParameterName = "@id";
         parm.Value = attachment.Id;
         parm = cmd.CreateParameter();
         cmd.Parameters.Add(parm);
         parm.ParameterName = "@content";
         parm.Value = content;
         cmd.CommandText = "update Attachment set File = @content where Id = @id";
         await cmd.ExecuteNonQueryAsync();
      } finally {
         await db.Database.CloseConnectionAsync();
      }
   }

   public static Task InsertAttachment(DbContext db, Attachment attachment, Stream content){
      var strat = db.Database.CreateExecutionStrategy();
      return strat.ExecuteAsync(async () => {
         using (var trans = await db.Database.BeginTransactionAsync())
         {
             db.Set<Attachment>.Add(attachment);
             await db.SaveChangesAsync();
             await Write(db, attachment, content);
             trans.Commit();
             db.ChangeTracker.AcceptAllChanges();
         }
      });
   }

1 Comment

This method can also be adapted to large strings with reader.GetTextReader() or parm.Value = [TextReader].

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.