Skip to content

Commit

Permalink
Add backend project that has an improved ResourceContractResolver tha…
Browse files Browse the repository at this point in the history
…t generates types of reserved hal properties to help Swagger generators explore types properly
  • Loading branch information
Laszlo Zold committed Mar 22, 2022
1 parent 254c273 commit 6c9e89c
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 1 deletion.
22 changes: 22 additions & 0 deletions HalKit.Backend/HalKit.Backend.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>default</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageProjectUrl>https://github.com/viagogo/HalKit</PackageProjectUrl>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Reflection.Emit" Version="4.7.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\HalKit\HalKit.csproj" />
</ItemGroup>

<Import Project="..\Version.props" />
</Project>
80 changes: 80 additions & 0 deletions HalKit.Backend/Json/HalKitTypeBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using Newtonsoft.Json.Serialization;

namespace HalKit.Backend.Json
{
/// <summary>
/// Generates type run-time, based on a sample code found at https://stackoverflow.com/questions/3862226/how-to-dynamically-create-a-class
/// </summary>
public static class HalKitTypeBuilder
{
public static Type CompileResultType(string typeName, IReadOnlyDictionary<string, JsonProperty> properties)
{
var tb = GetTypeBuilder(typeName);
var constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

foreach (var field in properties)
{
CreateProperty(tb, field.Key, field.Value.PropertyType);
}

var objectType = tb.CreateTypeInfo().AsType();
return objectType;
}

private static TypeBuilder GetTypeBuilder(string typeName)
{
var an = new AssemblyName("DynamicHalKit");
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder tb = moduleBuilder.DefineType(typeName,
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout,
null);
return tb;
}

private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
{
var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

var propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
var getPropertyMethodBuilder = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
var getIl = getPropertyMethodBuilder.GetILGenerator();

getIl.Emit(OpCodes.Ldarg_0);
getIl.Emit(OpCodes.Ldfld, fieldBuilder);
getIl.Emit(OpCodes.Ret);

var setPropertyMethodBuilder =
tb.DefineMethod("set_" + propertyName,
MethodAttributes.Public |
MethodAttributes.SpecialName |
MethodAttributes.HideBySig,
null, new[] { propertyType });

var setIl = setPropertyMethodBuilder.GetILGenerator();
var modifyProperty = setIl.DefineLabel();
var exitSet = setIl.DefineLabel();

setIl.MarkLabel(modifyProperty);
setIl.Emit(OpCodes.Ldarg_0);
setIl.Emit(OpCodes.Ldarg_1);
setIl.Emit(OpCodes.Stfld, fieldBuilder);

setIl.Emit(OpCodes.Nop);
setIl.MarkLabel(exitSet);
setIl.Emit(OpCodes.Ret);

propertyBuilder.SetGetMethod(getPropertyMethodBuilder);
propertyBuilder.SetSetMethod(setPropertyMethodBuilder);
}
}
}
197 changes: 197 additions & 0 deletions HalKit.Backend/Json/TypedResourceContractResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using HalKit.Json;
using HalKit.Models.Response;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

namespace HalKit.Backend.Json
{
/// <summary>
/// Resolves a <see cref="JsonContract"/> for a given <see cref="Resource"/>.
/// Generates proper type for the reserved JSON properties of "_links" and "_embedded"
/// through reflection so that Swashbuckle.AspNetCore or other tools can properly work out the schema
/// </summary>
public class TypedResourceContractResolver : DefaultContractResolver
{
private static readonly Dictionary<Type, IList<JsonProperty>> ContractPropertiesByType
= new Dictionary<Type, IList<JsonProperty>>();

private readonly JsonSerializerSettings _settings;

/// <summary>
/// Initializes a new instance of the <see cref="TypedResourceContractResolver"/>
/// class.
/// </summary>
public TypedResourceContractResolver(JsonSerializerSettings settings)
{
_settings = settings;
}

protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var allProperties = base.CreateProperties(type, memberSerialization);
if (!typeof(Resource).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()))
{
return allProperties;
}

IList<JsonProperty> props;
if (!ContractPropertiesByType.TryGetValue(type, out props))
{
var contractProperties = new List<JsonProperty>();
var embeddedPropertyMap = new Dictionary<string, JsonProperty>();
var linksPropertyMap = new Dictionary<string, JsonProperty>();
foreach (var property in allProperties)
{
var isLinkOrEmbeddedProperty = false;
var attributes = property.AttributeProvider.GetAttributes(false);
foreach (var attribute in attributes)
{
var embeddedAttribute = attribute as EmbeddedAttribute;
if (embeddedAttribute != null)
{
isLinkOrEmbeddedProperty = true;
embeddedPropertyMap.Add(embeddedAttribute.Rel, property);
}

var relAttribute = attribute as RelAttribute;
if (relAttribute != null)
{
isLinkOrEmbeddedProperty = true;
linksPropertyMap.Add(relAttribute.Rel, property);
}
}

// This doesn't have a Rel or Embedded attribute so it's just a normal property
if (!isLinkOrEmbeddedProperty)
{
contractProperties.Add(property);
}
}

if (linksPropertyMap.Any())
{
contractProperties.Add(CreateReservedHalJsonProperty(type, "_links", linksPropertyMap));
}

if (embeddedPropertyMap.Any())
{
contractProperties.Add(CreateReservedHalJsonProperty(type, "_embedded", embeddedPropertyMap));
}

if (!ContractPropertiesByType.TryGetValue(type, out props))
{
lock (ContractPropertiesByType)
{
if (!ContractPropertiesByType.TryGetValue(type, out props))
{
ContractPropertiesByType.Add(type, contractProperties);
props = contractProperties;
}
}
}
}
return props;
}

private JsonProperty CreateReservedHalJsonProperty(
Type type,
string name,
IReadOnlyDictionary<string, JsonProperty> propertyMap)
{
return new JsonProperty
{
PropertyName = name,
PropertyType = HalKitTypeBuilder.CompileResultType($"{type.Name}{name}", propertyMap),
ValueProvider = new ReservedHalPropertyValueProvider(_settings, propertyMap),
NullValueHandling = NullValueHandling.Ignore,
Readable = propertyMap.Values.Any(p => p.Readable),
Writable = propertyMap.Values.Any(p => p.Writable),
ShouldSerialize = o => true,
GetIsSpecified = o => true,
SetIsSpecified = null,
Order = int.MaxValue,
};
}

private class ReservedHalPropertyValueProvider : IValueProvider
{
private readonly JsonSerializerSettings _settings;
private readonly IReadOnlyDictionary<string, JsonProperty> _propertyMap;

public ReservedHalPropertyValueProvider(
JsonSerializerSettings settings,
IReadOnlyDictionary<string, JsonProperty> propertyMap)
{
_settings = settings;
_propertyMap = propertyMap;
}

public object GetValue(object target)
{
// Use a SortedDictionary since it just seems "right" for the
// "self" link to be first
var reservedPropertyValue = new SortedDictionary<string, object>(new RelComparer());
foreach (var rel in _propertyMap.Keys)
{
var property = _propertyMap[rel];
var propertyValue = property.ValueProvider.GetValue(target);
if (propertyValue != null)
{
reservedPropertyValue.Add(rel, propertyValue);
}
}

return reservedPropertyValue.Count > 0 ? reservedPropertyValue : null;
}

public void SetValue(object target, object value)
{
var valueDictionary = value as IDictionary<string, object>;
if (valueDictionary == null)
{
return;
}

foreach (var rel in valueDictionary.Keys)
{
JsonProperty property;
if (!_propertyMap.TryGetValue(rel, out property))
{
continue;
}

var serializer = JsonSerializer.Create(_settings);
var propertyJson = valueDictionary[rel] as JToken;
var propertyValue = propertyJson != null
? propertyJson.ToObject(property.PropertyType, serializer)
: null;
property.ValueProvider.SetValue(target, propertyValue);
}
}
}

private class RelComparer : IComparer<string>
{
public int Compare(string rel, string otherRel)
{
const string self = "self";
if (rel == self)
{
return -1;
}

if (otherRel == self)
{
return 1;
}

return string.Compare(rel, otherRel, StringComparison.Ordinal);
}
}
}
}
6 changes: 6 additions & 0 deletions HalKit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{EC8D73FD-F
Version.props = Version.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HalKit.Backend", "HalKit.Backend\HalKit.Backend.csproj", "{F018259F-3574-4675-A78C-5D3165039522}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -37,6 +39,10 @@ Global
{56D3F770-FD3A-4ADF-A4A0-A5104F5A0012}.Debug|Any CPU.Build.0 = Debug|Any CPU
{56D3F770-FD3A-4ADF-A4A0-A5104F5A0012}.Release|Any CPU.ActiveCfg = Release|Any CPU
{56D3F770-FD3A-4ADF-A4A0-A5104F5A0012}.Release|Any CPU.Build.0 = Release|Any CPU
{F018259F-3574-4675-A78C-5D3165039522}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F018259F-3574-4675-A78C-5D3165039522}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F018259F-3574-4675-A78C-5D3165039522}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F018259F-3574-4675-A78C-5D3165039522}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
2 changes: 1 addition & 1 deletion Version.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.0.3</VersionPrefix>
<VersionPrefix>1.0.4</VersionPrefix>
<VersionSuffix>
</VersionSuffix>
</PropertyGroup>
Expand Down

0 comments on commit 6c9e89c

Please sign in to comment.