A common choice that needs to be made when implementing a web application that supports uploading binary content, like PDF files or Avatar images, is whether to store the binary content in the file system or cloud storage or perhaps in the database. This article will not debate what solution is best under what circumstances. But that said, storing binary content data directly in the database has some strong advantages to it. Being able to store other content together with the binary content in a single database transaction is one of them. Having all content data in one place also simplifies the restoring of backups.
Introduction
Today, we will be creating a simple API with a single endpoint for uploading an image. We will be using the ASP.NET Core web application / API template in Visual Studio 2019 for this purpose. To simplify testing, we will also add Swagger and Entity framework to our solution. The result will be that we can upload an image using an endpoint like this:
The Solution
To follow along with this article, you need to have Visual Studio 2019 and .NET Core 3.1 installed on your local environment.
Creating the Solution
The first step is to create a new project in Visual Studio and choose the “ASP.NET Core Web Application” template. Give the project a new name, I chose “Get-images-from-db-by-url”, then on the following screen, choose “API” as displayed in the screenshot below:
What you get with this template is a simple API returning some JSON data representing a weather forecast. We will now turn this basic solution into something more interesting.
To begin with, we change the properties settings for Get-images-from-db-by-url
project so that when debugging, you are displayed the root of the application “/” instead of the “/weatherforecast” URL. You find the setting for this here:
Replace weatherforecast
with an empty string in the field placed to the right of the launch browser checkbox. This will make Visual Studio launch the application using the “/” URL instead of the “/weatherforecast” URL.
We will now do two more things before we can start with the fun stuff:
- Add Swagger to simplify the process of uploading an image to the backend.
- Add Entity Framework to support writing/retrieving data from a database.
Add Swagger
Swagger is a tool which adds auto-generated documentation and a UI to your API. Install Swagger to your project by running the following command in your package manager console.
Install-Package Swashbuckle.AspNetCore
After that, you need to now make two changes to your Startup.cs file. You should already have a method for “ConfigureServices
” in your Startup.cs file. Add services.AddSwaggerGen()
to it. The result should look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddSwaggerGen();
}
In the method named “Configure
”, you should add the following two lines:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.RoutePrefix = string.Empty;
});
You can now try to debug the application and hopefully, you will see the swagger UI interface presented to you.
Add Entity Framework
Add entity framework to your project by running the following two commands in your package manager console:
Install-Package Microsoft.EntityFrameworkCore.Sqlite
Install-Package Microsoft.EntityFrameworkCore.Tools
Then create a new folder named “Models” and create a new class named Image
in that folder. The image
class will represent a single image in this case. Here, we need properties for Id
, a data property representing the binary image data and a property storing the file suffix.
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Get_images_from_db_by_url.Models
{
public class Image
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Key]
public Guid Id { get; set; }
public byte[] Data { get; set; }
public string Suffix { get; set; }
}
}
Before we can complete the setup of Entity Framework, we also need to create a DBContext
class. Add the following class to the project:
using Microsoft.EntityFrameworkCore;
using Get_images_from_db_by_url.Models;
namespace Get_images_from_db_by_url
{
public class EFContext : DbContext
{
public DbSet<img /> Images { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options) =>
options.UseSqlite("Data Source=database.db");
}
}
Now run the following two commands to create the initial migration and the database.db file in your solution:
Add-Migration InitialCreate
Update-Database
Implementing the API Endpoint for Uploading an Image
Hopefully, the solution should now build properly. Before we go any further, we should clean up the existing solution a bit. We start by renaming the Controllers/WeatherForecastController.cs file to Controllers/ImageController.cs, also update the name of the class to match the new file name. Then remove the file WeatherForecast.cs.
Replace the content of the ImageController
with the following:
using System;
using System.IO;
using System.Threading.Tasks;
using Get_images_from_db_by_url.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Get_images_from_db_by_url.Controllers
{
[ApiController]
[Route("[controller]")]
public class ImageController : ControllerBase
{
[HttpPost("image")]
public async Task<guid> Image(IFormFile image)
{
using (var db = new EFContext())
{
using (var ms = new MemoryStream())
{
image.CopyTo(ms);
var img = new Image()
{
Suffix = Path.GetExtension(image.FileName),
Data = ms.ToArray()
};
db.Images.Add(img);
await db.SaveChangesAsync();
return img.Id;
}
}
}
}
}
This method “Image
” takes an IFormFile
as a parameter, reads it using a memory stream object and stores the result in the database, and then returns the Id
(a generated GUID) back to the user.
Your swagger interface should now look something like this after you pressed the “Try it out” button:
And after uploading an image, you should be getting a GUID in return:
Implementing a Middleware to Retrieve the Image From the Database
In .NET 4.X, you would implement this functionality using an HTTP Handler, but in .NET Core, this concept of handlers is all gone. What we want is to intercept the request, analyze it and if the request matches some predefined conditions, we want to serve an image back to the user, taken from the database.
The conditions we want to act on is if the URL path contains “/dynamic/images/” and if the resource requested has an extension of either png, jpg or gif.
Create a new folder in the project and name it “Middleware”. In this new folder, create two new classes, DynamicImageMiddleware
and DynamicImageProvider
.
The content of the DynamicImageProvider
should be the following:
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace Get_images_from_db_by_url.Middleware
{
public class DynamicImageProvider
{
private readonly RequestDelegate next;
public IServiceProvider service;
private string pathCondition = "/dynamic/images/";
public DynamicImageProvider(RequestDelegate next)
{
this.next = next;
}
private static readonly string[] suffixes = new string[] {
".png",
".jpg",
".gif"
};
private bool IsImagePath(PathString path)
{
if (path == null || !path.HasValue)
return false;
return suffixes.Any
(x => path.Value.EndsWith(x, StringComparison.OrdinalIgnoreCase));
}
public async Task Invoke(HttpContext context)
{
try
{
var path = context.Request.Path;
if (IsImagePath(path) && path.Value.Contains(pathCondition))
{
byte[] buffer = null;
var id = Guid.Parse(Path.GetFileName(path).Split(".")[0]);
using (var db = new EFContext())
{
var imageFromDb =
await db.Images.SingleOrDefaultAsync(x => x.Id == id);
buffer = imageFromDb.Data;
}
if (buffer != null && buffer.Length > 1)
{
context.Response.ContentLength = buffer.Length;
context.Response.ContentType = "image/" +
Path.GetExtension(path).Replace(".","");
await context.Response.Body.WriteAsync(buffer, 0, buffer.Length);
}
else
context.Response.StatusCode = 404;
}
}
finally
{
if (!context.Response.HasStarted)
await next(context);
}
}
}
}
Nothing complex is going on here. The method “Invoke(HttpContext context)
” will be executed by the .NET Core framework and through the HttpContext
, you have access to the request
and the response
object.
We are only interested in requests with the URL path containing “/dynamic/images” and which has an extension of either png, jpg, or gif.
We retrieve the image GUID from the path, retrieve the image for the database, and set the response content. Since the response object needs a stream, we need to utilize a memory stream in this case.
The content of the finally
section is very important since you don’t want to send the response to any other middleware down the chain after you have written to it. But, in case you didn’t, you want it to continue its normal route using the next(context)
method.
The next step is to add the content of the DymaicImageMiddleware.cs file:
using Microsoft.AspNetCore.Builder;
namespace Get_images_from_db_by_url.Middleware
{
public static class DynamicImageMiddleware
{
public static IApplicationBuilder UseDynamicImageMiddleware
(this IApplicationBuilder builder)
{
return builder.UseMiddleware<dynamicimageprovider>();
}
}
}
This file contains an extension method to be used in the Startup.cs file.
Use the extension method in the Startup.cs file, in the “Configure
” method and, in my case after the “app.UseRouting()
” line.
app.UseDynamicImageMiddleware();
How to Test it
You test it by following these steps:
- Upload an image using the Swagger UI and the Image POST endpoint
- Make note of the extension of the file uploaded and the GUID returned by the API
- Execute /dynamic/images/[GUID].[EXTENSION] like in my case: https://localhost:44362/dynamic/images/36400564-b28d-45a3-a259-0afbb8c5ec51.png and hopefully, you will be getting back what seems to be a static image from a static URL but in reality, the image is provided from a table in a database.
Possible Improvements
One advantage of this approach explained in this article is that images served through a static URL are cacheable in the browser. You can control the cache by adding an “Expires
” header to the response
object like this:
string ExpireDate = DateTime.UtcNow.AddDays(10)
.ToString("ddd, dd MMM yyyy HH:mm:ss",
System.Globalization.CultureInfo.InvariantCulture);
context.Response.Headers.Add("Expires", ExpireDate + " GMT");
Furthermore, you can utilize a memory cache server-side if you feel the need for it. A common use case is also to support resizing images server-side by providing a length and width of the image in the query string.
History
- 8th September, 2020: Initial version