-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add backend project that has an improved ResourceContractResolver tha…
…t generates types of reserved hal properties to help Swagger generators explore types properly
- Loading branch information
Laszlo Zold
committed
Jan 27, 2022
1 parent
254c273
commit a54e4b3
Showing
5 changed files
with
299 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net6.0</TargetFramework> | ||
<ImplicitUsings>enable</ImplicitUsings> | ||
<Nullable>enable</Nullable> | ||
</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> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters