Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 168 additions & 17 deletions src/Linq2GraphQL.Client/Utilities.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,185 @@
using Linq2GraphQL.Client.Visitors;
using System.Collections;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using Linq2GraphQL.Client.Visitors;

namespace Linq2GraphQL.Client;

public static class Utilities
{
private const int MaxArgumentDepth = 8;

/// <summary>Separates one argument from the next so that adjacent values cannot run together.</summary>
private const char Separator = (char)31;

/// <summary>
/// Identifies a set of field arguments so that the same field requested with different arguments becomes
/// several aliased selections. The result is derived from the argument values only, because the alias has
/// to be recomputed from the same values when the response is read back.
/// </summary>
/// <returns>An alias suffix, or null when there are no arguments to distinguish.</returns>
public static string GetArgumentsId(IEnumerable<object> objects)
{
if (objects == null) return null;
var objs = objects.Where(o => o != null);
if (!objs.Any()) return null;
if (objects == null)
{
return null;
}

unchecked
var values = objects.Where(o => o != null).ToList();
if (values.Count == 0)
{
var hash = 19;
foreach (var obj in objs)
{
hash = hash * 31 + obj.GetHashCode();
}
return null;
}

return hash.ToString().Replace("-", "_");
var builder = new StringBuilder();
foreach (var value in values)
{
AppendValue(builder, value, 0);
builder.Append(Separator);
}

return "_" + Hash(builder).ToString("x16");
}

public static void ParseExpression(Expression body, QueryNode parent)
{
var parameterVisitor = new ParameterVisitor(new MemberNode(null, null));
var topNode = parameterVisitor.ParseExpression(body);

topNode.PopulateChildQueryNodes(parent);

QueryExpressionVisitor.Parse(body, parent);
}

}
/// <summary>
/// Writes a value in a form that is identical for equal values and stable between processes, which
/// <see cref="object.GetHashCode" /> is not for reference types.
/// </summary>
private static void AppendValue(StringBuilder builder, object value, int depth)
{
switch (value)
{
case null:
builder.Append("null");
return;

case string text:
builder.Append('"').Append(text).Append('"');
return;

case bool flag:
builder.Append(flag ? "true" : "false");
return;

case Enum enumValue:
builder.Append(enumValue.GetType().FullName).Append('.').Append(enumValue.ToString("D"));
return;

case DateTime dateTime:
builder.Append(dateTime.ToString("O", CultureInfo.InvariantCulture));
return;

case DateTimeOffset dateTimeOffset:
builder.Append(dateTimeOffset.ToString("O", CultureInfo.InvariantCulture));
return;

case TimeSpan timeSpan:
builder.Append(timeSpan.ToString("c", CultureInfo.InvariantCulture));
return;

case CustomScalar scalar:
AppendValue(builder, scalar.Value, depth + 1);
return;

case IFormattable formattable:
// Numbers, Guid, DateOnly, TimeOnly.
builder.Append(formattable.ToString(null, CultureInfo.InvariantCulture));
return;

case IEnumerable items:
AppendItems(builder, items, depth);
return;

default:
AppendProperties(builder, value, depth);
return;
}
}

private static void AppendItems(StringBuilder builder, IEnumerable items, int depth)
{
if (depth >= MaxArgumentDepth)
{
builder.Append("[...]");
return;
}

builder.Append('[');
foreach (var item in items)
{
AppendValue(builder, item, depth + 1);
builder.Append(',');
}

builder.Append(']');
}

/// <summary>Input objects are compared by their contents, in a fixed property order.</summary>
private static void AppendProperties(StringBuilder builder, object value, int depth)
{
var type = value.GetType();
builder.Append(type.FullName);

if (depth >= MaxArgumentDepth)
{
builder.Append("{...}");
return;
}

builder.Append('{');

var properties = type
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(e => e.CanRead && e.GetIndexParameters().Length == 0)
.OrderBy(e => e.Name, StringComparer.Ordinal);

foreach (var property in properties)
{
builder.Append(property.Name).Append('=');

object propertyValue;
try
{
propertyValue = property.GetValue(value);
}
catch (Exception)
{
// A property that throws cannot contribute to the identity of the arguments.
builder.Append("<unreadable>;");
continue;
}

AppendValue(builder, propertyValue, depth + 1);
builder.Append(';');
}

builder.Append('}');
}

/// <summary>FNV-1a, for a 64 bit value that is the same in every process.</summary>
private static ulong Hash(StringBuilder builder)
{
const ulong offsetBasis = 14695981039346656037;
const ulong prime = 1099511628211;

var hash = offsetBasis;

foreach (var chunk in builder.GetChunks())
{
foreach (var character in chunk.Span)
{
hash = (hash ^ (byte)character) * prime;
hash = (hash ^ (byte)(character >> 8)) * prime;
}
}

return hash;
}
}
102 changes: 102 additions & 0 deletions src/Linq2GraphQL.Client/Visitors/ExpressionEvaluator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using System.Linq.Expressions;
using System.Reflection;

namespace Linq2GraphQL.Client.Visitors;

/// <summary>
/// Turns the argument expressions of a GraphQL field into the values that are sent as query variables.
/// Constants and captured locals are read directly; everything else falls back to compiling the
/// expression, which is comparatively expensive.
/// </summary>
internal static class ExpressionEvaluator
{
internal static object Evaluate(Expression expression)
{
return TryEvaluateFast(expression, out var value) ? value : Compile(expression);
}

private static bool TryEvaluateFast(Expression expression, out object value)
{
switch (expression)
{
case null:
value = null;
return true;

case ConstantExpression constant:
value = constant.Value;
return true;

case MemberExpression member:
return TryEvaluateMember(member, out value);

case UnaryExpression unary when IsTransparentConversion(unary):
return TryEvaluateFast(unary.Operand, out value);

default:
value = null;
return false;
}
}

private static bool TryEvaluateMember(MemberExpression member, out object value)
{
// A static member, or a captured local, which the compiler turns into a field on a closure object
// that is held by a ConstantExpression.
object target = null;

if (member.Expression != null && !TryEvaluateFast(member.Expression, out target))
{
value = null;
return false;
}

switch (member.Member)
{
case FieldInfo field:
value = field.GetValue(target);
return true;

case PropertyInfo property when property.CanRead && property.GetIndexParameters().Length == 0:
value = property.GetValue(target);
return true;

default:
value = null;
return false;
}
}

/// <summary>
/// True for a conversion that cannot change the value, so the operand can be read instead: boxing,
/// an upcast, or a lift into <see cref="Nullable{T}" />.
/// </summary>
private static bool IsTransparentConversion(UnaryExpression unary)
{
if (unary.NodeType != ExpressionType.Convert &&
unary.NodeType != ExpressionType.ConvertChecked &&
unary.NodeType != ExpressionType.TypeAs)
{
return false;
}

if (unary.Method != null)
{
return false;
}

var from = unary.Operand.Type;
var to = Nullable.GetUnderlyingType(unary.Type) ?? unary.Type;

return to.IsAssignableFrom(from);
}

private static object Compile(Expression expression)
{
var body = expression.Type == typeof(object)
? expression
: Expression.Convert(expression, typeof(object));

return Expression.Lambda<Func<object>>(body).Compile()();
}
}
Loading
Loading