Skip to content

Instantly share code, notes, and snippets.

@leandromoh
Last active February 23, 2022 18:05
Show Gist options
  • Save leandromoh/f85181d843cc6b3661d00cc84490d192 to your computer and use it in GitHub Desktop.
Save leandromoh/f85181d843cc6b3661d00cc84490d192 to your computer and use it in GitHub Desktop.
Validate dynamic schema on C#
public class CreateRequest
{
public string Name { get; set; }
public string CreatedBy { get; set; }
public JObject Schema { get; set; }
}
public class CreateRequestValidator : AbstractValidator<CreateRequest>
{
private readonly Regex _indexer = new Regex(@"\[\d+\]", RegexOptions.Compiled);
private readonly IReadOnlyDictionary<string, Type> _validSchemas;
public CreateOrderTemplateRequestValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.CreatedBy).NotEmpty();
var requestTypes = GetAllOrderRequestTypes(typeof(SendRequest));
var schemas = requestTypes.SelectMany(type => GetSchema(type));
var validSchemas = new Dictionary<string, Type>();
foreach (var (key, value) in schemas)
{
validSchemas[key] = value;
}
_validSchemas = validSchemas;
RuleFor(x => x.Schema).Custom((schema, context) =>
{
var flattened = schema
.Descendants()
.OfType<JValue>()
.ToDictionary(jv => jv.Path, jv => jv);
foreach (var (path, value) in flattened)
{
var adjustedPath = _indexer.Replace(path, "[]");
if (_validSchemas.TryGetValue(adjustedPath, out var expectedType))
{
try
{
value.ToObject(expectedType);
}
catch
{
context.AddFailure($"'{adjustedPath}' has type '{expectedType.Name}', value '{value}' is not valid.");
}
}
else
{
context.AddFailure($"'{adjustedPath}' is not a valid.");
}
}
});
}
private IReadOnlyDictionary<string, Type> GetSchema(Type type)
{
var fixture = new Fixture() { RepeatCount = 1 };
var instance = fixture.Create(type, new SpecimenContext(fixture));
var json = JsonConvert.SerializeObject(instance);
var schema = JObject.Parse(json);
var flattened = schema
.Descendants()
.OfType<JValue>()
.ToDictionary(jv => _indexer.Replace(jv.Path, "[]"), jv =>
{
var parts = jv.Path.Split(".");
Type type = instance.GetType();
foreach (var member in parts)
{
var isCollection = member.EndsWith("]");
if (isCollection)
{
var collectionMember = _indexer.Replace(member, string.Empty);
type = type.GetProperty(collectionMember).PropertyType.GenericTypeArguments.First();
}
else
{
type = type.GetProperty(member).PropertyType;
}
}
return type;
});
return flattened;
}
private static IEnumerable<Type> GetAllOrderRequestTypes(Type baseOrderRequest)
{
return baseOrderRequest.Assembly.GetTypes().Where(type => type.IsSubclassOf(baseOrderRequest));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment