3
\$\begingroup\$

I am working on an e-commerce website. I have a CarMake dropdown which contains car make names and their corresponding Id, for example: { 1:Alfa romeo}, { 2:Audi }, { 3:BMW }, etc

I want to keep this lookup in cache, so I won't have to make a trip to DB every time I want to get the corresponding Name or Id...

So in my infrastructure layer, I read the data from DB and keep it in cache, I am using a static CarMakeLookup class:

public static class CarMakeLookup
{
    private static readonly Dictionary<short, string> _carMakeIdLookup;
    private static readonly Dictionary<string, short> _carMakeNameLookup;

    static CarMakeLookup()
    {
        _carMakeIdLookup = new Dictionary<short, string>();
        _carMakeNameLookup = new Dictionary<string, short>();

        using (var context = ApplicationDbContext.Create())
        {
            DropdownRepository dropdownRepository = new DropdownRepository(context);

            var carMakes = dropdownRepository.GetCarMakes();
            foreach (var carMake in carMakes)
            {
                _carMakeIdLookup[carMake.CarMakeId] = carMake.CarMakeName;
                _carMakeNameLookup[carMake.CarMakeName.ToLower()] = carMake.CarMakeId;
            }
        }
    }

    public static string GetCarMakeName(short carMakeId)
    {
        if (carMakeId <= 0)
        {
            return string.Empty;
        }

        if (_carMakeIdLookup.ContainsKey(carMakeId))
        {
            return _carMakeIdLookup[carMakeId];
        }

        return string.Empty;
    }

    public static short GetCarMakeId(string carMakeName)
    {
        if (!string.IsNullOrEmpty(carMakeName))
        {
            string lowerCaseCarMakeName = carMakeName.ToLower();
            if (_carMakeNameLookup.ContainsKey(lowerCaseCarMakeName))
            {
                return _carMakeNameLookup[lowerCaseCarMakeName];
            }
        }

        return -1;
    }

    public static Dictionary<short, string> GetCarMakeLookup()
    {
        return _carMakeIdLookup;
    }
}

I also need to display car make in a dropdown in my View... so I need a List<SelectListItem> to be displayed as a dropdown. So in my Application layer I have another static class which holds this list:

public static class CarCache
{
    static CarCache()
    {
        var selectCarMake = new SelectListItem { Value = "-1", Text = "Select a car", Disabled = true, Selected = true };
        CarMakeItemsIncludingSelect = new List<SelectListItem>(); // Dropdown's first option should be "select a car"
        CarMakeItemsIncludingSelect.Insert(0, selectCarMake);

        foreach (KeyValuePair<short, string> carMakeKeyPair in CarMakeLookup.GetCarMakeLookup())
        {
            var selectListItem = new SelectListItem { Value = carMakeKeyPair.Key.ToString(), Text = carMakeKeyPair.Value };
            CarMakeItemsIncludingSelect.Add(selectListItem);
        }
    }

    public static List<SelectListItem> CarMakeItemsIncludingSelect { get; private set; }

    public static string GetCarMakeName(short carMakeId)
    {
        return CarMakeLookup.GetCarMakeName(carMakeId);
    }

    public static short GetCarMakeId(string carMakeName)
    {
        return CarMakeLookup.GetCarMakeId(carMakeName);
    }
}

I have a few more dropdowns like this in my application, for example HouseType, JobType, etc

Everything works fine, the only problem is that my application startup time is slow... I assume one problem is that when I start debugging the application, the application starts building all these static cache classes, and since these classes are static I cannot inject the DbContext into them... so each dropdown should build its own DdContext instance, get corresponding data from DB and Dispose of DbContext. There is no performance problem with the production server...

Any recommendation on how to improve this cache?

\$\endgroup\$
3
\$\begingroup\$

Global static state makes your code untestable...

I would define your car make lookup as:

public class CarMakeLookup
{
    public CarMakeLookup(IEnumerable<(short Id, string Make)> data)
    {
        Makes = data.ToDictionary(d => d.Id, d => d.Make);
        Ids = data.ToDictionary(d => d.Make, d => d.Id);
    }

    public IReadOnlyDictionary<short, string> Makes { get; }
    public IReadOnlyDictionary<string, short> Ids { get; }
}

And read it with:

public class CarMakeReader : IReader<CarMakeLookup>
{
    public CarMakeReader(Func<ApplicationDbContext> context) => 
        Context = context;

    Func<DbContext> Context { get; }

    public Task<CarMakeLookup> ReadAsync()
    {
        using (var context = Context())
            return new CarMakeLookup(
                from cm in context.CarMakes
                select (cm.CarMakeId, cm.CarMakeName));            
    }
}

Where IReader<> is simply:

public interface IReader<TSet>
{
    Task<TSet> ReadAsync();
}

There should be also an extension method Cache defined nearby:

public static class Reader
{
    public static IReader<TSet> Cache<TSet>(this IReader<TSet> source) =>
        new CachingReader<TSet>(source);
}

Where:

class CachingReader<TSet> : IReader<TSet>
{
    public CachingReader(IReader<TSet> source) => 
        Lookup = new Lazy<Task<TSet>>(() => source.ReadAsync());

    Lazy<Task<TSet>> Lookup { get; }
    public Task<TSet> ReadAsync() => Lookup.Value;
}

Now register you reader in the IoC container as a decorated singleton:

containerBuilder.RegisterInstance(ctx => new CarMakeLookup(
    ctx.Resolve<Func<ApplicationDbContext>>())
    .Cache()).AsImplementedInterfaces();
\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer, I am not able to fully understand your answer... where are you caching the data? are they cached in DbContext? \$\endgroup\$ – Hooman Bahreini Oct 11 at 22:51
  • 1
    \$\begingroup\$ @HoomanBahreini I register a singleton. Will share a link to a demo repository a little bit latter today. \$\endgroup\$ – Dmitry Nogin Oct 11 at 22:53
  • 1
    \$\begingroup\$ @HoomanBahreini Here is the Demo... \$\endgroup\$ – Dmitry Nogin Oct 12 at 5:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.