using System.Collections.Immutable; using System.Data; using System.Globalization; using System.Reflection; using Connected.Annotations; using Connected.Entities.Annotations; using Connected.Interop; using Connected.Middleware; using Connected.Threading; using Microsoft.Extensions.Logging; namespace Connected.Data.Schema; internal class SchemaService : ISchemaService { public SchemaService(IMiddlewareService middleware, ILogger logger) { Middleware = new AsyncLazy>(middleware.Query()); Logger = logger; } private ILogger Logger { get; } private AsyncLazy> Middleware { get; } public async Task Synchronize(List? entities) { if (Middleware is null) { Logger.LogWarning("No ISchemaMiddleware is registered."); return; } if (entities is null || !entities.Any()) return; foreach (var entity in entities) { if (!IsPersistent(entity)) continue; Logger.LogTrace("Synchronizing entity '{entity}", entity.Name); var synchronized = false; var schema = CreateSchema(entity); if (schema.Ignore) continue; foreach (var middleware in await Middleware.Value) { /* * We are looking for the first middleware which returns true, * which means it supports entity synchronization. */ if (!await middleware.IsEntitySupported(entity)) continue; /* * Note that sharding synchronization will be handled by the middleware. */ await middleware.Synchronize(entity, schema); synchronized = true; break; } /* * We should notify the environment that entity is no synchronized. * Maybe we should throw the exception here because unsynchronized * entities could cause system instabillity. */ if (!synchronized) Logger.LogWarning("No middleware synchronized the entity ({entity}).", entity.Name); } } /// /// Determines if the entity supports persistence. Virtual entities does not support persistence which /// means they don't have physical storage. /// /// The type of the entity to check for persistence. /// true if the entity supports persistence, false otherwise. private static bool IsPersistent(Type entityType) { var persistence = entityType.GetCustomAttribute(); return persistence is null || persistence.Persistence.HasFlag(ColumnPersistence.Write); } private static ISchema CreateSchema(Type type) { var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic); var att = ResolveSchemaAttribute(type); var result = new EntitySchema { Name = att.Name, Schema = att.Schema }; var columns = new List(); foreach (var property in properties) { if (!property.CanWrite) continue; if (property.FindAttribute() is PersistenceAttribute pa && pa.IsVirtual) continue; var column = new SchemaColumn(result, property) { Name = ResolveColumnName(property), DataType = DataExtensions.ToDbType(property) }; var pk = property.FindAttribute(); if (pk != null) { column.IsPrimaryKey = true; column.IsIdentity = pk.Identity; column.IsUnique = true; column.IsIndex = true; } var idx = property.FindAttribute(); if (idx != null) { column.IsIndex = true; column.IsUnique = idx.Unique; column.Index = idx.Name; } var ordinal = property.FindAttribute(); if (ordinal != null) column.Ordinal = ordinal.Value; if (column.DataType == DbType.Decimal || column.DataType == DbType.VarNumeric) { var numeric = property.FindAttribute(); if (numeric != null) { column.Precision = numeric.Percision; column.Scale = numeric.Scale; } else { column.Precision = 20; column.Scale = 5; } } else if (column.DataType == DbType.Date || column.DataType == DbType.DateTime || column.DataType == DbType.DateTime2 || column.DataType == DbType.DateTimeOffset || column.DataType == DbType.Time) { var dateAtt = property.FindAttribute(); if (dateAtt is not null) { column.DateKind = dateAtt.Kind; column.DatePrecision = dateAtt.Precision; } else { column.DateKind = DateKind.DateTime2; column.DatePrecision = 7; } } else if (column.DataType == DbType.Binary) { var bin = property.FindAttribute(); if (bin is not null) column.BinaryKind = bin.Kind; } else if (column.DataType == DbType.String || column.DataType == DbType.AnsiString || column.DataType == DbType.AnsiStringFixedLength || column.DataType == DbType.StringFixedLength) { column.MaxLength = 50; } ParseDefaultValue(column, property); if (property.FindAttribute() is not null) column.IsVersion = true; else { var maxLength = property.FindAttribute(); if (maxLength is not null) column.MaxLength = maxLength.Value; } var nullable = property.FindAttribute(); if (nullable is null) column.IsNullable = property.PropertyType.IsNullableType(); else column.IsNullable = nullable.IsNullable; columns.Add(column); } if (columns.Any()) result.Columns.AddRange(columns.OrderBy(f => f.Ordinal).ThenBy(f => f.Name)); return result; } private static SchemaAttribute ResolveSchemaAttribute(Type type) { var att = type.GetCustomAttribute() ?? new TableAttribute(); if (string.IsNullOrWhiteSpace(att.Name)) att.Name = type.Name.ToCamelCase(); if (string.IsNullOrEmpty(att.Schema)) att.Schema = SchemaAttribute.DefaultSchema; return att; } private static string ResolveColumnName(PropertyInfo property) { if (property.FindAttribute() is not MemberAttribute mapping || string.IsNullOrWhiteSpace(mapping.Member)) return property.Name.ToCamelCase(); return mapping.Member; } private static void ParseDefaultValue(SchemaColumn column, PropertyInfo property) { if (property.FindAttribute() is not DefaultAttribute def) return; var value = def.Value; if (def.Value is not null && def.Value.GetType().IsEnum) value = TypeConversion.Convert(def.Value, def.Value.GetType().GetEnumUnderlyingType()); column.DefaultValue = TypeConversion.Convert(value, CultureInfo.InvariantCulture); } }