diff options
author | vman <public@vaughnnugent.com> | 2022-12-09 13:54:16 -0500 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-12-09 13:54:16 -0500 |
commit | 8b5f3eebb9f8d9bd55e922a809ffa3bd52e33401 (patch) | |
tree | 024fcdd2445b2fe37fc96d2870879d0f6aa5626f /VNLib.Plugins.Extensions.Data | |
parent | c9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 (diff) |
Sql essentials classes moved, secret loading updates
Diffstat (limited to 'VNLib.Plugins.Extensions.Data')
-rw-r--r-- | VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs | 41 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/SQL/SqlColumnNameAttribute.cs (renamed from VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs) | 21 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/SQL/SqlTableNameAttribute.cs | 40 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs | 16 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/SQL/TableManager.cs | 5 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageContext.cs | 48 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs | 72 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageEntry.cs | 44 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs | 390 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs | 8 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs | 7 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/TransactionalDbContext.cs | 34 | ||||
-rw-r--r-- | VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj | 1 |
13 files changed, 428 insertions, 299 deletions
diff --git a/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs index e6ee6b1..1f9164e 100644 --- a/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs +++ b/VNLib.Plugins.Extensions.Data/SQL/DbExtensions.cs @@ -44,13 +44,8 @@ namespace VNLib.Plugins.Extensions.Data.SQL /* * Object rental for propery dictionaries used for custom result objects */ - private static readonly ObjectRental<Dictionary<string, PropertyInfo>> DictStore; - - static DbExtensions() - { - //Setup dict store - DictStore = ObjectRental.Create<Dictionary<string, PropertyInfo>>(null, static dict => dict.Clear(), 20); - } + private static ObjectRental<Dictionary<string, PropertyInfo>> DictStore { get; } = ObjectRental.Create<Dictionary<string, PropertyInfo>>(null, static dict => dict.Clear(), 20); + /// <summary> /// Creates a new <see cref="DbParameter"/> configured for <see cref="ParameterDirection.Input"/> with the specified value @@ -225,7 +220,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objectType.GetProperties()) { //try to get the column name attribute of the propery - SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true); //Attribute is valid and coumn name is not empty if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) { @@ -245,7 +240,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (DbColumn col in columns) { //Get the propery if its specified by its column-name attribute - if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop)) + if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop)) { //make sure the column has a value if (col.ColumnOrdinal.HasValue) @@ -288,7 +283,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objectType.GetProperties()) { //try to get the column name attribute of the propery - SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true); //Attribute is valid and coumn name is not empty if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) { @@ -308,7 +303,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (DbColumn col in columns) { //Get the propery if its specified by its column-name attribute - if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop)) + if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop)) { //make sure the column has a value if (col.ColumnOrdinal.HasValue) @@ -335,7 +330,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// <typeparam name="T"></typeparam> /// <param name="reader"></param> /// <returns>The created object, or default if no rows are available</returns> - public static T GetFirstObject<T>(this DbDataReader reader) where T : new() + public static T? GetFirstObject<T>(this DbDataReader reader) where T : new() { //make sure its worth collecting object meta if (!reader.HasRows) @@ -355,7 +350,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objectType.GetProperties()) { //try to get the column name attribute of the propery - SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true); //Attribute is valid and coumn name is not empty if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) { @@ -369,7 +364,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (DbColumn col in columns) { //Get the propery if its specified by its column-name attribute - if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue) + if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue) { //Get the object object val = reader.GetValue(col.ColumnOrdinal.Value); @@ -390,7 +385,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// <typeparam name="T"></typeparam> /// <param name="reader"></param> /// <returns>The created object, or default if no rows are available</returns> - public static async Task<T> GetFirstObjectAsync<T>(this DbDataReader reader) where T : new() + public static async Task<T?> GetFirstObjectAsync<T>(this DbDataReader reader) where T : new() { //Read if (await reader.ReadAsync()) @@ -405,7 +400,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objectType.GetProperties()) { //try to get the column name attribute of the propery - SqlColumnName colAtt = prop.GetCustomAttribute<SqlColumnName>(true); + SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute<SqlColumnNameAttribute>(true); //Attribute is valid and coumn name is not empty if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) { @@ -419,7 +414,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (DbColumn col in columns) { //Get the propery if its specified by its column-name attribute - if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo prop) && col.ColumnOrdinal.HasValue) + if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue) { //Get the object object val = reader.GetValue(col.ColumnOrdinal.Value); @@ -436,11 +431,11 @@ namespace VNLib.Plugins.Extensions.Data.SQL } /// <summary> /// Executes a nonquery operation with the specified command using the object properties set with the - /// <see cref="SqlVariable"/> attributes + /// <see cref="SqlVariableAttribute"/> attributes /// </summary> /// <typeparam name="T"></typeparam> /// <param name="cmd"></param> - /// <param name="obj">The object containing the <see cref="SqlVariable"/> properties to write to command variables</param> + /// <param name="obj">The object containing the <see cref="SqlVariableAttribute"/> properties to write to command variables</param> /// <returns>The number of rows affected</returns> /// <exception cref="TypeLoadException"></exception> /// <exception cref="ArgumentException"></exception> @@ -460,7 +455,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objtype.GetProperties()) { //try to get the variable attribute of the propery - SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true); + SqlVariableAttribute varprops = prop.GetCustomAttribute<SqlVariableAttribute>(true); //This property is an sql variable, so lets add it if (varprops == null) { @@ -470,7 +465,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) { //Add the parameter to the command list - cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction; + cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction; } } //Prepare the sql statement @@ -504,7 +499,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL foreach (PropertyInfo prop in objtype.GetProperties()) { //try to get the variable attribute of the propery - SqlVariable varprops = prop.GetCustomAttribute<SqlVariable>(true); + SqlVariableAttribute? varprops = prop.GetCustomAttribute<SqlVariableAttribute>(true); //This property is an sql variable, so lets add it if (varprops == null) { @@ -514,7 +509,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) { //Add the parameter to the command list - cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.Nullable).Direction = varprops.Direction; + cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction; } } //Prepare the sql statement diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnNameAttribute.cs index 0039fb5..c18dab9 100644 --- a/VNLib.Plugins.Extensions.Data/SQL/SqlColumnName.cs +++ b/VNLib.Plugins.Extensions.Data/SQL/SqlColumnNameAttribute.cs @@ -3,9 +3,9 @@ * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data -* File: SqlColumnName.cs +* File: SqlColumnNameAttribute.cs * -* SqlColumnName.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* SqlColumnNameAttribute.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify @@ -30,12 +30,12 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// Property attribute that specifies the property represents an SQL column in the database /// </summary> [AttributeUsage(AttributeTargets.Property)] - public class SqlColumnName : Attribute + public sealed class SqlColumnNameAttribute : Attribute { public bool Nullable { get; } public bool Unique { get; } public bool PrimaryKey { get; } - public string ColumnName { get; init; } + public string ColumnName { get; } /// <summary> /// Specifies the property is an SQL column name /// </summary> @@ -43,7 +43,7 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// <param name="primaryKey"></param> /// <param name="nullable"></param> /// <param name="unique"></param> - public SqlColumnName(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false) + public SqlColumnNameAttribute(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false) { this.ColumnName = columnName; this.PrimaryKey = primaryKey; @@ -51,15 +51,4 @@ namespace VNLib.Plugins.Extensions.Data.SQL this.Unique = unique; } } - - /// <summary> - /// Allows a type to declare itself as a <see cref="System.Data.DataTable"/> with the specified name - /// </summary> - [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)] - public class SqlTableName : Attribute - { - public string TableName { get; } - - public SqlTableName(string tableName) => TableName = tableName; - } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlTableNameAttribute.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlTableNameAttribute.cs new file mode 100644 index 0000000..9c870ea --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/SQL/SqlTableNameAttribute.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: SqlColumnName.cs +* +* SqlColumnName.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Data.SQL +{ + + /// <summary> + /// Allows a type to declare itself as a <see cref="System.Data.DataTable"/> with the specified name + /// </summary> + [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)] + public sealed class SqlTableNameAttribute : Attribute + { + public string TableName { get; } + + public SqlTableNameAttribute(string tableName) => TableName = tableName; + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs index d33854a..b18d27b 100644 --- a/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs +++ b/VNLib.Plugins.Extensions.Data/SQL/SqlVariable.cs @@ -31,13 +31,13 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// Property attribute that specifies the property is to be used for a given command variable /// </summary> [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] - public class SqlVariable : Attribute + public sealed class SqlVariableAttribute : Attribute { - public string VariableName { get; init; } - public DbType DataType { get; init; } - public ParameterDirection Direction { get; init; } - public int Size { get; init; } - public bool Nullable { get; init; } + public string VariableName { get; } + public DbType DataType { get; } + public ParameterDirection Direction { get; } + public int Size { get; } + public bool IsNullable { get; } /// <summary> /// Specifies the property to be used as an SQL variable /// </summary> @@ -46,13 +46,13 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// <param name="direction">Data direction during execution</param> /// <param name="size">Column size</param> /// <param name="isNullable">Is this property allowed to be null</param> - public SqlVariable(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable) + public SqlVariableAttribute(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable) { this.VariableName = variableName; this.DataType = dataType; this.Direction = direction; this.Size = size; - this.Nullable = isNullable; + this.IsNullable = isNullable; } } } diff --git a/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs index 14c4e64..a7f7873 100644 --- a/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs +++ b/VNLib.Plugins.Extensions.Data/SQL/TableManager.cs @@ -43,12 +43,13 @@ namespace VNLib.Plugins.Extensions.Data.SQL /// </summary> protected string TableName { get; } - public TableManager(Func<DbConnection> factory, string tableName) + protected TableManager(Func<DbConnection> factory, string tableName) { this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); this.TableName = !string.IsNullOrWhiteSpace(tableName) ? tableName : throw new ArgumentNullException(nameof(tableName)); } - public TableManager(Func<DbConnection> factory) + + protected TableManager(Func<DbConnection> factory) { this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); this.TableName = ""; diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageContext.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageContext.cs new file mode 100644 index 0000000..d7f6e29 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageContext.cs @@ -0,0 +1,48 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageContext.cs +* +* LWStorageContext.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using Microsoft.EntityFrameworkCore; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ +#nullable disable + internal sealed class LWStorageContext : TransactionalDbContext + { + private readonly string TableName; + public DbSet<LWStorageEntry> Descriptors { get; set; } + + public LWStorageContext(DbContextOptions options, string tableName) + :base(options) + { + TableName = tableName; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //Set table name + modelBuilder.Entity<LWStorageEntry>() + .ToTable(TableName); + } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs index 3766a97..72665f3 100644 --- a/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs @@ -26,13 +26,15 @@ using System; using System.IO; using System.Text.Json; using System.Collections; +using System.IO.Compression; using System.Threading.Tasks; using System.Collections.Generic; +using System.Text.Json.Serialization; using VNLib.Utils; using VNLib.Utils.Async; using VNLib.Utils.Extensions; -using System.Text.Json.Serialization; +using VNLib.Utils.Memory; namespace VNLib.Plugins.Extensions.Data.Storage { @@ -43,7 +45,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string> { - public static readonly JsonSerializerOptions SerializerOptions = new() + private static readonly JsonSerializerOptions SerializerOptions = new() { DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, NumberHandling = JsonNumberHandling.Strict, @@ -54,25 +56,28 @@ namespace VNLib.Plugins.Extensions.Data.Storage DefaultBufferSize = Environment.SystemPageSize, }; - private Dictionary<string, string> StringStorage; + + internal LWStorageEntry Entry { get; } + + private readonly Lazy<Dictionary<string, string>> StringStorage; /// <summary> /// The currnt descriptor's identifier string within its backing table. Usually the primary key. /// </summary> - public string DescriptorID { get; init; } + public string DescriptorID => Entry.Id; /// <summary> /// The identifier of the user for which this descriptor belongs to /// </summary> - public string UserID { get; init; } + public string UserID => Entry.UserId!; /// <summary> /// The <see cref="DateTime"/> when the descriptor was created /// </summary> - public DateTimeOffset Created { get; init; } + public DateTimeOffset Created => Entry.Created; /// <summary> /// The last time this descriptor was updated /// </summary> - public DateTimeOffset LastModified { get; init; } - + public DateTimeOffset LastModified => Entry.LastModified; + ///<inheritdoc/> protected override AsyncUpdateCallback UpdateCb { get; } ///<inheritdoc/> @@ -80,23 +85,31 @@ namespace VNLib.Plugins.Extensions.Data.Storage ///<inheritdoc/> protected override JsonSerializerOptions JSO => SerializerOptions; - internal LWStorageDescriptor(LWStorageManager manager) + internal LWStorageDescriptor(LWStorageManager manager, LWStorageEntry entry) { + Entry = entry; UpdateCb = manager.UpdateDescriptorAsync; DeleteCb = manager.RemoveDescriptorAsync; + StringStorage = new(OnStringStoreLoad); } - internal async ValueTask PrepareAsync(Stream data) + internal Dictionary<string, string> OnStringStoreLoad() { - try + if(Entry.Data == null || Entry.Data.Length == 0) + { + return new(StringComparer.OrdinalIgnoreCase); + } + else { - //Deserialze async - StringStorage = await VnEncoding.JSONDeserializeFromBinaryAsync<Dictionary<string,string>>(data, SerializerOptions); + //Calc and alloc decode buffer + int bufferSize = (int)(Entry.Data.Length * 1.75); + using UnsafeMemoryHandle<byte> decodeBuffer = Memory.UnsafeAlloc<byte>(bufferSize); + + //Decode and deserialize the data + return BrotliDecoder.TryDecompress(Entry.Data, decodeBuffer, out int written) + ? JsonSerializer.Deserialize<Dictionary<string, string>>(Entry.Data, SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase) + : throw new InvalidDataException("Failed to decompress data"); } - //Ignore a json exceton, a new store will be generated - catch (JsonException) - { } - StringStorage ??= new(); } /// <inheritdoc/> @@ -106,8 +119,9 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="ObjectDisposedException"></exception> public T? GetObject<T>(string key) { + Check(); //De-serialize and return object - return StringStorage.TryGetValue(key, out string? val) ? val.AsJsonObject<T>(SerializerOptions) : default; + return StringStorage.Value.TryGetValue(key, out string? val) ? val.AsJsonObject<T>(SerializerOptions) : default; } /// <inheritdoc/> @@ -140,7 +154,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage public string GetStringValue(string key) { Check(); - return StringStorage.TryGetValue(key, out string? val) ? val : string.Empty; + return StringStorage.Value.TryGetValue(key, out string? val) ? val : string.Empty; } /// <summary> @@ -161,13 +175,13 @@ namespace VNLib.Plugins.Extensions.Data.Storage if (string.IsNullOrWhiteSpace(value)) { //If the value is null and properies exist, remove the entry - StringStorage.Remove(key); + StringStorage.Value.Remove(key); Modified |= true; } else { //Set the value - StringStorage[key] = value; + StringStorage.Value[key] = value; //Set modified flag Modified |= true; } @@ -195,10 +209,22 @@ namespace VNLib.Plugins.Extensions.Data.Storage Check(); return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; } + + ///<inheritdoc/> + public override async ValueTask ReleaseAsync() + { + await base.ReleaseAsync(); + //Cleanup dict on exit + if (StringStorage.IsValueCreated) + { + StringStorage.Value.Clear(); + } + } + ///<inheritdoc/> - public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => StringStorage.GetEnumerator(); + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => StringStorage.Value.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); ///<inheritdoc/> - protected override object GetResource() => StringStorage; + protected override object GetResource() => StringStorage.Value; } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageEntry.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageEntry.cs new file mode 100644 index 0000000..5c5da61 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageEntry.cs @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LwStorageEntry.cs +* +* LwStorageEntry.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Plugins.Extensions.Data.Abstractions; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + internal sealed class LWStorageEntry : DbModelBase, IUserEntity + { + public override string Id { get; set; } + + public override DateTime Created { get; set; } + + public override DateTime LastModified { get; set; } + + public string? UserId { get; set; } + + public byte[]? Data { get; set; } + } +}
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs index 63d41af..027fa90 100644 --- a/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs @@ -25,13 +25,16 @@ using System; using System.IO; using System.Data; +using System.Linq; using System.Threading; -using System.Data.Common; +using System.IO.Compression; using System.Threading.Tasks; -using VNLib.Utils; +using Microsoft.EntityFrameworkCore; -using VNLib.Plugins.Extensions.Data.SQL; +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Memory; namespace VNLib.Plugins.Extensions.Data.Storage { @@ -39,73 +42,29 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <summary> /// Provides single table database object storage services /// </summary> - public sealed class LWStorageManager : EnumerableTable<LWStorageDescriptor> - { - const int DTO_SIZE = 7; - const int MAX_DATA_SIZE = 8000; - - //Mssql statments - private const string GET_DESCRIPTOR_STATMENT_ID_MSQQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id;"; - private const string GET_DESCRIPTOR_STATMENT_UID_MSQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID;"; - - private const string GET_DESCRIPTOR_STATMENT_ID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id\r\nLIMIT 1;"; - private const string GET_DESCRIPTOR_STATMENT_UID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID\r\nLIMIT 1;"; - - private const string CREATE_DESCRIPTOR_STATMENT = "INSERT INTO @table\r\n(UserID,Id,Created,LastModified)\r\nVALUES (@UserID,@Id,@Created,@LastModified);"; - - private const string UPDATE_DESCRIPTOR_STATMENT = "UPDATE @table\r\nSET [Data]=@Data\r\n,[LastModified]=@LastModified\r\nWHERE Id=@Id;"; - private const string REMOVE_DESCRIPTOR_STATMENT = "DELETE FROM @table\r\nWHERE Id=@Id"; - private const string CLEANUP_STATEMENT = "DELETE FROM @table\r\nWHERE [created]<@timeout;"; - private const string ENUMERATION_STATMENT = "SELECT [Id],\r\n[UserID],\r\n[Data],\r\n[LastModified],\r\n[Created]\r\nFROM @table;"; - - private readonly string GetFromUD; - private readonly string Cleanup; - private readonly int keySize; - + public sealed class LWStorageManager + { /// <summary> /// The generator function that is invoked when a new <see cref="LWStorageDescriptor"/> is to /// be created without an explicit id /// </summary> public Func<string> NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N"); + private readonly DbContextOptions DbOptions; + private readonly string TableName; + + private LWStorageContext GetContext() => new(DbOptions, TableName); + /// <summary> /// Creates a new <see cref="LWStorageManager"/> with /// </summary> - /// <param name="factory">A <see cref="DbConnection"/> factory function that will generate and open connections to a database</param> + /// <param name="options">The db context options to create database connections with</param> /// <param name="tableName">The name of the table to operate on</param> - /// <param name="pkCharSize">The maximum number of characters of the DescriptorID and </param> - /// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> - public LWStorageManager(Func<DbConnection> factory, string tableName, int pkCharSize) : base(factory, tableName) + public LWStorageManager(DbContextOptions options, string tableName) { - //Compile statments with specified tableid - Insert = CREATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); - - //Test connector type to compile MSSQL statments vs Sqlite/Mysql - using (DbConnection testConnection = GetConnection()) - { - //Determine if MSSql connections are being used - bool isMsSql = testConnection.GetType().FullName!.Contains("SqlConnection", StringComparison.OrdinalIgnoreCase); - - if (isMsSql) - { - GetFromUD = GET_DESCRIPTOR_STATMENT_UID_MSQL.Replace("@table", tableName); - Select = GET_DESCRIPTOR_STATMENT_ID_MSQQL.Replace("@table", tableName); - } - else - { - Select = GET_DESCRIPTOR_STATMENT_ID.Replace("@table", tableName); - GetFromUD = GET_DESCRIPTOR_STATMENT_UID.Replace("@table", tableName); - } - } - - Update = UPDATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); - Delete = REMOVE_DESCRIPTOR_STATMENT.Replace("@table", tableName); - Cleanup = CLEANUP_STATEMENT.Replace("@table", tableName); - //Set key size - keySize = pkCharSize; - //Set default generator - Enumerate = ENUMERATION_STATMENT.Replace("@table", tableName); + DbOptions = options ?? throw new ArgumentNullException(nameof(options)); + TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); } /// <summary> @@ -123,44 +82,52 @@ namespace VNLib.Plugins.Extensions.Data.Storage { throw new ArgumentNullException(nameof(userId)); } + //If no override id was specified, generate a new one descriptorIdOverride ??= NewDescriptorIdGenerator(); - //Set created time - DateTimeOffset now = DateTimeOffset.UtcNow; - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(cancellation); - //Setup transaction with repeatable read iso level - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); - //Create command for text command - await using DbCommand cmd = Database.CreateTextCommand(Insert, transaction); - //add parameters - _ = cmd.AddParameter("@Id", descriptorIdOverride, DbType.String, keySize); - _ = cmd.AddParameter("@UserID", userId, DbType.String, keySize); - _ = cmd.AddParameter("@Created", now, DbType.DateTimeOffset, DTO_SIZE); - _ = cmd.AddParameter("@LastModified", now, DbType.DateTimeOffset, DTO_SIZE); - //Prepare operation - await cmd.PrepareAsync(cancellation); - //Exec and if successful will return > 0, so we can properly return a descriptor - int result = await cmd.ExecuteNonQueryAsync(cancellation); - //Commit transaction - await transaction.CommitAsync(cancellation); - if (result <= 0) + + DateTime createdOrModifedTime = DateTime.UtcNow; + + await using LWStorageContext ctx = GetContext(); + await ctx.OpenTransactionAsync(cancellation); + + //Make sure the descriptor doesnt exist only by its descriptor id + if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation)) { - throw new LWDescriptorCreationException("Failed to create the new descriptor because the database retuned an invalid update row count"); + throw new LWDescriptorCreationException($"A descriptor with id {descriptorIdOverride} already exists"); } - //Rent new descriptor - LWStorageDescriptor desciptor = new(this) + + //Cache time + DateTime now = DateTime.UtcNow; + + //Create the new descriptor + LWStorageEntry entry = new() { - DescriptorID = descriptorIdOverride, - UserID = userId, Created = now, - LastModified = now + LastModified = now, + Id = descriptorIdOverride, + UserId = userId, }; - //Set data to null - await desciptor.PrepareAsync(null); - return desciptor; + + //Add and save changes + ctx.Descriptors.Add(entry); + + ERRNO result = await ctx.SaveChangesAsync(cancellation); + + if (!result) + { + //Rollback and raise exception + await ctx.RollbackTransctionAsync(cancellation); + throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved"); + } + else + { + //Commit transaction and return the new descriptor + await ctx.CommitTransactionAsync(cancellation); + return new LWStorageDescriptor(this, entry); + } } + /// <summary> /// Attempts to retrieve <see cref="LWStorageDescriptor"/> for a given user-id. The caller is responsible for /// consitancy state of the descriptor @@ -171,45 +138,41 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="ArgumentNullException"></exception> public async Task<LWStorageDescriptor?> GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default) { + //Allow null/empty entrys to just return null if (string.IsNullOrWhiteSpace(userid)) { throw new ArgumentNullException(nameof(userid)); } - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(cancellation); - //Setup transaction with repeatable read iso level - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); - //Create a new command based on the command text - await using DbCommand cmd = Database.CreateTextCommand(GetFromUD, transaction); - //Add userid parameter - _ = cmd.AddParameter("@UserID", userid, DbType.String, keySize); - //Prepare operation - await cmd.PrepareAsync(cancellation); - //Get the reader - DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); - try + + //Init db + await using LWStorageContext db = GetContext(); + //Begin transaction + await db.OpenTransactionAsync(cancellation); + //Get entry + LWStorageEntry? entry = await (from s in db.Descriptors + where s.UserId == userid + select s) + .SingleOrDefaultAsync(cancellation); + + //Close transactions and return + if (entry == null) { - //Make sure the record was found - if (!await reader.ReadAsync(cancellation)) - { - return null; - } - return await GetItemAsync(reader, CancellationToken.None); + await db.RollbackTransctionAsync(cancellation); + return null; } - finally + else { - //Close the reader - await reader.CloseAsync(); - //Commit the transaction - await transaction.CommitAsync(cancellation); + await db.CommitTransactionAsync(cancellation); + return new (this, entry); } } + /// <summary> /// Attempts to retrieve the <see cref="LWStorageDescriptor"/> for the given descriptor id. The caller is responsible for /// consitancy state of the descriptor /// </summary> /// <param name="descriptorId">Unique identifier for the descriptor</param> + /// <param name="cancellation">A token to cancel the opreeaiton</param> /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns> /// <exception cref="ArgumentNullException"></exception> public async Task<LWStorageDescriptor?> GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default) @@ -219,35 +182,30 @@ namespace VNLib.Plugins.Extensions.Data.Storage { throw new ArgumentNullException(nameof(descriptorId)); } - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(cancellation); - //Setup transaction with repeatable read iso level - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); - //We dont have the routine stored - await using DbCommand cmd = Database.CreateTextCommand(Select, transaction); - //Set userid (unicode length) - _ = cmd.AddParameter("@Id", descriptorId, DbType.String, keySize); - //Prepare operation - await cmd.PrepareAsync(cancellation); - //Get the reader - DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); - try + + //Init db + await using LWStorageContext db = GetContext(); + //Begin transaction + await db.OpenTransactionAsync(cancellation); + //Get entry + LWStorageEntry? entry = await (from s in db.Descriptors + where s.Id == descriptorId + select s) + .SingleOrDefaultAsync(cancellation); + + //Close transactions and return + if (entry == null) { - if (!await reader.ReadAsync(cancellation)) - { - return null; - } - return await GetItemAsync(reader, CancellationToken.None); + await db.RollbackTransctionAsync(cancellation); + return null; } - finally + else { - //Close the reader - await reader.CloseAsync(); - //Commit the transaction - await transaction.CommitAsync(cancellation); + await db.CommitTransactionAsync(cancellation); + return new (this, entry); } } + /// <summary> /// Cleanup entries before the specified <see cref="TimeSpan"/>. Entires are store in UTC time /// </summary> @@ -255,6 +213,7 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <param name="cancellation">A token to cancel the operation</param> /// <returns>The number of entires cleaned</returns>S public Task<ERRNO> CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation); + /// <summary> /// Cleanup entries before the specified <see cref="DateTime"/>. Entires are store in UTC time /// </summary> @@ -263,21 +222,27 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <returns>The number of entires cleaned</returns> public async Task<ERRNO> CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default) { - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(cancellation); - //Begin a new transaction - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); - //Setup the cleanup command for the current database - await using DbCommand cmd = Database.CreateTextCommand(Cleanup, transaction); - //Setup timeout parameter as a datetime - cmd.AddParameter("@timeout", compareTime, DbType.DateTime); - await cmd.PrepareAsync(cancellation); - //Exec and if successful will return > 0, so we can properly return a descriptor - int result = await cmd.ExecuteNonQueryAsync(cancellation); + //Init db + await using LWStorageContext db = GetContext(); + //Begin transaction + await db.OpenTransactionAsync(cancellation); + + //Get all expired entires + LWStorageEntry[] expired = await (from s in db.Descriptors + where s.Created < compareTime + select s) + .ToArrayAsync(cancellation); + + //Delete + db.Descriptors.RemoveRange(expired); + + //Save changes + ERRNO count = await db.SaveChangesAsync(cancellation); + //Commit transaction - await transaction.CommitAsync(cancellation); - return result; + await db.CommitTransactionAsync(cancellation); + + return count; } /// <summary> @@ -288,38 +253,55 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="LWStorageUpdateFailedException"></exception> internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data) { - LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; - int result = 0; + LWStorageEntry entry = (descriptorObj as LWStorageDescriptor)!.Entry; + ERRNO result = 0; try { - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(); - //Setup transaction with repeatable read iso level - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); - //Create command for stored procedure - await using DbCommand cmd = Database.CreateTextCommand(Update, transaction); - //Add parameters - _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); - _ = cmd.AddParameter("@Data", data, DbType.Binary, MAX_DATA_SIZE); - _ = cmd.AddParameter("@LastModified", DateTime.UtcNow, DbType.DateTime2, DTO_SIZE); - //Prepare operation - await cmd.PrepareAsync(); - //exec and store result - result = await cmd.ExecuteNonQueryAsync(); - //Commit - await transaction.CommitAsync(); + await using LWStorageContext ctx = GetContext(); + await ctx.OpenTransactionAsync(CancellationToken.None); + + //Begin tracking + ctx.Descriptors.Attach(entry); + + //Convert stream to vnstream + VnMemoryStream vms = (VnMemoryStream)data; + using (IMemoryHandle<byte> encBuffer = Memory.SafeAlloc<byte>((int)vms.Length)) + { + //try to compress + if(!BrotliEncoder.TryCompress(vms.AsSpan(), encBuffer.Span, out int compressed)) + { + throw new InvalidDataException("Failed to compress the descriptor data"); + } + //Set the data + entry.Data = encBuffer.Span.ToArray(); + } + //Update modified time + entry.LastModified = DateTime.UtcNow; + + //Save changes + result = await ctx.SaveChangesAsync(CancellationToken.None); + + //Commit or rollback + if (result) + { + await ctx.CommitTransactionAsync(CancellationToken.None); + } + else + { + await ctx.RollbackTransctionAsync(CancellationToken.None); + } } catch (Exception ex) { throw new LWStorageUpdateFailedException("", ex); } //If the result is 0 then the update failed - if (result <= 0) + if (!result) { - throw new LWStorageUpdateFailedException($"Descriptor {descriptor.DescriptorID} failed to update", null); + throw new LWStorageUpdateFailedException($"Descriptor {entry.Id} failed to update"); } } + /// <summary> /// Function to remove the specified descriptor /// </summary> @@ -327,53 +309,39 @@ namespace VNLib.Plugins.Extensions.Data.Storage /// <exception cref="LWStorageRemoveFailedException"></exception> internal async Task RemoveDescriptorAsync(object descriptorObj) { - LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; + LWStorageEntry descriptor = (descriptorObj as LWStorageDescriptor)!.Entry; + ERRNO result; try { - //Open a new sql client - await using DbConnection Database = GetConnection(); - await Database.OpenAsync(); - //Setup transaction with repeatable read iso level - await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); - //Create sql command - await using DbCommand cmd = Database.CreateTextCommand(Delete, transaction); - //set descriptor id - _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); - //Prepare operation - await cmd.PrepareAsync(); - //Execute (the descriptor my already be removed, as long as the transaction doesnt fail we should be okay) - _ = await cmd.ExecuteNonQueryAsync(); - //Commit - await transaction.CommitAsync(); + //Init db + await using LWStorageContext db = GetContext(); + //Begin transaction + await db.OpenTransactionAsync(); + + //Delete the user from the database + db.Descriptors.Remove(descriptor); + + //Save changes and commit if successful + result = await db.SaveChangesAsync(); + + if (result) + { + await db.CommitTransactionAsync(); + } + else + { + await db.RollbackTransctionAsync(); + } } catch (Exception ex) { throw new LWStorageRemoveFailedException("", ex); } - } - - ///<inheritdoc/> - protected async override Task<LWStorageDescriptor> GetItemAsync(DbDataReader reader, CancellationToken cancellationToken) - { - //Open binary stream for the data column - await using Stream data = reader.GetStream("Data"); - //Create new descriptor - LWStorageDescriptor desciptor = new(this) + if (!result) { - //Set desctiptor data - DescriptorID = reader.GetString("Id"), - UserID = reader.GetString("UserID"), - Created = reader.GetDateTime("Created"), - LastModified = reader.GetDateTime("LastModified") - }; - //Load the descriptor's data - await desciptor.PrepareAsync(data); - return desciptor; - } - ///<inheritdoc/> - protected override ValueTask CleanupItemAsync(LWStorageDescriptor item, CancellationToken cancellationToken) - { - return item.ReleaseAsync(); + throw new LWStorageRemoveFailedException("Failed to delete the user account because of a database failure, the user may already be deleted"); + } } + } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs index 8e36d6c..806912c 100644 --- a/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs @@ -23,7 +23,7 @@ */ using System; -using VNLib.Utils; +using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Data.Storage { @@ -34,5 +34,11 @@ namespace VNLib.Plugins.Extensions.Data.Storage public class LWStorageRemoveFailedException : ResourceDeleteFailedException { internal LWStorageRemoveFailedException(string error, Exception inner) : base(error, inner) { } + + public LWStorageRemoveFailedException() + {} + + public LWStorageRemoveFailedException(string message) : base(message) + {} } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs index 96ea4eb..fe555bf 100644 --- a/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs @@ -23,7 +23,7 @@ */ using System; -using VNLib.Utils; +using VNLib.Utils.Resources; namespace VNLib.Plugins.Extensions.Data.Storage { @@ -34,5 +34,10 @@ namespace VNLib.Plugins.Extensions.Data.Storage public class LWStorageUpdateFailedException : ResourceUpdateFailedException { internal LWStorageUpdateFailedException(string error, Exception inner) : base(error, inner) { } + + public LWStorageUpdateFailedException() + {} + public LWStorageUpdateFailedException(string message) : base(message) + {} } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/TransactionalDbContext.cs b/VNLib.Plugins.Extensions.Data/TransactionalDbContext.cs index 1320dc1..6b835c5 100644 --- a/VNLib.Plugins.Extensions.Data/TransactionalDbContext.cs +++ b/VNLib.Plugins.Extensions.Data/TransactionalDbContext.cs @@ -25,12 +25,13 @@ using System; using System.Threading; using System.Threading.Tasks; + using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; namespace VNLib.Plugins.Extensions.Data { - public abstract class TransactionalDbContext : DbContext, IAsyncDisposable, IDisposable + public abstract class TransactionalDbContext : DbContext, IAsyncDisposable { /// <summary> /// <inheritdoc/> @@ -47,13 +48,28 @@ namespace VNLib.Plugins.Extensions.Data /// The transaction that was opened on the current context /// </summary> public IDbContextTransaction? Transaction { get; set; } + + +#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize, ignore because base.Dispose() is called ///<inheritdoc/> - public override void Dispose() + public sealed override void Dispose() { //dispose the transaction - this.Transaction?.Dispose(); + Transaction?.Dispose(); base.Dispose(); } + + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + //If transaction has been created, dispose the transaction + if (Transaction != null) + { + await Transaction.DisposeAsync(); + } + await base.DisposeAsync(); + } +#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize /// <summary> /// Opens a single transaction on the current context. If a transaction is already open, @@ -78,16 +94,6 @@ namespace VNLib.Plugins.Extensions.Data { return Transaction != null ? Transaction.RollbackAsync(token) : Task.CompletedTask; } - ///<inheritdoc/> - public override async ValueTask DisposeAsync() - { - //If transaction has been created, dispose the transaction - if(this.Transaction != null) - { - await this.Transaction.DisposeAsync(); - } - await base.DisposeAsync(); - GC.SuppressFinalize(this); - } + } }
\ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj index 2a780aa..acca4b3 100644 --- a/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj +++ b/VNLib.Plugins.Extensions.Data/VNLib.Plugins.Extensions.Data.csproj @@ -42,6 +42,7 @@ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.11" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.11" /> </ItemGroup> <ItemGroup> |