Browse Source

Added cacheing to Client Store, and Management API

master
Teknikode 7 months ago
parent
commit
fc86f237fb

+ 94
- 19
IdentityServer/Controllers/ManageController.cs View File

@@ -14,10 +14,13 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using Teknik.Configuration;
using Teknik.IdentityServer.Models;
using Teknik.IdentityServer.Models.Manage;
using Teknik.IdentityServer.Services;
using Teknik.Logging;
using Teknik.Utilities;

@@ -28,17 +31,22 @@ namespace Teknik.IdentityServer.Controllers
[ApiController]
public class ManageController : DefaultController
{
private const string _UserInfoCacheKey = "UserInfo";

private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IMemoryCache _cache;

public ManageController(
ILogger<Logger> logger,
Config config,
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager) : base(logger, config)
SignInManager<ApplicationUser> signInManager,
IMemoryCache cache) : base(logger, config)
{
_userManager = userManager;
_signInManager = signInManager;
_cache = cache;
}

[HttpPost]
@@ -91,7 +99,11 @@ namespace Teknik.IdentityServer.Controllers

var result = await _userManager.DeleteAsync(foundUser);
if (result.Succeeded)
{
RemoveCachedUser(model.Username);

return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to delete user.", identityErrors = result.Errors });
}
@@ -104,8 +116,8 @@ namespace Teknik.IdentityServer.Controllers
{
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });
var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
return new JsonResult(new { success = true, data = foundUser != null });
}

@@ -115,12 +127,11 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
return new JsonResult(new { success = true, data = foundUser.ToJson() });
}

return new JsonResult(new { success = false, message = "User does not exist." });
}

@@ -132,7 +143,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Password))
return new JsonResult(new { success = false, message = "Password is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
bool valid = await _userManager.CheckPasswordAsync(foundUser, model.Password);
@@ -148,7 +159,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
string token = await _userManager.GeneratePasswordResetTokenAsync(foundUser);
@@ -168,7 +179,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Password))
return new JsonResult(new { success = false, message = "Password is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.ResetPasswordAsync(foundUser, model.Token, model.Password);
@@ -191,7 +202,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.NewPassword))
return new JsonResult(new { success = false, message = "New Password is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.ChangePasswordAsync(foundUser, model.CurrentPassword, model.NewPassword);
@@ -210,12 +221,15 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.SetEmailAsync(foundUser, model.Email);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

var token = await _userManager.GenerateEmailConfirmationTokenAsync(foundUser);
return new JsonResult(new { success = true, data = token });
}
@@ -234,9 +248,12 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Token))
return new JsonResult(new { success = false, message = "Token is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

var result = await _userManager.ConfirmEmailAsync(foundUser, model.Token);
if (result.Succeeded)
return new JsonResult(new { success = true });
@@ -253,14 +270,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.AccountStatus = model.AccountStatus;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update account status.", identityErrors = result.Errors });
}
@@ -274,14 +296,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.AccountType = model.AccountType;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update account type.", identityErrors = result.Errors });
}
@@ -295,14 +322,19 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
foundUser.PGPPublicKey = model.PGPPublicKey;

var result = await _userManager.UpdateAsync(foundUser);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to update pgp public key.", identityErrors = result.Errors });
}
@@ -316,7 +348,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(username);
var foundUser = await GetCachedUser(username);
if (foundUser != null)
{
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);
@@ -333,9 +365,12 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

await _userManager.ResetAuthenticatorKeyAsync(foundUser);
string unformattedKey = await _userManager.GetAuthenticatorKeyAsync(foundUser);

@@ -353,7 +388,7 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Code))
return new JsonResult(new { success = false, message = "Code is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
// Strip spaces and hypens
@@ -367,6 +402,9 @@ namespace Teknik.IdentityServer.Controllers
var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, true);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);
return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
}
@@ -386,12 +424,17 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
var result = await _userManager.SetTwoFactorEnabledAsync(foundUser, false);
if (result.Succeeded)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

return new JsonResult(new { success = true });
}
else
return new JsonResult(new { success = false, message = "Unable to disable Two-Factor Authentication.", identityErrors = result.Errors });
}
@@ -405,11 +448,14 @@ namespace Teknik.IdentityServer.Controllers
if (string.IsNullOrEmpty(model.Username))
return new JsonResult(new { success = false, message = "Username is required" });

var foundUser = await _userManager.FindByNameAsync(model.Username);
var foundUser = await GetCachedUser(model.Username);
if (foundUser != null)
{
if (foundUser.TwoFactorEnabled)
{
// Remove the UserInfo Cache
RemoveCachedUser(model.Username);

var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(foundUser, 10);

return new JsonResult(new { success = true, data = recoveryCodes.ToArray() });
@@ -607,5 +653,34 @@ namespace Teknik.IdentityServer.Controllers

return result.ToString().ToLowerInvariant();
}

private async Task<ApplicationUser> GetCachedUser(string username)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentNullException("username");

// Check the cache
string cacheKey = _UserInfoCacheKey + username;
ApplicationUser foundUser;
if (!_cache.TryGetValue(cacheKey, out foundUser))
{
foundUser = await _userManager.FindByNameAsync(username);
if (foundUser != null)
{
_cache.AddToCache(cacheKey, foundUser, new TimeSpan(1, 0, 0));
}
}

return foundUser;
}

private void RemoveCachedUser(string username)
{
if (string.IsNullOrEmpty(username))
throw new ArgumentNullException("username");

string cacheKey = _UserInfoCacheKey + username;
_cache.Remove(cacheKey);
}
}
}

+ 244
- 0
IdentityServer/Data/Migrations/ApplicationDb/20190129061223_UserEditDate.Designer.cs View File

@@ -0,0 +1,244 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Teknik.IdentityServer;

namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190129061223_UserEditDate")]
partial class UserEditDate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();

b.Property<string>("Name")
.HasMaxLength(256);

b.Property<string>("NormalizedName")
.HasMaxLength(256);

b.HasKey("Id");

b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");

b.ToTable("AspNetRoles");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

b.Property<string>("ClaimType");

b.Property<string>("ClaimValue");

b.Property<string>("RoleId")
.IsRequired();

b.HasKey("Id");

b.HasIndex("RoleId");

b.ToTable("AspNetRoleClaims");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

b.Property<string>("ClaimType");

b.Property<string>("ClaimValue");

b.Property<string>("UserId")
.IsRequired();

b.HasKey("Id");

b.HasIndex("UserId");

b.ToTable("AspNetUserClaims");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");

b.Property<string>("ProviderKey");

b.Property<string>("ProviderDisplayName");

b.Property<string>("UserId")
.IsRequired();

b.HasKey("LoginProvider", "ProviderKey");

b.HasIndex("UserId");

b.ToTable("AspNetUserLogins");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");

b.Property<string>("RoleId");

b.HasKey("UserId", "RoleId");

b.HasIndex("RoleId");

b.ToTable("AspNetUserRoles");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");

b.Property<string>("LoginProvider");

b.Property<string>("Name");

b.Property<string>("Value");

b.HasKey("UserId", "LoginProvider", "Name");

b.ToTable("AspNetUserTokens");
});

modelBuilder.Entity("Teknik.IdentityServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();

b.Property<int>("AccessFailedCount");

b.Property<int>("AccountStatus");

b.Property<int>("AccountType");

b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();

b.Property<DateTime>("CreationDate");

b.Property<string>("Email")
.HasMaxLength(256);

b.Property<bool>("EmailConfirmed");

b.Property<DateTime>("LastEdit");

b.Property<DateTime>("LastSeen");

b.Property<bool>("LockoutEnabled");

b.Property<DateTimeOffset?>("LockoutEnd");

b.Property<string>("NormalizedEmail")
.HasMaxLength(256);

b.Property<string>("NormalizedUserName")
.HasMaxLength(256);

b.Property<string>("PGPPublicKey");

b.Property<string>("PasswordHash");

b.Property<string>("PhoneNumber");

b.Property<bool>("PhoneNumberConfirmed");

b.Property<string>("SecurityStamp");

b.Property<bool>("TwoFactorEnabled");

b.Property<string>("UserName")
.HasMaxLength(256);

b.HasKey("Id");

b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");

b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");

b.ToTable("AspNetUsers");
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);

b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});

modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("Teknik.IdentityServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

+ 24
- 0
IdentityServer/Data/Migrations/ApplicationDb/20190129061223_UserEditDate.cs View File

@@ -0,0 +1,24 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
public partial class UserEditDate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastEdit",
table: "AspNetUsers",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastEdit",
table: "AspNetUsers");
}
}
}

+ 3
- 1
IdentityServer/Data/Migrations/ApplicationDb/ApplicationDbContextModelSnapshot.cs View File

@@ -15,7 +15,7 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.0-preview2-35157")
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024")
.HasAnnotation("Relational:MaxIdentifierLength", 128)
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);

@@ -150,6 +150,8 @@ namespace Teknik.IdentityServer.Data.Migrations.ApplicationDb

b.Property<bool>("EmailConfirmed");

b.Property<DateTime>("LastEdit");

b.Property<DateTime>("LastSeen");

b.Property<bool>("LockoutEnabled");

+ 5
- 0
IdentityServer/Models/ApplicationUser.cs View File

@@ -21,6 +21,8 @@ namespace Teknik.IdentityServer.Models

public string PGPPublicKey { get; set; }

public DateTime LastEdit { get; set; }

public ApplicationUser() : base()
{
Init();
@@ -35,6 +37,7 @@ namespace Teknik.IdentityServer.Models
{
CreationDate = DateTime.Now;
LastSeen = DateTime.Now;
LastEdit = DateTime.Now;
AccountType = AccountType.Basic;
AccountStatus = AccountStatus.Active;
PGPPublicKey = null;
@@ -46,6 +49,7 @@ namespace Teknik.IdentityServer.Models
claims.Add(new Claim("username", UserName));
claims.Add(new Claim("creation-date", CreationDate.ToString("o")));
claims.Add(new Claim("last-seen", LastSeen.ToString("o")));
claims.Add(new Claim("last-edit", LastEdit.ToString("o")));
claims.Add(new Claim("account-type", AccountType.ToString()));
claims.Add(new Claim("account-status", AccountStatus.ToString()));
claims.Add(new Claim("recovery-email", Email ?? string.Empty));
@@ -62,6 +66,7 @@ namespace Teknik.IdentityServer.Models
new JProperty("username", UserName),
new JProperty("creation-date", CreationDate),
new JProperty("last-seen", LastSeen),
new JProperty("last-edit", LastEdit),
new JProperty("account-type", AccountType),
new JProperty("account-status", AccountStatus),
new JProperty("recovery-email", Email),

+ 3
- 2
IdentityServer/Startup.cs View File

@@ -23,8 +23,7 @@ using Teknik.Logging;
using Microsoft.AspNetCore.Authorization;
using Teknik.IdentityServer.Models;
using IdentityServer4.Services;
using System.Collections.Generic;
using Teknik.Utilities;


namespace Teknik.IdentityServer
{
@@ -128,6 +127,7 @@ namespace Teknik.IdentityServer
options.Cors.CorsPaths.Add(new PathString("/connect/endsession"));
options.Cors.CorsPaths.Add(new PathString("/connect/checksession"));
options.Cors.CorsPaths.Add(new PathString("/connect/introspect"));
options.Caching.ClientStoreExpiration = TimeSpan.FromHours(1);
})
.AddOperationalStore(options =>
options.ConfigureDbContext = builder =>
@@ -135,6 +135,7 @@ namespace Teknik.IdentityServer
.AddConfigurationStore(options =>
options.ConfigureDbContext = builder =>
builder.UseSqlServer(config.DbConnection, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
.AddConfigurationStoreCache()
.AddAspNetIdentity<ApplicationUser>()
.AddRedirectUriValidator<TeknikRedirectUriValidator>()
.AddDeveloperSigningCredential();

+ 1
- 1
Teknik/Areas/Admin/Views/Admin/UserSearch.cshtml View File

@@ -25,4 +25,4 @@
</div>
</div>

<bundle src="js/userInfo.min.js" append-version="true"></bundle>
<bundle src="js/userSearch.min.js" append-version="true"></bundle>

+ 1
- 1
Teknik/bundleconfig.json View File

@@ -20,7 +20,7 @@
{
"outputFileName": "./wwwroot/js/userSearch.min.js",
"inputFiles": [
"./wwwroot/js/app/Admin/UserInfo.js"
"./wwwroot/js/app/Admin/UserSearch.js"
]
},
{

+ 33
- 0
Utilities/MemoryCacheHelper.cs View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Extensions.Caching.Memory;

namespace Teknik.Utilities
{
public static class MemoryCacheHelper
{
public static bool GetCacheValue<T>(this IMemoryCache cache, string key, out T value)
{
return cache.TryGetValue(key, out value);
}

public static bool GetCacheValue(this IMemoryCache cache, string key, out object value)
{
return cache.TryGetValue(key, out value);
}

public static void AddToCache(this IMemoryCache cache, string key, object value, TimeSpan expiration)
{
var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expiration);

cache.AddToCache(key, value, cacheOptions);
}

public static void AddToCache(this IMemoryCache cache, string key, object value, MemoryCacheEntryOptions options)
{
cache.Set(key, value, options);
}
}
}

Loading…
Cancel
Save