totem 과 zlangner 의 아이디어를 사용하여 KnownTypeConverter
json 데이터에 선택적 요소가 없을 수 있다는 점을 고려하면서 가장 적합한 상속자를 결정할 수 있는 것을 만들었습니다 .
따라서 서비스는 문서 배열 (수신 및 발신)이 포함 된 JSON 응답을 보냅니다. 문서에는 공통 요소 집합과 다른 요소가 있습니다. 이 경우 보내는 문서와 관련된 요소는 선택 사항이며 없을 수 있습니다.
이와 관련 Document
하여 공통 속성 세트를 포함하는 기본 클래스 가 작성되었습니다. 두 개의 상속자 클래스도 작성됩니다.- OutgoingDocument
두 개의 선택적 요소를 추가 "device_id"
하고 "msg_id"
; - IncomingDocument
하나의 필수 요소를 추가합니다 "sender_id"
.
이 작업은 json 데이터를 기반으로하는 변환기를 작성하는 것입니다. KnownTypeAttribute의 정보는 수신 한 최대 정보를 저장할 수있는 가장 적합한 클래스를 결정할 수 있습니다. json 데이터에 선택적 요소가 없을 수도 있다는 점도 고려해야합니다. json 요소와 데이터 모델의 속성 비교 수를 줄이기 위해 기본 클래스의 속성을 고려하지 않고 상속자 클래스의 속성 만 json 요소와 상관시키지 않기로 결정했습니다.
서비스 데이터 :
{
"documents": [
{
"document_id": "76b7be75-f4dc-44cd-90d2-0d1959922852",
"date": "2019-12-10 11:32:49",
"processed_date": "2019-12-10 11:32:49",
"sender_id": "9dedee17-e43a-47f1-910e-3a88ff6bc258",
},
{
"document_id": "5044a9ac-0314-4e9a-9e0c-817531120753",
"date": "2019-12-10 11:32:44",
"processed_date": "2019-12-10 11:32:44",
}
],
"total": 2
}
데이터 모델 :
/// <summary>
/// Service response model
/// </summary>
public class DocumentsRequestIdResponse
{
[JsonProperty("documents")]
public Document[] Documents { get; set; }
[JsonProperty("total")]
public int Total { get; set; }
}
// <summary>
/// Base document
/// </summary>
[JsonConverter(typeof(KnownTypeConverter))]
[KnownType(typeof(OutgoingDocument))]
[KnownType(typeof(IncomingDocument))]
public class Document
{
[JsonProperty("document_id")]
public Guid DocumentId { get; set; }
[JsonProperty("date")]
public DateTime Date { get; set; }
[JsonProperty("processed_date")]
public DateTime ProcessedDate { get; set; }
}
/// <summary>
/// Outgoing document
/// </summary>
public class OutgoingDocument : Document
{
// this property is optional and may not be present in the service's json response
[JsonProperty("device_id")]
public string DeviceId { get; set; }
// this property is optional and may not be present in the service's json response
[JsonProperty("msg_id")]
public string MsgId { get; set; }
}
/// <summary>
/// Incoming document
/// </summary>
public class IncomingDocument : Document
{
// this property is mandatory and is always populated by the service
[JsonProperty("sender_sys_id")]
public Guid SenderSysId { get; set; }
}
변환기:
public class KnownTypeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return System.Attribute.GetCustomAttributes(objectType).Any(v => v is KnownTypeAttribute);
}
public override bool CanWrite => false;
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// load the object
JObject jObject = JObject.Load(reader);
// take custom attributes on the type
Attribute[] attrs = Attribute.GetCustomAttributes(objectType);
Type mostSuitableType = null;
int countOfMaxMatchingProperties = -1;
// take the names of elements from json data
HashSet<string> jObjectKeys = GetKeys(jObject);
// take the properties of the parent class (in our case, from the Document class, which is specified in DocumentsRequestIdResponse)
HashSet<string> objectTypeProps = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Select(p => p.Name)
.ToHashSet();
// trying to find the right "KnownType"
foreach (var attr in attrs.OfType<KnownTypeAttribute>())
{
Type knownType = attr.Type;
if(!objectType.IsAssignableFrom(knownType))
continue;
// select properties of the inheritor, except properties from the parent class and properties with "ignore" attributes (in our case JsonIgnoreAttribute and XmlIgnoreAttribute)
var notIgnoreProps = knownType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => !objectTypeProps.Contains(p.Name)
&& p.CustomAttributes.All(a => a.AttributeType != typeof(JsonIgnoreAttribute) && a.AttributeType != typeof(System.Xml.Serialization.XmlIgnoreAttribute)));
// get serializable property names
var jsonNameFields = notIgnoreProps.Select(prop =>
{
string jsonFieldName = null;
CustomAttributeData jsonPropertyAttribute = prop.CustomAttributes.FirstOrDefault(a => a.AttributeType == typeof(JsonPropertyAttribute));
if (jsonPropertyAttribute != null)
{
// take the name of the json element from the attribute constructor
CustomAttributeTypedArgument argument = jsonPropertyAttribute.ConstructorArguments.FirstOrDefault();
if(argument != null && argument.ArgumentType == typeof(string) && !string.IsNullOrEmpty((string)argument.Value))
jsonFieldName = (string)argument.Value;
}
// otherwise, take the name of the property
if (string.IsNullOrEmpty(jsonFieldName))
{
jsonFieldName = prop.Name;
}
return jsonFieldName;
});
HashSet<string> jKnownTypeKeys = new HashSet<string>(jsonNameFields);
// by intersecting the sets of names we determine the most suitable inheritor
int count = jObjectKeys.Intersect(jKnownTypeKeys).Count();
if (count == jKnownTypeKeys.Count)
{
mostSuitableType = knownType;
break;
}
if (count > countOfMaxMatchingProperties)
{
countOfMaxMatchingProperties = count;
mostSuitableType = knownType;
}
}
if (mostSuitableType != null)
{
object target = Activator.CreateInstance(mostSuitableType);
using (JsonReader jObjectReader = CopyReaderForObject(reader, jObject))
{
serializer.Populate(jObjectReader, target);
}
return target;
}
throw new SerializationException($"Could not serialize to KnownTypes and assign to base class {objectType} reference");
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
private HashSet<string> GetKeys(JObject obj)
{
return new HashSet<string>(((IEnumerable<KeyValuePair<string, JToken>>) obj).Select(k => k.Key));
}
public static JsonReader CopyReaderForObject(JsonReader reader, JObject jObject)
{
JsonReader jObjectReader = jObject.CreateReader();
jObjectReader.Culture = reader.Culture;
jObjectReader.DateFormatString = reader.DateFormatString;
jObjectReader.DateParseHandling = reader.DateParseHandling;
jObjectReader.DateTimeZoneHandling = reader.DateTimeZoneHandling;
jObjectReader.FloatParseHandling = reader.FloatParseHandling;
jObjectReader.MaxDepth = reader.MaxDepth;
jObjectReader.SupportMultipleContent = reader.SupportMultipleContent;
return jObjectReader;
}
}
추신 : 내 경우에는 변환기가 상속자를 선택하지 않은 경우 (JSON 데이터에 기본 클래스의 정보 만 포함되거나 JSON 데이터에의 선택적 요소가 포함되어 있지 않은 경우 발생할 수 있음 OutgoingDocument
) OutgoingDocument
클래스 의 객체 KnownTypeAttribute
속성 목록에서 첫 번째로 나열되므로 생성됩니다 . 귀하의 요청 KnownTypeConverter
에 따라이 상황에서 구현을 다양하게 변경할 수 있습니다 .