{
    "componentChunkName": "component---src-templates-single-blog-js",
    "path": "/news/file-uploads-with-filepond-and-net-core/",
    "result": {"data":{"wpPost":{"id":"cG9zdDo1MDg3","content":"\n<p>We&#8217;ve had a number of interesting projects recently and we&#8217;ve seen a large demand for modern user-friendly file uploads. <a href=\"https://github.com/pqina/filepond\">FilePond</a>, a JavaScript file upload library, fit the bill and we found it extremely powerful when integrated with .NET core (or .NET 5) and AWS services.  Here&#8217;s a quick look at how we do it. </p>\n\n\n\n<h3>.NET Core File Upload Model</h3>\n\n\n\n<p>We first created a simple model to represent our files in the database. It&#8217;s got a unique identifier, Id. A few properties we can capture from the file, it&#8217;s name, type, size and if the user has chosen to delete the file. A GUID ( Globally Unique Identifier ), which we will use as the reference to store these files in our bucket. Finally, the foreign key for the related object in our system, CaseId.</p>\n\n\n\n<pre class=\"wp-block-code\"><code>    public class Attachment\n    {\n        public int Id { get; set; }\n        public string Filename { get; set; }\n        public string Filetype { get; set; }\n        public long FileSize { get; set; }\n        public string Guid { get; set; }\n        public bool Deleted { get; set; }\n        public DateTime CreatedOn { get; set; }\n\n        public int CaseId { get; set; }\n    }</code></pre>\n\n\n\n<p>After writing the model, we need a new controller with dependency injection for our EF context and we also include a reference to the Amazon S3 SDK. We won&#8217;t be implementing our S3 helper methods here, there&#8217;s plenty of examples to be found on the <a href=\"https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html\">Amazon S3 documentation</a>.</p>\n\n\n\n<pre class=\"wp-block-code\"><code>    &#91;Route(\"api/&#91;controller]\")]\n    &#91;ApiController]\n    public class AttachmentController : ControllerBase\n    {\n        private readonly CaseContext _context;\n        private readonly IAmazonS3 _amazonS3;\n\n        public AttachmentController(CaseContext context, IAmazonS3 amazonS3)\n        {\n            _context = context;\n            _amazonS3 = amazonS3;\n        }\n\n        private static string GetBucketName() =&gt; \"Bucket\";\n\n        /// You might not even be using S3, you'll have to implement these, sorry! \n        /// Check out the amazon docs for more information:\n        /// https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html\n        private Task&lt;bool&gt; UploadToS3Async(MemoryStream newMemoryStream, object guid)\n        {\n            throw new NotImplementedException();\n        }\n\n        private Task&lt;bool&gt; DeleteFromS3Async(DeleteObjectRequest deleteObjectRequest)\n        {\n            throw new NotImplementedException();\n        }\n\n        private Stream GetS3FileStreamAsync(string bucketName, string imageKey)\n        {\n            throw new NotImplementedException();\n        }\n    }</code></pre>\n\n\n\n<h3>Get the file upload on the page</h3>\n\n\n\n<p>We&#8217;re working on something akin to the standard Edit view scaffolded by our Case model controller, but we need to get the new file input on the page. The <a href=\"https://pqina.nl/filepond/docs/patterns/installation/\">FilePond docs</a> are a good place to start when getting the upload element onto the page. We need to include the scripts and styles, then generate the FilePond element with the included scripts. </p>\n\n\n\n<pre class=\"wp-block-code\"><code>@model Models.Case\n&lt;!-- In the document head --&gt;\n&lt;link href=\"https://unpkg.com/filepond/dist/filepond.css\" rel=\"stylesheet\"&gt;\n\n&lt;!-- On your page --&gt;\n&lt;input type=\"file\"\n            class=\"filepond\"\n            id=\"filepond\"\n            name=\"file\"\n            multiple &gt;\n&lt;!-- Before the end of the body tag --&gt;\n&lt;script src=\"https://unpkg.com/filepond/dist/filepond.js\"&gt;&lt;/script&gt;\n&lt;script&gt;\nconst inputElement = document.querySelector('input&#91;type=\"file\"]');\nconst pond = FilePond.create( inputElement );\n&lt;/script&gt;</code></pre>\n\n\n\n<h3>Getting FilePond Talking to Our Controller</h3>\n\n\n\n<p>FilePond needs access to our server to save the files, but the front end library has no access without our intervention. So we need to point FilePond at our controller URL:</p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;script&gt;\nlet ApiUrl = \"/attachment/\"\nconst pond = FilePond.create(inputElement,{\n    server: {\n        url: ApiUrl,\n    }\n});\n&lt;/script&gt;</code></pre>\n\n\n\n<h4>Process</h4>\n\n\n\n<p>Okay, now we&#8217;re talking. Not much happening yet. When <a href=\"https://pqina.nl/filepond/docs/api/server/#process\">FilePond talks to the server to create a file</a> it makes a few assumptions.  FilePond sends the file and expects the server to return a <strong>unique id</strong>, we could use the Guid. This id is then expected to be used to revert uploads or restore earlier uploads.  Let&#8217;s handle that processing ourselves:</p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;HttpPost]\npublic async Task&lt;ActionResult> Process(&#91;FromForm] int caseId, IFormFile file, CancellationToken cancellationToken)\n{\n    if (file is null)\n    {\n        return BadRequest(\"Process Error: No file submitted\");\n    }\n\n    // We do some internal application validation here with our caseId\n\n    try\n    {\n        // get a guid to use as the filename as they're highly unique\n        var guid = Guid.NewGuid().ToString();\n        var newimage = string.Format(\"{0}.{1}\", guid, file.FileName.Split('.').LastOrDefault());\n        // upload to AWS\n        using var newMemoryStream = new MemoryStream();\n        await file.CopyToAsync(newMemoryStream, cancellationToken);\n        var uploadResponse = await S3Helper.UploadToS3Async(_amazonS3, newMemoryStream, S3Helper.GetRaffleBucketPath(), newimage, cancellationToken);\n\n        if (!uploadResponse)\n        {\n            return BadRequest(\"Process Error: Upload Failed.\");\n        }\n        var attachment = new Attachment\n        {\n            FileName = Path.GetFileNameWithoutExtension(file.FileName),\n            FileType = Path.GetExtension(file.FileName).Replace(\".\", String.Empty),\n            FileSize = file.Length,\n            CreatedOn = DateTime.Now,\n            CaseId = caseId,\n            S3url = string.Format(\n                \"https://{0}.s3.{1}.amazonaws.com/{2}/{3}\",\n                S3Helper.BucketName,\n                _amazonS3.Config.RegionEndpoint.SystemName,\n                S3Helper.RaffleBucketSubdirectory,\n                newimage\n            ),\n            Guid = guid\n        };\n        await _context.AddAsync(attachment, cancellationToken);\n        await _context.SaveChangesAsync(cancellationToken);\n        return Ok(guid);\n    }\n    catch (Exception e)\n    {\n        return BadRequest($\"Process Error: {e.Message}\"); // Oops!\n    }\n}</code></pre>\n\n\n\n<p>Because we needed an extra piece of information in our system, we override FilePond&#8217;s process method to add the view model&#8217;s CaseId into form data. </p>\n\n\n\n<pre class=\"wp-block-code\"><code>FilePond.setOptions({\n    server: {\n        url: \"/attachment/\",\n        process: (fieldName, file, metadata, load, error, progress, abort) =&gt; {\n            const formData = new FormData();\n            formData.append(fieldName, file, file.name);\n            formData.append(\"CaseID\", \"@Model.CaseId\");\n\n            const request = new XMLHttpRequest();\n            request.open('POST', ApiUrl);\n            // Setting computable to false switches the loading indicator to infinite mode\n            request.upload.onprogress = (e) =&gt; {\n                progress(e.lengthComputable, e.loaded, e.total);\n            };\n\n            request.onload = function () {\n            if (request.status &gt;= 200 &amp;&amp; request.status &lt; 300) {\n                load(request.responseText);// the load method accepts either a string (id) or an object\n            }\n            else {\n                error('Error during Upload!');\n            }\n        };\n\n        request.send(formData);\n        //expose an abort method so the request can be cancelled\n        return {\n            abort: () =&gt; {\n                // This function is entered if the user has tapped the cancel button\n                request.abort();\n                // Let FilePond know the request has been cancelled\n                abort();\n            }\n        };\n        }, // we've not implemented these endpoints yet, so leave them null!\n        fetch: null,\n        remove: null,\n    }\n})</code></pre>\n\n\n\n<h4>Revert</h4>\n\n\n\n<p>Okay, we&#8217;ve got files landing in our S3 bucket left, right and centre. Let&#8217;s get rid of some. Specifically, <a href=\"https://pqina.nl/filepond/docs/api/server/#revert\">allow the user to revert their upload</a>. FilePond expects a delete request to your server api route to land in the right place, so mark the method with [HttpDelete].</p>\n\n\n\n<pre class=\"wp-block-code\"><code>// DELETE: api/RaffleImagesUpload/\n// To protect from overposting attacks, enable the specific properties you want to bind to, for\n// more details, see https://go.microsoft.com/fwlink/?linkid=2123754.\n&#91;HttpDelete]\npublic async Task&lt;ActionResult&gt; Revert()\n{\n\n    // The server id will be send in the delete request body as plain text\n    using StreamReader reader = new(Request.Body, Encoding.UTF8);\n    string guid = await reader.ReadToEndAsync();\n    if (string.IsNullOrEmpty(guid))\n    {\n        return BadRequest(\"Revert Error: Invalid unique file ID\");\n    }\n    var attachment = _context.Attachments.FirstOrDefault(i =&gt; i.Guid == guid);\n    // We do some internal application validation here\n    try\n    {\n        // Form the request to delete from s3\n        var deleteObjectRequest = new DeleteObjectRequest\n        {\n            BucketName = GetBucketName(), // add your own bucket name\n            Key = guid\n        };\n        // https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-netcore.html\n        await DeleteFromS3Async(deleteObjectRequest);\n            \n        attachment.Deleted = true;\n        _context.Update(attachment);\n        await _context.SaveChangesAsync();\n        return Ok();\n    }\n    catch (AmazonS3Exception e)\n    {\n        return BadRequest(string.Format(\"Revert Error:'{0}' when writing an object\", e.Message));\n    }\n    catch (Exception e)\n    {\n        return BadRequest(string.Format(\"Revert Error:'{0}' when writing an object\", e.Message));\n    }\n}</code></pre>\n\n\n\n<h4>Load</h4>\n\n\n\n<p>We can process and revert images. But when we refresh our page, there&#8217;s nothing left behind. We solve this by implementing the <a href=\"https://pqina.nl/filepond/docs/api/server/#load\">Load endpoint</a> and adding the file guids to the view. First into the view:</p>\n\n\n\n<pre class=\"wp-block-code\"><code>const pond = FilePond.create(inputElement, {\n    server: {\n        url: api,\n        process: process, // Function moved for brevity\n        remove: remove, // Function moved for brevity\n        load: \"./load/\",\n    },\n    files: &#91;\n        @foreach(var attachment in Model.Attachments.Where(f => !f.Deleted))\n        {\n            &lt;text>\n            {\n                source: \"@attachment.Guid\",\n                options: {\n                    type: 'local', // local to indicate an already uploaded file, so it hits the load endpoint\n                }\n            },\n            &lt;/text>\n        }\n    ],\n})</code></pre>\n\n\n\n<p>The endpoint is just as easy. FilePond will send the request to your specified location with a string representing the file&#8217;s server id. Use that ID to find your entity and pull it back down from the cloud.</p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;HttpGet(\"Load/{id}\")]\npublic async Task&lt;IActionResult&gt; Load(string id)\n{\n    if (string.IsNullOrEmpty(id))\n    {\n        return NotFound(\"Load Error: Invalid parameters\");\n    }\n    var attachment = await _context.Attachments.SingleOrDefaultAsync(i =&gt; i.Guid.Equals(id));\n    if (attachment is null)\n    {\n        return NotFound(\"Load Error: File not found\");\n    }\n\n    var imageKey = string.Format(\"{0}.{1}\", attachment.Guid, attachment.FileType);\n    using Stream ImageStream = GetS3FileStreamAsync(GetBucketName(), imageKey);\n    Response.Headers.Add(\"Content-Disposition\", new ContentDisposition\n    {\n        FileName = string.Format(\"{0}.{1}\", attachment.FileName, attachment.FileType),\n        Inline = true // false = prompt the user for downloading; true = browser to try to show the file inline\n    }.ToString());\n    return File(ImageStream, \"image/\" + attachment.FileType);\n}</code></pre>\n\n\n\n<h4>Remove</h4>\n\n\n\n<p>Attachments loaded from the API don&#8217;t interact with the &#8220;Revert&#8221; method. If you want to delete one of these files we need to implement the &#8220;Remove&#8221; method. <a href=\"https://pqina.nl/filepond/docs/api/server/#remove\">FilePond doesn&#8217;t enable this one by default</a>, so it&#8217;s all up to us. We found the logic to be identical, so we added a override method to point the api to the Revert server endpoint.</p>\n\n\n\n<pre class=\"wp-block-code\"><code>remove: (source, load, error) =&gt; {\n    const request = new XMLHttpRequest();\n    request.open('DELETE', api);\n    // Setting computable to false switches the loading indicator to infinite mode\n    request.upload.onprogress = (e) =&gt; {\n        progress(e.lengthComputable, e.loaded, e.total);\n    };\n    request.onload = function () {\n        if (request.status &gt;= 200 &amp;&amp; request.status &lt; 300) {\n            load();// the load method accepts either a string (id) or an object\n        }\n        else {\n            error('Error while removing file!');\n        }\n    }\n    request.send(source);\n}</code></pre>\n\n\n\n<p>And that&#8217;s that. All done. We&#8217;ve enjoyed using FilePond and it&#8217;s helped us to create some amazing experiences for our users. </p>\n\n\n\n<p>Any feedback? <a href=\"/contact\">Please get in touch</a>. <a href=\"https://gist.github.com/MarkIanHolland/ce6e8e03527cbe4b85804bc7bb2ef1df\" rel=\"noreferrer noopener\" target=\"_blank\">Gist</a>!</p>\n","slug":"file-uploads-with-filepond-and-net-core","date":"06/09/19","title":"File Uploads with FilePond and .NET Core","featuredImage":{"node":{"localFile":{"publicURL":"/static/82b1539f53a611de86df01eddbca8072/cc9f9a80-8058-11e9-9a6a-d6cb898b9fcf.png"},"altText":""}},"author":{"node":{"name":"MH - Studio Manager","slug":"mh-senior-developer-mh"}},"seo":{"metaDesc":"Our quick example for powerful file uploads with .NET Core, AWS services and FilePond, a JavaScript file upload library.","title":"File Uploads with FilePond and .NET Core - Code and Create"}}},"pageContext":{"id":"cG9zdDo1MDg3","postId":5087,"prevPost":{"id":"cG9zdDo1MTA5","databaseId":5109,"slug":"warrington-is-good-enough"},"nextPost":{"id":"cG9zdDo1MDcw","databaseId":5070,"slug":"why-design-has-become-easier-in-the-past-9-years-and-longer"}}},
    "staticQueryHashes": []}