【C#】IFileProvider+IMemoryCache 用法

目前在維護的系統架構主要是 Web Site Project 跟 ASP.NET MVC,近期因 .NET Core 的諸多好處,又新建了 .NET Core 的專案,因此最近都在嘗試將以前的 Code 重寫成也適合 .NET Core 專案使用的寫法。

假如我有一個 File 設定檔,我需要從 File 取得資料並做 Cache,當資料有異動時需清除 Cache 並重新抓資料的話,以前的寫法長這樣

public class BookRepo
{
    private string _fileFilePath;
    private const string _CACHEKEY = "BookRepo";
    private static ObjectCache _memoryCache;

    public BookRepo(string filePath)
    {
        if (_memoryCache == null)
            _memoryCache = MemoryCache.Default;

        _fileFilePath = filePath;
    }

    public BookModel GetBookModel()
    {
        BookModel bookModel = (BookModel)_memoryCache.Get(_CACHEKEY);

        if (bookModel == null)
        {
            if (File.Exists(this._fileFilePath))
            {
                bookModel = new BookModel(JsonConvert.DeserializeObject<Book>(File.ReadAllText(this._fileFilePath)));
                CacheItemPolicy policy = new CacheItemPolicy();
                policy.ChangeMonitors.Add(new HostFileChangeMonitor(new List<string> { this._fileFilePath }));
                //這邊我cache的是BookModel而不是Book, 因為new BookModel的時候我有將資料再做一些處理,希望cache的是有處理過的物件,
                //這樣使用的時候才能直接使用處理過的資料,而不用每次都處理一次資料
                _memoryCache.Set(_CACHEKEY, bookModel, policy);
            }
            else
                throw new Exception(
                    $"File is not exists. Time:{DateTime.Now.ToString()} Source:{this._fileFilePath}");
        }

        return bookModel;
    }
}

這段程式碼在 .NET Core 也是可以運作的,只是發現 .NET Core 在使用 MemoryCache 時是直接用 IMemoryCache;文件的部分也發現是使用 IFileProvider,因此改寫成以下版本

public class BookRepo : IBookRepo
{
    private readonly IMemoryCache _memoryCache;
    private const string _CACHEKEY = "BookRepo";
    private string _filePath;
    private IFileProvider _fileProvider;

    public BookRepo(string path, IFileProvider fileProvider, IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
        _fileProvider = fileProvider;
        _filePath = path;
    }

    public BookModel GetBookModel()
    {
        if (!_memoryCache.TryGetValue(_CACHEKEY, out BookModel bookModel))
        {
            bookModel = new BookModel(GetDataFromFile());
            SetCache(bookModel);
        }

        return bookModel;
    }

    private IEnumerable<Book> GetDataFromFile()
    {
        IFileInfo fileInfo = _fileProvider.GetFileInfo(_filePath);
        FileUtility fileUtility = new FileUtility(fileInfo);
        return fileUtility.GetFileContent<IEnumerable<Book>>();
    }

    private void SetCache(BookModel bookModel)
    {
        IChangeToken changeToken =_fileProvider.Watch(_filePath);
        MemoryCacheEntryOptions cacheOptions = new MemoryCacheEntryOptions().AddExpirationToken(changeToken);
        _memoryCache.Set(_CACHEKEY, bookModel, cacheOptions);
    }
}

讀取檔案的功能拉出來當通用

public class FileUtility
{
    private readonly IFileInfo _contents;

    public FileUtility(IFileInfo contents)
    {
        _contents = contents;
    }

    public string GetFileContent()
    {
        int runCount = 1;
        while (runCount < 4)
        {
            try
            {
                if (_contents.Exists)
                {
                    using (StreamReader reader = new StreamReader(_contents.CreateReadStream()))
                    {
                        return reader.ReadToEnd();
                    }
                }
                else
                {
                    throw new FileNotFoundException($"File is not exists. Time:{DateTime.Now}", _contents.PhysicalPath);
                }
            }
            catch (IOException ex)
            {
                if (runCount == 3 || ex.HResult != -2147024864)//0x80070020 - The process cannot access the file because it is being used by another process.
                {
                    throw;
                }
                else
                {
                    runCount++;
                }
            }
        }

        return null;
    }
    public T GetFileContent<T>()
    {
        string fileContent = GetFileContent();
        return JsonConvert.DeserializeObject<T>(fileContent);
    }
}

更精簡一點的寫法,但我自己還不是看很習慣這種寫法,乍看會停格個幾秒思考這是在幹麻…

    public class BookRepo : IBookRepo
    {
        private readonly IMemoryCache _memoryCache;
        private const string _CACHEKEY = "BookRepo";
        private string _filePath;
        private IFileProvider _fileProvider;

        public BookRepo(string path, IFileProvider fileProvider, IMemoryCache memoryCache)
        {
            _memoryCache = memoryCache;
            _fileProvider = fileProvider;
            _filePath = path;
        }

        public BookModel GetBookModel()
        {
            return _memoryCache.GetOrCreate(_CACHEKEY, entry =>
            {
                entry.AddExpirationToken(_fileProvider.Watch(_filePath));
                return new BookModel(new FileUtility(_fileProvider.GetFileInfo(_filePath))
                    .GetFileContent<IEnumerable<Book>>());
            });
        }

改完之後在 .NET Core 的 Startup.cs 注入就可以使用了

using Microsoft.Extensions.FileProviders;
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        IFileProvider physicalProvider = new PhysicalFileProvider(AppDomain.CurrentDomain.BaseDirectory);
        services.AddSingleton<IFileProvider>(physicalProvider);
        services.AddMemoryCache();
    }
}

Web Site Project 需要這樣使用

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
public class BookUtility
{
    public static IBookModel GetBookModel()
    {
        string filePath = Path.Combine("Setting", "Book.json");
        PhysicalFileProvider provider = new PhysicalFileProvider(AppDomain.CurrentDomain.BaseDirectory);
        //這邊需要傳singleton的memorycache進去,如果傳new MemoryCache()進去等於每次都是新的,就不會有預期的cache效果
        return new BookRepo(filePath, provider, MemoryCacheSingleton.Instance).GetBookModel();
    }
}

public sealed class MemoryCacheSingleton
{
    private static MemoryCache _memoryCache;
    private static object _lockObj = new object();

    public static MemoryCache Instance
    {
        get
        {
            lock (_lockObj)
            {
                if (_memoryCache == null)
                {
                    _memoryCache = new MemoryCache(new MemoryCacheOptions());
                }
                return _memoryCache;
            }
        }
    }
}


Web Site Project 安裝這兩個的時候著實驚了我一下
Microsoft.Extensions.Caching.Memory
Microsoft.Extensions.FileProviders
沒想到有如此多相依的 lib…

安裝 Microsoft.Extensions.Caching.Memory 跳出來的畫面
安裝 Microsoft.Extensions.FileProviders 跳出來的畫面

參考資料:
https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.caching.memory.imemorycache?view=dotnet-plat-ext-7.0

分類: C#

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *