DEV Community

Cover image for Securely Handle User Uploads With Service Injection (MVC) | Dear Coder
Kasey Wahl
Kasey Wahl

Posted on

Securely Handle User Uploads With Service Injection (MVC) | Dear Coder

Dear Coder,

In my last letter, I wrote about four rules to keep in mind while handling uploads from users in a web application. Today I want to give some step-by-step instructions on how to handle uploads while satisfying those rules.

You might recall that I'm building an application called ToonSpace for users to share their webtoons with each other. Today, I'll build the service that will accomplish that goal.

Create Image Service Interface

I create a new folder on the top level of my Solution Explorer named Services and add a new public Interface IImageService.

Services Folder

This Interface will hold a task that Encodes the image from a user upload, a task that encodes an image from a url, a string that decodes the image, and a string that records the content type of the image.

I name my service IImageService and add these four fields, making sure to declare the Interface is public.

IImageService

I also add my AspNetCore.Http using directive:

using Microsoft.AspCore.Http;
Enter fullscreen mode Exit fullscreen mode

Create Basic Image Service Concrete Class

Once the interface is finished, I create a corresponding concrete class called BasicImageService that holds executing code for the interface.

Inside my Services folder, I create a class called BasicImageService.

I add three using directives:

using Microsoft.AspNetCore.Http;
using System.IO;
using System.Net.Http;
Enter fullscreen mode Exit fullscreen mode

Let's look at what each of these methods actually allow me to do in the concrete class:

        public async Task<byte[]> EncodeImageAsync(IFormFile image)
        {
            if (image == null)
            {
                return null;
            }
            using var ms = new MemoryStream();
            await image.CopyToAsync(ms);
            return ms.ToArray();

        }
Enter fullscreen mode Exit fullscreen mode

EncodeImageAsync encodes a user-submitted image from an upload control and converts the file to a byte array to be stored in my database. Later, I'll leverage the multi-part form-data feature to allow users to submit an image file within a form, which is why EncodeImageAsync takes the IFormFile interface type.

        public async Task<byte[]> EncodeImageURLAsync(string imageURL)
        {
            var client = new HttpClient();

            var response = await client.GetAsync(imageURL);

            Stream stream = await response.Content.ReadAsStreamAsync();

            var ms = new MemoryStream();
            await stream.CopyToAsync(ms);

            return ms.ToArray();

        }
Enter fullscreen mode Exit fullscreen mode

EncodeImageURLAsync takes the path to an image and turns it into a byte array to be used in a similar fashion to EncodeImageAsync, storing an image in the database as a byte array.

        public string DecodeImage(byte[] image, string contentType)
        {
            if (image == null)
            {
                return null;
            }
            var convertedImage = Convert.ToBase64String(image);
            return $"data:{contentType};base64,{convertedImage}";
        }
Enter fullscreen mode Exit fullscreen mode

The first two methods dealt with encoding an image to store in my database. DecodeImage allows me to convert the stored data into a base64 string and pull it from the database to display as an image.

        public string RecordContentType(IFormFile image)
        {
            if (image == null)
            {
                return null;
            }
            return image.ContentType;
        }
Enter fullscreen mode Exit fullscreen mode

Finally, I need to record the ContentType (png, jpeg, etc.) so I can properly formulate the string to display the image.

The finished Concrete Class looks like this:
BasicImageService

Register Basic Image Service

Now that I've built my interface and corresponding concrete class for my Basic Image Service, I have to register it as a service in my Startup so I can inject it across the project.

I simply access my Startup file in my Solution Explorer and scroll to the _ConfigureServices method:
Startup.cs

I add my Basic Image Service to my list of services by adding using ToonSpace.Services to the top of the document and adding my service to the list of services like with the IImageService interface and BasicImageService concrete class:

services.AddScoped<IImageService, BasicImageService>();
Enter fullscreen mode Exit fullscreen mode

Now my service is ready to be used throughout the project.

Inject Service into Controller

I've registered an image service that will allow users to securely upload their webtoons to my application, so now comes the fun part! I inject my service into the controller by adding a using directive, constructor, and parameters:

Inject Image Service

Add the Image Upload to the Form

I need to jump over to the View where the user is going to upload so I can instruct the controller what to do when the user uploads.

I access the Create scaffolded View in my Uploads Views folder to access the form that has already been built for me.

The first thing I need to do is change the encode type to enctype="multipart/form-data" which is an HTML spec that allows the form to now accept uploads instead of just fields.

Then, scroll to the Image div and add an Image Id, a type of "file", and a "form-control-file" class (for styling).

            <div class="form-group">
                <label asp-for="Image" class="control-label"></label>
                <input id="Image" type="file" asp-for="Image" class="form-control-file" />
                <span asp-validation-for="Image" class="text-danger"></span>
            </div>
Enter fullscreen mode Exit fullscreen mode

Code Instructions in Create Action of Controller

I open my UploadsController to the Create Action and remove Image and ContentType from the Bind. I add add IFormFile Image as an argument in the Create Method, which Microsoft.AspNetCore.Http allows me to do. I add Microsoft.AspNetCore.Http to my using directives.

The Bind now looks like this:

public async Task<IActionResult> Create([Bind("Id,GenreId,Title,Artist,Created,Image,ContentType")] Upload upload, IFormFile Image)
Enter fullscreen mode Exit fullscreen mode

I then set the upload image and content type equal to the data coming in from the image service if the model state is valid.

                upload.ContentType = _imageService.ContentType(Image);
                upload.Image = await _imageService.EncodeImageAsync(Image);
Enter fullscreen mode Exit fullscreen mode

The user's upload will now save to the database after I've written these lines of code in the Create Action.

Display The User's Image in the View

The only thing I have left to do is spin up the user's image from the database and display it in the View. To do this, I access my Views folder and find the "Details" view under "Uploads."

I write some quick markup for illustration purposes:

@model ToonSpace.Models.Upload
@using ToonSpace.Services
@inject IImageService _imageService

@{
    ViewData["Title"] = "Details";
}

<h1>Details</h1>

<div>
    <h4>Upload</h4>
    <hr />

    <div class="card">
        <div class="col-8">
            <img id="Profile" class="img-fluid" src="@_imageService.DecodeImage(Model.Image, Model.ContentType)" />
        </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

Now, all the user needs to do is upload an image to the database and they will be able to see the image displayed in the Details View.

There's much more to be done to make this platform functional and secure, but today's letter is already longer than usual, and so I'll leave you to rest.

Godspeed in your keystrokes.

Clickity Clacks,

Kasey

Top comments (0)