1
0
mirror of synced 2024-11-15 01:33:51 +00:00
PettingZoo/PettingZoo.Tapeti/AssemblyParser/AssemblyParser.cs
Mark van Renswoude c75ea0cc62 Implemented DataAnnotation based validation for Tapeti messages
Fixed class name missing when selecting from Tapeti class
Added Filter to class selection dialog
Fixed or removed some minor todo's
2022-01-03 15:04:00 +01:00

163 lines
5.9 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Runtime.Loader;
using Newtonsoft.Json;
using PettingZoo.Core.Generator;
using PettingZoo.Core.Validation;
namespace PettingZoo.Tapeti.AssemblyParser
{
public class AssemblyParser : IDisposable
{
private readonly string[] extraAssembliesPaths;
private AssemblyLoadContext? loadContext;
public AssemblyParser(params string[] extraAssembliesPaths)
{
this.extraAssembliesPaths = extraAssembliesPaths;
}
public void Dispose()
{
loadContext?.Unload();
GC.SuppressFinalize(this);
}
public IEnumerable<IClassTypeExample> GetExamples(Stream assemblyStream)
{
if (loadContext == null)
{
/*
Using the MetadataLoadContext introduces extra complexity since types can not be compared
(a string from the loaded assembly does not equal our typeof(string) for example).
So instead we'll use a regular AssemblyLoadContext. Not ideal, and will probably cause other side-effects
if we're not careful, but I don't feel like writing a full metadata parser right now.
If you have a better idea, it's open-source! :-)
*/
loadContext = new AssemblyLoadContext(null, true);
foreach (var extraAssembly in extraAssembliesPaths.SelectMany(p => Directory.Exists(p)
? Directory.GetFiles(p, "*.dll")
: Enumerable.Empty<string>()))
{
loadContext.LoadFromAssemblyPath(extraAssembly);
}
}
var assembly = loadContext.LoadFromStream(assemblyStream);
foreach (var type in assembly.GetTypes().Where(t => t.IsClass))
yield return new TypeExample(type);
}
private class TypeExample : IClassTypeExample, IValidatingExample
{
public string AssemblyName => type.Assembly.GetName().Name ?? "";
public string? Namespace => type.Namespace;
public string ClassName => type.Name;
private readonly Type type;
private bool validationInitialized;
private bool validationAvailable;
public TypeExample(Type type)
{
this.type = type;
}
public string Generate()
{
var serialized = TypeToJObjectConverter.Convert(type);
return serialized.ToString(Formatting.Indented);
}
public bool CanValidate()
{
return InitializeValidation();
}
public void Validate(string payload)
{
if (!InitializeValidation())
return;
// Json exceptions are already handled by the PayloadEditorViewModel
var deserialized = JsonConvert.DeserializeObject(payload, type);
if (deserialized == null)
throw new PayloadValidationException(AssemblyParserStrings.JsonDeserializationNull, null);
try
{
var validationContext = new ValidationContext(deserialized);
Validator.ValidateObject(deserialized, validationContext, true);
}
catch (ValidationException e)
{
var members = string.Join(", ", e.ValidationResult.MemberNames);
if (!string.IsNullOrEmpty(members))
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrorsMembers, members, e.ValidationResult.ErrorMessage), null);
throw new PayloadValidationException(string.Format(AssemblyParserStrings.ValidationErrors, e.ValidationResult.ErrorMessage), null);
}
}
private bool InitializeValidation()
{
if (validationInitialized)
return validationAvailable;
// Attempt to create an instance (only works if all dependencies are present, which is not yet
// guaranteed because we aren't fetching NuGet dependencies yet). We're giving it a fighting chance
// by referencing Tapeti.Annotations, System.ComponentModel.Annotations and Tapeti.DataAnnotations.Extensions in
// this class library.
try
{
var instance = Activator.CreateInstance(type);
if (instance != null)
{
// Attributes are only evaluated when requested, so call validation once to give it a better chance to
// detect if we'll be able to validate the message
try
{
var validationContext = new ValidationContext(instance);
Validator.ValidateObject(instance, validationContext, true);
validationAvailable = true;
}
catch (ValidationException)
{
// The fact that it validated is good enough, this can be expected with an empty object
validationAvailable = true;
}
catch (Exception)
{
// ignored
}
}
}
catch (Exception)
{
// No go, try to create an example without validation
}
validationInitialized = true;
return validationAvailable;
}
}
}
}