From 254c273e21aead7d5cb37fc32708b2e7fbd518f5 Mon Sep 17 00:00:00 2001 From: Laszlo Zold Date: Mon, 29 Nov 2021 23:36:04 +0100 Subject: [PATCH 1/2] Remove DeclaringType from reserved HAL json properties as they are not really declared in that type --- ReleaseNotes.md | 3 +++ Version.props | 2 +- src/HalKit/Json/ResourceContractResolver.cs | 1 - 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index c8d8ea3..95979d0 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,6 @@ +### New in 1.0.3 (Released 30/11/2021) +* Remove DeclaringType from reserved HAL JSON properties + ### New in 1.0 (Released 05/09/2018) * Upgrade packages to netstandard diff --git a/Version.props b/Version.props index a3a44a4..640e8c1 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ - 1.0.2 + 1.0.3 diff --git a/src/HalKit/Json/ResourceContractResolver.cs b/src/HalKit/Json/ResourceContractResolver.cs index 3f89e96..b287fbd 100644 --- a/src/HalKit/Json/ResourceContractResolver.cs +++ b/src/HalKit/Json/ResourceContractResolver.cs @@ -104,7 +104,6 @@ private JsonProperty CreateReservedHalJsonProperty( { PropertyName = name, PropertyType = typeof(Dictionary), - DeclaringType = type, ValueProvider = new ReservedHalPropertyValueProvider(_settings, propertyMap), NullValueHandling = NullValueHandling.Ignore, Readable = propertyMap.Values.Any(p => p.Readable), From 6c9e89cdf08259903012adf92a2771ea8336ac2b Mon Sep 17 00:00:00 2001 From: Laszlo Zold Date: Thu, 2 Dec 2021 12:52:07 +0100 Subject: [PATCH 2/2] Add backend project that has an improved ResourceContractResolver that generates types of reserved hal properties to help Swagger generators explore types properly --- HalKit.Backend/HalKit.Backend.csproj | 22 ++ HalKit.Backend/Json/HalKitTypeBuilder.cs | 80 +++++++ .../Json/TypedResourceContractResolver.cs | 197 ++++++++++++++++++ HalKit.sln | 6 + Version.props | 2 +- 5 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 HalKit.Backend/HalKit.Backend.csproj create mode 100644 HalKit.Backend/Json/HalKitTypeBuilder.cs create mode 100644 HalKit.Backend/Json/TypedResourceContractResolver.cs diff --git a/HalKit.Backend/HalKit.Backend.csproj b/HalKit.Backend/HalKit.Backend.csproj new file mode 100644 index 0000000..cab6c2c --- /dev/null +++ b/HalKit.Backend/HalKit.Backend.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + enable + enable + default + true + https://github.com/viagogo/HalKit + + + + + + + + + + + + + diff --git a/HalKit.Backend/Json/HalKitTypeBuilder.cs b/HalKit.Backend/Json/HalKitTypeBuilder.cs new file mode 100644 index 0000000..957d1d3 --- /dev/null +++ b/HalKit.Backend/Json/HalKitTypeBuilder.cs @@ -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 +{ + /// + /// Generates type run-time, based on a sample code found at https://stackoverflow.com/questions/3862226/how-to-dynamically-create-a-class + /// + public static class HalKitTypeBuilder + { + public static Type CompileResultType(string typeName, IReadOnlyDictionary 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); + } + } +} \ No newline at end of file diff --git a/HalKit.Backend/Json/TypedResourceContractResolver.cs b/HalKit.Backend/Json/TypedResourceContractResolver.cs new file mode 100644 index 0000000..aead6f5 --- /dev/null +++ b/HalKit.Backend/Json/TypedResourceContractResolver.cs @@ -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 +{ + /// + /// Resolves a for a given . + /// 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 + /// + public class TypedResourceContractResolver : DefaultContractResolver + { + private static readonly Dictionary> ContractPropertiesByType + = new Dictionary>(); + + private readonly JsonSerializerSettings _settings; + + /// + /// Initializes a new instance of the + /// class. + /// + public TypedResourceContractResolver(JsonSerializerSettings settings) + { + _settings = settings; + } + + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) + { + var allProperties = base.CreateProperties(type, memberSerialization); + if (!typeof(Resource).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + return allProperties; + } + + IList props; + if (!ContractPropertiesByType.TryGetValue(type, out props)) + { + var contractProperties = new List(); + var embeddedPropertyMap = new Dictionary(); + var linksPropertyMap = new Dictionary(); + 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 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 _propertyMap; + + public ReservedHalPropertyValueProvider( + JsonSerializerSettings settings, + IReadOnlyDictionary 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(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; + 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 + { + 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); + } + } + } +} diff --git a/HalKit.sln b/HalKit.sln index 19ca998..cb72252 100644 --- a/HalKit.sln +++ b/HalKit.sln @@ -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 @@ -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 diff --git a/Version.props b/Version.props index 640e8c1..53f3cff 100644 --- a/Version.props +++ b/Version.props @@ -1,6 +1,6 @@ - 1.0.3 + 1.0.4