using System.ComponentModel.DataAnnotations.Schema; using System.Reflection; using System.Text; using AMWD.Common.EntityFrameworkCore.Attributes; using Microsoft.EntityFrameworkCore; #if NET5_0_OR_GREATER using Microsoft.EntityFrameworkCore.Metadata; #endif namespace AMWD.Common.EntityFrameworkCore.Extensions { /// /// Extensions for the of entity framework core. /// public static class ModelBuilderExtensions { /// /// Applies indices and unique constraints to the properties. /// /// The database model builder. /// A reference to this instance after the operation has completed. [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0019", Justification = "No pattern comparison in this case due to readability.")] public static ModelBuilder ApplyIndexAttributes(this ModelBuilder builder) { foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var indexAttribute = entityType.ClrType .GetProperty(property.Name) ?.GetCustomAttribute(typeof(DatabaseIndexAttribute), false) as DatabaseIndexAttribute; if (indexAttribute != null) { var index = entityType.AddIndex(property); index.IsUnique = indexAttribute.IsUnique; if (!string.IsNullOrWhiteSpace(indexAttribute.Name)) { #if NET5_0_OR_GREATER index.SetDatabaseName(indexAttribute.Name.Trim()); #else index.SetName(indexAttribute.Name.Trim()); #endif } } } } return builder; } /// /// Converts all table and column names to snake_case_names. /// /// The database model builder. /// A reference to this instance after the operation has completed. public static ModelBuilder ApplySnakeCase(this ModelBuilder builder) { foreach (var entityType in builder.Model.GetEntityTypes()) { // skip conversion when table name is explicitly set if ((entityType.ClrType.GetCustomAttribute(typeof(TableAttribute), false) as TableAttribute) == null) { #if NET5_0_OR_GREATER entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName())); #else entityType.SetTableName(ConvertToSnakeCase(entityType.GetTableName())); #endif } #if NET5_0_OR_GREATER var identifier = StoreObjectIdentifier.Table(entityType.GetTableName(), entityType.GetSchema()); #endif foreach (var property in entityType.GetProperties()) { // skip conversion when column name is explicitly set if ((entityType.ClrType.GetProperty(property.Name)?.GetCustomAttribute(typeof(ColumnAttribute), false) as ColumnAttribute) == null) { #if NET5_0_OR_GREATER property.SetColumnName(ConvertToSnakeCase(property.GetColumnName(identifier))); #else property.SetColumnName(ConvertToSnakeCase(property.GetColumnName())); #endif } } } return builder; } /// /// Converts a string to its snake_case equivalent. /// /// /// Code borrowed from Npgsql.NameTranslation.NpgsqlSnakeCaseNameTranslator. /// See https://github.com/npgsql/npgsql/blob/f2b2c98f45df6d2a78eec00ae867f18944d717ca/src/Npgsql/NameTranslation/NpgsqlSnakeCaseNameTranslator.cs#L76-L136. /// /// The value to convert. private static string ConvertToSnakeCase(string value) { var sb = new StringBuilder(); var state = SnakeCaseState.Start; for (int i = 0; i < value.Length; i++) { if (value[i] == ' ') { if (state != SnakeCaseState.Start) state = SnakeCaseState.NewWord; } else if (char.IsUpper(value[i])) { switch (state) { case SnakeCaseState.Upper: bool hasNext = (i + 1 < value.Length); if (i > 0 && hasNext) { char nextChar = value[i + 1]; if (!char.IsUpper(nextChar) && nextChar != '_') { sb.Append('_'); } } break; case SnakeCaseState.Lower: case SnakeCaseState.NewWord: sb.Append('_'); break; } sb.Append(char.ToLowerInvariant(value[i])); state = SnakeCaseState.Upper; } else if (value[i] == '_') { sb.Append('_'); state = SnakeCaseState.Start; } else { if (state == SnakeCaseState.NewWord) sb.Append('_'); sb.Append(value[i]); state = SnakeCaseState.Lower; } } return sb.ToString(); } private enum SnakeCaseState { Start, Lower, Upper, NewWord } } }