Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Serving Images Stored in a Database through a Static URL using .NET Core 3.1

0.00/5 (No votes)
11 Sep 2020 1  
A small API application, written in .NET Core 3.1 to demonstrate how to retrieve images stored in a database using a normal static image URL
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:

Image 1

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:

Image 2

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:

Image 3

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:

  1. Add Swagger to simplify the process of uploading an image to the backend.
  2. 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:

Image 4

And after uploading an image, you should be getting a GUID in return:

Image 5

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.

Image 6

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:

  1. Upload an image using the Swagger UI and the Image POST endpoint
  2. Make note of the extension of the file uploaded and the GUID returned by the API
  3. 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here