mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[PM-5518] Sql-backed IDistributedCache (#3791)
* Sql-backed IDistributedCache * sqlserver cache table * remove unused using * setup EF entity * cache indexes * add back cipher * revert SetupEntityFramework change * ef cache * EntityFrameworkCache * IServiceScopeFactory for db context * implement EntityFrameworkCache * move to _serviceScopeFactory * move to config file * ef migrations * fixes * datetime and error codes * revert migrations * migrations * format * static and namespace fix * use time provider * Move SQL migration and remove EF one for the moment * Add clean migration of just the new table * Formatting * Test Custom `IDistributedCache` Implementation * Add Back Logging * Remove Double Logging * Skip Test When Not EntityFrameworkCache * Format --------- Co-authored-by: Matt Bishop <mbishop@bitwarden.com> Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
parent
b8f71271eb
commit
0d3a7b3dd5
@ -39,6 +39,7 @@
|
|||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.6" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.31" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.31" />
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Configurations;
|
||||||
|
|
||||||
|
public class CacheEntityTypeConfiguration : IEntityTypeConfiguration<Cache>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Cache> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.HasKey(s => s.Id)
|
||||||
|
.IsClustered();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Property(s => s.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(s => s.ExpiresAtTime)
|
||||||
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder.ToTable(nameof(Cache));
|
||||||
|
}
|
||||||
|
}
|
315
src/Infrastructure.EntityFramework/EntityFrameworkCache.cs
Normal file
315
src/Infrastructure.EntityFramework/EntityFrameworkCache.cs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework;
|
||||||
|
|
||||||
|
public class EntityFrameworkCache : IDistributedCache
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
// Used for debugging in tests
|
||||||
|
public Task scanTask;
|
||||||
|
#endif
|
||||||
|
private static readonly TimeSpan _defaultSlidingExpiration = TimeSpan.FromMinutes(20);
|
||||||
|
private static readonly TimeSpan _expiredItemsDeletionInterval = TimeSpan.FromMinutes(30);
|
||||||
|
private DateTimeOffset _lastExpirationScan;
|
||||||
|
private readonly Action _deleteExpiredCachedItemsDelegate;
|
||||||
|
private readonly object _mutex = new();
|
||||||
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
public EntityFrameworkCache(
|
||||||
|
IServiceScopeFactory serviceScopeFactory,
|
||||||
|
TimeProvider timeProvider = null)
|
||||||
|
{
|
||||||
|
_deleteExpiredCachedItemsDelegate = DeleteExpiredCacheItems;
|
||||||
|
_serviceScopeFactory = serviceScopeFactory;
|
||||||
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] Get(string key)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var cache = dbContext.Cache
|
||||||
|
.Where(c => c.Id == key && _timeProvider.GetUtcNow().DateTime <= c.ExpiresAtTime)
|
||||||
|
.SingleOrDefault();
|
||||||
|
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UpdateCacheExpiration(cache))
|
||||||
|
{
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
return cache?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GetAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var cache = await dbContext.Cache
|
||||||
|
.Where(c => c.Id == key && _timeProvider.GetUtcNow().DateTime <= c.ExpiresAtTime)
|
||||||
|
.SingleOrDefaultAsync(cancellationToken: token);
|
||||||
|
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UpdateCacheExpiration(cache))
|
||||||
|
{
|
||||||
|
await dbContext.SaveChangesAsync(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
return cache?.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Refresh(string key) => Get(key);
|
||||||
|
|
||||||
|
public Task RefreshAsync(string key, CancellationToken token = default) => GetAsync(key, token);
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
GetDatabaseContext(scope).Cache
|
||||||
|
.Where(c => c.Id == key)
|
||||||
|
.ExecuteDelete();
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAsync(string key, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
await GetDatabaseContext(scope).Cache
|
||||||
|
.Where(c => c.Id == key)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken: token);
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var cache = dbContext.Cache.Find(key);
|
||||||
|
var insert = cache == null;
|
||||||
|
cache = SetCache(cache, key, value, options);
|
||||||
|
if (insert)
|
||||||
|
{
|
||||||
|
dbContext.Add(cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
if (IsDuplicateKeyException(e))
|
||||||
|
{
|
||||||
|
// There is a possibility that multiple requests can try to add the same item to the cache, in
|
||||||
|
// which case we receive a 'duplicate key' exception on the primary key column.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
|
|
||||||
|
token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
var cache = await dbContext.Cache.FindAsync(new object[] { key }, cancellationToken: token);
|
||||||
|
var insert = cache == null;
|
||||||
|
cache = SetCache(cache, key, value, options);
|
||||||
|
if (insert)
|
||||||
|
{
|
||||||
|
await dbContext.AddAsync(cache, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await dbContext.SaveChangesAsync(token);
|
||||||
|
}
|
||||||
|
catch (DbUpdateException e)
|
||||||
|
{
|
||||||
|
if (IsDuplicateKeyException(e))
|
||||||
|
{
|
||||||
|
// There is a possibility that multiple requests can try to add the same item to the cache, in
|
||||||
|
// which case we receive a 'duplicate key' exception on the primary key column.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScanForExpiredItemsIfRequired();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cache SetCache(Cache cache, string key, byte[] value, DistributedCacheEntryOptions options)
|
||||||
|
{
|
||||||
|
var utcNow = _timeProvider.GetUtcNow().DateTime;
|
||||||
|
|
||||||
|
// resolve options
|
||||||
|
if (!options.AbsoluteExpiration.HasValue &&
|
||||||
|
!options.AbsoluteExpirationRelativeToNow.HasValue &&
|
||||||
|
!options.SlidingExpiration.HasValue)
|
||||||
|
{
|
||||||
|
options = new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = _defaultSlidingExpiration
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
// do an insert
|
||||||
|
cache = new Cache { Id = key };
|
||||||
|
}
|
||||||
|
|
||||||
|
var slidingExpiration = (long?)options.SlidingExpiration?.TotalSeconds;
|
||||||
|
|
||||||
|
// calculate absolute expiration
|
||||||
|
DateTime? absoluteExpiration = null;
|
||||||
|
if (options.AbsoluteExpirationRelativeToNow.HasValue)
|
||||||
|
{
|
||||||
|
absoluteExpiration = utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);
|
||||||
|
}
|
||||||
|
else if (options.AbsoluteExpiration.HasValue)
|
||||||
|
{
|
||||||
|
if (options.AbsoluteExpiration.Value <= utcNow)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("The absolute expiration value must be in the future.");
|
||||||
|
}
|
||||||
|
|
||||||
|
absoluteExpiration = options.AbsoluteExpiration.Value.DateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set values on cache
|
||||||
|
cache.Value = value;
|
||||||
|
cache.SlidingExpirationInSeconds = slidingExpiration;
|
||||||
|
cache.AbsoluteExpiration = absoluteExpiration;
|
||||||
|
if (slidingExpiration.HasValue)
|
||||||
|
{
|
||||||
|
cache.ExpiresAtTime = utcNow.AddSeconds(slidingExpiration.Value);
|
||||||
|
}
|
||||||
|
else if (absoluteExpiration.HasValue)
|
||||||
|
{
|
||||||
|
cache.ExpiresAtTime = absoluteExpiration.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Either absolute or sliding expiration needs to be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool UpdateCacheExpiration(Cache cache)
|
||||||
|
{
|
||||||
|
var utcNow = _timeProvider.GetUtcNow().DateTime;
|
||||||
|
if (cache.SlidingExpirationInSeconds.HasValue && (cache.AbsoluteExpiration.HasValue || cache.AbsoluteExpiration != cache.ExpiresAtTime))
|
||||||
|
{
|
||||||
|
if (cache.AbsoluteExpiration.HasValue && (cache.AbsoluteExpiration.Value - utcNow).TotalSeconds <= cache.SlidingExpirationInSeconds)
|
||||||
|
{
|
||||||
|
cache.ExpiresAtTime = cache.AbsoluteExpiration.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cache.ExpiresAtTime = utcNow.AddSeconds(cache.SlidingExpirationInSeconds.Value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanForExpiredItemsIfRequired()
|
||||||
|
{
|
||||||
|
lock (_mutex)
|
||||||
|
{
|
||||||
|
var utcNow = _timeProvider.GetUtcNow().DateTime;
|
||||||
|
if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval)
|
||||||
|
{
|
||||||
|
_lastExpirationScan = utcNow;
|
||||||
|
#if DEBUG
|
||||||
|
scanTask =
|
||||||
|
#endif
|
||||||
|
Task.Run(_deleteExpiredCachedItemsDelegate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteExpiredCacheItems()
|
||||||
|
{
|
||||||
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
GetDatabaseContext(scope).Cache
|
||||||
|
.Where(c => _timeProvider.GetUtcNow().DateTime > c.ExpiresAtTime)
|
||||||
|
.ExecuteDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DatabaseContext GetDatabaseContext(IServiceScope serviceScope)
|
||||||
|
{
|
||||||
|
return serviceScope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDuplicateKeyException(DbUpdateException e)
|
||||||
|
{
|
||||||
|
// MySQL
|
||||||
|
if (e.InnerException is MySqlConnector.MySqlException myEx)
|
||||||
|
{
|
||||||
|
return myEx.ErrorCode == MySqlConnector.MySqlErrorCode.DuplicateKeyEntry;
|
||||||
|
}
|
||||||
|
// SQL Server
|
||||||
|
else if (e.InnerException is Microsoft.Data.SqlClient.SqlException msEx)
|
||||||
|
{
|
||||||
|
return msEx.Errors != null &&
|
||||||
|
msEx.Errors.Cast<Microsoft.Data.SqlClient.SqlError>().Any(error => error.Number == 2627);
|
||||||
|
}
|
||||||
|
// Postgres
|
||||||
|
else if (e.InnerException is Npgsql.PostgresException pgEx)
|
||||||
|
{
|
||||||
|
return pgEx.SqlState == "23505";
|
||||||
|
}
|
||||||
|
// Sqlite
|
||||||
|
else if (e.InnerException is Microsoft.Data.Sqlite.SqliteException liteEx)
|
||||||
|
{
|
||||||
|
return liteEx.SqliteErrorCode == 19 && liteEx.SqliteExtendedErrorCode == 1555;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
13
src/Infrastructure.EntityFramework/Models/Cache.cs
Normal file
13
src/Infrastructure.EntityFramework/Models/Cache.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
|
||||||
|
public class Cache
|
||||||
|
{
|
||||||
|
[StringLength(449)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
public byte[] Value { get; set; }
|
||||||
|
public DateTime ExpiresAtTime { get; set; }
|
||||||
|
public long? SlidingExpirationInSeconds { get; set; }
|
||||||
|
public DateTime? AbsoluteExpiration { get; set; }
|
||||||
|
}
|
@ -32,6 +32,7 @@ public class DatabaseContext : DbContext
|
|||||||
public DbSet<GroupSecretAccessPolicy> GroupSecretAccessPolicy { get; set; }
|
public DbSet<GroupSecretAccessPolicy> GroupSecretAccessPolicy { get; set; }
|
||||||
public DbSet<ServiceAccountSecretAccessPolicy> ServiceAccountSecretAccessPolicy { get; set; }
|
public DbSet<ServiceAccountSecretAccessPolicy> ServiceAccountSecretAccessPolicy { get; set; }
|
||||||
public DbSet<ApiKey> ApiKeys { get; set; }
|
public DbSet<ApiKey> ApiKeys { get; set; }
|
||||||
|
public DbSet<Cache> Cache { get; set; }
|
||||||
public DbSet<Cipher> Ciphers { get; set; }
|
public DbSet<Cipher> Ciphers { get; set; }
|
||||||
public DbSet<Collection> Collections { get; set; }
|
public DbSet<Collection> Collections { get; set; }
|
||||||
public DbSet<CollectionCipher> CollectionCiphers { get; set; }
|
public DbSet<CollectionCipher> CollectionCiphers { get; set; }
|
||||||
|
@ -69,41 +69,7 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings)
|
public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
var selectedDatabaseProvider = globalSettings.DatabaseProvider;
|
var (provider, connectionString) = GetDatabaseProvider(globalSettings);
|
||||||
var provider = SupportedDatabaseProviders.SqlServer;
|
|
||||||
var connectionString = string.Empty;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(selectedDatabaseProvider))
|
|
||||||
{
|
|
||||||
switch (selectedDatabaseProvider.ToLowerInvariant())
|
|
||||||
{
|
|
||||||
case "postgres":
|
|
||||||
case "postgresql":
|
|
||||||
provider = SupportedDatabaseProviders.Postgres;
|
|
||||||
connectionString = globalSettings.PostgreSql.ConnectionString;
|
|
||||||
break;
|
|
||||||
case "mysql":
|
|
||||||
case "mariadb":
|
|
||||||
provider = SupportedDatabaseProviders.MySql;
|
|
||||||
connectionString = globalSettings.MySql.ConnectionString;
|
|
||||||
break;
|
|
||||||
case "sqlite":
|
|
||||||
provider = SupportedDatabaseProviders.Sqlite;
|
|
||||||
connectionString = globalSettings.Sqlite.ConnectionString;
|
|
||||||
break;
|
|
||||||
case "sqlserver":
|
|
||||||
connectionString = globalSettings.SqlServer.ConnectionString;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default to attempting to use SqlServer connection string if globalSettings.DatabaseProvider has no value.
|
|
||||||
connectionString = globalSettings.SqlServer.ConnectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
services.SetupEntityFramework(connectionString, provider);
|
services.SetupEntityFramework(connectionString, provider);
|
||||||
|
|
||||||
if (provider != SupportedDatabaseProviders.SqlServer)
|
if (provider != SupportedDatabaseProviders.SqlServer)
|
||||||
@ -730,7 +696,20 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
services.AddDistributedMemoryCache();
|
var (databaseProvider, databaseConnectionString) = GetDatabaseProvider(globalSettings);
|
||||||
|
if (databaseProvider == SupportedDatabaseProviders.SqlServer)
|
||||||
|
{
|
||||||
|
services.AddDistributedSqlServerCache(o =>
|
||||||
|
{
|
||||||
|
o.ConnectionString = databaseConnectionString;
|
||||||
|
o.SchemaName = "dbo";
|
||||||
|
o.TableName = "Cache";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
services.AddSingleton<IDistributedCache, EntityFrameworkCache>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Cosmos?.ConnectionString))
|
if (!string.IsNullOrEmpty(globalSettings.DistributedCache?.Cosmos?.ConnectionString))
|
||||||
@ -746,7 +725,7 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
services.AddKeyedSingleton<IDistributedCache, MemoryDistributedCache>("persistent");
|
services.AddKeyedSingleton("persistent", (s, _) => s.GetRequiredService<IDistributedCache>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -762,4 +741,45 @@ public static class ServiceCollectionExtensions
|
|||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (SupportedDatabaseProviders provider, string connectionString)
|
||||||
|
GetDatabaseProvider(GlobalSettings globalSettings)
|
||||||
|
{
|
||||||
|
var selectedDatabaseProvider = globalSettings.DatabaseProvider;
|
||||||
|
var provider = SupportedDatabaseProviders.SqlServer;
|
||||||
|
var connectionString = string.Empty;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(selectedDatabaseProvider))
|
||||||
|
{
|
||||||
|
switch (selectedDatabaseProvider.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "postgres":
|
||||||
|
case "postgresql":
|
||||||
|
provider = SupportedDatabaseProviders.Postgres;
|
||||||
|
connectionString = globalSettings.PostgreSql.ConnectionString;
|
||||||
|
break;
|
||||||
|
case "mysql":
|
||||||
|
case "mariadb":
|
||||||
|
provider = SupportedDatabaseProviders.MySql;
|
||||||
|
connectionString = globalSettings.MySql.ConnectionString;
|
||||||
|
break;
|
||||||
|
case "sqlite":
|
||||||
|
provider = SupportedDatabaseProviders.Sqlite;
|
||||||
|
connectionString = globalSettings.Sqlite.ConnectionString;
|
||||||
|
break;
|
||||||
|
case "sqlserver":
|
||||||
|
connectionString = globalSettings.SqlServer.ConnectionString;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default to attempting to use SqlServer connection string if globalSettings.DatabaseProvider has no value.
|
||||||
|
connectionString = globalSettings.SqlServer.ConnectionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (provider, connectionString);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
14
src/Sql/dbo/Tables/Cache.sql
Normal file
14
src/Sql/dbo/Tables/Cache.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE [dbo].[Cache]
|
||||||
|
(
|
||||||
|
[Id] NVARCHAR (449) NOT NULL,
|
||||||
|
[Value] VARBINARY (MAX) NOT NULL,
|
||||||
|
[ExpiresAtTime] DATETIMEOFFSET (7) NOT NULL,
|
||||||
|
[SlidingExpirationInSeconds] BIGINT NULL,
|
||||||
|
[AbsoluteExpiration] DATETIMEOFFSET (7) NULL,
|
||||||
|
CONSTRAINT [PK_Cache] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_Cache_ExpiresAtTime]
|
||||||
|
ON [dbo].[Cache]([ExpiresAtTime] ASC);
|
||||||
|
GO
|
@ -3,9 +3,11 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Infrastructure.Dapper;
|
using Bit.Infrastructure.Dapper;
|
||||||
using Bit.Infrastructure.EntityFramework;
|
using Bit.Infrastructure.EntityFramework;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using Xunit.Sdk;
|
using Xunit.Sdk;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.IntegrationTest;
|
namespace Bit.Infrastructure.IntegrationTest;
|
||||||
@ -13,6 +15,7 @@ namespace Bit.Infrastructure.IntegrationTest;
|
|||||||
public class DatabaseDataAttribute : DataAttribute
|
public class DatabaseDataAttribute : DataAttribute
|
||||||
{
|
{
|
||||||
public bool SelfHosted { get; set; }
|
public bool SelfHosted { get; set; }
|
||||||
|
public bool UseFakeTimeProvider { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
|
public override IEnumerable<object[]> GetData(MethodInfo testMethod)
|
||||||
{
|
{
|
||||||
@ -52,7 +55,7 @@ public class DatabaseDataAttribute : DataAttribute
|
|||||||
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
|
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
|
||||||
{
|
{
|
||||||
var dapperSqlServerCollection = new ServiceCollection();
|
var dapperSqlServerCollection = new ServiceCollection();
|
||||||
dapperSqlServerCollection.AddLogging(configureLogging);
|
AddCommonServices(dapperSqlServerCollection, configureLogging);
|
||||||
dapperSqlServerCollection.AddDapperRepositories(SelfHosted);
|
dapperSqlServerCollection.AddDapperRepositories(SelfHosted);
|
||||||
var globalSettings = new GlobalSettings
|
var globalSettings = new GlobalSettings
|
||||||
{
|
{
|
||||||
@ -65,19 +68,35 @@ public class DatabaseDataAttribute : DataAttribute
|
|||||||
dapperSqlServerCollection.AddSingleton(globalSettings);
|
dapperSqlServerCollection.AddSingleton(globalSettings);
|
||||||
dapperSqlServerCollection.AddSingleton<IGlobalSettings>(globalSettings);
|
dapperSqlServerCollection.AddSingleton<IGlobalSettings>(globalSettings);
|
||||||
dapperSqlServerCollection.AddSingleton(database);
|
dapperSqlServerCollection.AddSingleton(database);
|
||||||
dapperSqlServerCollection.AddDataProtection();
|
dapperSqlServerCollection.AddDistributedSqlServerCache((o) =>
|
||||||
|
{
|
||||||
|
o.ConnectionString = database.ConnectionString;
|
||||||
|
o.SchemaName = "dbo";
|
||||||
|
o.TableName = "Cache";
|
||||||
|
});
|
||||||
yield return dapperSqlServerCollection.BuildServiceProvider();
|
yield return dapperSqlServerCollection.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var efCollection = new ServiceCollection();
|
var efCollection = new ServiceCollection();
|
||||||
efCollection.AddLogging(configureLogging);
|
AddCommonServices(efCollection, configureLogging);
|
||||||
efCollection.SetupEntityFramework(database.ConnectionString, database.Type);
|
efCollection.SetupEntityFramework(database.ConnectionString, database.Type);
|
||||||
efCollection.AddPasswordManagerEFRepositories(SelfHosted);
|
efCollection.AddPasswordManagerEFRepositories(SelfHosted);
|
||||||
efCollection.AddSingleton(database);
|
efCollection.AddSingleton(database);
|
||||||
efCollection.AddDataProtection();
|
efCollection.AddSingleton<IDistributedCache, EntityFrameworkCache>();
|
||||||
yield return efCollection.BuildServiceProvider();
|
yield return efCollection.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AddCommonServices(IServiceCollection services, Action<ILoggingBuilder> configureLogging)
|
||||||
|
{
|
||||||
|
services.AddLogging(configureLogging);
|
||||||
|
services.AddDataProtection();
|
||||||
|
|
||||||
|
if (UseFakeTimeProvider)
|
||||||
|
{
|
||||||
|
services.AddSingleton<TimeProvider, FakeTimeProvider>();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
71
test/Infrastructure.IntegrationTest/DistributedCacheTests.cs
Normal file
71
test/Infrastructure.IntegrationTest/DistributedCacheTests.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.IntegrationTest;
|
||||||
|
|
||||||
|
public class DistributedCacheTests
|
||||||
|
{
|
||||||
|
[DatabaseTheory, DatabaseData(UseFakeTimeProvider = true)]
|
||||||
|
public async Task Simple_NotExpiredItem_StartsScan(IDistributedCache cache, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
if (cache is not EntityFrameworkCache efCache)
|
||||||
|
{
|
||||||
|
// We don't write the SqlServer cache implementation so we don't need to test it
|
||||||
|
// also it doesn't use TimeProvider under the hood so we'd have to delay the test
|
||||||
|
// for 30 minutes to get it to work. So just skip it.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fakeTimeProvider = (FakeTimeProvider)timeProvider;
|
||||||
|
|
||||||
|
cache.Set("test-key", "some-value"u8.ToArray(), new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
SlidingExpiration = TimeSpan.FromMinutes(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should have expired and not be returned
|
||||||
|
var firstValue = cache.Get("test-key");
|
||||||
|
|
||||||
|
// Scan for expired items is supposed to run every 30 minutes
|
||||||
|
fakeTimeProvider.Advance(TimeSpan.FromMinutes(31));
|
||||||
|
|
||||||
|
var secondValue = cache.Get("test-key");
|
||||||
|
|
||||||
|
// This should have forced the EF cache to start a scan task
|
||||||
|
Assert.NotNull(efCache.scanTask);
|
||||||
|
// We don't want the scan task to throw an exception, unwrap it.
|
||||||
|
await efCache.scanTask;
|
||||||
|
|
||||||
|
Assert.NotNull(firstValue);
|
||||||
|
Assert.Null(secondValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData(UseFakeTimeProvider = true)]
|
||||||
|
public async Task ParallelReadsAndWrites_Work(IDistributedCache cache, TimeProvider timeProvider)
|
||||||
|
{
|
||||||
|
var fakeTimeProvider = (FakeTimeProvider)timeProvider;
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(Enumerable.Range(1, 100), async (index, _) =>
|
||||||
|
{
|
||||||
|
await cache.SetAsync($"test-{index}", "some-value"u8.ToArray(), new DistributedCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(index),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Parallel.ForEachAsync(Enumerable.Range(1, 100), async (index, _) =>
|
||||||
|
{
|
||||||
|
var value = await cache.GetAsync($"test-{index}");
|
||||||
|
Assert.NotNull(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task MultipleWritesOnSameKey_ShouldNotThrow(IDistributedCache cache)
|
||||||
|
{
|
||||||
|
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
|
||||||
|
await cache.SetAsync("test-duplicate", "some-value"u8.ToArray());
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.6.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
|
BIN
test/Infrastructure.IntegrationTest/test.db
Normal file
BIN
test/Infrastructure.IntegrationTest/test.db
Normal file
Binary file not shown.
15
util/Migrator/DbScripts/2024-07-01_00_DistributedCache.sql
Normal file
15
util/Migrator/DbScripts/2024-07-01_00_DistributedCache.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
IF OBJECT_ID('[dbo].[Cache]') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[Cache] (
|
||||||
|
[Id] [nvarchar](449) NOT NULL,
|
||||||
|
[Value] [varbinary](max) NOT NULL,
|
||||||
|
[ExpiresAtTime] [datetimeoffset](7) NOT NULL,
|
||||||
|
[SlidingExpirationInSeconds] [bigint] NULL,
|
||||||
|
[AbsoluteExpiration] [datetimeoffset](7) NULL,
|
||||||
|
CONSTRAINT [PK_Cache] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_Cache_ExpiresAtTime]
|
||||||
|
ON [dbo].[Cache]([ExpiresAtTime] ASC);
|
||||||
|
END
|
||||||
|
GO
|
2671
util/MySqlMigrations/Migrations/20240702142224_DistributedCache.Designer.cs
generated
Normal file
2671
util/MySqlMigrations/Migrations/20240702142224_DistributedCache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DistributedCache : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Cache",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "varchar(449)", maxLength: 449, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Value = table.Column<byte[]>(type: "longblob", nullable: true),
|
||||||
|
ExpiresAtTime = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
SlidingExpirationInSeconds = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
AbsoluteExpiration = table.Column<DateTime>(type: "datetime(6)", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Cache", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cache_ExpiresAtTime",
|
||||||
|
table: "Cache",
|
||||||
|
column: "ExpiresAtTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Cache");
|
||||||
|
}
|
||||||
|
}
|
@ -755,6 +755,33 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.ToTable("ProviderPlan", (string)null);
|
b.ToTable("ProviderPlan", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("varchar(449)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AbsoluteExpiration")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAtTime")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<long?>("SlidingExpirationInSeconds")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Value")
|
||||||
|
.HasColumnType("longblob");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("ExpiresAtTime")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("Cache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
2678
util/PostgresMigrations/Migrations/20240702142233_DistributedCache.Designer.cs
generated
Normal file
2678
util/PostgresMigrations/Migrations/20240702142233_DistributedCache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DistributedCache : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Cache",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "character varying(449)", maxLength: 449, nullable: false),
|
||||||
|
Value = table.Column<byte[]>(type: "bytea", nullable: true),
|
||||||
|
ExpiresAtTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
SlidingExpirationInSeconds = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
AbsoluteExpiration = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Cache", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cache_ExpiresAtTime",
|
||||||
|
table: "Cache",
|
||||||
|
column: "ExpiresAtTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Cache");
|
||||||
|
}
|
||||||
|
}
|
@ -760,6 +760,33 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.ToTable("ProviderPlan", (string)null);
|
b.ToTable("ProviderPlan", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("character varying(449)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AbsoluteExpiration")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAtTime")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<long?>("SlidingExpirationInSeconds")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Value")
|
||||||
|
.HasColumnType("bytea");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("ExpiresAtTime")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("Cache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
2660
util/SqliteMigrations/Migrations/20240702142228_DistributedCache.Designer.cs
generated
Normal file
2660
util/SqliteMigrations/Migrations/20240702142228_DistributedCache.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class DistributedCache : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Cache",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "TEXT", maxLength: 449, nullable: false),
|
||||||
|
Value = table.Column<byte[]>(type: "BLOB", nullable: true),
|
||||||
|
ExpiresAtTime = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
SlidingExpirationInSeconds = table.Column<long>(type: "INTEGER", nullable: true),
|
||||||
|
AbsoluteExpiration = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Cache", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Cache_ExpiresAtTime",
|
||||||
|
table: "Cache",
|
||||||
|
column: "ExpiresAtTime");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Cache");
|
||||||
|
}
|
||||||
|
}
|
@ -744,6 +744,33 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.ToTable("ProviderPlan", (string)null);
|
b.ToTable("ProviderPlan", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(449)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("AbsoluteExpiration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAtTime")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long?>("SlidingExpirationInSeconds")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<byte[]>("Value")
|
||||||
|
.HasColumnType("BLOB");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("ExpiresAtTime")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("Cache", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
|
Loading…
Reference in New Issue
Block a user