diff --git a/src/JsonApiSerializer/JsonApiSerializer.csproj b/src/JsonApiSerializer/JsonApiSerializer.csproj
index 3c856dd..a7523a2 100644
--- a/src/JsonApiSerializer/JsonApiSerializer.csproj
+++ b/src/JsonApiSerializer/JsonApiSerializer.csproj
@@ -2,15 +2,15 @@
netstandard1.0;netstandard1.5;net45True
- 1.4.0
+ 1.5.0Alex DaviesCodecutoutJsonApiSerializer supports configurationless serializing and deserializing objects into the json:api format (http://jsonapi.org).https://github.com/codecutout/JsonApiSerializer/blob/master/LICENSEhttps://github.com/codecutout/JsonApiSerializerjsonapiserializer jsonapi json:api json.net serialization deserialization jsonapi.net
- 1.4.0.0
- 1.4.0.0
+ 1.5.0.0
+ 1.5.0.0
diff --git a/src/JsonApiSerializer/JsonConverters/DocumentRootConverter.cs b/src/JsonApiSerializer/JsonConverters/DocumentRootConverter.cs
index cabbd82..746cbf4 100644
--- a/src/JsonApiSerializer/JsonConverters/DocumentRootConverter.cs
+++ b/src/JsonApiSerializer/JsonConverters/DocumentRootConverter.cs
@@ -16,9 +16,14 @@ internal class DocumentRootConverter : JsonConverter
{
public static bool CanConvertStatic(Type objectType)
{
- return TypeInfoShim.GetInterfaces(objectType.GetTypeInfo())
- .Select(x => x.GetTypeInfo())
- .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDocumentRoot<>));
+ var typeInfo = objectType.GetTypeInfo();
+
+ var interfaces = TypeInfoShim.GetInterfaces(typeInfo)
+ .Select(x => x.GetTypeInfo());
+ if (typeInfo.IsInterface)
+ interfaces = new[] { typeInfo }.Concat(interfaces);
+
+ return interfaces.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IDocumentRoot<>));
}
public override bool CanConvert(Type objectType)
diff --git a/src/JsonApiSerializer/Util/ExpressionUtil.cs b/src/JsonApiSerializer/Util/ExpressionUtil.cs
new file mode 100644
index 0000000..5f924b2
--- /dev/null
+++ b/src/JsonApiSerializer/Util/ExpressionUtil.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace JsonApiSerializer.Util
+{
+ internal class ExpressionUtil
+ {
+ ///
+ /// Provides utility for evaluating expressions.
+ ///
+ private class ExpressionEvaluator : ExpressionVisitor
+ {
+ ///
+ /// Determines whether an expression can be evaluated locally
+ ///
+ /// The expression to evaluate locally.
+ ///
+ /// true if this instance can be evaluated locallay; otherwise, false.
+ ///
+ public static bool TryEvaluate(Expression exp, out T result)
+ {
+ bool shouldBox = false;
+
+ //check the the expression will evaluate to hte type we want to get out
+ if (exp.Type != typeof(T) && typeof(T).GetTypeInfo().IsAssignableFrom(exp.Type.GetTypeInfo()))
+ {
+ if (typeof(T) == typeof(object))
+ {
+ //we can box our non-object to coerce the result as an object
+ shouldBox = true;
+ }
+ else
+ {
+ //our types are just not compatible so stop here
+ result = default(T);
+ return false;
+ }
+ }
+
+ //if its a constant we can avoid compiling a lambda and get the value directly
+ if (exp is ConstantExpression constExp)
+ {
+ result = (T)constExp.Value;
+ return true;
+ }
+
+ //check to see if we can evaluate
+ if (!ExpressionEvaluator.CanEvaluate(exp))
+ {
+ result = default(T);
+ return false;
+ }
+
+ try
+ {
+ if (shouldBox)
+ exp = Expression.Convert(exp, typeof(object));
+
+ var lambdaExp = Expression.Lambda>(exp);
+ var lambda = lambdaExp.Compile();
+ result = lambda();
+ return true;
+ }
+ catch (Exception)
+ {
+ //we shouldnt get here, but if we do its because something is wrong
+ //with the expression, which indictes we cannot evaluate it locally
+ result = default(T);
+ return false;
+ }
+ }
+
+
+ ///
+ /// Determines whether an expression can be evaluated locally
+ ///
+ /// The expression to evaluate locally.
+ ///
+ /// true if this instance can be evaluated locallay; otherwise, false.
+ ///
+ public static bool CanEvaluate(Expression exp)
+ {
+ var evaluator = new ExpressionEvaluator();
+ evaluator.Visit(exp);
+ return evaluator.CanEvaluateLocally;
+ }
+
+ public bool CanEvaluateLocally { get; private set; } = true;
+
+ private ExpressionEvaluator() { }
+
+ public override Expression Visit(Expression node)
+ {
+ //if its using a parameter it means we can not evaulate the locally
+ CanEvaluateLocally &= node.NodeType != ExpressionType.Parameter;
+ if (!CanEvaluateLocally)
+ return node;
+ return base.Visit(node);
+ }
+ }
+
+ ///
+ /// Provides utility for reading a path of property access
+ ///
+ private class PropertyPathReader : ExpressionVisitor
+ {
+ public static bool TryReadPropertyPath(Expression exp, out IEnumerable path)
+ {
+ var ppr = new PropertyPathReader();
+ try
+ {
+ ppr.Visit(exp);
+ path = ppr.Path;
+ return ppr.Success;
+ }
+ catch
+ {
+ path = default(IEnumerable);
+ return false;
+ }
+ }
+
+ public List Path { get; private set; } = new List();
+ public bool Success { get; private set; } = true;
+
+ private PropertyPathReader() { }
+
+ public override Expression Visit(Expression node)
+ {
+ switch (node)
+ {
+ case MemberExpression me:
+ Visit(me.Expression);
+ Path.Add(node);
+ return node;
+ case BinaryExpression be when node.NodeType == ExpressionType.ArrayIndex:
+ Visit(be.Left);
+ Path.Add(node);
+ return node;
+ case MethodCallExpression mce:
+ Visit(mce.Object);
+ Path.Add(node);
+ return node;
+ case IndexExpression ie:
+ Visit(ie.Object);
+ Path.Add(node);
+ return node;
+ case ParameterExpression pe:
+ Path.Add(node);
+ return node;
+ default:
+ Success = false;
+ return node;
+ }
+ }
+ }
+
+ ///
+ /// Determines if an expression represents an index access
+ ///
+ /// The expression to check
+ /// The indexer used in the in the index access
+ /// true if expression was an index access; otherwise, false.
+ public static bool IsIndexAccess(Expression exp, out Expression indexExpression)
+ {
+ //Expression can have several ways to indicate an index access, so have to check them all
+ switch (exp)
+ {
+ case BinaryExpression be when be.NodeType == ExpressionType.ArrayIndex:
+ indexExpression = be.Right;
+ return true;
+ case MethodCallExpression mce when IsDefaultMemberGet(mce) && mce.Arguments.Count == 1:
+ indexExpression = mce.Arguments[0];
+ return true;
+ case IndexExpression ie when ie.Arguments.Count == 1:
+ indexExpression = ie.Arguments[0];
+ return true;
+ default:
+ indexExpression = default(Expression);
+ return false;
+ }
+ }
+
+ ///
+ /// Detemrines if an expression in a default member get
+ ///
+ ///
+ ///
+ public static bool IsDefaultMemberGet(MethodCallExpression methodCallExp)
+ {
+ var defaultMembers = TypeInfoShim.GetDefaultMembers(methodCallExp.Object.Type.GetTypeInfo());
+ //check method call has the same name as the get default member
+ //have to check on names as some cases the Methods are the same yet not equal
+ return defaultMembers.OfType().Any(pi => pi.GetMethod.Name == methodCallExp.Method.Name);
+ }
+
+ ///
+ /// Attempts to evaluate the expression locally to produce a result.
+ ///
+ /// the result type.
+ /// The expression to evaluate locally.
+ /// The output of the expression
+ /// true if expression was evaluated; otherwise, false.
+ public static bool TryEvaluate(Expression exp, out T result)
+ {
+ return ExpressionEvaluator.TryEvaluate(exp, out result);
+ }
+
+ ///
+ /// Attemps to read a property path defined in an expression in the order it appears
+ /// typically a property path is represented outside-in e.g. (((x).Author).Name) is Name->Author->x
+ /// while this method will read the path inside-out e.g. (((x).Author).Name) as x->Author->Name
+ ///
+ /// The expression to read the property path
+ /// Each property access in order of parameter to final property
+ /// true if path could be determined; otherwise, false.
+ public static bool TryReadPropertyPath(Expression exp, out IEnumerable propertyPath)
+ {
+ return PropertyPathReader.TryReadPropertyPath(exp, out propertyPath);
+ }
+
+ ///
+ /// Tries to parse a path in the form 'x.Authors[3].FirstName' into a lambda expression selecting the same property
+ ///
+ /// The type of the intiial parameter in the path
+ /// A stirng path to the property to be selected
+ /// The lambda expression selecting the property
+ /// If true an exception is thrown rather than a return value
+ /// true if path could be parsed; otherwise, false.
+ public static bool TryParsePath(Type modelRoot, string path, out LambdaExpression expression, bool throwOnError)
+ {
+ expression = null;
+ if (modelRoot == null)
+ return throwOnError ? throw new ArgumentNullException(nameof(modelRoot)) : false;
+ if (string.IsNullOrWhiteSpace(path))
+ return throwOnError ? throw new ArgumentException(nameof(path)) : false;
+
+ var pathParts = path.Split('.');
+ var parameterRegex = new Regex(@"(?:^(?\w+))");
+ var pathPartRegex = new Regex(string.Join("|", new[]
+ {
+ @"(?:\.(?\w+))",
+ @"(?:\[(?\d+)\])",
+ @"(?:\['(?\w+)'\])",
+ @"(?:\[""(?\w+)""\])",
+ }));
+
+ var parameterMatch = parameterRegex.Match(path);
+ if (!parameterMatch.Success)
+ return throwOnError ? throw new Exception($"{nameof(path)} '{path}' must begin with a parameter e.g. 'x.Author.Name'") : false;
+
+ var paramExp = Expression.Parameter(modelRoot, parameterMatch.Groups["parameter"].Value);
+ Expression exp = paramExp;
+
+ foreach (var pathPartMatch in pathPartRegex.Matches(path).Cast())
+ {
+ var property = pathPartMatch.Groups["property"];
+ if (property.Success)
+ {
+ var propInfo = TypeInfoShim.GetProperty(exp.Type.GetTypeInfo(), property.Value);
+ exp = Expression.MakeMemberAccess(exp, propInfo);
+ continue;
+ }
+
+ var intIndex = pathPartMatch.Groups["intIndex"];
+ if (intIndex.Success)
+ {
+ var indexProperty = TypeInfoShim.GetDefaultMembers(exp.Type.GetTypeInfo())
+ .OfType()
+ .FirstOrDefault(x => x.SetMethod.GetParameters().Length == 2);
+ if (indexProperty == null)
+ return throwOnError ? throw new Exception($"Unable to find default index member for type '{exp.Type.GetTypeInfo()}'") : false;
+ exp = Expression.MakeIndex(exp, indexProperty, new[] { Expression.Constant(int.Parse(intIndex.Value)) });
+ continue;
+ }
+
+ var stringIndex = pathPartMatch.Groups["stringIndex"];
+ if (stringIndex.Success)
+ {
+ var indexProperty = TypeInfoShim.GetDefaultMembers(exp.Type.GetTypeInfo())
+ .OfType()
+ .FirstOrDefault(x => x.SetMethod.GetParameters().Length == 2);
+ if (indexProperty == null)
+ return throwOnError ? throw new Exception($"Unable to find default index member for type '{exp.Type.GetTypeInfo()}'") : false;
+ exp = Expression.MakeIndex(exp, indexProperty, new[] { Expression.Constant(stringIndex.Value) });
+ continue;
+ }
+ }
+ expression = Expression.Lambda(exp, paramExp);
+ return true;
+ }
+ }
+}
diff --git a/src/JsonApiSerializer/Util/SourcePointer.cs b/src/JsonApiSerializer/Util/SourcePointer.cs
new file mode 100644
index 0000000..47138cc
--- /dev/null
+++ b/src/JsonApiSerializer/Util/SourcePointer.cs
@@ -0,0 +1,209 @@
+using JsonApiSerializer.ContractResolvers;
+using JsonApiSerializer.JsonApi;
+using JsonApiSerializer.JsonApi.WellKnown;
+using JsonApiSerializer.JsonConverters;
+using Newtonsoft.Json.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace JsonApiSerializer.Util
+{
+ public class SourcePointer
+ {
+ ///
+ /// Utility class to write and encode source pointers
+ ///
+ private class SourcePointerBuilder
+ {
+ private readonly StringBuilder _sb = new StringBuilder();
+
+ public SourcePointerBuilder Append(string value)
+ {
+ var encoded = value
+ .Replace("~", "~0")
+ .Replace("/", "~1")
+ .Replace(@"\", @"\\")
+ .Replace("\"", "\\");
+ _sb.Append("/").Append(encoded);
+ return this;
+ }
+
+ public SourcePointerBuilder Append(MemberInfo memberInfo, JsonApiSerializerSettings settings)
+ {
+ var contractResolver = settings.ContractResolver as JsonApiContractResolver;
+
+ //Try and determine what hte json name for the member is, that is the one we want to output
+ var declaringContract = contractResolver.ResolveContract(memberInfo.DeclaringType) as JsonObjectContract;
+ var jsonProp = declaringContract?.Properties?.FirstOrDefault(x => x.UnderlyingName == memberInfo.Name);
+
+ this.Append(jsonProp?.PropertyName ?? memberInfo.Name);
+ return this;
+ }
+
+ public override string ToString()
+ {
+ return _sb.ToString();
+ }
+ }
+
+ ///
+ /// Determines the equivalent JsonApi source pointer for a given model property
+ ///
+ /// The type of the paramter in the model path
+ /// The path to the model property. The first parameter must be the parameter e.g. x.Authors[2].FirstName
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ public static string FromModel(string modelPath, JsonApiSerializerSettings settings)
+ {
+ return FromModel(typeof(TRoot), modelPath, settings);
+ }
+
+ ///
+ /// Determines the equivalent JsonApi source pointer for a given model property
+ ///
+ /// The type of the paramter in the model path
+ /// The path to the model property. The first parameter must be the parameter e.g. x.Authors[2].FirstName
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ public static string FromModel(Type modelRoot, string modelPath, JsonApiSerializerSettings settings)
+ {
+ ExpressionUtil.TryParsePath(modelRoot, modelPath, out var modelPathExpression, throwOnError: true);
+ SourcePointer.TryFromModel(modelPathExpression, settings, out string sourcePointer, throwOnError: true);
+ return sourcePointer;
+ }
+
+ ///
+ /// Determines the equivalent JsonApi source pointer for a given model property
+ ///
+ /// The type of the paramter in the model path
+ /// Expression selecting the property
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ public static string FromModel(Expression> modelExpressionPath, JsonApiSerializerSettings settings)
+ {
+ var success = SourcePointer.TryFromModel((LambdaExpression)modelExpressionPath, settings, out string sourcePointer, throwOnError: true);
+ return sourcePointer;
+ }
+
+
+
+ ///
+ /// Tries to determine the equivalent JsonApi source pointer for a given model property
+ ///
+ /// The type of the paramter in the model path
+ /// Expression selecting property
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ /// true if source pointer could be determined; otherwise, false
+ public static bool TryFromModel(Type modelRoot, string modelPath, JsonApiSerializerSettings settings, out string sourcePointer)
+ {
+ sourcePointer = default(string);
+ return ExpressionUtil.TryParsePath(modelRoot, modelPath, out var modelPathExpression, throwOnError: false)
+ && SourcePointer.TryFromModel(modelPathExpression, settings, out sourcePointer, throwOnError: false);
+ }
+
+ ///
+ /// Tries to determine the equivalent JsonApi source pointer for a given model property
+ ///
+ /// The type of the paramter in the model path
+ /// The path to the model property. The first parameter must be the parameter e.g. x.Authors[2].FirstName
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ /// true if source pointer could be determined; otherwise, false
+ public static bool TryFromModel(string modelPath, JsonApiSerializerSettings settings, out string sourcePointer)
+ {
+ return TryFromModel(typeof(TRoot), modelPath, settings, out sourcePointer);
+ }
+
+ ///
+ /// Tries to determine the equivalent JsonApi source pointer for a given model property
+ ///
+ ///
+ /// Expression selecting the property
+ /// The JsonApiSerializerSettings used to during deserialization
+ /// JsonApi source pointer
+ /// true if source pointer could be determined; otherwise, false
+ public static bool TryFromModel(Expression> modelExpressionPath, JsonApiSerializerSettings settings, out string sourcePointer)
+ {
+ return SourcePointer.TryFromModel((LambdaExpression)modelExpressionPath, settings, out sourcePointer, throwOnError: false);
+ }
+
+ private static bool TryFromModel(LambdaExpression modelExpressionPath, JsonApiSerializerSettings settings, out string jsonApiPath, bool throwOnError)
+ {
+ jsonApiPath = null;
+ if (modelExpressionPath.Parameters.Count != 1)
+ return throwOnError ? throw new NotSupportedException("Only single parameter member selection expression can be resolved") : false;
+
+ if (!ExpressionUtil.TryReadPropertyPath(modelExpressionPath.Body, out var expressionList))
+ return throwOnError ? throw new Exception($"Unable to process expresion '{modelExpressionPath}' as a property path") : false;
+
+ var pointer = new SourcePointerBuilder();
+ var contractResolver = (JsonApiContractResolver)settings.ContractResolver;
+
+ //if we didnt start at a document root, add the 'data' to the path
+ var isDocRoot = DocumentRootConverter.CanConvertStatic(modelExpressionPath.Parameters[0].Type);
+ if (!isDocRoot)
+ pointer.Append("data");
+
+ foreach (var exp in expressionList)
+ {
+ if(exp is ParameterExpression)
+ {
+ //initial parameters are not expression in the jsonApiPath
+ continue;
+ }
+ else if(ExpressionUtil.IsIndexAccess(exp, out var indexExpression))
+ {
+ if (!ExpressionUtil.TryEvaluate