using System.ComponentModel.DataAnnotations.Schema; using System.Data; using System.Reflection; using FluentMigrator.Builders.Alter.Table; using FluentMigrator.Builders.Create; using FluentMigrator.Builders.Create.Table; using FluentMigrator.Infrastructure.Extensions; using FluentMigrator.Model; using LinqToDB.Mapping; using Nop.Core; using Nop.Core.Infrastructure; using Nop.Data.Mapping; using Nop.Data.Mapping.Builders; namespace Nop.Data.Extensions; /// /// FluentMigrator extensions /// public static class FluentMigratorExtensions { #region Utils private const int DATE_TIME_PRECISION = 6; private static Dictionary> TypeMapping { get; } = new Dictionary> { [typeof(int)] = c => c.AsInt32(), [typeof(long)] = c => c.AsInt64(), [typeof(string)] = c => c.AsNopString(int.MaxValue).Nullable(), [typeof(bool)] = c => c.AsBoolean(), [typeof(decimal)] = c => c.AsDecimal(18, 4), [typeof(DateTime)] = c => c.AsNopDateTime2(), [typeof(byte[])] = c => c.AsBinary(int.MaxValue), [typeof(Guid)] = c => c.AsGuid() }; private static void DefineByOwnType(string columnName, Type propType, CreateTableExpressionBuilder create, bool canBeNullable = false) { if (string.IsNullOrEmpty(columnName)) throw new ArgumentException("The column name cannot be empty"); if (propType == typeof(string) || propType.FindInterfaces((t, o) => t.FullName?.Equals(o.ToString(), StringComparison.InvariantCultureIgnoreCase) ?? false, "System.Collections.IEnumerable").Length > 0) canBeNullable = true; var column = create.WithColumn(columnName); TypeMapping[propType](column); if (propType == typeof(DateTime)) create.CurrentColumn.Precision = DATE_TIME_PRECISION; if (canBeNullable) create.Nullable(); } #endregion /// /// Defines the column type as date that is combined with a time of day and a specified precision /// public static ICreateTableColumnOptionOrWithColumnSyntax AsNopDateTime2(this ICreateTableColumnAsTypeSyntax syntax) { var dataSettings = DataSettingsManager.LoadSettings(); return dataSettings.DataProvider switch { DataProviderType.MySql => syntax.AsCustom($"datetime({DATE_TIME_PRECISION})"), DataProviderType.SqlServer => syntax.AsCustom($"datetime2({DATE_TIME_PRECISION})"), _ => syntax.AsDateTime2() }; } /// /// Defines the column type as string with specified size /// public static ICreateTableColumnOptionOrWithColumnSyntax AsNopString(this ICreateTableColumnAsTypeSyntax syntax, int size) { var dataSettings = DataSettingsManager.LoadSettings(); return dataSettings.DataProvider switch { DataProviderType.MySql when size == int.MaxValue => syntax.AsCustom($"LONGTEXT CHARACTER SET utf8mb4"), DataProviderType.MySql => syntax.AsCustom($"NVARCHAR({size}) CHARACTER SET utf8mb4"), _ => syntax.AsString(size) }; } /// /// Specifies a foreign key /// /// The foreign key column /// The primary table name /// The primary tables column name /// Behavior for DELETEs /// /// Set column options or create a new column or set a foreign key cascade rule public static ICreateTableColumnOptionOrForeignKeyCascadeOrWithColumnSyntax ForeignKey(this ICreateTableColumnOptionOrWithColumnSyntax column, string primaryTableName = null, string primaryColumnName = null, Rule onDelete = Rule.Cascade) where TPrimary : BaseEntity { if (string.IsNullOrEmpty(primaryTableName)) primaryTableName = NameCompatibilityManager.GetTableName(typeof(TPrimary)); if (string.IsNullOrEmpty(primaryColumnName)) primaryColumnName = nameof(BaseEntity.Id); return column.Indexed().ForeignKey(primaryTableName, primaryColumnName).OnDelete(onDelete); } /// /// Specifies a foreign key /// /// The foreign key column /// The primary table name /// The primary tables column name /// Behavior for DELETEs /// /// Alter/add a column with an optional foreign key public static IAlterTableColumnOptionOrAddColumnOrAlterColumnOrForeignKeyCascadeSyntax ForeignKey(this IAlterTableColumnOptionOrAddColumnOrAlterColumnSyntax column, string primaryTableName = null, string primaryColumnName = null, Rule onDelete = Rule.Cascade) where TPrimary : BaseEntity { if (string.IsNullOrEmpty(primaryTableName)) primaryTableName = NameCompatibilityManager.GetTableName(typeof(TPrimary)); if (string.IsNullOrEmpty(primaryColumnName)) primaryColumnName = nameof(BaseEntity.Id); return column.Indexed().ForeignKey(primaryTableName, primaryColumnName).OnDelete(onDelete); } /// /// Retrieves expressions into ICreateExpressionRoot /// /// The root expression for a CREATE operation /// Entity type public static void TableFor(this ICreateExpressionRoot expressionRoot) where TEntity : BaseEntity { var type = typeof(TEntity); var builder = expressionRoot.Table(NameCompatibilityManager.GetTableName(type)) as CreateTableExpressionBuilder; builder.RetrieveTableExpressions(type); } /// /// Retrieves expressions for building an entity table /// /// An expression builder for a FluentMigrator.Expressions.CreateTableExpression /// Type of entity public static void RetrieveTableExpressions(this CreateTableExpressionBuilder builder, Type type) { var typeFinder = Singleton.Instance .FindClassesOfType(typeof(IEntityBuilder)) .FirstOrDefault(t => t.BaseType?.GetGenericArguments().Contains(type) ?? false); if (typeFinder != null) (EngineContext.Current.ResolveUnregistered(typeFinder) as IEntityBuilder)?.MapEntity(builder); var expression = builder.Expression; if (!expression.Columns.Any(c => c.IsPrimaryKey)) { var pk = new ColumnDefinition { Name = nameof(BaseEntity.Id), Type = DbType.Int32, IsIdentity = true, TableName = NameCompatibilityManager.GetTableName(type), ModificationType = ColumnModificationType.Create, IsPrimaryKey = true }; expression.Columns.Insert(0, pk); builder.CurrentColumn = pk; } var propertiesToAutoMap = type .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty) .Where(pi => pi.DeclaringType != typeof(BaseEntity) && pi.CanWrite && !pi.HasAttribute() && !pi.HasAttribute() && !expression.Columns.Any(x => x.Name.Equals(NameCompatibilityManager.GetColumnName(type, pi.Name), StringComparison.OrdinalIgnoreCase)) && TypeMapping.ContainsKey(GetTypeToMap(pi.PropertyType).propType)); foreach (var prop in propertiesToAutoMap) { var columnName = NameCompatibilityManager.GetColumnName(type, prop.Name); var (propType, canBeNullable) = GetTypeToMap(prop.PropertyType); DefineByOwnType(columnName, propType, builder, canBeNullable); } } public static (Type propType, bool canBeNullable) GetTypeToMap(this Type type) { if (Nullable.GetUnderlyingType(type) is Type uType) return (uType, true); return (type, false); } }