Skip to main content

ASP.NET Boilerplate: how to generate and download files on-demands

Usually in our applications, we need to create PDF, XLS or CVS files and serve them on-demand to be downloaded by users. The goal of this post is to show how to implement this functionality in an ASP.NET Boilerplate (ABP) project.

This post follows this answer in the ABP's official forum.

1. Overview


In general, the recommended solution is to implement it in two iterations:
  1. The client requests to generate the file. In server side, the file is generated and stored (in memory or on the hard disk) and the server returns an ID for it.
  2. The client requests to download the file, using the ID for the file.

2. Implementation


For a better understanding, we will show an example of how to serve PDF files on-demand, breaking down by tier (from ABP's architecture) the solution and analyzing its responsibilities.

2.1. Application Tier


When the user request to download a file, we'll generate it in this tier. It's recommended to implements this functionality in this tier due to reasons like re-utilization, responsibility (business logic matters) and separation of concerns.

The PDF file generation can be done in two ways:
  1. Generate the file and store it on the server's hard disc.
  2. Generate the file and store it in memory.

The selection is an architecture matter and must consider the non-functional requirements.

Storing files in the server's hard disk


In this case, we're going to generate the PDF file and store it in a temporal folder on the server's hard disk.

This is a sample code:

namespace namespace DemoAbp.Services
{
    public class DemoAbpAppService : ApplicationService, IDemoAbpAppService 
    {
        public FileTokenOutput GeneratePdf()
        {
            // Generate unique token based in a timestamp
            string token = DateTime.Now.Ticks + ".pdf";

            // Store files in the temporal folder
            FileStream fileStream = new FileStream(
                    AppDomain.CurrentDomain.BaseDirectory + "tmp\\" + token, 
                    FileMode.Create, 
                    FileAccess.Write,
                    FileShare.ReadWrite);

            // Generate de PDF file and write the content in the
            // "fileStream" stream.

            return new FileOutput
            {
                File = new FileDto()
                {
                    Token = token,
                    Filename = "MyFilename.pdf"
                }
            };     
        }
    }
}

Storing files in-memory


In this case, we're going to use the ABD cache support (check this) to store in-memory the generated PDF file. But first we need to inject the ICacheManager dependency in the service

This is a sample code:

namespace namespace DemoAbp.Services
{
    public class DemoAbpAppService : ApplicationService, IDemoAbpAppService 
    {
        // Other dependencies

        // ABD cache dependency
        private readonly ICacheManager cacheManager;

        // Inject other dependencies
        public DemoAppService(ICacheManager cacheManager)
        {
            this.cacheManager = cacheManager;
        }

        public FileTokenOutput GeneratePdf(IdInput input)
        {
            // Generate unique token based in a timestamp
            string token = DateTime.Now.Ticks + ".pdf";

            MemoryStream memoryStream = new MemoryStream();

            // Generate the PDF file and write the content in the
            // in the "ms" stream

            // Storing the stream in the cache.
            // The third param sets to delete the object after 1 minute
            cacheManager.GetCache("pdf").Set(
                    token, 
                    memoryStream,
                    TimeSpan.FromMinutes(1));
   
            return new FileOutput
            {
                File = new FileDto()
                {
                    Token = token,
                    Filename = "MyFilename.pdf"
                }
            };
        }
    }
}

In this example, we used a MemoryStream object to write the PDF file content in memory. Next, we'll save the MemoryStream object in the cache, using the token as an identifier.

2.2. Web Tier


In this tier, we will serve the downloads requests for PDF files and will use a MVC controller to do it.

Storing files in the server's hard disk


The controller is very easy. First, load the file named token from the temporal folder (previously mentioned), write the content on the HTTP response stream using the helper function File and delete the file at end.

This is a sample code:

namespace DemoAbp.Controllers
{
    [AbpMvcAuthorize]
    public class FileController : DemoAbpControllerBase
    {
        public FileResult DownloadPdfFile(string token)
        {
            try
            {
                // Load file from the temporal folder
                FileStream fileStream = new FileStream(
                        AppDomain.CurrentDomain.BaseDirectory + "tmp\\" + token,
                        FileMode.Open, 
                        FileAccess.Read,
                        FileShare.Delete);

                // Set the proper HTTP response content type
                FileStreamResult fileStreamResult =
                        File(fileStream, "application/pdf", token);
                return fileStreamResult;
            }
            catch (Exception e)
            {
                throw new UserFriendlyException("Error", "Error downloading file. Please try again.", e);
            }
            finally
            {
                System.IO.File.Delete(AppDomain.CurrentDomain.BaseDirectory + "tmp\\" + token);
            }
        }
    }
}

Storing files in-memory


Similarly, the implementation is very easy plus a few details. First, we need to inject ICacheManager dependency to get access to the cache mechanism. After that, load the PDF file from cache as a MemoryStream type, using the token parameter as it's identifier. At the end, remove the object from the cache.

This is a sample code:

namespace DemoAbp.Controllers
{
    [AbpMvcAuthorize]
    public class FileController : DemoAbpControllerBase
    {
        private readonly ICacheManager cacheManager;

        public FileController(ICacheManager cacheManager)
        {
            this.cacheManager = cacheManager;
        }

        public FileResult DownloadPdfFile(string token)
        {
            try
            {
                // Loading file from cache.
                MemoryStream ms = (MemoryStream) this.cacheManager.GetCache("pdf").GetOrDefault(token);

                // Set the proper HTTP response content type
                FileContentResult fileContentResult =
                        File(ms.ToArray(), "application/pdf", token);
                return fileContentResult;
            }
            catch (Exception e)
            {
                throw new UserFriendlyException("Error", "Error downloading file. Please try again.", e);
            }
            finally
            {
                this.cacheManager.GetCache("pdf").Remove(token);
            }
        }
    }
}

2.3. Presentation Tier


In this tier, at first, we are going to request generate the PDF file and will get an ID (token) as a result. Then, we'll use token to request the file download from server.

The following is a sample in an AngularJS app.



$scope.exportToPdf = function() {
    abp.ui.setBusy(null, demoAbpService.generatePdf({}).success(function (result) {
        // Get PDF file token
        var token = result.file.token;

        // Get PDF file name
        var filename = result.file.filename;
        
        // Do HTTP request to download file
        $http.post('/file/downloadPdfFile', { token }, { responseType: 'arraybuffer' }).then(
            function successCallback(response) {
                // This callback will be executed asynchronously when the request will be available.
                var file = new Blob([response.data], { type: 'application/pdf' });
                var fileURL = window.URL.createObjectURL(file);

                // Create an element to execute the download.
                var element = document.createElement("a");
                document.body.appendChild(element);
                element.style = "display: none";                
                element.href = fileURL;
                element.download = filename;
                element.click();

                abp.notify.success('PDF file generated successfully.');
        }, 
        function errorCallback(response) {
            // This callback will be executed asynchronously if there is an error or if server return an error code.
            abp.notify.error('Error generating the PDF file.');
        });
    }));
}

3. Conclusion


In this post, we show a solution to how to download file on-demand in an ABP project. As could be appreciated, the implementation take place in 3 tiers and involved differents technologies. This is a basic implementation, you surely have to fit it to your needs. I hope it is useful.

Comments

  1. Hi. You won't know how useful this post was for me. Thank you very much for sharing.

    Small correction on the presentation code in Javascript.

    WRONG

    var element = document.createElement("element");

    CORRECT

    var element = document.createElement("a");

    Rationale: The HTML element being created should be a link ("a"), so that it respond to the click action appropriately.

    ReplyDelete

Post a Comment

Popular posts from this blog

How to use Font Awesome with IText

This post is about on how to use Font Awesome with IText library to generate awesome icons in our PDF documents. In this post, we are using  ITextSharp , a C# port for IText, but those are the same functionalities.

TinyMCE: anti-forgery token failed on file upload in an ASP.NET Boilerplate application

TinyMCE is an excellent full featured and well documented web editor that we can use in our web applications. But, integration with ASP.NET MVC 5 (as part of the ASP.NET Boilerplate technology stack) may be tricky to accomplish. When you set up the file upload plugin in TinyMCE, you have to consider the ASP.NET MVC 5 anti-forgery system. Otherwise, HTTP requests may fail loading resources to server due to an invalid or empty anti-forgery token error. This is a security and technology concern that can run out your time if you don't know about it. This article explains how this mechanism works and shows some solutions in an ASP.NET Boilerplate application.