You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Data/Schema/SchemaService.cs

250 lines
6.5 KiB

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<SchemaService> logger)
{
Middleware = new AsyncLazy<ImmutableList<ISchemaMiddleware>>(middleware.Query<ISchemaMiddleware>());
Logger = logger;
}
private ILogger<SchemaService> Logger { get; }
private AsyncLazy<ImmutableList<ISchemaMiddleware>> Middleware { get; }
public async Task Synchronize(List<Type>? 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;
}
/*
* 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);
}
}
/// <summary>
/// Determines if the entity supports persistence. Virtual entities does not support persistence which
/// means they don't have physical storage.
/// </summary>
/// <param name="entityType">The type of the entity to check for persistence.</param>
/// <returns><c>true</c> if the entity supports persistence, <c>false</c> otherwise.</returns>
private static bool IsPersistent(Type entityType)
{
var persistence = entityType.GetCustomAttribute<PersistenceAttribute>();
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<SchemaColumn>();
foreach (var property in properties)
{
if (!property.CanWrite)
continue;
if (property.FindAttribute<PersistenceAttribute>() is PersistenceAttribute pa && pa.IsVirtual)
continue;
var column = new SchemaColumn(result)
{
Name = ResolveColumnName(property),
DataType = DataExtensions.ToDbType(property)
};
var pk = property.FindAttribute<PrimaryKeyAttribute>();
if (pk != null)
{
column.IsPrimaryKey = true;
column.IsIdentity = pk.Identity;
column.IsUnique = true;
column.IsIndex = true;
}
var idx = property.FindAttribute<IndexAttribute>();
if (idx != null)
{
column.IsIndex = true;
column.IsUnique = idx.Unique;
column.Index = idx.Name;
}
var ordinal = property.FindAttribute<OrdinalAttribute>();
if (ordinal != null)
column.Ordinal = ordinal.Value;
if (column.DataType == DbType.Decimal
|| column.DataType == DbType.VarNumeric)
{
var numeric = property.FindAttribute<NumericAttribute>();
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<DateAttribute>();
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<BinaryAttribute>();
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<ETagAttribute>() is not null)
column.IsVersion = true;
else
{
var maxLength = property.FindAttribute<LengthAttribute>();
if (maxLength is not null)
column.MaxLength = maxLength.Value;
}
var nullable = property.FindAttribute<NullableAttribute>();
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<SchemaAttribute>() ?? 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<MemberAttribute>() 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<DefaultAttribute>() 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<string>(value, CultureInfo.InvariantCulture);
}
}