using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Runtime.Serialization; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace InABox.Core { public interface IColumn { string Property { get; } Expression Expression { get; } Type Type { get; } } public static class Column { public static IColumn Create(Type concrete, string property) { var type = typeof(Column<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type, property) as IColumn; return result!; } } public class Column : SerializableExpression, IColumn { public Type Type { get { if (Expression == null) throw new Exception(string.Format("Expression [{0}] may not be null!", Property)); if (Expression is IndexExpression) return DatabaseSchema.Property(typeof(T), Property).PropertyType; return Expression.Type; } } public bool IsEqualTo(string name) => !string.IsNullOrWhiteSpace(name) && string.Equals(Property, name); public bool IsEqualTo(Column column) => string.Equals(Property, column.Property); public bool IsParentOf(string name) => !string.IsNullOrWhiteSpace(name) && name.StartsWith(Property + "."); public Column() { } public Column(IProperty property) { Property = property.Name; Expression = property.Expression(); } public Column(Expression> expression) : base(expression) { //String[] parts = expression.ToString().Split(new String[] { "=>" }, StringSplitOptions.RemoveEmptyEntries); //string property = String.Join(".", parts.Last().Split('.').Skip(1)); //property = property.Replace("Convert(", "").Replace("(","").Replace(")", ""); Property = CoreUtils.GetFullPropertyName(expression, "."); } public Column(string property) { Property = property; var iprop = DatabaseSchema.Property(typeof(T), property); if (iprop != null) Expression = iprop.Expression(); else Expression = CoreUtils.CreateMemberExpression(typeof(T), property); } public Column Cast() where TNew: T { return new Column(Property); } public bool TryCast([NotNullWhen(true)] out Column? newColumn) { if(DatabaseSchema.Property(typeof(TNew), Property) is IProperty property) { newColumn = new Column(property); return true; } else { newColumn = null; return false; } } public string Property { get; private set; } public override void Deserialize(SerializationInfo info, StreamingContext context) { } public override void Serialize(SerializationInfo info, StreamingContext context) { } public static explicit operator Column(Column v) { var result = new Column(); var exp = CoreUtils.ExpressionToString(typeof(T), v.Expression, true); result.Expression = CoreUtils.StringToExpression(exp); result.Property = v.Property; return result; } public override string ToString() { var name = Expression.ToString().Replace("x => ", "").Replace("x.", ""); if (Expression.NodeType == System.Linq.Expressions.ExpressionType.Index) { var chars = name.SkipWhile(x => !x.Equals('[')).TakeWhile(x => !x.Equals(']')); name = string.Join("", chars).Replace("[", "").Replace("]", "").Replace("\"", ""); } return name; //return Property.ToString(); } } public interface IColumns : ISerializeBinary { int Count { get; } bool Any(); IEnumerable GetColumns(); IEnumerable ColumnNames(); Dictionary AsDictionary(); IColumns Add(string column); IColumns Add(IColumn column); IColumns Add(Expression> column); IColumns DefaultColumns(params ColumnType[] types); } public enum ColumnType { ExcludeVisible, /// /// Do not include in the columns. /// ExcludeID, IncludeOptional, IncludeForeignKeys, /// /// Include all columns that are accessible through entity links present in the root class. /// IncludeLinked, IncludeAggregates, IncludeFormulae, /// /// Include any columns that are a . /// IncludeUserProperties, /// /// Include all columns that are accessible through entity links, even nested ones. /// IncludeNestedLinks, IncludeEditable, /// /// Add all columns that are data actually on the entity, and not on entity links or calculated fields. /// DataColumns, /// /// Add all columns found. /// All } public static class Columns { public static IColumns Create(Type concrete) { if (!typeof(T).IsAssignableFrom(concrete)) throw new Exception($"Columns: {concrete.EntityName()} does not implement {typeof(T).EntityName()}"); var type = typeof(Columns<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type); return (result as IColumns)!; } public static IColumns Create(Type concrete) { var type = typeof(Columns<>).MakeGenericType(concrete); var result = Activator.CreateInstance(type) as IColumns; return result!; } public static IColumns Create(Type concrete, IEnumerable columns) { var type = typeof(Columns<>).MakeGenericType(concrete); var result = (IColumns)Activator.CreateInstance(type); foreach (var column in columns) result.Add(column); return result; } public static IColumns Create(Type concrete, params string[] columns) { var type = typeof(Columns<>).MakeGenericType(concrete); var result = (IColumns)Activator.CreateInstance(type); foreach (var column in columns) result.Add(column); return result; } } public class Columns : IColumns, IEnumerable> { private readonly List> columns; public Columns() { columns = new List>(); } /// /// Create a new , using as the internal list. /// /// /// is passed by reference and is stored as the internal list of the ; /// hence if one wishes to not be modified, one should pass a copy of the list. /// /// public Columns(List> columns) { this.columns = columns; } public Columns Cast() where TNew : T { var cols = new Columns(); foreach(var column in columns) { cols.Add(column.Cast()); } return cols; } /// /// Cast the columns to , keeping the columns that are found in both and . /// /// /// public Columns CastIntersection() { var cols = new Columns(); foreach(var column in columns) { if (column.TryCast(out var newColumn)) { cols.Add(newColumn); } } return cols; } public override string ToString() { return String.Join("; ", columns.Select(x => x.Property)); } public int IndexOf(String columnname) { return ColumnNames().ToList().IndexOf(columnname); } public int IndexOf(Expression> expression) { return ColumnNames().ToList().IndexOf(CoreUtils.GetFullPropertyName(expression,".")); } public Columns(params Expression>[] expressions) : this() { foreach (var expression in expressions) columns.Add(new Column(expression)); } public Columns(IEnumerable properties) : this() { foreach (var property in properties) columns.Add(new Column(property)); } public Column[] Items { get { return columns != null ? columns.ToArray() : new Column[] { }; } set { columns.Clear(); columns.AddRange(value); } } public int Count => columns.Count; public IEnumerable GetColumns() => columns; public bool Any() => columns.Any(); public bool Contains(string column) => Items.Any(x => x.IsEqualTo(column)); public bool Contains(Column column) => Items.Any(x => x.IsEqualTo(column)); public IColumns Add(string column) { if(CoreUtils.TryGetProperty(typeof(T), column, out var propertyInfo)) { if (!propertyInfo.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity)) && !propertyInfo.PropertyType.GetInterfaces().Any(x => x == typeof(IEntityLink))) { var exists = columns.Any(x => x.Expression.ToString().Replace("x.", "").Equals(column)); if (!exists) columns.Add(new Column(column)); } } else { var prop = DatabaseSchema.Property(typeof(T), column); if (prop != null) { var exists = columns.Any(x => x.Expression.Equals(prop.Expression())); if (!exists) columns.Add(new Column(column)); } } return this; } public Columns AddSubColumns(Expression> super, Columns? sub) { sub ??= CoreUtils.GetColumns(sub); var prefix = CoreUtils.GetFullPropertyName(super, ".") + "."; foreach(var column in sub.ColumnNames()) { columns.Add(new Column(prefix + column)); } return this; } public IColumns Add(Expression> expression) { return Add(CoreUtils.GetFullPropertyName(expression, ".")); } public Columns Add(Column column) { if(!columns.Any(x => x.Property.Equals(column.Property))) { columns.Add(column); } return this; } public IColumns Add(IColumn column) { if (column is Column col) return Add(col); return this; } public IEnumerable ColumnNames() { return Items.Select(c => c.Property); //List result = new List(); //foreach (var col in Items) // result.Add(col.Property); //return result; } public Dictionary AsDictionary() { Dictionary< String, Type> result = new Dictionary< String, Type>(); foreach (var column in Items) result[column.Property] = column.Type; return result; } public IColumns DefaultColumns(params ColumnType[] types) { return Default(types); } public Columns Add(IEnumerable> columns) { this.columns.AddRange(columns); return this; } public Columns Add(params string[] columnnames) { foreach (var name in columnnames) Add(name); return this; } public Columns Add(IEnumerable columnnames) { foreach (var name in columnnames) Add(name); return this; } public Columns Add(Expression> expression) { try { var property = CoreUtils.GetFullPropertyName(expression, "."); var exists = columns.Any(x => x.Expression.ToString().Replace("x.", "").Equals(property)); if (!exists) columns.Add(new Column(expression)); } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } return this; } public Columns Add(Expression> expression) { try { var property = CoreUtils.GetFullPropertyName(expression, "."); var exists = columns.Any(x => x.Expression.ToString().Replace("x.", "").Equals(property)); if (!exists) { columns.Add(new Column(property)); } } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } return this; } public Columns Remove(string column) { var col = new Column(column); columns.RemoveAll(x => x.ToString() == col.ToString()); return this; } public static explicit operator Columns(Columns vs) { var result = new Columns(); var items = vs.Items.Cast>().ToArray(); result.Items = items; //List> cols = new List>(); //foreach (var v in vs.Items) // cols.Add((Column)v); //result.Items = cols.ToArray(); return result; } public Columns Default(params ColumnType[] types) { columns.Clear(); var props = DatabaseSchema.Properties(typeof(T)) .Where(x => x.Setter() != null) .OrderBy(x => x.PropertySequence()).ToList(); if (types.Contains(ColumnType.All)) { foreach (var prop in props) columns.Add(new Column(prop.Name)); return this; } else if (types.Contains(ColumnType.DataColumns)) { foreach(var prop in props) { if (prop.IsCalculated) { continue; } if (prop.HasParentEntityLink()) { if(prop.Parent?.HasParentEntityLink() == true || !prop.Name.EndsWith(".ID")) { continue; } } columns.Add(new Column(prop.Name)); } if(types.Length == 1) { return this; } } if (typeof(T).IsSubclassOf(typeof(Entity)) && !types.Contains(ColumnType.ExcludeID)) columns.Add(new Column("ID")); for (int iCol = 0; iCol < props.Count; iCol++) { var prop = props[iCol]; var bOK = true; var bIsForeignKey = false; var bNullEditor = prop.Editor is NullEditor; if (prop is CustomProperty) { if (!types.Any(x => x.Equals(ColumnType.IncludeUserProperties))) bOK = false; else columns.Add(new Column(prop.Name)); } if (bOK) if (prop.Name.Contains(".") && !(prop is CustomProperty)) { var ancestors = prop.Name.Split('.'); var anclevel = 2; for (var i = 1; i < ancestors.Length; i++) { var ancestor = string.Join(".", ancestors.Take(i)); var ancprop = CoreUtils.GetProperty(typeof(T), ancestor); bNullEditor = bNullEditor || ancprop.GetCustomAttribute() != null; if (ancprop.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity))) anclevel++; else if (ancprop.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))) { if (types.Contains(ColumnType.IncludeLinked) || types.Contains(ColumnType.IncludeForeignKeys)) { if (types.Contains(ColumnType.IncludeNestedLinks) || ancestors.Length <= anclevel) { if (prop.Name.EndsWith(".ID") && types.Contains(ColumnType.IncludeForeignKeys)) { bIsForeignKey = true; break; } if (!types.Contains(ColumnType.IncludeLinked)) { bOK = false; break; } } else { bOK = false; break; } } else { bOK = false; break; } } } } if (bOK) { var visible = prop.Editor != null ? bNullEditor ? Visible.Hidden : prop.Editor.Visible : Visible.Optional; var editable = prop.Editor != null ? bNullEditor ? Editable.Hidden : prop.Editor.Editable : Editable.Enabled; bOK = (types.Any(x => x.Equals(ColumnType.IncludeForeignKeys)) && bIsForeignKey) || (!types.Any(x => x.Equals(ColumnType.ExcludeVisible)) && visible.Equals(Visible.Default)) || (types.Any(x => x.Equals(ColumnType.IncludeOptional)) && visible.Equals(Visible.Optional)) || (types.Any(x => x.Equals(ColumnType.IncludeEditable)) && editable.ColumnVisible()); } var property = bOK ? DatabaseSchema.Property(typeof(T), prop.Name) : null; if (property is StandardProperty) { if (bOK && !types.Any(x => x.Equals(ColumnType.IncludeAggregates))) bOK = CoreUtils.GetProperty(typeof(T), prop.Name).GetCustomAttribute() == null; if (bOK && !types.Any(x => x.Equals(ColumnType.IncludeFormulae))) bOK = CoreUtils.GetProperty(typeof(T), prop.Name).GetCustomAttribute() == null; } if (bOK && !columns.Any(x => string.Equals(x.Property?.ToUpper(), prop.Name?.ToUpper()))) { if (prop.Editor is LookupEditor le) { if (le.OtherColumns != null) { var prefix = String.Join(".",prop.Name.Split('.').Reverse().Skip(1).Reverse()); foreach (var col in le.OtherColumns) { String newcol = prefix + "." + col.Key; if (!columns.Any(x => String.Equals(newcol, x.Property))) columns.Add(new Column(newcol)); } } } if (!columns.Any(x => String.Equals(prop.Name, x.Property))) columns.Add(new Column(prop.Name)); } } return this; } #region Binary Serialization public void SerializeBinary(CoreBinaryWriter writer) { writer.Write(columns.Count); foreach(var column in columns) { writer.Write(column.Property); } } public void DeserializeBinary(CoreBinaryReader reader) { columns.Clear(); var nColumns = reader.ReadInt32(); for(int i = 0; i < nColumns; ++i) { var property = reader.ReadString(); columns.Add(new Column(property)); } } #endregion public IEnumerator> GetEnumerator() { return columns.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return columns.GetEnumerator(); } } public static class ColumnsExtensions { public static Columns ToColumns(this IEnumerable> columns) { return new Columns(columns.ToList()); } } public static class ColumnSerialization { /// /// Inverse of . /// /// /// public static Columns? ReadColumns(this CoreBinaryReader reader) { if (reader.ReadBoolean()) { var columns = new Columns(); columns.DeserializeBinary(reader); return columns; } return null; } /// /// Inverse of . /// /// /// public static void Write(this CoreBinaryWriter writer, Columns? columns) { if (columns is null) { writer.Write(false); } else { writer.Write(true); columns.SerializeBinary(writer); } } } public class ColumnJsonConverter : JsonConverter { public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { if(value is null) { writer.WriteNull(); return; } var property = (CoreUtils.GetPropertyValue(value, "Expression") as Expression) ?? throw new Exception("'Column.Expression' may not be null"); var prop = CoreUtils.ExpressionToString(value.GetType().GenericTypeArguments[0], property, true); var name = CoreUtils.GetPropertyValue(value, "Property") as string; writer.WriteStartObject(); writer.WritePropertyName("$type"); writer.WriteValue(value.GetType().FullName); writer.WritePropertyName("Expression"); writer.WriteValue(prop); writer.WritePropertyName("Property"); writer.WriteValue(name); writer.WriteEndObject(); } public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.Null) return null; var data = new Dictionary(); while (reader.TokenType != JsonToken.EndObject && reader.Read()) if (reader.Value != null) { var key = reader.Value.ToString(); reader.Read(); if (String.Equals(key, "$type")) objectType = Type.GetType(reader.Value.ToString()) ?? objectType; else data[key] = reader.Value; } var prop = data["Property"].ToString(); var result = Activator.CreateInstance(objectType, prop); return result; } public override bool CanConvert(Type objectType) { if (objectType.IsConstructedGenericType) { var ot = objectType.GetGenericTypeDefinition(); var tt = typeof(Column<>); if (ot == tt) return true; } return false; } } }