Skip to content

Commit

Permalink
Add direct support for structs (#710)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored Nov 6, 2024
1 parent 545aa83 commit 2c2c578
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 11 deletions.
4 changes: 1 addition & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@
<PackageVersion Include="Parlot" Version="1.0.2" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
<PackageVersion Include="TimeZoneConverter" Version="6.1.0" />

<PackageVersion Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
<!-- Benchmarks -->
<PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
<PackageVersion Include="DotLiquid" Version="2.2.692" />
<PackageVersion Include="Liquid.NET" Version="0.10.0" />
<PackageVersion Include="Scriban" Version="5.11.0" />
<PackageVersion Include="Handlebars.Net" Version="2.1.6" />

<!-- Global Package References -->
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<GlobalPackageReference Include="PolySharp" Version="1.14.1" PrivateAssets="all" />

<!-- Testing -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
Expand Down
23 changes: 23 additions & 0 deletions Fluid.Tests/Domain/CustomStruct.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Fluid.Tests.Domain
{
public struct CustomStruct
{
public int X1 { get; set; }

private int _x2;

public int X2
{
get { return _x2; }
set { _x2 = value; }
}

private int x3;

public int X3
{
get { return x3; }
set { x3 = value; }
}
}
}
23 changes: 18 additions & 5 deletions Fluid.Tests/MemberAccessStrategyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,9 @@ public void ShouldResolveEnums()
[Fact]
public void ShouldResolveStructs()
{
// We can't create an open delegate on a Struc (dotnet limitation?), so instead create custom delegates
// https://sharplab.io/#v2:EYLgtghglgdgNAFxAJwK7wCYgNQB8ACATAAwCwAUEQIwX7EAE+VAdACLIQDusA5gNwUKANwjJ6ABwCSMAGYB7egF56CAJ7iApnJkAKAApzYCAJTMA4hoR7kczcjU6ARAA1HxgeRFiMhJROny5pYWCACylgAWchg6pgDCyBoQCBqsGgA2GjzJGjpqmto6+ACsADwGRnD0RgB8xu4UQA==

var options = new TemplateOptions();
options.MemberAccessStrategy.Register<Shape>();
options.MemberAccessStrategy.Register<Point>(nameof(Point.X), new DelegateAccessor<Point, int>((point, name, context) => point.X));
options.MemberAccessStrategy.Register<Point>(nameof(Point.Y), new DelegateAccessor<Point, int>((point, name, context) => point.Y));
options.MemberAccessStrategy.Register<Point>();

var circle = new Shape
{
Expand All @@ -273,6 +269,23 @@ public void ShouldResolveStructs()
var template = _parser.Parse("{{Coordinates.X}} {{Coordinates.Y}}");
Assert.Equal("1 2", template.Render(new TemplateContext(circle, options, false)));
}

[Fact]
public void ShouldFindBackingFields()
{
var options = new TemplateOptions();
options.MemberAccessStrategy.Register<CustomStruct>();

var s = new CustomStruct
{
X1 = 1,
X2 = 2,
X3 = 3
};

var template = _parser.Parse("{{X1}} {{X2}} {{X3}}");
Assert.Equal("1 2 3", template.Render(new TemplateContext(s, options, false)));
}
}

public class Class1
Expand Down
50 changes: 47 additions & 3 deletions Fluid/Accessors/PropertyInfoAccessor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Reflection;
using System.Reflection.Emit;

namespace Fluid.Accessors
{
Expand All @@ -8,16 +9,59 @@ public sealed class PropertyInfoAccessor : IMemberAccessor

public PropertyInfoAccessor(PropertyInfo propertyInfo)
{
var delegateType = typeof(Func<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
var d = propertyInfo.GetGetMethod().CreateDelegate(delegateType);
Delegate d;

if (!propertyInfo.DeclaringType.IsValueType)
{
var delegateType = typeof(Func<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
d = propertyInfo.GetGetMethod().CreateDelegate(delegateType);
}
else
{
// We can't create an open delegate on a struct (dotnet limitation?), so instead create custom delegates
// https://sharplab.io/#v2:EYLgtghglgdgNAFxAJwK7wCYgNQB8ACATAAwCwAUEQIwX7EAE+VAdACLIQDusA5gNwUKANwjJ6ABwCSMAGYB7egF56CAJ7iApnJkAKAApzYCAJTMA4hoR7kczcjU6ARAA1HxgeRFiMhJROny5pYWCACylgAWchg6pgDCyBoQCBqsGgA2GjzJGjpqmto6+ACsADwGRnD0RgB8xu4UQA==
// Instead we generate IL to access the backing field directly

d = GetGetter(propertyInfo.DeclaringType, propertyInfo.Name);
}

if (d == null)
{
_invoker = null;
}

var invokerType = typeof(Invoker<,>).MakeGenericType(propertyInfo.DeclaringType, propertyInfo.PropertyType);
_invoker = Activator.CreateInstance(invokerType, [d]) as IInvoker;
}

public object Get(object obj, string name, TemplateContext ctx)
{
return _invoker.Invoke(obj);
return _invoker?.Invoke(obj);
}

private static Delegate GetGetter(Type declaringType, string fieldName)
{
string[] names = [fieldName.ToLowerInvariant(), $"<{fieldName}>k__BackingField", "_" + fieldName.ToLowerInvariant()];

var field = names
.Select(n => declaringType.GetField(n, BindingFlags.Instance | BindingFlags.NonPublic))
.FirstOrDefault(x => x != null);

if (field == null)
{
return null;
}

var parameterTypes = new[] { typeof(object), declaringType };

var method = new DynamicMethod(fieldName + "Get", field.FieldType, parameterTypes, typeof(PropertyInfoAccessor).Module, true);

var emitter = method.GetILGenerator();
emitter.Emit(OpCodes.Ldarg_1);
emitter.Emit(OpCodes.Ldfld, field);
emitter.Emit(OpCodes.Ret);

return method.CreateDelegate(typeof(Func<,>).MakeGenericType(declaringType, field.FieldType));
}

private interface IInvoker
Expand Down
1 change: 1 addition & 0 deletions Fluid/Fluid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
<PackageReference Include="System.Text.Json" />
<PackageReference Include="Microsoft.CSharp" />
<PackageReference Include="System.Reflection.Emit.Lightweight" />
</ItemGroup>

<!-- Keep specific targets since it removes some dependencies -->
Expand Down

0 comments on commit 2c2c578

Please sign in to comment.