diff --git a/Core/Assets/ErrorMessages.resx b/Core/Assets/ErrorMessages.resx index eb587b7..5846f46 100644 --- a/Core/Assets/ErrorMessages.resx +++ b/Core/Assets/ErrorMessages.resx @@ -1,9 +1,10 @@  - + - + @@ -13,10 +14,14 @@ 1.3 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + Tag {0} has itself in Parents, which is invalid. @@ -79,5 +84,5 @@ Tab characters were detected, which is an invalid structure for MusicBee tag hierarchy templates. - + \ No newline at end of file diff --git a/Core/Common/FileTypes.cs b/Core/Common/FileTypes.cs index ac6d419..70cdd06 100644 --- a/Core/Common/FileTypes.cs +++ b/Core/Common/FileTypes.cs @@ -10,7 +10,7 @@ public static class FileTypes /// public static List<(string FileExtension, string Name)> AllNonDatabaseFileTypes => [ - MusicBeeTagHierarchyTemplate, + MusicBeeTagHierarchyTemplate ]; /// diff --git a/Core/Common/TagDatabaseSearchMode.cs b/Core/Common/TagDatabaseSearchMode.cs index 2c53ad5..3b99112 100644 --- a/Core/Common/TagDatabaseSearchMode.cs +++ b/Core/Common/TagDatabaseSearchMode.cs @@ -3,7 +3,7 @@ using TagHierarchyManager.Models; namespace TagHierarchyManager.Common; /// -/// An enum representing the different search modes for the . +/// An enum representing the different search modes for the . /// public enum TagDatabaseSearchMode { @@ -26,5 +26,5 @@ public enum TagDatabaseSearchMode /// /// Represents a search mode where it must match the exact query. /// - ExactMatch = 3, + ExactMatch = 3 } \ No newline at end of file diff --git a/Core/ErrorMessages.cs b/Core/ErrorMessages.cs deleted file mode 100644 index 6d7a630..0000000 --- a/Core/ErrorMessages.cs +++ /dev/null @@ -1,3 +0,0 @@ -using TagHierarchyManager.Models; - -namespace TagHierarchyManager; diff --git a/Core/Exporters/MusicBeeTagHierarchyExporter.cs b/Core/Exporters/MusicBeeTagHierarchyExporter.cs index 0e0bf6c..73b98d4 100644 --- a/Core/Exporters/MusicBeeTagHierarchyExporter.cs +++ b/Core/Exporters/MusicBeeTagHierarchyExporter.cs @@ -18,9 +18,9 @@ public class MusicBeeTagHierarchyExporter : IExporter { ArgumentNullException.ThrowIfNull(db); - List topLevelTags = db.Tags.Where(tag => tag.IsTopLevel).OrderBy(tag => tag.Name).ToList(); + var topLevelTags = db.Tags.Where(tag => tag.IsTopLevel).OrderBy(tag => tag.Name).ToList(); StringBuilder currentString = new(); - foreach (Tag topLevelTag in topLevelTags) + foreach (var topLevelTag in topLevelTags) ProcessRecursively(currentString, db, topLevelTag, string.Empty); return currentString.ToString(); @@ -29,7 +29,7 @@ public class MusicBeeTagHierarchyExporter : IExporter private static void ProcessRecursively(StringBuilder currentString, TagDatabase db, Tag currentTag, string indent = "") { - List tagChildren = db.GetTagChildren(currentTag.Id).OrderBy(tag => tag.Name).ToList(); + var tagChildren = db.GetTagChildren(currentTag.Id).OrderBy(tag => tag.Name).ToList(); if (tagChildren.Count > 0) { currentString.AppendLine(indent + currentTag.Name); @@ -45,13 +45,13 @@ public class MusicBeeTagHierarchyExporter : IExporter ProcessTagBindings(currentTag, indent, currentString); } - foreach (Tag childTag in tagChildren) + foreach (var childTag in tagChildren) ProcessRecursively(currentString, db, childTag, indent); } private static void ProcessTagBindings(Tag currentTag, string indent, StringBuilder builder) { - foreach (string line in + foreach (var line in currentTag.TagBindings.Select(tagBinding => indent + currentTag.Name + $"::{tagBinding}")) builder.AppendLine(line); } diff --git a/Core/Importers/Importer.cs b/Core/Importers/Importer.cs index 7ba5d28..628f680 100644 --- a/Core/Importers/Importer.cs +++ b/Core/Importers/Importer.cs @@ -3,8 +3,9 @@ using TagHierarchyManager.Models; namespace TagHierarchyManager.Importers; /// -/// An abstract class implementing an importer, converting a file in a specific format to a Dictionary of s.
-/// The resulting Dictionary can then be used to fill a on creation. +/// An abstract class implementing an importer, converting a file in a specific format to a Dictionary of +/// s.
+/// The resulting Dictionary can then be used to fill a on creation. ///
public abstract class Importer { @@ -14,7 +15,7 @@ public abstract class Importer protected Importer() { } - + /// /// Gets or sets the name of the format, for use in file dialogs. /// @@ -25,10 +26,13 @@ public abstract class Importer /// Gets all the contained text of a given file, then sends that string to . ///
/// The location of the file to be imported. - /// A representing the asynchronous operation, returning a Dictionary of objects. + /// + /// A representing the asynchronous operation, returning a Dictionary of + /// objects. + /// public async Task> ImportFromFileAsync(string filePath) { - string importedData = await File.ReadAllTextAsync(filePath).ConfigureAwait(false); + var importedData = await File.ReadAllTextAsync(filePath).ConfigureAwait(false); return await this.ProcessDataToDatabaseAsync(importedData).ConfigureAwait(false); } diff --git a/Core/Importers/MusicBeeTagHierarchyImporter.cs b/Core/Importers/MusicBeeTagHierarchyImporter.cs index 235460b..7ae2e34 100644 --- a/Core/Importers/MusicBeeTagHierarchyImporter.cs +++ b/Core/Importers/MusicBeeTagHierarchyImporter.cs @@ -3,8 +3,10 @@ using TagHierarchyManager.Common; using TagHierarchyManager.Models; namespace TagHierarchyManager.Importers; + /// -/// Implements an importer for converting a MusicBee tag hierarchy template to a Dictionary of s. +/// Implements an importer for converting a MusicBee tag hierarchy template to a Dictionary of +/// s. /// /// TODO add manual intervention for tags with duplicate names. public class MusicBeeTagHierarchyImporter : Importer @@ -13,7 +15,7 @@ public class MusicBeeTagHierarchyImporter : Importer private const string TagBindingSeparator = "::"; private static readonly string[] CommentSymbols = [";", "//"]; - + /// /// Initializes a new instance of the class. /// @@ -23,23 +25,26 @@ public class MusicBeeTagHierarchyImporter : Importer } /// - /// Converts a MusicBee tag hierarchy template into a Dictionary of s. + /// Converts a MusicBee tag hierarchy template into a Dictionary of s. /// /// The tag hierarchy data string. - /// A representing the asynchronous operation, returning a Dictionary of s. + /// + /// A representing the asynchronous operation, returning a Dictionary of + /// s. + /// protected override async Task> ProcessDataToDatabaseAsync(string importedData) { importedData = importedData.TrimEnd(); - + ValidateHierarchyData(importedData); - + Dictionary tagsToImport = new(); - - int previousIndentLevel = 0; + + var previousIndentLevel = 0; List parentStack = []; - int lineCounter = 1; + var lineCounter = 1; - string lastTagName = string.Empty; + var lastTagName = string.Empty; using StringReader reader = new(importedData); while (await reader.ReadLineAsync().ConfigureAwait(false) is { } line) { @@ -47,7 +52,7 @@ public class MusicBeeTagHierarchyImporter : Importer TagHierarchyLine parsedLine = new(line, lineCounter); UpdateParentStack(parentStack, parsedLine, previousIndentLevel, lastTagName); - + ImportTag(tagsToImport, parsedLine, parentStack); lastTagName = parsedLine.TagName; @@ -64,6 +69,24 @@ public class MusicBeeTagHierarchyImporter : Importer currentTag.TagBindings.Add(tagBinding); } + private static void ImportTag(Dictionary importDict, TagHierarchyLine line, + List parentStack) + { + var existingTag = importDict.GetValueOrDefault(line.TagName); + if (existingTag is null) + { + ImportedTag newTag = new() + { + Name = line.TagName, + IsTopLevel = parentStack.Count <= 0 + }; + importDict[line.TagName] = newTag; + } + + ProcessParents(importDict[line.TagName], parentStack); + AddTagBindingIfMissing(importDict[line.TagName], line.Binding); + } + private static bool IsEmptyOrComment(string line) { if (CommentSymbols.Any(symbol => line.StartsWith(symbol, StringComparison.Ordinal))) @@ -72,6 +95,19 @@ public class MusicBeeTagHierarchyImporter : Importer return string.IsNullOrEmpty(line) || string.IsNullOrWhiteSpace(line); } + + private static void ProcessParents(ImportedTag tag, List parentStack) + { + if (parentStack.Count == 0) + { + tag.IsTopLevel = true; + return; + } + + var parentName = parentStack[^1]; + if (tag.Name != parentName) tag.Parents.Add(parentName); + } + private static void UpdateParentStack(List parentStack, TagHierarchyLine currentLine, int previousIndent, string parentName) { @@ -97,40 +133,8 @@ public class MusicBeeTagHierarchyImporter : Importer // TODO change on the fly instead of erroring out? if (tagHierarchyData.Contains('\t')) throw new ArgumentException(ErrorMessages.ImporterMusicBeeTabsDetected); - if (tagHierarchyData.StartsWith(' ')) throw new ArgumentException(ErrorMessages.ImporterMusicBeeStartsWithSpace); - } - - private static void ImportTag(Dictionary importDict, TagHierarchyLine line, List parentStack) - { - ImportedTag? existingTag = importDict.GetValueOrDefault(line.TagName); - if (existingTag is null) - { - ImportedTag newTag = new() - { - Name = line.TagName, - IsTopLevel = parentStack.Count <= 0, - }; - importDict[line.TagName] = newTag; - } - - ProcessParents(importDict[line.TagName], parentStack); - AddTagBindingIfMissing(importDict[line.TagName], line.Binding); - } - - - private static void ProcessParents(ImportedTag tag, List parentStack) - { - if (parentStack.Count == 0) - { - tag.IsTopLevel = true; - return; - } - - string parentName = parentStack[^1]; - if (tag.Name != parentName) - { - tag.Parents.Add(parentName); - } + if (tagHierarchyData.StartsWith(' ')) + throw new ArgumentException(ErrorMessages.ImporterMusicBeeStartsWithSpace); } private struct TagHierarchyLine @@ -143,9 +147,9 @@ public class MusicBeeTagHierarchyImporter : Importer public TagHierarchyLine(string line, int lineCounter) { this.LineNumber = lineCounter; - string trimmedLine = line.TrimStart(); + var trimmedLine = line.TrimStart(); - int separatorIndex = trimmedLine.LastIndexOf(TagBindingSeparator, StringComparison.Ordinal); + var separatorIndex = trimmedLine.LastIndexOf(TagBindingSeparator, StringComparison.Ordinal); if (separatorIndex != -1) { this.TagName = trimmedLine[..separatorIndex]; @@ -156,7 +160,7 @@ public class MusicBeeTagHierarchyImporter : Importer this.TagName = trimmedLine; } - int indentRemainder = (line.Length - trimmedLine.Length) % IndentSize; + var indentRemainder = (line.Length - trimmedLine.Length) % IndentSize; if (indentRemainder != 0) throw new ArgumentException( string.Format(ErrorMessages.ImporterMusicBeeIndentUneven, lineCounter)); diff --git a/Core/Models/Tag/Tag.cs b/Core/Models/Tag/Tag.cs index 19ecbd3..bd5756d 100644 --- a/Core/Models/Tag/Tag.cs +++ b/Core/Models/Tag/Tag.cs @@ -5,13 +5,19 @@ namespace TagHierarchyManager.Models; /// /// An object representing a tag entry in a tag hierarchy database. /// -public partial class Tag +public class Tag { /// /// Gets or sets a list of strings containing aliases (or "also known as"), saved in the database as semicolons. /// public List Aliases { get; set; } = []; + /// + /// Gets or sets the date the tag was created. + /// Defaults to null to cater to migrated databases to prevent inaccurate information. + /// + public DateTime? CreatedAt { get; set; } + /// /// Gets or sets he internal ID of a tag entry.

/// Can be zero when constructing a Tag to send to the database, but should be more than zero for tags retrieved @@ -34,7 +40,7 @@ public partial class Tag /// This is mainly for internal use by the user. ///
public string Notes { get; set; } = string.Empty; - + /// /// Gets or sets a list of the tag entry's parent IDs for interaction with the database. /// @@ -52,12 +58,6 @@ public partial class Tag /// public List TagBindings { get; set; } = []; - /// - /// Gets or sets the date the tag was created. - /// Defaults to null to cater to migrated databases to prevent inaccurate information. - /// - public DateTime? CreatedAt { get; set; } - /// /// Gets or sets the date the tag was modified. /// @@ -82,17 +82,17 @@ public partial class Tag { if (string.IsNullOrWhiteSpace(this.Name)) throw new TagValidationException(ErrorMessages.TagNameIsBlank); - + if (!this.IsTopLevel && this.ParentIds.Count == 0 && this.Parents.Count == 0) throw new TagValidationException( string.Format(ErrorMessages.TagOrphanAttempt, this.Name) - ); + ); if (this.Parents.Contains(this.Name) || this.ParentIds.Contains(this.Id)) throw new TagValidationException( string.Format(ErrorMessages.TagSelfParentAttempt, this.Name) ); - + return true; } } \ No newline at end of file diff --git a/Core/Models/Tag/TagValidationException.cs b/Core/Models/Tag/TagValidationException.cs index 6a88da2..3ba3e24 100644 --- a/Core/Models/Tag/TagValidationException.cs +++ b/Core/Models/Tag/TagValidationException.cs @@ -2,7 +2,15 @@ public class TagValidationException : Exception { - public TagValidationException() { } - public TagValidationException(string message) : base(message) { } - public TagValidationException(string message, Exception inner) : base(message, inner) { } + public TagValidationException() + { + } + + public TagValidationException(string message) : base(message) + { + } + + public TagValidationException(string message, Exception inner) : base(message, inner) + { + } } \ No newline at end of file diff --git a/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs b/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs index 84967d9..17c4b47 100644 --- a/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.DeleteMethods.cs @@ -13,8 +13,8 @@ partial class TagDatabase public void ClearTags() { this.CheckInitialisation(); - using SqliteTransaction transaction = this.currentConnection.BeginTransaction(); - SqliteCommand deleteCommand = this.currentConnection.CreateCommand(); + using var transaction = this.currentConnection.BeginTransaction(); + var deleteCommand = this.currentConnection.CreateCommand(); deleteCommand.CommandText = """ -- noinspection SqlWithoutWhere DELETE FROM tag @@ -41,7 +41,7 @@ partial class TagDatabase public async Task DeleteTag(int id) { this.CheckInitialisation(); - Tag? targetTag = await this.SelectTagFromDatabase(id); + var targetTag = await this.SelectTagFromDatabase(id); this.PerformDeletionChecks(targetTag); await this.ExecuteTagDeletion(targetTag!); } @@ -51,7 +51,6 @@ partial class TagDatabase /// /// The name of the tag to delete. /// A representing the asynchronous operation. - private void DeleteFromCache(Tag targetTag) { Debug.WriteLine($"Deleting tag {targetTag.Name} ({targetTag.Id}) from cache."); @@ -62,19 +61,16 @@ partial class TagDatabase tag.ParentIds.Remove(targetTag.Id); tag.Parents.Remove(targetTag.Name); }); - - Tag? cachedTag = this.Tags.FirstOrDefault(t => t.Id == targetTag.Id); - if (cachedTag != null) - { - this.Tags.Remove(cachedTag); - } + + var cachedTag = this.Tags.FirstOrDefault(t => t.Id == targetTag.Id); + if (cachedTag != null) this.Tags.Remove(cachedTag); } private async Task ExecuteTagDeletion(Tag targetTag) { - await using SqliteTransaction transaction = + await using var transaction = (SqliteTransaction)await this.currentConnection!.BeginTransactionAsync().ConfigureAwait(false); - SqliteCommand command = this.currentConnection.CreateCommand(); + var command = this.currentConnection.CreateCommand(); command.Transaction = transaction; command.CommandText = """ DELETE FROM tag @@ -86,7 +82,7 @@ partial class TagDatabase await command.ExecuteNonQueryAsync().ConfigureAwait(false); await transaction.CommitAsync().ConfigureAwait(false); this.DeleteFromCache(targetTag); - TagsWritten?.Invoke( + this.TagsWritten?.Invoke( this, new DatabaseEditResult([], [], [(targetTag.Id, targetTag.Name)]) ); } diff --git a/Core/Models/TagDatabase/TagDatabase.Events.cs b/Core/Models/TagDatabase/TagDatabase.Events.cs index 588c93f..d5b5fd9 100644 --- a/Core/Models/TagDatabase/TagDatabase.Events.cs +++ b/Core/Models/TagDatabase/TagDatabase.Events.cs @@ -6,14 +6,17 @@ public partial class TagDatabase /// Event thrown when the database has successfully initialised and is ready for writing to. /// public event EventHandler InitialisationComplete = delegate { }; - - public sealed record DatabaseEditResult(IReadOnlyList Added, IReadOnlyList Updated, IReadOnlyList<(int id, string name)> Deleted); - + public event EventHandler? TagsWritten; - + private void OnInitialisationComplete(EventArgs e) { this.Logger.Debug("OnInitialised invoked!"); this.InitialisationComplete.Invoke(this, e); } + + public sealed record DatabaseEditResult( + IReadOnlyList Added, + IReadOnlyList Updated, + IReadOnlyList<(int id, string name)> Deleted); } \ No newline at end of file diff --git a/Core/Models/TagDatabase/TagDatabase.ExternalTransaction.cs b/Core/Models/TagDatabase/TagDatabase.ExternalTransaction.cs index 329324a..3a6e3fa 100644 --- a/Core/Models/TagDatabase/TagDatabase.ExternalTransaction.cs +++ b/Core/Models/TagDatabase/TagDatabase.ExternalTransaction.cs @@ -13,8 +13,8 @@ public partial class TagDatabase internal readonly SqliteTransaction Transaction = transaction; private readonly List _newlyAddedTags = []; - private readonly List _updatedTagSnapshots = []; private readonly List _updatedTags = []; + private readonly List _updatedTagSnapshots = []; public async Task CommitAsync() { @@ -39,10 +39,7 @@ public partial class TagDatabase public async Task RollbackAsync() { - foreach (var tag in this._newlyAddedTags) - { - tag.Id = 0; - } + foreach (var tag in this._newlyAddedTags) tag.Id = 0; foreach (var tag in this._updatedTagSnapshots) { @@ -50,7 +47,7 @@ public partial class TagDatabase if (index != -1) db.Tags[index] = tag; } - + await this.Transaction.RollbackAsync(); } @@ -72,7 +69,7 @@ public partial class TagDatabase Notes = tag.Notes, IsTopLevel = tag.IsTopLevel, CreatedAt = tag.CreatedAt, - UpdatedAt = tag.UpdatedAt, + UpdatedAt = tag.UpdatedAt }); this._updatedTags.Add(tag); } diff --git a/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs b/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs index beb2846..efcab4b 100644 --- a/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.ImportMethods.cs @@ -1,6 +1,5 @@ using Microsoft.Data.Sqlite; using TagHierarchyManager.Assets; -using TagHierarchyManager.Utilities; namespace TagHierarchyManager.Models; @@ -10,30 +9,31 @@ public partial class TagDatabase { if (this.currentConnection is null) throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); - - await using SqliteTransaction transaction = + + await using var transaction = (SqliteTransaction)await this.currentConnection.BeginTransactionAsync().ConfigureAwait(false); try { // phase 1: add all the tags, without anything that relies on other tables. - foreach (ImportedTag tag in importDict.Values) await this.WriteImportedTagToDatabase(transaction, tag); + foreach (var tag in importDict.Values) await this.WriteImportedTagToDatabase(transaction, tag); this.Tags = await this.GetAllTagsFromDatabase(transaction: transaction).ConfigureAwait(false); // phase 2: add the parents and aliases. - foreach (ImportedTag tag in importDict.Values) + foreach (var tag in importDict.Values) { - Tag? currentTag = this.Tags.SingleOrDefault(t => t.Name == tag.Name); + var currentTag = this.Tags.SingleOrDefault(t => t.Name == tag.Name); if (currentTag is null) { //var currentTagList = await this.SelectTagsFromDatabase(tag.Name).ConfigureAwait(false); //currentTag = currentTagList[0]; } + if (currentTag is null) throw new InvalidOperationException(ErrorMessages.TagDatabaseTagNotFound); - - + + // todo search parent here then save the parent IDs. await this.SaveTagParents(transaction, currentTag.Id, tag.Parents, currentTag).ConfigureAwait(false); } @@ -52,8 +52,8 @@ public partial class TagDatabase { if (this.currentConnection is null) throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); - - SqliteCommand addCommand = this.currentConnection.CreateCommand(); + + var addCommand = this.currentConnection.CreateCommand(); addCommand.Transaction = transaction; addCommand.CommandText = """ INSERT INTO tag (name, notes, top_level, tags_to_bind, also_known_as, date_modified) diff --git a/Core/Models/TagDatabase/TagDatabase.Initialisation.cs b/Core/Models/TagDatabase/TagDatabase.Initialisation.cs index c40a16e..dae2195 100644 --- a/Core/Models/TagDatabase/TagDatabase.Initialisation.cs +++ b/Core/Models/TagDatabase/TagDatabase.Initialisation.cs @@ -13,21 +13,6 @@ public partial class TagDatabase private const string InMemoryDbPath = ":memory:"; private const string TagHierarchyDbFileExt = ".thdb"; - - /// - /// Throws an exception if the database has not been initialised yet. - /// - /// - /// Thrown when the database has not been initialised or currentConnection is - /// null. - /// - [MemberNotNull(nameof(currentConnection), nameof(Connection))] - private void CheckInitialisation() - { - if (!this.Initialised || this.currentConnection is null || this.Connection is null) - throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); - } - /// /// Checks if the current connection exists and is open, and closes it. /// @@ -69,7 +54,7 @@ public partial class TagDatabase if (string.IsNullOrWhiteSpace(filePath)) throw new ArgumentException(ErrorMessages.TagDatabaseFilePathEmpty); - string fileExt = Path.GetExtension(filePath); + var fileExt = Path.GetExtension(filePath); if (fileExt != TagHierarchyDbFileExt) throw new ArgumentException(ErrorMessages.TagDatabaseInvalidFileExt); if (loadMode && !File.Exists(filePath)) throw new FileNotFoundException(null, filePath); @@ -77,6 +62,21 @@ public partial class TagDatabase return null; } + + /// + /// Throws an exception if the database has not been initialised yet. + /// + /// + /// Thrown when the database has not been initialised or currentConnection is + /// null. + /// + [MemberNotNull(nameof(currentConnection), nameof(Connection))] + private void CheckInitialisation() + { + if (!this.Initialised || this.currentConnection is null || this.Connection is null) + throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); + } + private async Task CreateDatabaseAsync(string filePath, bool overwrite = false, Dictionary? tagsToImport = null) { @@ -89,8 +89,8 @@ public partial class TagDatabase this.currentConnection = new SqliteConnection($"Data Source={filePath};Pooling=False"); await this.currentConnection.OpenAsync().ConfigureAwait(false); - SqliteCommand command = this.currentConnection.CreateCommand(); - + var command = this.currentConnection.CreateCommand(); + command.CommandText = """ CREATE TABLE "tag" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, @@ -143,7 +143,7 @@ public partial class TagDatabase this.Logger.Error(ex, "SQLite failed executing statement:\n{SqlStatement}", command.CommandText); throw; } - + await this.FinishInitialisationAsync(tagsToImport).ConfigureAwait(false); } catch (Exception ex) @@ -160,16 +160,16 @@ public partial class TagDatabase private async Task FinishInitialisationAsync(Dictionary? tagsToImport = null) { this.Logger.Information("[TagHierarchyDatabase.Initialise] Initialising..."); - SqliteCommand command = this.currentConnection?.CreateCommand() ?? - throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); + var command = this.currentConnection?.CreateCommand() ?? + throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); command.CommandText = "SELECT * FROM SETTINGS;"; try { - await using (SqliteDataReader reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) + await using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) { while (await reader.ReadAsync().ConfigureAwait(false)) { - string currentSetting = reader.GetString(0); + var currentSetting = reader.GetString(0); switch (currentSetting) { case "version": @@ -183,12 +183,12 @@ public partial class TagDatabase } } } - + this.FilePath = this.currentConnection.DataSource; this.Name = this.currentConnection.DataSource != InMemoryDbPath ? Path.GetFileNameWithoutExtension(this.currentConnection.DataSource) : InMemoryDbName; - + await this.PerformNeededMigrations(); this.Initialised = true; @@ -203,11 +203,10 @@ public partial class TagDatabase this.Close(); throw; } - + if (tagsToImport is null) this.Tags = await this.GetAllTagsFromDatabase(); - - + this.OnInitialisationComplete(EventArgs.Empty); Debug.WriteLine( @@ -224,11 +223,8 @@ public partial class TagDatabase { if (connection is null) { - string? errorString = ValidateFilePath(filePath, true); - if (errorString is not null) - { - throw new ArgumentException(errorString); - } + var errorString = ValidateFilePath(filePath, true); + if (errorString is not null) throw new ArgumentException(errorString); } this.currentConnection = connection ?? new SqliteConnection($"Data Source={filePath};Pooling=False"); @@ -242,8 +238,8 @@ public partial class TagDatabase private async Task ValidateAsync(SqliteConnection connection) { - bool sqliteDatabaseCheck = await this.ValidateFileIsSqliteDatabaseAsync(connection).ConfigureAwait(false); - bool structureCheck = await this.ValidateDatabaseStructureAsync(connection).ConfigureAwait(false); + var sqliteDatabaseCheck = await this.ValidateFileIsSqliteDatabaseAsync(connection).ConfigureAwait(false); + var structureCheck = await this.ValidateDatabaseStructureAsync(connection).ConfigureAwait(false); return sqliteDatabaseCheck && structureCheck; } @@ -252,7 +248,7 @@ public partial class TagDatabase { try { - SqliteCommand tableCheckCommand = connection.CreateCommand(); + var tableCheckCommand = connection.CreateCommand(); tableCheckCommand.CommandText = """ SELECT name FROM sqlite_master WHERE name != 'sqlite_sequence' @@ -260,13 +256,13 @@ public partial class TagDatabase ORDER BY name """; this.Logger.Debug("[TagDatabaseObject.Load] Command created"); - bool notTagDatabase = false; - await using (SqliteDataReader reader = await tableCheckCommand.ExecuteReaderAsync().ConfigureAwait(false)) + var notTagDatabase = false; + await using (var reader = await tableCheckCommand.ExecuteReaderAsync().ConfigureAwait(false)) { while (await reader.ReadAsync().ConfigureAwait(false)) { if (notTagDatabase) continue; - string currentTable = reader.GetString(0); + var currentTable = reader.GetString(0); HashSet allowedTables = ["tag", "tag_parent_link", "alias", "settings"]; if (!allowedTables.Contains(currentTable)) notTagDatabase = true; } @@ -293,14 +289,11 @@ public partial class TagDatabase { try { - SqliteCommand validityCheck = connection.CreateCommand(); + var validityCheck = connection.CreateCommand(); validityCheck.CommandText = "pragma schema_version;"; int? schemaVersion = Convert.ToInt32(await validityCheck.ExecuteScalarAsync().ConfigureAwait(false), CultureInfo.InvariantCulture); - if (schemaVersion == 0) - { - throw new ArgumentException(ErrorMessages.TagDatabaseInvalidFile); - } + if (schemaVersion == 0) throw new ArgumentException(ErrorMessages.TagDatabaseInvalidFile); } catch (SqliteException ex) { diff --git a/Core/Models/TagDatabase/TagDatabase.Migration.cs b/Core/Models/TagDatabase/TagDatabase.Migration.cs index 6aee9b4..8b18530 100644 --- a/Core/Models/TagDatabase/TagDatabase.Migration.cs +++ b/Core/Models/TagDatabase/TagDatabase.Migration.cs @@ -6,7 +6,7 @@ namespace TagHierarchyManager.Models; public partial class TagDatabase { /// - /// Performs necessary migrations from older database versions. + /// Performs necessary migrations from older database versions. /// /// Thrown if the database's current connection is null. private async Task PerformNeededMigrations() @@ -25,10 +25,10 @@ public partial class TagDatabase var addDateAddedCommand = this.currentConnection.CreateCommand(); addDateAddedCommand.Transaction = transaction; addDateAddedCommand.CommandText = - $""" - ALTER TABLE tag ADD COLUMN date_created DATETIME DEFAULT NULL; - ALTER TABLE tag ADD COLUMN date_modified DATETIME DEFAULT NULL; - """; + """ + ALTER TABLE tag ADD COLUMN date_created DATETIME DEFAULT NULL; + ALTER TABLE tag ADD COLUMN date_modified DATETIME DEFAULT NULL; + """; addDateAddedCommand.ExecuteNonQuery(); var updateModifiedCommand = this.currentConnection.CreateCommand(); @@ -37,7 +37,8 @@ public partial class TagDatabase updateModifiedCommand.ExecuteNonQuery(); var deleteOldTableCommand = this.currentConnection.CreateCommand() ?? - throw new InvalidOperationException(ErrorMessages.TagDatabaseNotInitialised); + throw new InvalidOperationException(ErrorMessages + .TagDatabaseNotInitialised); deleteOldTableCommand.Transaction = transaction; deleteOldTableCommand.CommandText = "DROP TABLE alias;"; @@ -57,25 +58,25 @@ public partial class TagDatabase var nonUniqueCommand = this.currentConnection.CreateCommand(); nonUniqueCommand.Transaction = transaction; nonUniqueCommand.CommandText = - $""" - CREATE TABLE "tag_new" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "name" TEXT NOT NULL, - "notes" TEXT DEFAULT '', - "top_level" INTEGER NOT NULL DEFAULT 0, - "tags_to_bind" TEXT DEFAULT '', - "also_known_as" TEXT DEFAULT '', - "date_created" DATETIME, - "date_modified" DATETIME - ); + """ + CREATE TABLE "tag_new" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "notes" TEXT DEFAULT '', + "top_level" INTEGER NOT NULL DEFAULT 0, + "tags_to_bind" TEXT DEFAULT '', + "also_known_as" TEXT DEFAULT '', + "date_created" DATETIME, + "date_modified" DATETIME + ); - INSERT INTO tag_new SELECT * FROM tag; + INSERT INTO tag_new SELECT * FROM tag; - DROP TABLE tag; + DROP TABLE tag; - ALTER TABLE tag_new RENAME TO tag; - PRAGMA foreign_keys=ON; - """; + ALTER TABLE tag_new RENAME TO tag; + PRAGMA foreign_keys=ON; + """; nonUniqueCommand.ExecuteNonQuery(); var reenableForeignKeysCommand = this.currentConnection.CreateCommand(); reenableForeignKeysCommand.CommandText = "PRAGMA foreign_keys=ON;"; diff --git a/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs b/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs index 1779ee6..1fda109 100644 --- a/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs +++ b/Core/Models/TagDatabase/TagDatabase.QueryProcessorHandler.cs @@ -35,7 +35,7 @@ public partial class TagDatabase { ProcessTagParentSelectionInt(command, id); } - + /// /// Generates an SQLITE INSERT command for saving the tag to the database. /// diff --git a/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs b/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs index eca1af3..b9830c1 100644 --- a/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.ReadMethods.cs @@ -17,8 +17,8 @@ public partial class TagDatabase SqliteTransaction? transaction = null) { this.CheckInitialisation(); - - SqliteCommand command = this.currentConnection.CreateCommand(); + + var command = this.currentConnection.CreateCommand(); if (transaction is not null) command.Transaction = transaction; command.CommandText = """ SELECT @@ -36,7 +36,7 @@ public partial class TagDatabase GROUP BY tag.id """; - List tags = await this.ExecuteTagRetrievalDatabaseQuery(command).ConfigureAwait(false); + var tags = await this.ExecuteTagRetrievalDatabaseQuery(command).ConfigureAwait(false); tags.ForEach(tag => tag.Parents = tags .Where(t => tag.ParentIds.Contains(t.Id)) @@ -57,7 +57,7 @@ public partial class TagDatabase /// public List GetTagChildren(int id) { - List tags = this.Tags.Where(tag => tag.ParentIds.Contains(id)).ToList(); + var tags = this.Tags.Where(tag => tag.ParentIds.Contains(id)).ToList(); return tags; } @@ -71,17 +71,17 @@ public partial class TagDatabase /// public List GetTagChildren(string name) { - List tags = this.Tags.Where(tag => tag.Parents.Contains(name)).ToList(); + var tags = this.Tags.Where(tag => tag.Parents.Contains(name)).ToList(); return tags; } public async Task GetTagRelationshipCountAsync() { this.CheckInitialisation(); - SqliteCommand command = this.currentConnection.CreateCommand(); + var command = this.currentConnection.CreateCommand(); command.CommandText = "SELECT COUNT(*) FROM tag_parent_link"; - - int count = Convert.ToInt32(await command.ExecuteScalarAsync()); + + var count = Convert.ToInt32(await command.ExecuteScalarAsync()); return count; } @@ -94,7 +94,7 @@ public partial class TagDatabase public async Task SelectTagFromDatabase(int id, SqliteTransaction? transaction = null) { this.CheckInitialisation(); - SqliteCommand command = this.currentConnection.CreateCommand(); + var command = this.currentConnection.CreateCommand(); command.CommandText = """ SELECT tag.id, @@ -112,14 +112,14 @@ public partial class TagDatabase GROUP BY tag.id """; command.Parameters.AddWithValue("@tag_id", id); - List tags = await this.ExecuteTagRetrievalDatabaseQuery(command).ConfigureAwait(false); - Tag? selectedTag = tags.FirstOrDefault(); + var tags = await this.ExecuteTagRetrievalDatabaseQuery(command).ConfigureAwait(false); + var selectedTag = tags.FirstOrDefault(); if (selectedTag is null) return null; - - SqliteCommand parentCommand = this.currentConnection.CreateCommand(); + + var parentCommand = this.currentConnection.CreateCommand(); if (transaction is not null) parentCommand.Transaction = transaction; QueryProcessorHandler.ProcessTagParentSelectionCommand(parentCommand, selectedTag.Id); - List parents = await this.ExecuteTagRetrievalDatabaseQuery(parentCommand, false).ConfigureAwait(false); + var parents = await this.ExecuteTagRetrievalDatabaseQuery(parentCommand, false).ConfigureAwait(false); selectedTag.ParentIds = parents.Select(p => p.Id).ToList(); selectedTag.Parents = parents.Select(p => p.Name).ToList(); @@ -141,7 +141,7 @@ public partial class TagDatabase try { - await using SqliteDataReader reader = await command.ExecuteReaderAsync().ConfigureAwait(false); + await using var reader = await command.ExecuteReaderAsync().ConfigureAwait(false); if (!reader.HasRows) return tags; while (await reader.ReadAsync().ConfigureAwait(false)) { @@ -150,13 +150,13 @@ public partial class TagDatabase Id = reader.GetInt32(reader.GetOrdinal(IdColumnName)), Name = reader.GetString(reader.GetOrdinal(NameColumnName)), IsTopLevel = reader.GetBoolean(reader.GetOrdinal(TopLevelColumnName)), - Notes = reader.GetString(reader.GetOrdinal(NotesColumnName)), + Notes = reader.GetString(reader.GetOrdinal(NotesColumnName)) }; - string tagBindList = reader.GetString(reader.GetOrdinal(TagBindingsColumnName)); + var tagBindList = reader.GetString(reader.GetOrdinal(TagBindingsColumnName)); if (!string.IsNullOrEmpty(tagBindList)) addedTag.TagBindings = tagBindList.Split(';').ToList(); - string altNameList = reader.GetString(reader.GetOrdinal(AliasesColumnName)); + var altNameList = reader.GetString(reader.GetOrdinal(AliasesColumnName)); if (!string.IsNullOrEmpty(altNameList)) addedTag.Aliases = altNameList.Split(';').ToList(); if (!reader.IsDBNull(reader.GetOrdinal(DateCreatedColumnName))) @@ -170,11 +170,11 @@ public partial class TagDatabase var dbUpdatedAt = reader.GetDateTime(reader.GetOrdinal(DateModifiedColumnName)); addedTag.UpdatedAt = dbUpdatedAt; } - + if (fetchParents && !reader.IsDBNull(reader.GetOrdinal(ParentIdsColumnName))) { - List parents = + var parents = reader.GetString(reader.GetOrdinal(ParentIdsColumnName)).Split(';').Select(int.Parse).ToList(); addedTag.ParentIds = parents; } diff --git a/Core/Models/TagDatabase/TagDatabase.SearchMethods.cs b/Core/Models/TagDatabase/TagDatabase.SearchMethods.cs index 4b9f3d6..bfa9827 100644 --- a/Core/Models/TagDatabase/TagDatabase.SearchMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.SearchMethods.cs @@ -6,18 +6,18 @@ namespace TagHierarchyManager.Models; public partial class TagDatabase { /// - /// Performs a search on the Tags list, the results dependent on the specified search mode. + /// Performs a search on the Tags list, the results dependent on the specified search mode. /// /// The string to search the tag names for. - /// The mode to search for, see . + /// The mode to search for, see . /// A List of Tags representing the search results. /// - /// Thrown if the mode is out of the valid range specified by TagDatabaseSearchMode. + /// Thrown if the mode is out of the valid range specified by TagDatabaseSearchMode. /// public List Search(string searchQuery, TagDatabaseSearchMode mode) { searchQuery = StringNormaliser.FormatStringForSearch(searchQuery.Trim()); - List tags = mode switch + var tags = mode switch { TagDatabaseSearchMode.Fuzzy => this.Tags .Where(tag => StringNormaliser.FormatStringForSearch(tag.Name).Contains(searchQuery)) @@ -31,26 +31,26 @@ public partial class TagDatabase TagDatabaseSearchMode.ExactMatch => this.Tags .Where(tag => StringNormaliser.FormatStringForSearch(tag.Name) == searchQuery) .ToList(), - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) }; return tags; } - + /// - /// Performs a search on the Tags list, the results dependent on the specified search mode.
- /// Searches tag aliases, as well as tag names. + /// Performs a search on the Tags list, the results dependent on the specified search mode.
+ /// Searches tag aliases, as well as tag names. ///
/// The string to search the tag names and aliases for. - /// The mode to search for, see . + /// The mode to search for, see . /// A List of Tags representing the search results. /// - /// Thrown if the mode is out of the valid range specified by TagDatabaseSearchMode. + /// Thrown if the mode is out of the valid range specified by TagDatabaseSearchMode. /// public List SearchWithAliases(string searchQuery, TagDatabaseSearchMode mode) { searchQuery = StringNormaliser.FormatStringForSearch(searchQuery.Trim().ToLowerInvariant()); - List tags = mode switch + var tags = mode switch { TagDatabaseSearchMode.Fuzzy => this.Tags.Where(tag => StringNormaliser.FormatStringForSearch(tag.Name).Contains(searchQuery) || tag.Aliases.Any(alias => @@ -68,7 +68,7 @@ public partial class TagDatabase StringNormaliser.FormatStringForSearch(tag.Name) == searchQuery || tag.Aliases.Any(alias => StringNormaliser.FormatStringForSearch(alias) == searchQuery)) .ToList(), - _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null) }; return tags; diff --git a/Core/Models/TagDatabase/TagDatabase.SettingsHandler.cs b/Core/Models/TagDatabase/TagDatabase.SettingsHandler.cs index 0d0ecb8..84f8af8 100644 --- a/Core/Models/TagDatabase/TagDatabase.SettingsHandler.cs +++ b/Core/Models/TagDatabase/TagDatabase.SettingsHandler.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Microsoft.Data.Sqlite; using TagHierarchyManager.Assets; namespace TagHierarchyManager.Models; @@ -10,9 +9,10 @@ public partial class TagDatabase { this.DefaultTagBindings = value; this.CheckInitialisation(); - await this.Settings.UpdateSettingAsync(SettingsHandler.DefaultTagBindingKey, string.Join(';', value)).ConfigureAwait(false); + await this.Settings.UpdateSettingAsync(SettingsHandler.DefaultTagBindingKey, string.Join(';', value)) + .ConfigureAwait(false); } - + /// /// A class for handling settings for a at a lower level. /// @@ -31,7 +31,7 @@ public partial class TagDatabase private readonly Dictionary defaultSettings = new() { - { DefaultTagBindingKey, "genre" }, + { DefaultTagBindingKey, "genre" } }; private static IReadOnlyList RequiredSettingsKeys { get; } = @@ -51,7 +51,7 @@ public partial class TagDatabase if (await this.CheckSettingExistenceAsync(key).ConfigureAwait(false)) throw new ArgumentException(string.Format(ErrorMessages.TagDatabaseSettingKeyExists, key)); - SqliteCommand insertCommand = db.Connection.CreateCommand(); + var insertCommand = db.Connection.CreateCommand(); insertCommand.CommandText = $""" INSERT INTO settings (key, value) @@ -79,7 +79,7 @@ public partial class TagDatabase if (!await this.CheckSettingExistenceAsync(key).ConfigureAwait(false)) throw new KeyNotFoundException(string.Format(ErrorMessages.TagDatabaseSettingKeyNotFound, key)); - SqliteCommand command = db.Connection.CreateCommand(); + var command = db.Connection.CreateCommand(); command.CommandText = $""" DELETE FROM settings WHERE key == {SettingKeyParameter} @@ -97,13 +97,13 @@ public partial class TagDatabase { db.CheckInitialisation(); Dictionary settingsDict = new(); - SqliteCommand command = db.Connection.CreateCommand(); + var command = db.Connection.CreateCommand(); command.CommandText = "SELECT * FROM settings"; - await using SqliteDataReader reader = await command.ExecuteReaderAsync().ConfigureAwait(false); + await using var reader = await command.ExecuteReaderAsync().ConfigureAwait(false); while (await reader.ReadAsync().ConfigureAwait(false)) { - string key = reader.GetString(0); - string value = reader.GetString(1); + var key = reader.GetString(0); + var value = reader.GetString(1); settingsDict.Add(key, value); } @@ -122,13 +122,13 @@ public partial class TagDatabase if (!await this.CheckSettingExistenceAsync(key).ConfigureAwait(false)) throw new KeyNotFoundException(string.Format(ErrorMessages.TagDatabaseSettingKeyNotFound, key)); - SqliteCommand command = db.Connection.CreateCommand(); + var command = db.Connection.CreateCommand(); command.CommandText = $""" SELECT value FROM settings WHERE key == {SettingKeyParameter} """; command.Parameters.AddWithValue(SettingKeyParameter, key); - string? pokedSetting = (string?)await command.ExecuteScalarAsync().ConfigureAwait(false); + var pokedSetting = (string?)await command.ExecuteScalarAsync().ConfigureAwait(false); return pokedSetting; } @@ -155,7 +155,7 @@ public partial class TagDatabase if (!await this.CheckSettingExistenceAsync(key).ConfigureAwait(false)) throw new KeyNotFoundException(string.Format(ErrorMessages.TagDatabaseSettingKeyNotFound, key)); - SqliteCommand command = db.Connection.CreateCommand(); + var command = db.Connection.CreateCommand(); command.CommandText = $""" UPDATE settings SET value = {SettingValueParameter} @@ -173,13 +173,13 @@ public partial class TagDatabase { db.CheckInitialisation(); - SqliteCommand command = db.Connection.CreateCommand(); + var command = db.Connection.CreateCommand(); command.CommandText = $""" SELECT COUNT(*) FROM settings WHERE key = {SettingKeyParameter}; """; command.Parameters.AddWithValue(SettingKeyParameter, key); - int count = Convert.ToInt32(await command.ExecuteScalarAsync().ConfigureAwait(false), + var count = Convert.ToInt32(await command.ExecuteScalarAsync().ConfigureAwait(false), CultureInfo.InvariantCulture); return count > 0; } diff --git a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs index 391da96..933dd9d 100644 --- a/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs +++ b/Core/Models/TagDatabase/TagDatabase.WriteMethods.cs @@ -64,7 +64,7 @@ public partial class TagDatabase tag.Id = 0; throw; } - + var index = this.Tags.FindIndex(t => t.Id == tag.Id); this.Tags[index] = oldTag; throw; @@ -115,7 +115,7 @@ public partial class TagDatabase tag.Id = 0; throw; } - + var index = this.Tags.FindIndex(t => t.Id == tag.Id); this.Tags[index] = oldTag; throw; @@ -169,8 +169,6 @@ public partial class TagDatabase // process parents, grabbing the names first in case the user wants to change the parents. foreach (var parentName in parents) { - - } // clear existing tag parents so we have a clean slate. diff --git a/Core/TagHierarchyManager.csproj b/Core/TagHierarchyManager.csproj index 0526602..7799ac8 100644 --- a/Core/TagHierarchyManager.csproj +++ b/Core/TagHierarchyManager.csproj @@ -16,18 +16,18 @@ - - ResXFileCodeGenerator - Resources.Designer.cs - + + ResXFileCodeGenerator + Resources.Designer.cs + - - True - True - ErrorMessages.resx - + + True + True + ErrorMessages.resx + diff --git a/Core/TagHierarchyManager.csproj.DotSettings b/Core/TagHierarchyManager.csproj.DotSettings index f6e0ad5..f41684c 100644 --- a/Core/TagHierarchyManager.csproj.DotSettings +++ b/Core/TagHierarchyManager.csproj.DotSettings @@ -1,6 +1,12 @@ - + False - True - True - True - True \ No newline at end of file + True + True + True + True \ No newline at end of file diff --git a/Core/Utilities/StringNormaliser.cs b/Core/Utilities/StringNormaliser.cs index f790cd6..c581080 100644 --- a/Core/Utilities/StringNormaliser.cs +++ b/Core/Utilities/StringNormaliser.cs @@ -17,10 +17,10 @@ public static class StringNormaliser { if (string.IsNullOrEmpty(input)) return input; - string decomposedString = input.Normalize(NormalizationForm.FormD); + var decomposedString = input.Normalize(NormalizationForm.FormD); StringBuilder sb = new(); - foreach (char c in decomposedString.Where(c => + foreach (var c in decomposedString.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark)) sb.Append(c); return sb.ToString().ToLowerInvariant();