Browse Source

Upgrade to .NET 6 and PostgreSQL 14 + implementation of cached-worlds (#6950)

* Upgrade to .NET 6 and PostgreSQL 14 + implementation of cached-worlds in a reflection free way

* MySql odbc driver upgrade

Co-authored-by: LLT21 <[email protected]>
LLT21 3 years ago
parent
commit
29844ce3af

+ 5 - 5
frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:6.0.100 AS build
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 RUN apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
@@ -22,7 +22,7 @@ COPY src .
 RUN dotnet publish -c Release -o out -r linux-x64 /p:Database=mysql
 
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:6.0.0 AS runtime
 
 RUN apt-get update
 # The following installs standard versions unixodbc 2.3.6 and pgsqlodbc 11
@@ -32,9 +32,9 @@ RUN apt-get install -y unixodbc wget curl
 
 WORKDIR /odbc
 
-RUN curl -L -o mariadb-connector-odbc-3.1.13-debian-9-stretch-amd64.tar.gz https://downloads.mariadb.com/Connectors/odbc/connector-odbc-3.1.13/mariadb-connector-odbc-3.1.13-debian-9-stretch-amd64.tar.gz
-RUN tar -xvzf mariadb-connector-odbc-3.1.13-debian-9-stretch-amd64.tar.gz
-RUN cp mariadb-connector-odbc-3.1.13-debian-9-stretch-amd64/lib/mariadb/libm* /usr/lib/
+RUN curl -L -o mariadb-connector-odbc-3.1.14-debian-9-stretch-amd64.tar.gz https://downloads.mariadb.com/Connectors/odbc/connector-odbc-3.1.14/mariadb-connector-odbc-3.1.14-debian-9-stretch-amd64.tar.gz
+RUN tar -xvzf mariadb-connector-odbc-3.1.14-debian-9-stretch-amd64.tar.gz
+RUN cp mariadb-connector-odbc-3.1.14-debian-9-stretch-amd64/lib/mariadb/libm* /usr/lib/
 
 COPY --from=build /usr/local/unixODBC /usr/local/unixODBC
 

+ 8 - 8
frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:6.0.100 AS build
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 RUN apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
@@ -8,15 +8,15 @@ RUN apt-get install -y make build-essential libssl-dev zlib1g-dev libbz2-dev \
 WORKDIR /odbc
 
 # To compile the latest postgresql odbc driver, postgresql itself needs to be installed
-RUN curl -L -o postgresql-13.3.tar.gz https://ftp.postgresql.org/pub/source/v13.3/postgresql-13.3.tar.gz
+RUN curl -L -o postgresql-14.1.tar.gz https://ftp.postgresql.org/pub/source/v14.1/postgresql-14.1.tar.gz
 RUN curl -L -o unixODBC-2.3.9.tar.gz ftp://ftp.unixodbc.org/pub/unixODBC/unixODBC-2.3.9.tar.gz
-RUN curl -L -o psqlodbc-13.01.0000.tar.gz https://ftp.postgresql.org/pub/odbc/versions/src/psqlodbc-13.01.0000.tar.gz
+RUN curl -L -o psqlodbc-13.02.0000.tar.gz https://ftp.postgresql.org/pub/odbc/versions/src/psqlodbc-13.02.0000.tar.gz
 
-RUN tar -xvf postgresql-13.3.tar.gz
+RUN tar -xvf postgresql-14.1.tar.gz
 RUN tar -xvf unixODBC-2.3.9.tar.gz
-RUN tar -xvf psqlodbc-13.01.0000.tar.gz
+RUN tar -xvf psqlodbc-13.02.0000.tar.gz
 
-WORKDIR /odbc/postgresql-13.3
+WORKDIR /odbc/postgresql-14.1
 RUN ./configure
 RUN make
 RUN make install
@@ -30,7 +30,7 @@ RUN make install
 
 ENV PATH=/usr/local/unixODBC/lib:$PATH
 
-WORKDIR /odbc/psqlodbc-13.01.0000
+WORKDIR /odbc/psqlodbc-13.02.0000
 RUN ./configure --with-unixodbc=/usr/local/unixODBC --with-libpq=/usr/local/pgsql --prefix=/usr/local/pgsqlodbc
 RUN make
 RUN make install
@@ -40,7 +40,7 @@ COPY src .
 RUN dotnet publish -c Release -o out -r linux-x64  /p:Database=postgresql
 
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:6.0.0 AS runtime
 
 RUN apt-get update
 # The following installs standard versions unixodbc 2.3.6 and pgsqlodbc 11

+ 2 - 2
frameworks/CSharp/appmpower/appmpower.dockerfile

@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
+FROM mcr.microsoft.com/dotnet/sdk:6.0.100 AS build
 RUN apt-get update
 RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5
 
@@ -7,7 +7,7 @@ COPY src .
 RUN dotnet publish -c Release -o out -r linux-x64
 
 # Construct the actual image that will run
-FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
+FROM mcr.microsoft.com/dotnet/aspnet:6.0.0 AS runtime
 RUN apt-get update
 
 WORKDIR /app

+ 2 - 0
frameworks/CSharp/appmpower/benchmark_config.json

@@ -26,6 +26,7 @@
         "query_url": "/queries?c=",
         "update_url": "/updates?c=",
         "fortune_url": "/fortunes",
+        "cached_query_url": "/cached-worlds?c=",
         "port": 8080,
         "approach": "Realistic",
         "classification": "Platform",
@@ -47,6 +48,7 @@
         "query_url": "/queries?c=",
         "update_url": "/updates?c=",
         "fortune_url": "/fortunes",
+        "cached_query_url": "/cached-worlds?c=",
         "port": 8080,
         "approach": "Realistic",
         "classification": "Platform",

+ 2 - 0
frameworks/CSharp/appmpower/config.toml

@@ -19,6 +19,7 @@ urls.db = "/db"
 urls.query = "/queries?c="
 urls.update = "/updates?c="
 urls.fortune = "/fortunes"
+urls.cached_query = "/cached-worlds?c="
 approach = "Realistic"
 classification = "Micro"
 database = "Postgres"
@@ -34,6 +35,7 @@ urls.db = "/db"
 urls.query = "/queries?c="
 urls.update = "/updates?c="
 urls.fortune = "/fortunes"
+urls.cached_query = "/cached-worlds?c="
 approach = "Realistic"
 classification = "Micro"
 database = "MySQL"

+ 11 - 0
frameworks/CSharp/appmpower/src/CachedWorld.cs

@@ -0,0 +1,11 @@
+namespace appMpower
+{
+   public struct CachedWorld
+   {
+      public int Id { get; set; }
+
+      public int RandomNumber { get; set; }
+
+      public static implicit operator CachedWorld(World world) => new CachedWorld { Id = world.Id, RandomNumber = world.RandomNumber };
+   }
+}

+ 15 - 0
frameworks/CSharp/appmpower/src/CachedWorldSerializer.cs

@@ -0,0 +1,15 @@
+using System.Text.Json;
+
+namespace appMpower
+{
+   public class CachedWorldSerializer : Kestrel.IJsonSerializer<CachedWorld>
+   {
+      public void Serialize(Utf8JsonWriter utf8JsonWriter, CachedWorld world)
+      {
+         utf8JsonWriter.WriteStartObject();
+         utf8JsonWriter.WriteNumber("id", world.Id);
+         utf8JsonWriter.WriteNumber("randomNumber", world.RandomNumber);
+         utf8JsonWriter.WriteEndObject();
+      }
+   }
+}

+ 13 - 0
frameworks/CSharp/appmpower/src/Db/DataProvider.cs

@@ -0,0 +1,13 @@
+namespace appMpower.Db
+{
+   public static class DataProvider
+   {
+#if MYSQL
+      public static bool IsOdbcConnection = true; 
+      public const string ConnectionString = "Driver={MariaDB};Server=tfb-database;Database=hello_world;Uid=benchmarkdbuser;Pwd=benchmarkdbpass;Pooling=false;OPTIONS=67108864;FLAG_FORWARD_CURSOR=1"; 
+#else
+      public static bool IsOdbcConnection = true;
+      public const string ConnectionString = "Driver={PostgreSQL};Server=tfb-database;Database=hello_world;Uid=benchmarkdbuser;Pwd=benchmarkdbpass;UseServerSidePrepare=1;Pooling=false";
+#endif
+   }
+}

+ 40 - 38
frameworks/CSharp/appmpower/src/Db/PooledCommand.cs

@@ -7,12 +7,12 @@ namespace appMpower.Db
 {
    public class PooledCommand : IDbCommand
    {
-      private OdbcCommand _odbcCommand;
+      private IDbCommand _dbCommand;
       private PooledConnection _pooledConnection;
 
       public PooledCommand(PooledConnection pooledConnection)
       {
-         _odbcCommand = (OdbcCommand)pooledConnection.CreateCommand();
+         _dbCommand = pooledConnection.CreateCommand();
          _pooledConnection = pooledConnection;
       }
 
@@ -21,21 +21,21 @@ namespace appMpower.Db
          pooledConnection.GetCommand(commandText, this);
       }
 
-      internal PooledCommand(OdbcCommand odbcCommand, PooledConnection pooledConnection)
+      internal PooledCommand(IDbCommand dbCommand, PooledConnection pooledConnection)
       {
-         _odbcCommand = odbcCommand;
+         _dbCommand = dbCommand;
          _pooledConnection = pooledConnection;
       }
 
-      internal OdbcCommand OdbcCommand
+      internal IDbCommand DbCommand
       {
          get
          {
-            return _odbcCommand;
+            return _dbCommand;
          }
          set
          {
-            _odbcCommand = value;
+            _dbCommand = value;
          }
       }
 
@@ -55,11 +55,11 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.CommandText;
+            return _dbCommand.CommandText;
          }
          set
          {
-            _odbcCommand.CommandText = value;
+            _dbCommand.CommandText = value;
          }
       }
 
@@ -67,22 +67,22 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.CommandTimeout;
+            return _dbCommand.CommandTimeout;
          }
          set
          {
-            _odbcCommand.CommandTimeout = value;
+            _dbCommand.CommandTimeout = value;
          }
       }
       public CommandType CommandType
       {
          get
          {
-            return _odbcCommand.CommandType;
+            return _dbCommand.CommandType;
          }
          set
          {
-            _odbcCommand.CommandType = value;
+            _dbCommand.CommandType = value;
          }
       }
 
@@ -91,11 +91,11 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.Connection;
+            return _dbCommand.Connection;
          }
          set
          {
-            _odbcCommand.Connection = (OdbcConnection?)value;
+            _dbCommand.Connection = (IDbConnection?)value;
          }
       }
 #nullable disable
@@ -105,7 +105,7 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.Parameters;
+            return _dbCommand.Parameters;
          }
       }
 
@@ -114,11 +114,11 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.Transaction;
+            return _dbCommand.Transaction;
          }
          set
          {
-            _odbcCommand.Transaction = (OdbcTransaction?)value;
+            _dbCommand.Transaction = (IDbTransaction?)value;
          }
       }
 #nullable disable
@@ -127,80 +127,82 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcCommand.UpdatedRowSource;
+            return _dbCommand.UpdatedRowSource;
          }
          set
          {
-            _odbcCommand.UpdatedRowSource = value;
+            _dbCommand.UpdatedRowSource = value;
          }
       }
       public void Cancel()
       {
-         _odbcCommand.Cancel();
+         _dbCommand.Cancel();
       }
 
       public IDbDataParameter CreateParameter()
       {
-         return _odbcCommand.CreateParameter();
+         return _dbCommand.CreateParameter();
       }
 
       public IDbDataParameter CreateParameter(string name, DbType dbType, object value)
       {
-         OdbcParameter odbcParameter = null;
+         IDbDataParameter dbDataParameter = null;
 
          if (this.Parameters.Contains(name))
          {
-            odbcParameter = (OdbcParameter)this.Parameters[name];
-            odbcParameter.Value = value;
+            dbDataParameter = this.Parameters[name] as IDbDataParameter;
+            dbDataParameter.Value = value;
          }
          else
          {
-            odbcParameter = _odbcCommand.CreateParameter();
+            dbDataParameter = _dbCommand.CreateParameter();
 
-            odbcParameter.ParameterName = name;
-            odbcParameter.DbType = dbType;
-            odbcParameter.Value = value;
-            this.Parameters.Add(odbcParameter);
+            dbDataParameter.ParameterName = name;
+            dbDataParameter.DbType = dbType;
+            dbDataParameter.Value = value;
+            this.Parameters.Add(dbDataParameter);
          }
 
-         return odbcParameter;
+         return dbDataParameter;
       }
 
       public int ExecuteNonQuery()
       {
-         return _odbcCommand.ExecuteNonQuery();
+         return _dbCommand.ExecuteNonQuery();
       }
 
       public IDataReader ExecuteReader()
       {
-         return _odbcCommand.ExecuteReader();
+         return _dbCommand.ExecuteReader();
       }
 
       public async Task<int> ExecuteNonQueryAsync()
       {
-         return await _odbcCommand.ExecuteNonQueryAsync();
+         if (DataProvider.IsOdbcConnection) return await (_dbCommand as OdbcCommand).ExecuteNonQueryAsync();
+         return await (_dbCommand as DbCommand).ExecuteNonQueryAsync();
       }
 
       public async Task<DbDataReader> ExecuteReaderAsync(CommandBehavior behavior)
       {
-         return await _odbcCommand.ExecuteReaderAsync(behavior);
+         if (DataProvider.IsOdbcConnection) return await (_dbCommand as OdbcCommand).ExecuteReaderAsync(behavior);
+         return await (_dbCommand as DbCommand).ExecuteReaderAsync(behavior);
       }
 
       public IDataReader ExecuteReader(CommandBehavior behavior)
       {
-         return _odbcCommand.ExecuteReader(behavior);
+         return _dbCommand.ExecuteReader(behavior);
       }
 
 #nullable enable
       public object? ExecuteScalar()
       {
-         return _odbcCommand.ExecuteScalar();
+         return _dbCommand.ExecuteScalar();
       }
 #nullable disable
 
       public void Prepare()
       {
-         _odbcCommand.Prepare();
+         _dbCommand.Prepare();
       }
 
       public void Release()

+ 32 - 32
frameworks/CSharp/appmpower/src/Db/PooledConnection.cs

@@ -1,7 +1,6 @@
-using System;
 using System.Collections.Concurrent;
 using System.Data;
-using System.Data.Odbc;
+using System.Data.Common;
 using System.Threading.Tasks;
 
 namespace appMpower.Db
@@ -10,16 +9,16 @@ namespace appMpower.Db
    {
       private bool _released = false;
       private short _number = 0;
-      private OdbcConnection _odbcConnection;
+      private IDbConnection _dbConnection;
       private ConcurrentDictionary<string, PooledCommand> _pooledCommands;
 
       internal PooledConnection()
       {
       }
 
-      internal PooledConnection(OdbcConnection odbcConnection)
+      internal PooledConnection(IDbConnection dbConnection)
       {
-         _odbcConnection = odbcConnection;
+         _dbConnection = dbConnection;
          _pooledCommands = new ConcurrentDictionary<string, PooledCommand>();
       }
 
@@ -47,15 +46,15 @@ namespace appMpower.Db
          }
       }
 
-      public OdbcConnection OdbcConnection
+      public IDbConnection DbConnection
       {
          get
          {
-            return _odbcConnection;
+            return _dbConnection;
          }
          set
          {
-            _odbcConnection = value;
+            _dbConnection = value;
          }
       }
 
@@ -63,11 +62,11 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcConnection.ConnectionString;
+            return _dbConnection.ConnectionString;
          }
          set
          {
-            _odbcConnection.ConnectionString = value;
+            _dbConnection.ConnectionString = value;
          }
       }
 
@@ -75,7 +74,7 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcConnection.ConnectionTimeout;
+            return _dbConnection.ConnectionTimeout;
          }
       }
 
@@ -83,7 +82,7 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcConnection.Database;
+            return _dbConnection.Database;
          }
       }
 
@@ -91,7 +90,7 @@ namespace appMpower.Db
       {
          get
          {
-            return _odbcConnection.State;
+            return _dbConnection.State;
          }
       }
 
@@ -109,46 +108,41 @@ namespace appMpower.Db
 
       public IDbTransaction BeginTransaction()
       {
-         return _odbcConnection.BeginTransaction();
+         return _dbConnection.BeginTransaction();
       }
 
       public IDbTransaction BeginTransaction(IsolationLevel il)
       {
-         return _odbcConnection.BeginTransaction(il);
+         return _dbConnection.BeginTransaction(il);
       }
 
       public void ChangeDatabase(string databaseName)
       {
-         _odbcConnection.ChangeDatabase(databaseName);
+         _dbConnection.ChangeDatabase(databaseName);
       }
 
       public void Close()
       {
-         _odbcConnection.Close();
+         _dbConnection.Close();
          _released = true;
       }
 
       public IDbCommand CreateCommand()
       {
-         return _odbcConnection.CreateCommand();
-      }
-
-      public OdbcCommand CreateOdbcCommand()
-      {
-         return _odbcConnection.CreateCommand();
+         return _dbConnection.CreateCommand();
       }
 
       public void Open()
       {
-         if (_odbcConnection.State == ConnectionState.Closed)
+         if (_dbConnection.State == ConnectionState.Closed)
          {
-            _odbcConnection.Open();
+            _dbConnection.Open();
          }
       }
 
       public void Release()
       {
-         if (!_released && _odbcConnection.State == ConnectionState.Open)
+         if (!_released && _dbConnection.State == ConnectionState.Open)
          {
             PooledConnections.Release(this);
          }
@@ -156,7 +150,7 @@ namespace appMpower.Db
 
       public void Dispose()
       {
-         if (!_released && _odbcConnection.State == ConnectionState.Open)
+         if (!_released && _dbConnection.State == ConnectionState.Open)
          {
             PooledConnections.Dispose(this);
          }
@@ -164,9 +158,9 @@ namespace appMpower.Db
 
       public async Task OpenAsync()
       {
-         if (_odbcConnection.State == ConnectionState.Closed)
+         if (_dbConnection.State == ConnectionState.Closed)
          {
-            await _odbcConnection.OpenAsync();
+            await (_dbConnection as DbConnection).OpenAsync();
          }
       }
 
@@ -176,15 +170,21 @@ namespace appMpower.Db
 
          if (_pooledCommands.TryRemove(commandText, out internalCommand))
          {
-            pooledCommand.OdbcCommand = internalCommand.OdbcCommand;
+            pooledCommand.DbCommand = internalCommand.DbCommand;
             pooledCommand.PooledConnection = internalCommand.PooledConnection;
          }
          else
          {
-            pooledCommand.OdbcCommand = new OdbcCommand(commandText, this.OdbcConnection);
-            pooledCommand.OdbcCommand.Prepare();
+            pooledCommand.DbCommand = this.DbConnection.CreateCommand();
+            pooledCommand.DbCommand.CommandText = commandText;
             pooledCommand.PooledConnection = this;
 
+            //For future use with non odbc drivers like Npgsql which do not support Prepare
+            if (DataProvider.IsOdbcConnection)
+            {
+               pooledCommand.DbCommand.Prepare();
+            }
+
             //Console.WriteLine("prepare pool connection: " + this._number + " for command " + _pooledCommands.Count);
          }
 

+ 12 - 2
frameworks/CSharp/appmpower/src/Db/PooledConnections.cs

@@ -38,7 +38,17 @@ namespace appMpower.Db
          else
          {
             pooledConnection = new PooledConnection();
-            pooledConnection.OdbcConnection = new OdbcConnection(connectionString);
+
+            if (DataProvider.IsOdbcConnection)
+            {
+               pooledConnection.DbConnection = new OdbcConnection(connectionString);
+            }
+            else
+            {
+               //For future use with non odbc drivers which can be AOT compiled without reflection
+               //pooledConnection.DbConnection = new NpgsqlConnection(connectionString);
+            }
+
             _createdConnections++;
 
             if (_createdConnections == _maxConnections) _connectionsCreated = true;
@@ -63,7 +73,7 @@ namespace appMpower.Db
       {
          PooledConnection newPooledConnection = new PooledConnection();
 
-         newPooledConnection.OdbcConnection = pooledConnection.OdbcConnection;
+         newPooledConnection.DbConnection = pooledConnection.DbConnection;
          newPooledConnection.Number = pooledConnection.Number;
          newPooledConnection.PooledCommands = pooledConnection.PooledCommands;
 

+ 17 - 0
frameworks/CSharp/appmpower/src/HttpApplication.cs

@@ -13,6 +13,7 @@ namespace appMpower
       public static readonly byte[] _plainText = Encoding.UTF8.GetBytes("Hello, World!");
       private readonly static JsonMessageSerializer _jsonMessageSerializer = new JsonMessageSerializer();
       private readonly static WorldSerializer _worldSerializer = new WorldSerializer();
+      private readonly static CachedWorldSerializer _cachedWorldSerializer = new CachedWorldSerializer();
 
       public IFeatureCollection CreateContext(IFeatureCollection featureCollection)
       {
@@ -85,6 +86,22 @@ namespace appMpower
                Json.RenderMany(httpResponse.Headers, httpResponseBody.Writer, await RawDb.LoadMultipleUpdatesRows(count), _worldSerializer);
                return;
             }
+            else if (pathStringLength == 14 && pathStringStart == "c")
+            {
+               int count = 1;
+
+               if (!Int32.TryParse(request.QueryString.Substring(request.QueryString.LastIndexOf("=") + 1), out count) || count < 1)
+               {
+                  count = 1;
+               }
+               else if (count > 500)
+               {
+                  count = 500;
+               }
+
+               Json.RenderMany(httpResponse.Headers, httpResponseBody.Writer, await RawDb.LoadCachedQueries(count), _cachedWorldSerializer);
+               return;
+            }
          }
       }
 

+ 257 - 0
frameworks/CSharp/appmpower/src/Memory/CacheEntry.cs

@@ -0,0 +1,257 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace appMpower.Memory
+{
+   internal sealed partial class CacheEntry : ICacheEntry
+   {
+      private static readonly Action<object> ExpirationCallback = ExpirationTokensExpired;
+
+      private readonly MemoryCache _cache;
+
+      private CacheEntryTokens _tokens; // might be null if user is not using the tokens or callbacks
+      private TimeSpan? _absoluteExpirationRelativeToNow;
+      private TimeSpan? _slidingExpiration;
+      private long? _size;
+      private CacheEntry _previous; // this field is not null only before the entry is added to the cache and tracking is enabled
+      private object _value;
+      private CacheEntryState _state;
+
+      internal CacheEntry(object key, MemoryCache memoryCache)
+      {
+         Key = key ?? throw new ArgumentNullException(nameof(key));
+         _cache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache));
+         _previous = memoryCache.TrackLinkedCacheEntries ? CacheEntryHelper.EnterScope(this) : null;
+         _state = new CacheEntryState(CacheItemPriority.Normal);
+      }
+
+      /// <summary>
+      /// Gets or sets an absolute expiration date for the cache entry.
+      /// </summary>
+      public DateTimeOffset? AbsoluteExpiration { get; set; }
+
+      /// <summary>
+      /// Gets or sets an absolute expiration time, relative to now.
+      /// </summary>
+      public TimeSpan? AbsoluteExpirationRelativeToNow
+      {
+         get => _absoluteExpirationRelativeToNow;
+         set
+         {
+            // this method does not set AbsoluteExpiration as it would require calling Clock.UtcNow twice:
+            // once here and once in MemoryCache.SetEntry
+
+            if (value <= TimeSpan.Zero)
+            {
+               throw new ArgumentOutOfRangeException(
+                   nameof(AbsoluteExpirationRelativeToNow),
+                   value,
+                   "The relative expiration value must be positive.");
+            }
+
+            _absoluteExpirationRelativeToNow = value;
+         }
+      }
+
+      /// <summary>
+      /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
+      /// This will not extend the entry lifetime beyond the absolute expiration (if set).
+      /// </summary>
+      public TimeSpan? SlidingExpiration
+      {
+         get => _slidingExpiration;
+         set
+         {
+            if (value <= TimeSpan.Zero)
+            {
+               throw new ArgumentOutOfRangeException(
+                   nameof(SlidingExpiration),
+                   value,
+                   "The sliding expiration value must be positive.");
+            }
+
+            _slidingExpiration = value;
+         }
+      }
+
+      /// <summary>
+      /// Gets the <see cref="IChangeToken"/> instances which cause the cache entry to expire.
+      /// </summary>
+      public IList<IChangeToken> ExpirationTokens => GetOrCreateTokens().ExpirationTokens;
+
+      /// <summary>
+      /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache.
+      /// </summary>
+      public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks => GetOrCreateTokens().PostEvictionCallbacks;
+
+      /// <summary>
+      /// Gets or sets the priority for keeping the cache entry in the cache during a
+      /// memory pressure triggered cleanup. The default is <see cref="CacheItemPriority.Normal"/>.
+      /// </summary>
+      public CacheItemPriority Priority { get => _state.Priority; set => _state.Priority = value; }
+
+      /// <summary>
+      /// Gets or sets the size of the cache entry value.
+      /// </summary>
+      public long? Size
+      {
+         get => _size;
+         set
+         {
+            if (value < 0)
+            {
+               throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be non-negative.");
+            }
+
+            _size = value;
+         }
+      }
+
+      public object Key { get; private set; }
+
+      public object Value
+      {
+         get => _value;
+         set
+         {
+            _value = value;
+            _state.IsValueSet = true;
+         }
+      }
+
+      internal DateTimeOffset LastAccessed { get; set; }
+
+      internal EvictionReason EvictionReason { get => _state.EvictionReason; private set => _state.EvictionReason = value; }
+
+      public void Dispose()
+      {
+         if (!_state.IsDisposed)
+         {
+            _state.IsDisposed = true;
+
+            if (_cache.TrackLinkedCacheEntries)
+            {
+               CacheEntryHelper.ExitScope(this, _previous);
+            }
+
+            // Don't commit or propagate options if the CacheEntry Value was never set.
+            // We assume an exception occurred causing the caller to not set the Value successfully,
+            // so don't use this entry.
+            if (_state.IsValueSet)
+            {
+               _cache.SetEntry(this);
+
+               if (_previous != null && CanPropagateOptions())
+               {
+                  PropagateOptions(_previous);
+               }
+            }
+
+            _previous = null; // we don't want to root unnecessary objects
+         }
+      }
+
+      [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
+      internal bool CheckExpired(in DateTimeOffset now)
+          => _state.IsExpired
+              || CheckForExpiredTime(now)
+              || (_tokens != null && _tokens.CheckForExpiredTokens(this));
+
+      internal void SetExpired(EvictionReason reason)
+      {
+         if (EvictionReason == EvictionReason.None)
+         {
+            EvictionReason = reason;
+         }
+         _state.IsExpired = true;
+         _tokens?.DetachTokens();
+      }
+
+      [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
+      private bool CheckForExpiredTime(in DateTimeOffset now)
+      {
+         if (!AbsoluteExpiration.HasValue && !_slidingExpiration.HasValue)
+         {
+            return false;
+         }
+
+         return FullCheck(now);
+
+         bool FullCheck(in DateTimeOffset offset)
+         {
+            if (AbsoluteExpiration.HasValue && AbsoluteExpiration.Value <= offset)
+            {
+               SetExpired(EvictionReason.Expired);
+               return true;
+            }
+
+            if (_slidingExpiration.HasValue
+                && (offset - LastAccessed) >= _slidingExpiration)
+            {
+               SetExpired(EvictionReason.Expired);
+               return true;
+            }
+
+            return false;
+         }
+      }
+
+      internal void AttachTokens() => _tokens?.AttachTokens(this);
+
+      private static void ExpirationTokensExpired(object obj)
+      {
+         // start a new thread to avoid issues with callbacks called from RegisterChangeCallback
+         Task.Factory.StartNew(state =>
+         {
+            var entry = (CacheEntry)state;
+            entry.SetExpired(EvictionReason.TokenExpired);
+            entry._cache.EntryExpired(entry);
+         }, obj, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+      }
+
+      internal void InvokeEvictionCallbacks() => _tokens?.InvokeEvictionCallbacks(this);
+
+      // this simple check very often allows us to avoid expensive call to PropagateOptions(CacheEntryHelper.Current)
+      [MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
+      internal bool CanPropagateOptions() => (_tokens != null && _tokens.CanPropagateTokens()) || AbsoluteExpiration.HasValue;
+
+      internal void PropagateOptions(CacheEntry parent)
+      {
+         if (parent == null)
+         {
+            return;
+         }
+
+         // Copy expiration tokens and AbsoluteExpiration to the cache entries hierarchy.
+         // We do this regardless of it gets cached because the tokens are associated with the value we'll return.
+         _tokens?.PropagateTokens(parent);
+
+         if (AbsoluteExpiration.HasValue)
+         {
+            if (!parent.AbsoluteExpiration.HasValue || AbsoluteExpiration < parent.AbsoluteExpiration)
+            {
+               parent.AbsoluteExpiration = AbsoluteExpiration;
+            }
+         }
+      }
+
+      private CacheEntryTokens GetOrCreateTokens()
+      {
+         if (_tokens != null)
+         {
+            return _tokens;
+         }
+
+         CacheEntryTokens result = new CacheEntryTokens();
+         return Interlocked.CompareExchange(ref _tokens, result, null) ?? result;
+      }
+   }
+}

+ 32 - 0
frameworks/CSharp/appmpower/src/Memory/CacheEntryHelper.cs

@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Threading;
+
+namespace appMpower.Memory
+{
+   internal static class CacheEntryHelper
+   {
+      private static readonly AsyncLocal<CacheEntry> _current = new AsyncLocal<CacheEntry>();
+
+      internal static CacheEntry Current
+      {
+         get => _current.Value;
+         private set => _current.Value = value;
+      }
+
+      internal static CacheEntry EnterScope(CacheEntry current)
+      {
+         CacheEntry previous = Current;
+         Current = current;
+         return previous;
+      }
+
+      internal static void ExitScope(CacheEntry current, CacheEntry previous)
+      {
+         Debug.Assert(Current == current, "Entries disposed in invalid order");
+         Current = previous;
+      }
+   }
+}

+ 62 - 0
frameworks/CSharp/appmpower/src/Memory/CacheEntryState.cs

@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace appMpower.Memory
+{
+   internal sealed partial class CacheEntry
+   {
+      // this type exists just to reduce CacheEntry size by replacing many enum & boolean fields with one of a size of Int32
+      private struct CacheEntryState
+      {
+         private byte _flags;
+         private byte _evictionReason;
+         private byte _priority;
+
+         internal CacheEntryState(CacheItemPriority priority) : this() => _priority = (byte)priority;
+
+         internal bool IsDisposed
+         {
+            get => ((Flags)_flags & Flags.IsDisposed) != 0;
+            set => SetFlag(Flags.IsDisposed, value);
+         }
+
+         internal bool IsExpired
+         {
+            get => ((Flags)_flags & Flags.IsExpired) != 0;
+            set => SetFlag(Flags.IsExpired, value);
+         }
+
+         internal bool IsValueSet
+         {
+            get => ((Flags)_flags & Flags.IsValueSet) != 0;
+            set => SetFlag(Flags.IsValueSet, value);
+         }
+
+         internal EvictionReason EvictionReason
+         {
+            get => (EvictionReason)_evictionReason;
+            set => _evictionReason = (byte)value;
+         }
+
+         internal CacheItemPriority Priority
+         {
+            get => (CacheItemPriority)_priority;
+            set => _priority = (byte)value;
+         }
+
+         private void SetFlag(Flags option, bool value) => _flags = (byte)(value ? (_flags | (byte)option) : (_flags & ~(byte)option));
+
+         [Flags]
+         private enum Flags : byte
+         {
+            Default = 0,
+            IsValueSet = 1 << 0,
+            IsExpired = 1 << 1,
+            IsDisposed = 1 << 2,
+         }
+      }
+   }
+}

+ 140 - 0
frameworks/CSharp/appmpower/src/Memory/CacheEntryTokens.cs

@@ -0,0 +1,140 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Primitives;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace appMpower.Memory
+{
+   internal sealed partial class CacheEntry
+   {
+      // this type exists just to reduce average CacheEntry size
+      // which typically is not using expiration tokens or callbacks
+      private sealed class CacheEntryTokens
+      {
+         private List<IChangeToken> _expirationTokens;
+         private List<IDisposable> _expirationTokenRegistrations;
+         private List<PostEvictionCallbackRegistration> _postEvictionCallbacks; // this is not really related to tokens, but was moved here to shrink typical CacheEntry size
+
+         internal List<IChangeToken> ExpirationTokens => _expirationTokens ??= new List<IChangeToken>();
+         internal List<PostEvictionCallbackRegistration> PostEvictionCallbacks => _postEvictionCallbacks ??= new List<PostEvictionCallbackRegistration>();
+
+         internal void AttachTokens(CacheEntry cacheEntry)
+         {
+            if (_expirationTokens != null)
+            {
+               lock (this)
+               {
+                  for (int i = 0; i < _expirationTokens.Count; i++)
+                  {
+                     IChangeToken expirationToken = _expirationTokens[i];
+                     if (expirationToken.ActiveChangeCallbacks)
+                     {
+                        _expirationTokenRegistrations ??= new List<IDisposable>(1);
+                        IDisposable registration = expirationToken.RegisterChangeCallback(ExpirationCallback, cacheEntry);
+                        _expirationTokenRegistrations.Add(registration);
+                     }
+                  }
+               }
+            }
+         }
+
+         internal bool CheckForExpiredTokens(CacheEntry cacheEntry)
+         {
+            if (_expirationTokens != null)
+            {
+               for (int i = 0; i < _expirationTokens.Count; i++)
+               {
+                  IChangeToken expiredToken = _expirationTokens[i];
+                  if (expiredToken.HasChanged)
+                  {
+                     cacheEntry.SetExpired(EvictionReason.TokenExpired);
+                     return true;
+                  }
+               }
+            }
+            return false;
+         }
+
+         internal bool CanPropagateTokens() => _expirationTokens != null;
+
+         internal void PropagateTokens(CacheEntry parentEntry)
+         {
+            if (_expirationTokens != null)
+            {
+               lock (this)
+               {
+                  lock (parentEntry.GetOrCreateTokens())
+                  {
+                     foreach (IChangeToken expirationToken in _expirationTokens)
+                     {
+                        parentEntry.AddExpirationToken(expirationToken);
+                     }
+                  }
+               }
+            }
+         }
+
+         internal void DetachTokens()
+         {
+            // _expirationTokenRegistrations is not checked for null, because AttachTokens might initialize it under lock
+            // instead we are checking for _expirationTokens, because if they are not null, then _expirationTokenRegistrations might also be not null
+            if (_expirationTokens != null)
+            {
+               lock (this)
+               {
+                  List<IDisposable> registrations = _expirationTokenRegistrations;
+                  if (registrations != null)
+                  {
+                     _expirationTokenRegistrations = null;
+                     for (int i = 0; i < registrations.Count; i++)
+                     {
+                        IDisposable registration = registrations[i];
+                        registration.Dispose();
+                     }
+                  }
+               }
+            }
+         }
+
+         internal void InvokeEvictionCallbacks(CacheEntry cacheEntry)
+         {
+            if (_postEvictionCallbacks != null)
+            {
+               Task.Factory.StartNew(state => InvokeCallbacks((CacheEntry)state), cacheEntry,
+                   CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+            }
+         }
+
+         private static void InvokeCallbacks(CacheEntry entry)
+         {
+            List<PostEvictionCallbackRegistration> callbackRegistrations = Interlocked.Exchange(ref entry._tokens._postEvictionCallbacks, null);
+
+            if (callbackRegistrations == null)
+            {
+               return;
+            }
+
+            for (int i = 0; i < callbackRegistrations.Count; i++)
+            {
+               PostEvictionCallbackRegistration registration = callbackRegistrations[i];
+
+               try
+               {
+                  registration.EvictionCallback?.Invoke(entry.Key, entry.Value, entry.EvictionReason, registration.State);
+               }
+               catch (Exception e)
+               {
+                  // This will be invoked on a background thread, don't let it throw.
+                  entry._cache._logger.LogError(e, "EvictionCallback invoked failed");
+               }
+            }
+         }
+      }
+   }
+}

+ 520 - 0
frameworks/CSharp/appmpower/src/Memory/MemoryCache.cs

@@ -0,0 +1,520 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace appMpower.Memory
+{
+   /// <summary>
+   /// An implementation of <see cref="IMemoryCache"/> using a dictionary to
+   /// store its entries.
+   /// </summary>
+   public class MemoryCache : IMemoryCache
+   {
+      internal readonly ILogger _logger;
+
+      private readonly MemoryCacheOptions _options;
+      private readonly ConcurrentDictionary<object, CacheEntry> _entries;
+
+      private long _cacheSize;
+      private bool _disposed;
+      private DateTimeOffset _lastExpirationScan;
+
+      /// <summary>
+      /// Creates a new <see cref="MemoryCache"/> instance.
+      /// </summary>
+      /// <param name="optionsAccessor">The options of the cache.</param>
+      public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor)
+          : this(optionsAccessor, NullLoggerFactory.Instance) { }
+
+      /// <summary>
+      /// Creates a new <see cref="MemoryCache"/> instance.
+      /// </summary>
+      /// <param name="optionsAccessor">The options of the cache.</param>
+      /// <param name="loggerFactory">The factory used to create loggers.</param>
+      public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory loggerFactory)
+      {
+         if (optionsAccessor == null)
+         {
+            throw new ArgumentNullException(nameof(optionsAccessor));
+         }
+
+         if (loggerFactory == null)
+         {
+            throw new ArgumentNullException(nameof(loggerFactory));
+         }
+
+         _options = optionsAccessor.Value;
+         //_logger = loggerFactory.CreateLogger<MemoryCache>();
+         _logger = loggerFactory.CreateLogger("MemoryCache");
+
+         _entries = new ConcurrentDictionary<object, CacheEntry>();
+
+         if (_options.Clock == null)
+         {
+            _options.Clock = new SystemClock();
+         }
+
+         _lastExpirationScan = _options.Clock.UtcNow;
+         TrackLinkedCacheEntries = _options.TrackLinkedCacheEntries; // we store the setting now so it's consistent for entire MemoryCache lifetime
+      }
+
+      /// <summary>
+      /// Cleans up the background collection events.
+      /// </summary>
+      ~MemoryCache() => Dispose(false);
+
+      /// <summary>
+      /// Gets the count of the current entries for diagnostic purposes.
+      /// </summary>
+      public int Count => _entries.Count;
+
+      // internal for testing
+      internal long Size { get => Interlocked.Read(ref _cacheSize); }
+
+      internal bool TrackLinkedCacheEntries { get; }
+
+      private ICollection<KeyValuePair<object, CacheEntry>> EntriesCollection => _entries;
+
+      /// <inheritdoc />
+      public ICacheEntry CreateEntry(object key)
+      {
+         CheckDisposed();
+         ValidateCacheKey(key);
+
+         return new CacheEntry(key, this);
+      }
+
+      internal void SetEntry(CacheEntry entry)
+      {
+         if (_disposed)
+         {
+            // No-op instead of throwing since this is called during CacheEntry.Dispose
+            return;
+         }
+
+         if (_options.SizeLimit.HasValue && !entry.Size.HasValue)
+         {
+            //throw new InvalidOperationException(SR.Format(SR.CacheEntryHasEmptySize, nameof(entry.Size), nameof(_options.SizeLimit)));
+            throw new InvalidOperationException();
+         }
+
+         DateTimeOffset utcNow = _options.Clock.UtcNow;
+
+         DateTimeOffset? absoluteExpiration = null;
+         if (entry.AbsoluteExpirationRelativeToNow.HasValue)
+         {
+            absoluteExpiration = utcNow + entry.AbsoluteExpirationRelativeToNow;
+         }
+         else if (entry.AbsoluteExpiration.HasValue)
+         {
+            absoluteExpiration = entry.AbsoluteExpiration;
+         }
+
+         // Applying the option's absolute expiration only if it's not already smaller.
+         // This can be the case if a dependent cache entry has a smaller value, and
+         // it was set by cascading it to its parent.
+         if (absoluteExpiration.HasValue)
+         {
+            if (!entry.AbsoluteExpiration.HasValue || absoluteExpiration.Value < entry.AbsoluteExpiration.Value)
+            {
+               entry.AbsoluteExpiration = absoluteExpiration;
+            }
+         }
+
+         // Initialize the last access timestamp at the time the entry is added
+         entry.LastAccessed = utcNow;
+
+         if (_entries.TryGetValue(entry.Key, out CacheEntry priorEntry))
+         {
+            priorEntry.SetExpired(EvictionReason.Replaced);
+         }
+
+         bool exceedsCapacity = UpdateCacheSizeExceedsCapacity(entry);
+
+         if (!entry.CheckExpired(utcNow) && !exceedsCapacity)
+         {
+            bool entryAdded = false;
+
+            if (priorEntry == null)
+            {
+               // Try to add the new entry if no previous entries exist.
+               entryAdded = _entries.TryAdd(entry.Key, entry);
+            }
+            else
+            {
+               // Try to update with the new entry if a previous entries exist.
+               entryAdded = _entries.TryUpdate(entry.Key, entry, priorEntry);
+
+               if (entryAdded)
+               {
+                  if (_options.SizeLimit.HasValue)
+                  {
+                     // The prior entry was removed, decrease the by the prior entry's size
+                     Interlocked.Add(ref _cacheSize, -priorEntry.Size.Value);
+                  }
+               }
+               else
+               {
+                  // The update will fail if the previous entry was removed after retrival.
+                  // Adding the new entry will succeed only if no entry has been added since.
+                  // This guarantees removing an old entry does not prevent adding a new entry.
+                  entryAdded = _entries.TryAdd(entry.Key, entry);
+               }
+            }
+
+            if (entryAdded)
+            {
+               entry.AttachTokens();
+            }
+            else
+            {
+               if (_options.SizeLimit.HasValue)
+               {
+                  // Entry could not be added, reset cache size
+                  Interlocked.Add(ref _cacheSize, -entry.Size.Value);
+               }
+               entry.SetExpired(EvictionReason.Replaced);
+               entry.InvokeEvictionCallbacks();
+            }
+
+            if (priorEntry != null)
+            {
+               priorEntry.InvokeEvictionCallbacks();
+            }
+         }
+         else
+         {
+            if (exceedsCapacity)
+            {
+               // The entry was not added due to overcapacity
+               entry.SetExpired(EvictionReason.Capacity);
+
+               TriggerOvercapacityCompaction();
+            }
+            else
+            {
+               if (_options.SizeLimit.HasValue)
+               {
+                  // Entry could not be added due to being expired, reset cache size
+                  Interlocked.Add(ref _cacheSize, -entry.Size.Value);
+               }
+            }
+
+            entry.InvokeEvictionCallbacks();
+            if (priorEntry != null)
+            {
+               RemoveEntry(priorEntry);
+            }
+         }
+
+         StartScanForExpiredItemsIfNeeded(utcNow);
+      }
+
+      /// <inheritdoc />
+      public bool TryGetValue(object key, out object result)
+      {
+         ValidateCacheKey(key);
+         CheckDisposed();
+
+         DateTimeOffset utcNow = _options.Clock.UtcNow;
+
+         if (_entries.TryGetValue(key, out CacheEntry entry))
+         {
+            // Check if expired due to expiration tokens, timers, etc. and if so, remove it.
+            // Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
+            if (!entry.CheckExpired(utcNow) || entry.EvictionReason == EvictionReason.Replaced)
+            {
+               entry.LastAccessed = utcNow;
+               result = entry.Value;
+
+               if (TrackLinkedCacheEntries && entry.CanPropagateOptions())
+               {
+                  // When this entry is retrieved in the scope of creating another entry,
+                  // that entry needs a copy of these expiration tokens.
+                  entry.PropagateOptions(CacheEntryHelper.Current);
+               }
+
+               StartScanForExpiredItemsIfNeeded(utcNow);
+
+               return true;
+            }
+            else
+            {
+               // TODO: For efficiency queue this up for batch removal
+               RemoveEntry(entry);
+            }
+         }
+
+         StartScanForExpiredItemsIfNeeded(utcNow);
+
+         result = null;
+         return false;
+      }
+
+      /// <inheritdoc />
+      public void Remove(object key)
+      {
+         ValidateCacheKey(key);
+
+         CheckDisposed();
+         if (_entries.TryRemove(key, out CacheEntry entry))
+         {
+            if (_options.SizeLimit.HasValue)
+            {
+               Interlocked.Add(ref _cacheSize, -entry.Size.Value);
+            }
+
+            entry.SetExpired(EvictionReason.Removed);
+            entry.InvokeEvictionCallbacks();
+         }
+
+         StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
+      }
+
+      private void RemoveEntry(CacheEntry entry)
+      {
+         if (EntriesCollection.Remove(new KeyValuePair<object, CacheEntry>(entry.Key, entry)))
+         {
+            if (_options.SizeLimit.HasValue)
+            {
+               Interlocked.Add(ref _cacheSize, -entry.Size.Value);
+            }
+            entry.InvokeEvictionCallbacks();
+         }
+      }
+
+      internal void EntryExpired(CacheEntry entry)
+      {
+         // TODO: For efficiency consider processing these expirations in batches.
+         RemoveEntry(entry);
+         StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
+      }
+
+      // Called by multiple actions to see how long it's been since we last checked for expired items.
+      // If sufficient time has elapsed then a scan is initiated on a background task.
+      [MethodImpl(MethodImplOptions.AggressiveInlining)]
+      private void StartScanForExpiredItemsIfNeeded(DateTimeOffset utcNow)
+      {
+         if (_options.ExpirationScanFrequency < utcNow - _lastExpirationScan)
+         {
+            ScheduleTask(utcNow);
+         }
+
+         void ScheduleTask(DateTimeOffset utcNow)
+         {
+            _lastExpirationScan = utcNow;
+            Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this,
+                CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
+         }
+      }
+
+      private static void ScanForExpiredItems(MemoryCache cache)
+      {
+         DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock.UtcNow;
+
+         foreach (KeyValuePair<object, CacheEntry> item in cache._entries)
+         {
+            CacheEntry entry = item.Value;
+
+            if (entry.CheckExpired(now))
+            {
+               cache.RemoveEntry(entry);
+            }
+         }
+      }
+
+      private bool UpdateCacheSizeExceedsCapacity(CacheEntry entry)
+      {
+         if (!_options.SizeLimit.HasValue)
+         {
+            return false;
+         }
+
+         long newSize = 0L;
+         for (int i = 0; i < 100; i++)
+         {
+            long sizeRead = Interlocked.Read(ref _cacheSize);
+            newSize = sizeRead + entry.Size.Value;
+
+            if (newSize < 0 || newSize > _options.SizeLimit)
+            {
+               // Overflow occurred, return true without updating the cache size
+               return true;
+            }
+
+            if (sizeRead == Interlocked.CompareExchange(ref _cacheSize, newSize, sizeRead))
+            {
+               return false;
+            }
+         }
+
+         return true;
+      }
+
+      private void TriggerOvercapacityCompaction()
+      {
+         _logger.LogDebug("Overcapacity compaction triggered");
+
+         // Spawn background thread for compaction
+         ThreadPool.QueueUserWorkItem(s => OvercapacityCompaction((MemoryCache)s), this);
+      }
+
+      private static void OvercapacityCompaction(MemoryCache cache)
+      {
+         long currentSize = Interlocked.Read(ref cache._cacheSize);
+
+         cache._logger.LogDebug($"Overcapacity compaction executing. Current size {currentSize}");
+
+         double? lowWatermark = cache._options.SizeLimit * (1 - cache._options.CompactionPercentage);
+         if (currentSize > lowWatermark)
+         {
+            cache.Compact(currentSize - (long)lowWatermark, entry => entry.Size.Value);
+         }
+
+         cache._logger.LogDebug($"Overcapacity compaction executed. New size {Interlocked.Read(ref cache._cacheSize)}");
+      }
+
+      /// Remove at least the given percentage (0.10 for 10%) of the total entries (or estimated memory?), according to the following policy:
+      /// 1. Remove all expired items.
+      /// 2. Bucket by CacheItemPriority.
+      /// 3. Least recently used objects.
+      /// ?. Items with the soonest absolute expiration.
+      /// ?. Items with the soonest sliding expiration.
+      /// ?. Larger objects - estimated by object graph size, inaccurate.
+      public void Compact(double percentage)
+      {
+         int removalCountTarget = (int)(_entries.Count * percentage);
+         Compact(removalCountTarget, _ => 1);
+      }
+
+      private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntrySize)
+      {
+         var entriesToRemove = new List<CacheEntry>();
+         var lowPriEntries = new List<CacheEntry>();
+         var normalPriEntries = new List<CacheEntry>();
+         var highPriEntries = new List<CacheEntry>();
+         long removedSize = 0;
+
+         // Sort items by expired & priority status
+         DateTimeOffset now = _options.Clock.UtcNow;
+         foreach (KeyValuePair<object, CacheEntry> item in _entries)
+         {
+            CacheEntry entry = item.Value;
+            if (entry.CheckExpired(now))
+            {
+               entriesToRemove.Add(entry);
+               removedSize += computeEntrySize(entry);
+            }
+            else
+            {
+               switch (entry.Priority)
+               {
+                  case CacheItemPriority.Low:
+                     lowPriEntries.Add(entry);
+                     break;
+                  case CacheItemPriority.Normal:
+                     normalPriEntries.Add(entry);
+                     break;
+                  case CacheItemPriority.High:
+                     highPriEntries.Add(entry);
+                     break;
+                  case CacheItemPriority.NeverRemove:
+                     break;
+                  default:
+                     throw new NotSupportedException("Not implemented: " + entry.Priority);
+               }
+            }
+         }
+
+         ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, lowPriEntries);
+         ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, normalPriEntries);
+         ExpirePriorityBucket(ref removedSize, removalSizeTarget, computeEntrySize, entriesToRemove, highPriEntries);
+
+         foreach (CacheEntry entry in entriesToRemove)
+         {
+            RemoveEntry(entry);
+         }
+
+         // Policy:
+         // 1. Least recently used objects.
+         // ?. Items with the soonest absolute expiration.
+         // ?. Items with the soonest sliding expiration.
+         // ?. Larger objects - estimated by object graph size, inaccurate.
+         static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, Func<CacheEntry, long> computeEntrySize, List<CacheEntry> entriesToRemove, List<CacheEntry> priorityEntries)
+         {
+            // Do we meet our quota by just removing expired entries?
+            if (removalSizeTarget <= removedSize)
+            {
+               // No-op, we've met quota
+               return;
+            }
+
+            // Expire enough entries to reach our goal
+            // TODO: Refine policy
+
+            // LRU
+            priorityEntries.Sort((e1, e2) => e1.LastAccessed.CompareTo(e2.LastAccessed));
+            foreach (CacheEntry entry in priorityEntries)
+            {
+               entry.SetExpired(EvictionReason.Capacity);
+               entriesToRemove.Add(entry);
+               removedSize += computeEntrySize(entry);
+
+               if (removalSizeTarget <= removedSize)
+               {
+                  break;
+               }
+            }
+         }
+      }
+
+      public void Dispose()
+      {
+         Dispose(true);
+      }
+
+      protected virtual void Dispose(bool disposing)
+      {
+         if (!_disposed)
+         {
+            if (disposing)
+            {
+               GC.SuppressFinalize(this);
+            }
+
+            _disposed = true;
+         }
+      }
+
+      private void CheckDisposed()
+      {
+         if (_disposed)
+         {
+            Throw();
+         }
+
+         static void Throw() => throw new ObjectDisposedException(typeof(MemoryCache).FullName);
+      }
+
+      private static void ValidateCacheKey(object key)
+      {
+         if (key == null)
+         {
+            Throw();
+         }
+
+         static void Throw() => throw new ArgumentNullException(nameof(key));
+      }
+   }
+}

+ 67 - 0
frameworks/CSharp/appmpower/src/Memory/MemoryCacheOptions.cs

@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using Microsoft.Extensions.Internal;
+using Microsoft.Extensions.Options;
+
+namespace appMpower.Memory
+{
+   public class MemoryCacheOptions : IOptions<MemoryCacheOptions>
+   {
+      private long? _sizeLimit;
+      private double _compactionPercentage = 0.05;
+
+      public ISystemClock Clock { get; set; }
+
+      /// <summary>
+      /// Gets or sets the minimum length of time between successive scans for expired items.
+      /// </summary>
+      public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(1);
+
+      /// <summary>
+      /// Gets or sets the maximum size of the cache.
+      /// </summary>
+      public long? SizeLimit
+      {
+         get => _sizeLimit;
+         set
+         {
+            if (value < 0)
+            {
+               throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be non-negative.");
+            }
+
+            _sizeLimit = value;
+         }
+      }
+
+      /// <summary>
+      /// Gets or sets the amount to compact the cache by when the maximum size is exceeded.
+      /// </summary>
+      public double CompactionPercentage
+      {
+         get => _compactionPercentage;
+         set
+         {
+            if (value < 0 || value > 1)
+            {
+               throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(value)} must be between 0 and 1 inclusive.");
+            }
+
+            _compactionPercentage = value;
+         }
+      }
+
+      /// <summary>
+      /// Gets or sets whether to track linked entries. Disabled by default.
+      /// </summary>
+      /// <remarks>Prior to .NET 7 this feature was always enabled.</remarks>
+      public bool TrackLinkedCacheEntries { get; set; }
+
+      MemoryCacheOptions IOptions<MemoryCacheOptions>.Value
+      {
+         get { return this; }
+      }
+   }
+}

+ 24 - 0
frameworks/CSharp/appmpower/src/Microsoft/CachKey.cs

@@ -0,0 +1,24 @@
+using System;
+
+namespace PlatformBenchmarks
+{
+   public sealed class CacheKey : IEquatable<CacheKey>
+   {
+      private readonly int _value;
+
+      public CacheKey(int value)
+          => _value = value;
+
+      public bool Equals(CacheKey key)
+          => key._value == _value;
+
+      public override bool Equals(object obj)
+          => ReferenceEquals(obj, this);
+
+      public override int GetHashCode()
+          => _value;
+
+      public override string ToString()
+          => _value.ToString();
+   }
+}

+ 101 - 5
frameworks/CSharp/appmpower/src/RawDb.cs

@@ -2,8 +2,11 @@ using System;
 using System.Collections.Generic;
 using System.Data;
 using System.Data.Common;
+using System.Linq;
 using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Memory;
 using appMpower.Db;
+using PlatformBenchmarks;
 
 namespace appMpower
 {
@@ -13,9 +16,17 @@ namespace appMpower
       private static Random _random = new Random();
       private static string[] _queriesMultipleRows = new string[MaxBatch + 1];
 
+      private static readonly object[] _cacheKeys = Enumerable.Range(0, 10001).Select((i) => new CacheKey(i)).ToArray();
+
+      private static readonly appMpower.Memory.MemoryCache _cache = new appMpower.Memory.MemoryCache(
+            new appMpower.Memory.MemoryCacheOptions()
+            {
+               ExpirationScanFrequency = TimeSpan.FromMinutes(60)
+            });
+
       public static async Task<World> LoadSingleQueryRow()
       {
-         var pooledConnection = await PooledConnections.GetConnection(ConnectionStrings.OdbcConnection);
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
          pooledConnection.Open();
 
          var (pooledCommand, _) = CreateReadCommand(pooledConnection);
@@ -31,7 +42,7 @@ namespace appMpower
       {
          var worlds = new World[count];
 
-         var pooledConnection = await PooledConnections.GetConnection(ConnectionStrings.OdbcConnection);
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
          pooledConnection.Open();
 
          var (pooledCommand, dbDataParameter) = CreateReadCommand(pooledConnection);
@@ -52,7 +63,7 @@ namespace appMpower
       {
          var fortunes = new List<Fortune>();
 
-         var pooledConnection = await PooledConnections.GetConnection(ConnectionStrings.OdbcConnection);
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
          pooledConnection.Open();
 
          var pooledCommand = new PooledCommand("SELECT * FROM fortune", pooledConnection);
@@ -87,7 +98,7 @@ namespace appMpower
       {
          var worlds = new World[count];
 
-         var pooledConnection = await PooledConnections.GetConnection(ConnectionStrings.OdbcConnection);
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
          pooledConnection.Open();
 
          var (queryCommand, dbDataParameter) = CreateReadCommand(pooledConnection);
@@ -182,7 +193,7 @@ namespace appMpower
             queryString = _queriesMultipleRows[count] = PlatformBenchmarks.StringBuilderCache.GetStringAndRelease(stringBuilder);
          }
 
-         var pooledConnection = await PooledConnections.GetConnection(ConnectionStrings.OdbcConnection);
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
          pooledConnection.Open();
 
          var pooledCommand = new PooledCommand(queryString, pooledConnection);
@@ -231,5 +242,90 @@ namespace appMpower
 
          return System.Text.Encoding.Default.GetString(values);
       }
+
+      public static async Task PopulateCache()
+      {
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
+         pooledConnection.Open();
+
+         var (pooledCommand, dbDataParameter) = CreateReadCommand(pooledConnection);
+
+         using (pooledCommand)
+         {
+            var cacheKeys = _cacheKeys;
+            var cache = _cache;
+
+            for (var i = 1; i < 10001; i++)
+            {
+               dbDataParameter.Value = i;
+               cache.Set<CachedWorld>(cacheKeys[i], await ReadSingleRow(pooledCommand));
+            }
+         }
+
+         pooledCommand.Release();
+         pooledConnection.Release();
+      }
+
+      public static Task<CachedWorld[]> LoadCachedQueries(int count)
+      {
+         var result = new CachedWorld[count];
+         var cacheKeys = _cacheKeys;
+         var cache = _cache;
+         var random = _random;
+
+         for (var i = 0; i < result.Length; i++)
+         {
+            var id = random.Next(1, 10001);
+            var key = cacheKeys[id];
+
+            if (cache.TryGetValue(key, out object cached))
+            {
+               result[i] = (CachedWorld)cached;
+            }
+            else
+            {
+               //return LoadUncachedQueries(id, i, count, this, result);
+               return LoadUncachedQueries(id, i, count, result);
+            }
+         }
+
+         return Task.FromResult(result);
+      }
+
+      //static async Task<CachedWorld[]> LoadUncachedQueries(int id, int i, int count, RawDb rawdb, CachedWorld[] result)
+      static async Task<CachedWorld[]> LoadUncachedQueries(int id, int i, int count, CachedWorld[] result)
+      {
+         var pooledConnection = await PooledConnections.GetConnection(DataProvider.ConnectionString);
+         pooledConnection.Open();
+
+         var (pooledCommand, dbDataParameter) = CreateReadCommand(pooledConnection);
+
+         using (pooledCommand)
+         {
+            Func<ICacheEntry, Task<CachedWorld>> create = async (entry) =>
+            {
+               return await ReadSingleRow(pooledCommand);
+            };
+
+            var cacheKeys = _cacheKeys;
+            var key = cacheKeys[id];
+
+            dbDataParameter.Value = id;
+
+            for (; i < result.Length; i++)
+            {
+               result[i] = await _cache.GetOrCreateAsync<CachedWorld>(key, create);
+
+               id = _random.Next(1, 10001);
+               dbDataParameter.Value = id;
+               key = cacheKeys[id];
+            }
+
+            pooledCommand.Release();
+            pooledConnection.Release();
+         }
+
+         return result;
+      }
    }
 }

+ 15 - 7
frameworks/CSharp/appmpower/src/appMpower.csproj

@@ -1,27 +1,35 @@
 <Project Sdk="Microsoft.NET.Sdk.Web">
 
   <PropertyGroup>
-    <TargetFramework>net5.0</TargetFramework>
+    <TargetFramework>net6.0</TargetFramework>
     <OutputType>Exe</OutputType>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
 
-    <TrimMode>link</TrimMode>
-
     <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
     <SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
     <InvariantGlobalization>true</InvariantGlobalization>
-    <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
     <IlcDisableReflection>true</IlcDisableReflection>
-    <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
 
+    <!--
+    <TrimMode>link</TrimMode>
+    -->
+
+    <!-- Opt out of the "easy mode" of the CoreRT compiler (http://aka.ms/OptimizeCoreRT) -->
+    <TrimmerDefaultAction>link</TrimmerDefaultAction>
+    <IlcOptimizationPreference>Speed</IlcOptimizationPreference>
+    <IlcPgoOptimize>true</IlcPgoOptimize>
+    <IlcTrimMetadata>true</IlcTrimMetadata>
+
+    <!-- This benchmark is marked Stripped, so we might as well do this: -->
     <UseSystemResourceKeys>true</UseSystemResourceKeys>
     <EventSourceSupport>false</EventSourceSupport>
     <DebuggerSupport>false</DebuggerSupport>
+    <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="System.Data.Odbc" Version="5.0.0" />
-    <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="6.0.0-*" />
+    <PackageReference Include="System.Data.Odbc" Version="6.0.0" />
+    <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
   </ItemGroup>
 
   <PropertyGroup>