|
@@ -1,6 +1,7 @@
|
|
|
using System;
|
|
|
using System.Collections.Concurrent;
|
|
|
using System.Collections.Generic;
|
|
|
+using System.Collections.Immutable;
|
|
|
using System.Linq;
|
|
|
using System.Linq.Expressions;
|
|
|
using System.Reflection;
|
|
@@ -10,8 +11,8 @@ namespace InABox.Core
|
|
|
public static class DatabaseSchema
|
|
|
{
|
|
|
// {className: {propertyName: property}}
|
|
|
- private static Dictionary<string, Dictionary<string, IProperty>> _properties
|
|
|
- = new Dictionary<string, Dictionary<string, IProperty>>();
|
|
|
+ private static ConcurrentDictionary<Type, ImmutableDictionary<string, IProperty>> _properties
|
|
|
+ = new ConcurrentDictionary<Type, ImmutableDictionary<string, IProperty>>();
|
|
|
|
|
|
private struct SubObject
|
|
|
{
|
|
@@ -32,9 +33,9 @@ namespace InABox.Core
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static Dictionary<Type, List<SubObject>> _subObjects { get; } = new Dictionary<Type, List<SubObject>>();
|
|
|
+ private static ConcurrentDictionary<Type, ImmutableList<SubObject>> _subObjects { get; } = new ConcurrentDictionary<Type, ImmutableList<SubObject>>();
|
|
|
|
|
|
- private static List<SubObject>? GetSubObjectDefs(Type t)
|
|
|
+ private static IReadOnlyCollection<SubObject>? GetSubObjectDefs(Type t)
|
|
|
{
|
|
|
CheckProperties(t);
|
|
|
return _subObjects.GetValueOrDefault(t);
|
|
@@ -59,7 +60,7 @@ namespace InABox.Core
|
|
|
|
|
|
public static void InitializeSubObjects(BaseObject obj)
|
|
|
{
|
|
|
- var objs = GetSubObjectDefs(obj.GetType())?.ToArray();
|
|
|
+ var objs = GetSubObjectDefs(obj.GetType());
|
|
|
if(objs is null)
|
|
|
{
|
|
|
return;
|
|
@@ -73,20 +74,18 @@ namespace InABox.Core
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private static void RegisterSubObject(Type objectType, Type propertyType, string name)
|
|
|
+ // For synchronisation purposes, we register sub objects in bulk, removing the need for nested concurrent dictionaries.
|
|
|
+ private static void RegisterSubObjects(Type objectType, IEnumerable<Tuple<Type, string>> objects)
|
|
|
{
|
|
|
- lock (_updatelock)
|
|
|
+ if (!_subObjects.TryGetValue(objectType, out var subObjects))
|
|
|
{
|
|
|
- if (!_subObjects.TryGetValue(objectType, out var properties))
|
|
|
- {
|
|
|
- properties = new List<SubObject>();
|
|
|
- _subObjects[objectType] = properties;
|
|
|
- }
|
|
|
- if (!properties.Any(x => x.Name == name && x.PropertyType == propertyType))
|
|
|
- {
|
|
|
- properties.Add(new SubObject(objectType, propertyType, name));
|
|
|
- }
|
|
|
+ subObjects = ImmutableList<SubObject>.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)));
|
|
|
}
|
|
|
|
|
|
private static void RegisterProperties(Type master, Type type, string prefix, StandardProperty? parent)
|
|
@@ -101,129 +100,131 @@ namespace InABox.Core
|
|
|
(x.DeclaringType.IsSubclassOf(typeof(BaseObject))
|
|
|
|| x.DeclaringType.IsSubclassOf(typeof(BaseEditor)))
|
|
|
);
|
|
|
- var classProps = _properties.GetValueOrDefault(classname);
|
|
|
+ var classProps = _properties.GetValueOrDefault(master);
|
|
|
+
|
|
|
+ var subObjects = new List<Tuple<Type, string>>();
|
|
|
+ var newProperties = new List<IProperty>();
|
|
|
|
|
|
foreach (var prop in properties)
|
|
|
{
|
|
|
- var name = string.IsNullOrWhiteSpace(prefix) ? prop.Name : string.Format("{0}.{1}", prefix, prop.Name);
|
|
|
+ var name = prefix.IsNullOrWhiteSpace() ? prop.Name : $"{prefix}.{prop.Name}";
|
|
|
var p = classProps?.GetValueOrDefault(name);
|
|
|
- if (p == null)
|
|
|
+ if (p == null && !prop.GetAccessors(true)[0].IsStatic)
|
|
|
{
|
|
|
- var isstatic = prop.GetAccessors(true)[0].IsStatic;
|
|
|
- if (!isstatic)
|
|
|
+ BaseEditor? editor;
|
|
|
+ if (parent != null && parent.HasEditor && parent.Editor is NullEditor)
|
|
|
+ {
|
|
|
+ editor = parent.Editor;
|
|
|
+ }
|
|
|
+ else
|
|
|
{
|
|
|
- BaseEditor? editor;
|
|
|
- if (parent != null && parent.HasEditor && parent.Editor is NullEditor)
|
|
|
+ editor = prop.GetEditor();
|
|
|
+ }
|
|
|
+
|
|
|
+ var captionAttr = prop.GetCustomAttribute<Caption>();
|
|
|
+ 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)
|
|
|
{
|
|
|
- editor = parent.Editor;
|
|
|
+ caption = $"{caption} {subCaption}";
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- editor = prop.GetEditor();
|
|
|
- }
|
|
|
-
|
|
|
- var captionAttr = prop.GetCustomAttribute<Caption>();
|
|
|
- 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;
|
|
|
- }
|
|
|
+ caption = subCaption;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent
|
|
|
- // The same goes for sequence
|
|
|
- var page = parent?.Page;
|
|
|
- var sequence = parent?.Sequence;
|
|
|
- if (string.IsNullOrWhiteSpace(page))
|
|
|
+ // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent
|
|
|
+ // The same goes for sequence
|
|
|
+ var page = parent?.Page;
|
|
|
+ var sequence = parent?.Sequence;
|
|
|
+ if (string.IsNullOrWhiteSpace(page))
|
|
|
+ {
|
|
|
+ var sequenceAttribute = prop.GetCustomAttribute<EditorSequence>();
|
|
|
+ if (sequenceAttribute != null)
|
|
|
{
|
|
|
- var sequenceAttribute = prop.GetCustomAttribute<EditorSequence>();
|
|
|
- if (sequenceAttribute != null)
|
|
|
- {
|
|
|
- page = sequenceAttribute.Page;
|
|
|
- sequence = sequenceAttribute.Sequence;
|
|
|
- }
|
|
|
+ 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 = editor?.Clone() as BaseEditor;
|
|
|
+ if (editor != null)
|
|
|
+ {
|
|
|
+ editor.Page = page;
|
|
|
+ editor.Caption = caption;
|
|
|
+ editor.EditorSequence = (int)(sequence ?? 999);
|
|
|
|
|
|
- editor.Security = prop.GetCustomAttributes<SecurityAttribute>().ToArray();
|
|
|
- }
|
|
|
+ editor.Security = prop.GetCustomAttributes<SecurityAttribute>().ToArray();
|
|
|
+ }
|
|
|
|
|
|
- bool required = false;
|
|
|
- if (parent == null || parent.Required)
|
|
|
- {
|
|
|
- required = prop.GetCustomAttribute<RequiredColumnAttribute>() != null;
|
|
|
- }
|
|
|
+ bool required = false;
|
|
|
+ if (parent == null || parent.Required)
|
|
|
+ {
|
|
|
+ required = prop.GetCustomAttribute<RequiredColumnAttribute>() != null;
|
|
|
+ }
|
|
|
|
|
|
- LoggablePropertyAttribute? loggable = null;
|
|
|
- if (parent == null || parent.Loggable != null)
|
|
|
- {
|
|
|
- loggable = prop.GetCustomAttribute<LoggablePropertyAttribute>();
|
|
|
- }
|
|
|
+ LoggablePropertyAttribute? loggable = null;
|
|
|
+ if (parent == null || parent.Loggable != null)
|
|
|
+ {
|
|
|
+ loggable = prop.GetCustomAttribute<LoggablePropertyAttribute>();
|
|
|
+ }
|
|
|
|
|
|
- var newProperty = new StandardProperty
|
|
|
- {
|
|
|
- _class = master,
|
|
|
- //Class = classname,
|
|
|
- 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<EditableAttribute>() != null);
|
|
|
- if(parentWithEditable != null)
|
|
|
- {
|
|
|
- var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute<EditableAttribute>()!;
|
|
|
- newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
|
|
|
- }
|
|
|
- else if(prop.GetCustomAttribute<EditableAttribute>() is EditableAttribute attr)
|
|
|
- {
|
|
|
- newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
|
|
|
- }
|
|
|
+ var newProperty = new StandardProperty
|
|
|
+ {
|
|
|
+ _class = master,
|
|
|
+ //Class = classname,
|
|
|
+ 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<EditableAttribute>() != null);
|
|
|
+ if(parentWithEditable != null)
|
|
|
+ {
|
|
|
+ var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute<EditableAttribute>()!;
|
|
|
+ newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
|
|
|
+ }
|
|
|
+ else if(prop.GetCustomAttribute<EditableAttribute>() is EditableAttribute attr)
|
|
|
+ {
|
|
|
+ newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
|
|
|
+ }
|
|
|
|
|
|
- var isLink = prop.PropertyType.GetInterfaces().Contains(typeof(IEntityLink));
|
|
|
- var isEnclosedEntity = prop.PropertyType.GetInterfaces().Contains(typeof(IEnclosedEntity));
|
|
|
- var isBaseEditor = prop.PropertyType.Equals(typeof(BaseEditor)) ||
|
|
|
- prop.PropertyType.IsSubclassOf(typeof(BaseEditor));
|
|
|
- if ((isLink || isEnclosedEntity) && !isBaseEditor)
|
|
|
- {
|
|
|
- RegisterSubObject(type, prop.PropertyType, prop.Name);
|
|
|
- }
|
|
|
+ var isLink = typeof(IEntityLink).IsAssignableFrom(prop.PropertyType);
|
|
|
+ var isEnclosedEntity = typeof(IEnclosedEntity).IsAssignableFrom(prop.PropertyType);
|
|
|
+ var isBaseEditor = prop.PropertyType.Equals(typeof(BaseEditor)) ||
|
|
|
+ prop.PropertyType.IsSubclassOf(typeof(BaseEditor));
|
|
|
+ if ((isLink || isEnclosedEntity) && !isBaseEditor)
|
|
|
+ {
|
|
|
+ subObjects.Add(new Tuple<Type, string>(prop.PropertyType, prop.Name));
|
|
|
+ }
|
|
|
|
|
|
- if (isLink || isEnclosedEntity || isBaseEditor)
|
|
|
- {
|
|
|
- RegisterProperties(master, prop.PropertyType, name, newProperty);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- RegisterProperty(newProperty);
|
|
|
- }
|
|
|
+ if (isLink || isEnclosedEntity || isBaseEditor)
|
|
|
+ {
|
|
|
+ RegisterProperties(master, prop.PropertyType, name, newProperty);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ newProperties.Add(newProperty);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ RegisterProperties(master, newProperties);
|
|
|
+ RegisterSubObjects(type, subObjects);
|
|
|
+
|
|
|
if (type.IsSubclassOf(typeof(BaseObject)))
|
|
|
RegisterProperties(master, type.BaseType, prefix, parent);
|
|
|
}
|
|
@@ -248,55 +249,74 @@ namespace InABox.Core
|
|
|
}
|
|
|
|
|
|
private static readonly object _updatelock = new object();
|
|
|
+
|
|
|
+ private static void RegisterProperties(Type master, IEnumerable<IProperty> toAdd)
|
|
|
+ {
|
|
|
+ if (!_properties.TryGetValue(master, out var properties))
|
|
|
+ {
|
|
|
+ properties = ImmutableDictionary<string, IProperty>.Empty;
|
|
|
+ }
|
|
|
+ _properties[master] = properties.AddRange(
|
|
|
+ toAdd.Select(x => new KeyValuePair<string, IProperty>(x.Name, x)));
|
|
|
+ }
|
|
|
|
|
|
public static void RegisterProperty(IProperty entry)
|
|
|
{
|
|
|
- lock (_updatelock)
|
|
|
+ var type = entry.ClassType;
|
|
|
+ if (type is null) return;
|
|
|
+
|
|
|
+ if (!_properties.TryGetValue(type, out var properties))
|
|
|
{
|
|
|
- if (!_properties.ContainsKey(entry.Class))
|
|
|
- _properties[entry.Class] = new Dictionary<string, IProperty>();
|
|
|
- _properties[entry.Class][entry.Name] = entry;
|
|
|
+ properties = ImmutableDictionary<string, IProperty>.Empty;
|
|
|
}
|
|
|
+ _properties[type] = properties.Add(entry.Name, entry);
|
|
|
}
|
|
|
|
|
|
public static void Load(CustomProperty[] customproperties)
|
|
|
{
|
|
|
- foreach (var prop in customproperties)
|
|
|
- RegisterProperty(prop);
|
|
|
+ var perType = customproperties.GroupBy(x => x.ClassType);
|
|
|
+ foreach(var group in perType)
|
|
|
+ {
|
|
|
+ if (group.Key is null) continue;
|
|
|
+
|
|
|
+ RegisterProperties(group.Key, group);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private static void CheckProperties(Type type)
|
|
|
+ private static ImmutableDictionary<string, IProperty>? CheckProperties(Type type)
|
|
|
{
|
|
|
- var entityName = type.EntityName();
|
|
|
try
|
|
|
{
|
|
|
- var props = _properties.GetValueOrDefault(entityName);
|
|
|
+ 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: {entityName}\n{e.Message}\n{e.StackTrace}");
|
|
|
+ Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {type.EntityName()}\n{e.Message}\n{e.StackTrace}");
|
|
|
+ return null;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
public static IEnumerable<IProperty> Properties(Type type)
|
|
|
{
|
|
|
- CheckProperties(type);
|
|
|
- var entityName = type.EntityName();
|
|
|
- return _properties.GetValueOrDefault(entityName)?.Select(x => x.Value) ?? Array.Empty<IProperty>();
|
|
|
+ return CheckProperties(type)?.Select(x => x.Value) ?? Enumerable.Empty<IProperty>();
|
|
|
}
|
|
|
|
|
|
public static IProperty? Property(Type type, string name)
|
|
|
{
|
|
|
- CheckProperties(type);
|
|
|
- var entityName = type.EntityName();
|
|
|
-
|
|
|
- var prop = _properties.GetValueOrDefault(entityName)?.GetValueOrDefault(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)
|
|
@@ -309,7 +329,7 @@ namespace InABox.Core
|
|
|
{
|
|
|
entity.UserProperties.Load(Properties(entity.GetType())
|
|
|
.Where(x => x is CustomProperty)
|
|
|
- .Select(x => new KeyValuePair<string, object?>(x.Name, DefaultValue(x.PropertyType))));
|
|
|
+ .Select(x => new KeyValuePair<string, object?>(x.Name, x.PropertyType)));
|
|
|
}
|
|
|
}
|
|
|
}
|