How to cache images in .NET

This is a tutorial on how to implement a simple .NET console application for caching images in a local drive using IMemoryCache and how to use this cache service via Dependency Injection. For this tutorial, I am using .NET 6.0 and Visual Studio Community 2022.

Generally, this is what we are going to follow to accomplish this task:

  • Define ImageObject class
  • Implement ImageCacher class
  • Call ImageCacher in Program.cs

You can download the project here.

Create Console App project

Open Visual Studio, create a Console Application and call it ImageCacherConsoleApp. For the Solution name, enter ProjectCache.

I won’t talk about the project creation in depth, but I do recommend reading my previous post called Start a New Project in Visual Studio 2022 and .NET 6.0 which describes how to create two projects within a solution. In that tutorial, I created an ASP.NET Core Web API application, and a Console Application.

After you have created the project, your Solution Explorer should look like this:.

The ImageCacherConsoleApp project under ProjectCache

Create a directory for images

For sake of this demo, let’s put our images inside the bin directory as the application will search the images there. Specifically, create a folder called Images under the \bin\Debug\net6.0\ directory. Inside that folder, create at least one folder with some images in them. If you click the Show All Files icon on top, it will show you the bin directory and other hidden folders, as illustrated below, and the Images folder should be directly under net6.0.

If you have downloaded the project from the link I provided above, then you should be all set. The .net6.0 directory in that project should contain two folders with three images in each folder.

Images folder

Define ImageObject class

Let us first define the ImageObject class. This class will contain all the information about the image that we will be caching later. To create a new class under the ImageCacherConsoleApp project, simply right-click on the project. Click Add. Click Class. Enter ImageObject.cs for Name. You will now have ImageCacher.cs under the console app.

The new class called ImageObject.cs under the console app

And this is what the newly generated ImageObject class looks like. It’s basically an empty class, which we will modify next.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ImageCacherConsoleApp
{
    class ImageObject
    {
    }
}

ImageObject class will contain the following properties which are initialized through the constructor:

  1. FileName – this is the full path of the image file
  2. ContentType – this is the file type such as JPEG, PNG, GIF, etc.
  3. Content – this is of data type byte[] or byte array
  4. SubmitDate – this is the timestamp on which the file was created or modified on the server.

Our complete ImageObject class is shown below:

namespace CSCacher;
public class ImageObject
{
    public string FileName { get; set; }
    public string ContentType { get; set; }
    public byte[] Content { get; set; }
    public DateTime SubmitDate { get; set; }

    public ImageObject(string fn, string tp, byte[] ct, DateTime dt)
    {
        FileName = fn;
        ContentType = tp;
        Content = ct;
        SubmitDate = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, DateTimeKind.Utc);
    }
}

The class really does nothing but receives and stores the values passed on from ImageCacher that calls it.

Also, C#10 now allows the use of file-scoped namespace as can be seen on line #1. It puts an end to namespaces using their own curly braces which in turn removes an extra nesting, which I think looks better.

That’s about it for ImageObject. We implement our ImageCacher class next.

Implement ImageCacher Class

This class is simple really. It has ImageCacher() constructor and a GetImage() method. GetImage() is where everything happens and to explain what it does, look at pseudo code below:

ImageCacher(IMemoryCacher memoryCache)
{
}

string GetImage()
{
    Get content of cache, if any, using file path as key

    if (content of cache is empty)
    {      
        // Using file path, do the following
        Get file name
        Get image timestamp
        Get image type
        Read the contents of the file into byte array
        Store info into ImageObject instance

        Cache the ImageObject instance 

        Return the message "Loading image from drive"               
    }
    else
    {
        Return the message "Loading image from cache.
    }
}

The constructor will receive IMemoryCache via dependency injection. The cache can then be used throughout our controller.

GetMethod() simply returns a string that tells you whether the image requested was taken from the physical drive or from cache. Line #7 is where we check the content of cache using the file path as key. If it’s empty, then we load the image, and cache it. Otherwise, we just return what’s in the cache.

Complete source code is shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Microsoft.Extensions.Caching.Memory;

namespace ImageCacherConsoleApp;

public class ImageCacher
{
    IMemoryCache m_memoryCache;
    private ImageObject? m_imageObject;

    public ImageCacher(IMemoryCache memoryCache)
    {
        m_memoryCache = memoryCache;
        m_imageObject = null;       
    }

    public string GetImage(string pickedImagePath)
    {
        
        string msg = string.Empty;
        if (File.Exists(pickedImagePath))
        {
            m_memoryCache.TryGetValue(pickedImagePath, out m_imageObject);

            if (m_imageObject == null)
            {
                // Read time stamp of file.
                DateTime ourFileDate = File.GetLastWriteTime(pickedImagePath);
                ourFileDate = ourFileDate.AddMilliseconds(-ourFileDate.Millisecond);

                // Open the file and read the contents into a byte array
                byte[] byteArray = File.ReadAllBytes(pickedImagePath);
                m_imageObject = new ImageObject(pickedImagePath, "image/jpeg", byteArray, ourFileDate);

                // Set cache options
                var memCacheEntryOptions = new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(120));

                // Cache the image
                m_memoryCache.Set(pickedImagePath, m_imageObject, memCacheEntryOptions);

                msg = "Loading image from drive.";
            }
            else
            {
                msg = "Loading image from cache.";
            }
        }
        else
        {
            throw new FileNotFoundException();
        }
        return msg;
    }
}

On line #16, in the constructor, we get an instance of IMemoryCache via Dependency Injection.

On line #28, I read the cache content with TryGetValue() using the file path as the key, and getting the value back in m_imageObject variable. Here, we either get something from the cache(if it had been cached), or we get nothing. Technically, this method returns a boolean value – true or false. But you will notice that instead of checking the boolean return value, I check whether or not the value I get is actually null.

Line #32 to #46 is what we do if there was nothing in the cache. This is where we read the image into a byte array and store it in our cache.

On line #41, I first set how my cache should expire before I cache the image object. In that line of code, I set it to expire in 120 seconds, but you can set this to any value you want.

Line #44 is where we put our ImageObject into our cache. Its key in the cache is its path.

One final note about this is I throw a FileNotFoundException and the main program or whatever is using the ImageCacher instance should handle the exception.

And that’s our ImageCacher class. And now let’s call our ImageCacher from the main program!

Call ImageCacher in Program.cs

Our main program is Program.cs. Initially, the program contains a single line of code:

Console.WriteLine("Hello, World!");

To be able to get our ImageCacher application working, we need to do the following:

  • Install Microsoft.Extensions.Caching.Memory
  • Install Microsoft.Extensions.Hosting
  • Get an instance of IMemoryCache via IHost
  • Call ImageCacher multiple times with different image files.

To install Microsoft.Extensions.Caching.Memory, right-click on the project. Then click Manage Nuget Packages.

As illustrated above, enter memory in the search bar. Make sure you click Browse above it. Click on Microsoft.Extensions.Caching.Memory. On the right panel, click Install.

Do the same thing to install Microsoft.Extensions.Hosting. Enter hosting in the search bar to find it.

After installing these packages or namespaces, you are now ready to use. To get an instance of IMemoryCache, I followed what Microsoft Docs little tutorial on how to do for a console application.

To use the default IMemoryCache implementation, call the AddMemoryCache extension method to register all the required services with DI… The generic host is used to expose the ConfigureServices functionality.

They’re nice enough to provide a nice example for doing just that.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services => services.AddMemoryCache())
    .Build();

IMemoryCache cache = host.Services.GetRequiredService<IMemoryCache>();

string image1 = Environment.CurrentDirectory + @"\Images\abstract\" + "timur-kozmenko-GS84KG8yNDo-unsplash.jpg";
string image2 = Environment.CurrentDirectory + @"\Images\abstract\" + "timur-kozmenko-VJj70LBdrnM-unsplash.jpg";
string image3 = Environment.CurrentDirectory + @"\Images\abstract\" + "timur-kozmenko-zxVdi0iEO7M-unsplash.jpg";

ImageCacherConsoleApp.ImageCacher imageCacher = new ImageCacherConsoleApp.ImageCacher(cache);

try
{
    // 1st Pass
    Console.WriteLine(imageCacher.GetImage(image1));
    Console.WriteLine(imageCacher.GetImage(image2));
    Console.WriteLine(imageCacher.GetImage(image3));

    // 2nd Pass
    Console.WriteLine(imageCacher.GetImage(image1));
    Console.WriteLine(imageCacher.GetImage(image2));
    Console.WriteLine(imageCacher.GetImage(image3));
}
catch (Exception ex)
{
    // Do something
}

Line #5 and line #9 shows you how to get an instance of IMemoryCache through Dependency Injection (FYI: this is done differently in a web-based application as you will see in my later posts).

Line #11 to #13, we hard code the path to our test image files.

Line #15, we create an instance of ImageCacher .

Line #18 to #20, I call the GetImage() method and pass it the image path. At this point the messages should say I’m getting the images from the drive (not cache).

Line #23 to #25 is the second pass, and every call to GetMethod() should say I’m getting the images from the cache.

Running the application

Hit F5 to run your application. You should get something like this:

ImageCacher at work

Conclusion

This is a simple image caching tutorial using the new .NET 6.0 and Visual Studio Community 2022. The application demonstrates the use of IMemoryCache service for caching in a console application. This tutorial demonstrates how to use the Hosting extension to be able to add MemoryCache in the services collection, and get it ready for use. This also demonstrates how to use Dependency Injection in the application constructor to be able to get an instance of IMemoryCache.

Alejandrio Vasay
Alejandrio Vasay

Welcome to Coder Schmoder! I'm a .NET developer with a 15+ years of web and software development experience. I created this blog to impart my knowledge of programming to those who are interested in learning are just beginning in their programming journey.

I live in DFW, Texas and currently working as a .NET /Web Developer. I earned my Master of Computer Science degree from Texas A&M University, College Station, Texas. I hope, someday, to make enough money to travel to my birth country, the Philippines, and teach software development to people who don't have the means to learn it.

Articles: 21

Leave a Reply

Your email address will not be published. Required fields are marked *