using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace InABox.Core { public static class DatabaseSchema { // {className: {propertyName: property}} private static ConcurrentDictionary> _properties = new ConcurrentDictionary>(); private struct SubObject { public Type PropertyType { get; set; } public string Name { get; set; } public Action Setter { get; set; } public Func Getter { get; set; } public SubObject(Type objectType, Type propertyType, string name) { PropertyType = propertyType; Name = name; Setter = Expressions.Setter(objectType, name); Getter = Expressions.Getter(objectType, name); } } private static ConcurrentDictionary> _subObjects { get; } = new ConcurrentDictionary>(); private static IReadOnlyCollection? GetSubObjectDefs(Type t) { CheckProperties(t); return _subObjects.GetValueOrDefault(t); } public static IEnumerable GetSubObjects(BaseObject obj) { var objs = GetSubObjectDefs(obj.GetType()); if(objs is null) { yield break; } foreach (var subObjectDef in objs) { var subObj = subObjectDef.Getter(obj); if(subObj is BaseObject bObj) { yield return bObj; } } } public static void InitializeSubObjects(BaseObject obj) { var objs = GetSubObjectDefs(obj.GetType()); if(objs is null) { return; } foreach (var subObjectDef in objs) { var subObj = (Activator.CreateInstance(subObjectDef.PropertyType) as ISubObject)!; subObjectDef.Setter(obj, subObj); subObj.SetLinkedParent(obj); subObj.SetLinkedPath(subObjectDef.Name); } } // For synchronisation purposes, we register sub objects in bulk, removing the need for nested concurrent dictionaries. private static void RegisterSubObjects(Type objectType, IEnumerable> objects) { if (!_subObjects.TryGetValue(objectType, out var subObjects)) { subObjects = ImmutableList.Empty; } // No synchronisation issues, since the original collection is not being modified, just the entry in the concurrent dictionary is updated. _subObjects[objectType] = subObjects.AddRange( objects.Where(x => !subObjects.Any(y => x.Item1 == y.PropertyType && x.Item2 == y.Name)) .Select(x => new SubObject(objectType, x.Item1, x.Item2))); } public static void Clear() { _properties = new ConcurrentDictionary>(); } private static void RegisterProperties(Type master, Type type, string prefix, StandardProperty? parent, Dictionary newProperties) { try { var properties = CoreUtils.PropertyList( type, x => !x.PropertyType.IsInterface && (x.DeclaringType.IsSubclassOf(typeof(BaseObject)) || x.DeclaringType.IsSubclassOf(typeof(BaseEditor))) ); var subObjects = new List>(); foreach (var prop in properties) { var name = prefix + prop.Name; if (newProperties.ContainsKey(name)) continue; var getMethod = prop.GetGetMethod(); if (getMethod is null || !getMethod.IsPublic || getMethod.IsStatic) continue; BaseEditor? editor; if (parent != null && parent.HasEditor && parent.Editor is NullEditor) { editor = parent.Editor; } else { editor = prop.GetEditor(); } var captionAttr = prop.GetCustomAttribute(); var subCaption = captionAttr != null ? captionAttr.Text : prop.Name; var path = captionAttr == null || captionAttr.IncludePath; // If no caption attribute, we should always include the path var caption = parent?.Caption ?? ""; // We default to the parent caption if subCaption doesn't exist if (!string.IsNullOrWhiteSpace(subCaption)) { if (!string.IsNullOrWhiteSpace(caption) && path) { caption = $"{caption} {subCaption}"; } else { caption = subCaption; } } // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent var page = parent?.Page; var sequence = parent?.Sequence; var sequenceAttribute = prop.GetCustomAttribute(); if (sequenceAttribute != null) { if (string.IsNullOrWhiteSpace(page)) { page = sequenceAttribute.Page; } sequence = sequenceAttribute.Sequence; } editor = editor?.Clone() as BaseEditor; if (editor != null) { editor.Page = page; editor.Caption = caption; editor.EditorSequence = (int)(sequence ?? 999); editor.Security = prop.GetCustomAttributes().ToArray(); } bool required = false; if (parent == null || parent.Required) { required = prop.GetCustomAttribute() != null; } LoggablePropertyAttribute? loggable = null; if (parent == null || parent.Loggable != null) { loggable = prop.GetCustomAttribute(); } var newProperty = new StandardProperty { _class = master, Name = name, PropertyType = prop.PropertyType, Editor = editor ?? new NullEditor(), HasEditor = editor != null, Caption = caption, Sequence = sequence ?? 999, Page = page ?? "", Required = required, Loggable = loggable, Parent = parent, Property = prop }; var parentWithEditable = newProperty.GetOuterParent(x => x is StandardProperty st && st.Property.GetCustomAttribute() != null); if(parentWithEditable != null) { var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute()!; newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable); } else if(prop.GetCustomAttribute() is EditableAttribute attr) { newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable); } var isLink = prop.PropertyType.HasInterface(); var isEnclosedEntity = prop.PropertyType.HasInterface(); var isBaseEditor = prop.PropertyType.HasInterface(); if ((isLink || isEnclosedEntity) && !isBaseEditor) { subObjects.Add(new Tuple(prop.PropertyType, prop.Name)); } if (isLink || isEnclosedEntity || isBaseEditor) { RegisterProperties(master, prop.PropertyType, name + ".", newProperty, newProperties); } newProperties.Add(newProperty.Name, newProperty); } RegisterSubObjects(type, subObjects); // I don't actually think we need this, since PropertyList gives us properties of our parent. //if (type.IsSubclassOf(typeof(BaseObject)) && type.BaseType != typeof(BaseObject)) // RegisterProperties(master, type.BaseType, prefix, parent, newProperties); } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } private static void RegisterProperties(Type type) { var properties = new Dictionary(); RegisterProperties(type, type, "", null, properties); if(properties.Count > 0) { RegisterProperties(type, properties.Values); } } public static object? DefaultValue(Type type) { if (type.IsValueType) return Activator.CreateInstance(type); if (type.Equals(typeof(string))) return ""; return null; } private static readonly object _updatelock = new object(); private static void RegisterProperties(Type master, IEnumerable toAdd) { if (!_properties.TryGetValue(master, out var properties)) { properties = ImmutableDictionary.Empty; } var newDict = properties.ToDictionary(x => x.Key, x => x.Value); foreach(var prop in toAdd) { newDict[prop.Name] = prop; } _properties[master] = newDict.ToImmutableDictionary(); } public static void RegisterProperty(IProperty entry) { var type = entry.ClassType; if (type is null) return; if (!_properties.TryGetValue(type, out var properties)) { properties = ImmutableDictionary.Empty; } _properties[type] = properties.Add(entry.Name, entry); } public static void Load(CustomProperty[] customproperties) { var perType = customproperties.GroupBy(x => x.ClassType); foreach(var group in perType) { if (group.Key is null) continue; RegisterProperties(group.Key, group); } } private static ImmutableDictionary? CheckProperties(Type type) { try { var props = _properties.GetValueOrDefault(type); var hasprops = props?.Any(x => x.Value is StandardProperty) == true; if (!hasprops && type.IsSubclassOf(typeof(BaseObject))) { RegisterProperties(type); return _properties.GetValueOrDefault(type); } else { return props; } } catch (Exception e) { // This seems to be an intermittent error "Collection has been modified" when checking if the Dictionary has been populated already // I've added a .ToArray() to concretise the list, but who knows? Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {type.EntityName()}\n{e.Message}\n{e.StackTrace}"); return null; } } private static IEnumerable PropertiesInternal(Type type) => CheckProperties(type)?.Select(x => x.Value) ?? Enumerable.Empty(); /// /// Return the standard property list for ; this includes nested properties. /// /// /// public static IEnumerable Properties(Type type) => PropertiesInternal(type).Where(x => !x.IsParent); /// /// Return all properties that are defined directly on , and does not follow sub objects, but rather includes the /// sub object property itself. /// /// /// public static IEnumerable RootProperties(Type type) => PropertiesInternal(type).Where(x => x.Parent is null); /// /// Return the standard property list for ; this includes nested properties. /// /// /// public static IEnumerable Properties() => Properties(typeof(T)); public static IProperty? Property(Type type, string name) { var prop = CheckProperties(type)?.GetValueOrDefault(name); // Walk up the inheritance tree, see if an ancestor has this property if (prop == null && type.BaseType != null) prop = Property(type.BaseType, name); return prop; } public static IProperty? Property(Expression> expression) => Property(typeof(T), CoreUtils.GetFullPropertyName(expression, ".")); public static void InitializeObject(TObject entity) where TObject : BaseObject { entity.UserProperties.Load(Properties(entity.GetType()) .Where(x => x is CustomProperty) .Select(x => new KeyValuePair(x.Name, DefaultValue(x.PropertyType)))); } } }