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].

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.