Skip to content

Commit

Permalink
Merge pull request #485 from hypar-io/fix-484-type-loading
Browse files Browse the repository at this point in the history
catch type load errors while building type cache.
  • Loading branch information
wynged authored Jan 20, 2021
2 parents 7803d54 + 2eed89f commit 65d24aa
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.8.2
### Fixed
- Fixed #483 `Deserialization of profiles created in UpdateRepresentation`
- Fixed #484 `Failure to deserialize Model if any assembly can't be loaded.`


## 0.8.1
Expand Down
20 changes: 16 additions & 4 deletions Elements/src/Model.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,25 +182,37 @@ public string ToJson(bool indent = false)
/// </summary>
/// <param name="json">The JSON representing the model.</param>
/// <param name="errors">A collection of deserialization errors.</param>
public static Model FromJson(string json, List<string> errors = null)
/// <param name="forceTypeReload">Option to force reloading the inernal type cache. Use if you add types dynamically in your code.</param>
public static Model FromJson(string json, out List<string> errors, bool forceTypeReload = false)
{
// When user elements have been loaded into the app domain, they haven't always been
// loaded into the InheritanceConverter's Cache. This does have some overhead,
// but is useful here, at the Model level, to ensure user types are available.
JsonInheritanceConverter.RefreshAppDomainTypeCache();
errors = errors ?? new List<string>();
var deserializationErrors = new List<string>();
if (forceTypeReload)
{
JsonInheritanceConverter.RefreshAppDomainTypeCache(out var typeLoadErrors);
deserializationErrors.AddRange(typeLoadErrors);
}

var model = Newtonsoft.Json.JsonConvert.DeserializeObject<Model>(json, new JsonSerializerSettings()
{
Error = (sender, args) =>
{
errors.Add(args.ErrorContext.Error.Message);
deserializationErrors.Add(args.ErrorContext.Error.Message);
args.ErrorContext.Handled = true;
}
});
errors = deserializationErrors;
JsonInheritanceConverter.Elements.Clear();
return model;
}

public static Model FromJson(string json, bool forceTypeReload = false)
{
return FromJson(json, out _, forceTypeReload);
}

private List<Element> RecursiveGatherSubElements(object obj)
{
var elements = new List<Element>();
Expand Down
62 changes: 43 additions & 19 deletions Elements/src/Serialization/JSON/JsonInheritanceConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ private static Dictionary<string, Type> TypeCache
{
if (_typeCache == null)
{
_typeCache = BuildAppDomainTypeCache();
_typeCache = BuildAppDomainTypeCache(out _);
}
return _typeCache;
}
Expand Down Expand Up @@ -65,44 +65,68 @@ public JsonInheritanceConverter(string discriminator)
/// The type cache needs to contains all types that will have a discriminator.
/// This includes base types, like elements, and all derived types like Wall.
/// We use reflection to find all public types available in the app domain
/// that have a Newtonsoft.Json.JsonConverterAttribute whose converter type is the
/// that have a Newtonsoft.Json.JsonConverterAttribute whose converter type is the
/// Elements.Serialization.JSON.JsonInheritanceConverter.
/// </summary>
/// <returns>A dictionary containing all found types keyed by their full name.</returns>
private static Dictionary<string, Type> BuildAppDomainTypeCache()
private static Dictionary<string, Type> BuildAppDomainTypeCache(out List<string> failedAssemblyErrors)
{
var typeCache = new Dictionary<string, Type>();

var exportedTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(asm => asm.GetTypes().Where(t => t.IsPublic && t.IsClass));
var typesUsingInheritanceConverter = exportedTypes.Where(et =>
failedAssemblyErrors = new List<string>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var attrib = et.GetCustomAttribute<JsonConverterAttribute>();
if (attrib != null && attrib.ConverterType == typeof(JsonInheritanceConverter))
var types = Array.Empty<Type>();
try
{
return true;
types = assembly.GetTypes();
}
catch (ReflectionTypeLoadException)
{
failedAssemblyErrors.Add($"Failed to load assembly: {assembly.FullName}");
continue;
}
foreach (var t in types)
{
try
{
if (IsValidTypeForElements(t) && !typeCache.ContainsKey(t.FullName))
{
typeCache.Add(t.FullName, t);
}
}
catch (TypeLoadException)
{
failedAssemblyErrors.Add($"Failed to load type: {t.FullName}");
continue;
}
}
return false;
});
}

return typeCache;
}

var derivedTypes = typesUsingInheritanceConverter.SelectMany(baseType => exportedTypes.Where(exportedType => baseType.IsAssignableFrom(exportedType)));
foreach (var t in derivedTypes)
private static bool IsValidTypeForElements(Type t)
{
if (t.IsPublic && t.IsClass)
{
if (!typeCache.ContainsKey(t.FullName))
var attrib = t.GetCustomAttribute<JsonConverterAttribute>();
if (attrib != null && attrib.ConverterType == typeof(JsonInheritanceConverter))
{
typeCache.Add(t.FullName, t);
return true;
}
}

return typeCache;
return false;
}

/// <summary>
/// Call this method after assemblies have been loaded into the app
/// domain to ensure that the converter's cache is up to date.
/// </summary>
internal static void RefreshAppDomainTypeCache()
internal static void RefreshAppDomainTypeCache(out List<string> errors)
{
_typeCache = BuildAppDomainTypeCache();
_typeCache = BuildAppDomainTypeCache(out errors);
}

public static bool ElementwiseSerialization { get; set; } = false;
Expand Down Expand Up @@ -166,7 +190,7 @@ public override bool CanConvert(System.Type objectType)

public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
// The serialized value is an identifier, so the expectation is
// The serialized value is an identifier, so the expectation is
// that the element with that id has already been deserialized.
if (typeof(Element).IsAssignableFrom(objectType) && reader.Path.Split('.').Length == 1 && reader.Value != null)
{
Expand Down Expand Up @@ -202,7 +226,7 @@ public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type o
else
{
// Without a discriminator the call to GetObjectSubtype will
// fall through to returning either a [UserElement] type or
// fall through to returning either a [UserElement] type or
// the object type.
subtype = GetObjectSubtype(objectType, null, jObject);
}
Expand Down
15 changes: 6 additions & 9 deletions Elements/test/ModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,20 @@ public void SkipsUnknownTypesDuringDeserialization()
{
// We've changed an Elements.Beam to Elements.Foo
var modelStr = "{'Transform':{'Matrix':{'Components':[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0]}},'Elements':{'c6d1dc68-f800-47c1-9190-745b525ad569':{'discriminator':'Elements.Baz'}, '37f161d6-a892-4588-ad65-457b04b97236':{'discriminator':'Elements.Geometry.Profiles.WideFlangeProfile','d':1.1176,'tw':0.025908,'bf':0.4064,'tf':0.044958,'Perimeter':{'discriminator':'Elements.Geometry.Polygon','Vertices':[{'X':-0.2032,'Y':0.5588,'Z':0.0},{'X':-0.2032,'Y':0.51384199999999991,'Z':0.0},{'X':-0.012954,'Y':0.51384199999999991,'Z':0.0},{'X':-0.012954,'Y':-0.51384199999999991,'Z':0.0},{'X':-0.2032,'Y':-0.51384199999999991,'Z':0.0},{'X':-0.2032,'Y':-0.5588,'Z':0.0},{'X':0.2032,'Y':-0.5588,'Z':0.0},{'X':0.2032,'Y':-0.51384199999999991,'Z':0.0},{'X':0.012954,'Y':-0.51384199999999991,'Z':0.0},{'X':0.012954,'Y':0.51384199999999991,'Z':0.0},{'X':0.2032,'Y':0.51384199999999991,'Z':0.0},{'X':0.2032,'Y':0.5588,'Z':0.0}]},'Voids':null,'Id':'37f161d6-a892-4588-ad65-457b04b97236','Name':'W44x335'},'6b77d69a-204e-40f9-bc1f-ed84683e64c6':{'discriminator':'Elements.Material','Color':{'Red':0.60000002384185791,'Green':0.5,'Blue':0.5,'Alpha':1.0},'SpecularFactor':0.0,'GlossinessFactor':0.0,'Id':'6b77d69a-204e-40f9-bc1f-ed84683e64c6','Name':'steel'},'fd35bd2c-0108-47df-8e6d-42cc43e4eed0':{'discriminator':'Elements.Foo','Curve':{'discriminator':'Elements.Geometry.Arc','Center':{'X':0.0,'Y':0.0,'Z':0.0},'Radius':2.0,'StartAngle':0.0,'EndAngle':90.0},'StartSetback':0.25,'EndSetback':0.25,'Profile':'37f161d6-a892-4588-ad65-457b04b97236','Transform':{'Matrix':{'Components':[1.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.0,1.0,0.0]}},'Material':'6b77d69a-204e-40f9-bc1f-ed84683e64c6','Representation':{'SolidOperations':[{'discriminator':'Elements.Geometry.Solids.Sweep','Profile':'37f161d6-a892-4588-ad65-457b04b97236','Curve':{'discriminator':'Elements.Geometry.Arc','Center':{'X':0.0,'Y':0.0,'Z':0.0},'Radius':2.0,'StartAngle':0.0,'EndAngle':90.0},'StartSetback':0.25,'EndSetback':0.25,'Rotation':0.0,'IsVoid':false}]},'Id':'fd35bd2c-0108-47df-8e6d-42cc43e4eed0','Name':null}}}";
var errors = new List<string>();
var model = Model.FromJson(modelStr, errors);
var model = Model.FromJson(modelStr, out var errors);
foreach (var e in errors)
{
this._output.WriteLine(e);
}

// We expect three geometric elements,
// We expect three geometric elements,
// but the baz will not deserialize.
Assert.Equal(3, model.Elements.Count);
Assert.Equal(2, errors.Count);
}

/// <summary>
/// Test whether two models, containing user defined types, can be
/// Test whether two models, containing user defined types, can be
/// deserialized and merged into one model.
/// </summary>
[Fact(Skip = "ModelMerging")]
Expand Down Expand Up @@ -218,8 +217,7 @@ public void DeserializationConstructsWithMissingProperties()

// Remove the Location property
c.Property("Location").Remove();
var errors = new List<string>();
var newModel = Model.FromJson(obj.ToString(), errors);
var newModel = Model.FromJson(obj.ToString(), out var errors);
var newColumn = newModel.AllElementsOfType<Column>().First();
Assert.Equal(Vector3.Origin, newColumn.Location);
}
Expand All @@ -238,8 +236,7 @@ public void DeserializationSkipsNullProperties()

// Nullify a property.
c.Property("Location").Value = null;
var errors = new List<string>();
var newModel = Model.FromJson(obj.ToString(), errors);
var newModel = Model.FromJson(obj.ToString(), out var errors);
foreach (var e in errors)
{
this._output.WriteLine(e);
Expand All @@ -259,7 +256,7 @@ public void DeserializesToGeometricElementsWhenTypeIsUnknownAndRepresentationExi
model.AddElements(beam, column);
var json = model.ToJson(true);

// We want to test that unknown element types will still deserialize
// We want to test that unknown element types will still deserialize
// to geometric elements.
json = json.Replace("Elements.Beam", "Foo");
json = json.Replace("Elements.Column", "Bar");
Expand Down
3 changes: 1 addition & 2 deletions Elements/test/UserElementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,7 @@ public void CreateCustomElement()
this.Model.AddElement(ue);

var json = this.Model.ToJson();
var errors = new List<string>();
var newModel = Model.FromJson(json, errors);
var newModel = Model.FromJson(json, out var errors);
Assert.Empty(errors);
var newUe = newModel.AllElementsOfType<TestUserElement>().First();

Expand Down

0 comments on commit 65d24aa

Please sign in to comment.