さまざまなWebサイトからJSONオブジェクトを取り込み(情報のスクレイピングを考えて)、C#オブジェクトに変換するリーダーを設定しようとしています。現在、デシリアライズプロセスにJSON.NETを使用しています。私が実行している問題は、クラス内のインターフェイスレベルのプロパティを処理する方法を知らないことです。自然の何か:
public IThingy Thing
エラーが発生します:
タイプIThingyのインスタンスを作成できませんでした。タイプはインターフェースまたは抽象クラスであり、インスタンス化できません。
私が取り組んでいるコードは敏感であり、ユニットテストが非常に重要であると考えられているので、ThingyではなくIThingyであることは比較的重要です。 Thingyのような本格的なオブジェクトでは、アトミックテストスクリプトのオブジェクトのモックは不可能です。それらはインターフェースでなければなりません。
JSON.NETのドキュメントをしばらく熟読しましたが、このサイトでこれに関連する質問はすべて1年以上前のものです。何か助け?
また、重要な場合、私のアプリは.NET 4.0で作成されています。
@SamualDavisは 関連する質問 で優れたソリューションを提供しました。これをここで要約します。
JSONストリームをインターフェイスプロパティを持つ具象クラスに逆シリアル化する必要がある場合、具象クラスをクラスのコンストラクターへのパラメーターとして含めることができます! NewtonSoftデシリアライザは、これらの具象クラスを使用してプロパティをデシリアライズする必要があることを理解できるほどスマートです。
以下に例を示します。
public class Visit : IVisit
{
/// <summary>
/// This constructor is required for the JSON deserializer to be able
/// to identify concrete classes to use when deserializing the interface properties.
/// </summary>
public Visit(MyLocation location, Guest guest)
{
Location = location;
Guest = guest;
}
public long VisitId { get; set; }
public ILocation Location { get; set; }
public DateTime VisitDate { get; set; }
public IGuest Guest { get; set; }
}
( この質問 からコピー)
着信JSONを制御していない(したがって$ typeプロパティが含まれていることを確認できない)場合は、具体的な型を明示的に指定できるカスタムコンバーターを記述しました。
public class Model
{
[JsonConverter(typeof(ConcreteTypeConverter<Something>))]
public ISomething TheThing { get; set; }
}
これは、具体的な型を明示的に指定しながら、Json.Netのデフォルトのシリアライザー実装を使用するだけです。
概要が利用可能です このブログ投稿で 。ソースコードは次のとおりです。
public class ConcreteTypeConverter<TConcrete> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
//assume we can convert to anything for now
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
//explicitly specify the concrete type we want to create
return serializer.Deserialize<TConcrete>(reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
//use the default serialization - it works fine
serializer.Serialize(writer, value);
}
}
コンバーターを使用する理由Newtonsoft.Json
には、この正確な問題を解決するためのネイティブ機能があります。
TypeNameHandling
のJsonSerializerSettings
をTypeNameHandling.Auto
に設定します
JsonConvert.SerializeObject(
toSerialize,
new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto
});
これにより、すべてのタイプがjsonに配置されます。これは、タイプの具体的なインスタンスとしてではなく、インターフェイスまたは抽象クラスとして保持されます。
私はそれをテストしました、そしてそれはリストでさえ魅力のように働きます。
ソースと代替の手動実装: Code Inside Blog
インターフェースの複数の実装の逆シリアル化を有効にするには、属性を介さずにJsonConverterを使用できます。
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);
DTOJsonConverterは、各インターフェイスを具体的な実装にマッピングします。
class DTOJsonConverter : Newtonsoft.Json.JsonConverter
{
private static readonly string ISCALAR_FULLNAME = typeof(Interfaces.IScalar).FullName;
private static readonly string IENTITY_FULLNAME = typeof(Interfaces.IEntity).FullName;
public override bool CanConvert(Type objectType)
{
if (objectType.FullName == ISCALAR_FULLNAME
|| objectType.FullName == IENTITY_FULLNAME)
{
return true;
}
return false;
}
public override object ReadJson(Newtonsoft.Json.JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
if (objectType.FullName == ISCALAR_FULLNAME)
return serializer.Deserialize(reader, typeof(DTO.ClientScalar));
else if (objectType.FullName == IENTITY_FULLNAME)
return serializer.Deserialize(reader, typeof(DTO.ClientEntity));
throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
DTOJsonConverterは、デシリアライザーにのみ必要です。シリアル化プロセスは変更されていません。 Jsonオブジェクトは、具象型名を埋め込む必要はありません。
この SO post は、ジェネリックJsonConverterを使用して同じソリューションをさらに一歩進めます。
このクラスを使用して、抽象型を実際の型にマッピングします。
public class AbstractConverter<TReal, TAbstract> : JsonConverter where TReal : TAbstract
{
public override Boolean CanConvert(Type objectType)
=> objectType == typeof(TAbstract);
public override Object ReadJson(JsonReader reader, Type type, Object value, JsonSerializer jser)
=> jser.Deserialize<TReal>(reader);
public override void WriteJson(JsonWriter writer, Object value, JsonSerializer jser)
=> jser.Serialize(writer, value);
}
...そして逆シリアル化するとき:
var settings = new JsonSerializerSettings
{
Converters = {
new AbstractConverter<Thing, IThingy>(),
new AbstractConverter<Thing2, IThingy2>()
},
};
JsonConvert.DeserializeObject(json, type, settings);
これは便利だと思いました。あなたもかもしれない。
使用例
public class Parent
{
[JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
IChildModel Child { get; set; }
}
カスタム作成コンバーター
public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
where TConcrete : TInterface, new()
{
public override TInterface Create(Type objectType)
{
return new TConcrete();
}
}
Nicholas Westbyは awesome article で優れたソリューションを提供しました。
JSONをそのようなインターフェースを実装する多くの可能なクラスの1つにデシリアライズする場合:
public class Person
{
public IProfession Profession { get; set; }
}
public interface IProfession
{
string JobTitle { get; }
}
public class Programming : IProfession
{
public string JobTitle => "Software Developer";
public string FavoriteLanguage { get; set; }
}
public class Writing : IProfession
{
public string JobTitle => "Copywriter";
public string FavoriteWord { get; set; }
}
public class Samples
{
public static Person GetProgrammer()
{
return new Person()
{
Profession = new Programming()
{
FavoriteLanguage = "C#"
}
};
}
}
カスタムJSONコンバーターを使用できます。
public class ProfessionConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(IProfession);
}
public override void WriteJson(JsonWriter writer,
object value, JsonSerializer serializer)
{
throw new InvalidOperationException("Use default serialization.");
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue,
JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var profession = default(IProfession);
switch (jsonObject["JobTitle"].Value())
{
case "Software Developer":
profession = new Programming();
break;
case "Copywriter":
profession = new Writing();
break;
}
serializer.Populate(jsonObject.CreateReader(), profession);
return profession;
}
}
また、カスタムコンバーターを使用することを知らせるために、「Profession」プロパティをJsonConverter属性で装飾する必要があります。
public class Person
{
[JsonConverter(typeof(ProfessionConverter))]
public IProfession Profession { get; set; }
}
そして、インターフェイスを使用してクラスをキャストできます。
Person person = JsonConvert.DeserializeObject<Person>(jsonString);
あなたが試みるかもしれない2つのこと:
Try/parseモデルを実装します。
public class Organisation {
public string Name { get; set; }
[JsonConverter(typeof(RichDudeConverter))]
public IPerson Owner { get; set; }
}
public interface IPerson {
string Name { get; set; }
}
public class Tycoon : IPerson {
public string Name { get; set; }
}
public class Magnate : IPerson {
public string Name { get; set; }
public string IndustryName { get; set; }
}
public class Heir: IPerson {
public string Name { get; set; }
public IPerson Benefactor { get; set; }
}
public class RichDudeConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPerson));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// pseudo-code
object richDude = serializer.Deserialize<Heir>(reader);
if (richDude == null)
{
richDude = serializer.Deserialize<Magnate>(reader);
}
if (richDude == null)
{
richDude = serializer.Deserialize<Tycoon>(reader);
}
return richDude;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Left as an exercise to the reader :)
throw new NotImplementedException();
}
}
または、オブジェクトモデルでそうすることができる場合は、IPersonとリーフオブジェクトの間に具体的な基本クラスを実装し、それに対して逆シリアル化します。
1つ目は実行時に失敗する可能性があり、2つ目はオブジェクトモデルの変更を必要とし、出力を最小公分母に均質化します。
Oliverによって参照されたConcreteListTypeConverterに興味があるかもしれない人のために、ここに私の試みがあります。
public class ConcreteListTypeConverter<TInterface, TImplementation> : JsonConverter where TImplementation : TInterface
{
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var res = serializer.Deserialize<List<TImplementation>>(reader);
return res.ConvertAll(x => (TInterface) x);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
}
次のようなautofac設定があるとします。
public class AutofacContractResolver : DefaultContractResolver
{
private readonly IContainer _container;
public AutofacContractResolver(IContainer container)
{
_container = container;
}
protected override JsonObjectContract CreateObjectContract(Type objectType)
{
JsonObjectContract contract = base.CreateObjectContract(objectType);
// use Autofac to create types that have been registered with it
if (_container.IsRegistered(objectType))
{
contract.DefaultCreator = () => _container.Resolve(objectType);
}
return contract;
}
}
次に、クラスが次のようになっていると仮定します。
public class TaskController
{
private readonly ITaskRepository _repository;
private readonly ILogger _logger;
public TaskController(ITaskRepository repository, ILogger logger)
{
_repository = repository;
_logger = logger;
}
public ITaskRepository Repository
{
get { return _repository; }
}
public ILogger Logger
{
get { return _logger; }
}
}
したがって、逆シリアル化におけるリゾルバーの使用法は次のようになります。
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<TaskRepository>().As<ITaskRepository>();
builder.RegisterType<TaskController>();
builder.Register(c => new LogService(new DateTime(2000, 12, 12))).As<ILogger>();
IContainer container = builder.Build();
AutofacContractResolver contractResolver = new AutofacContractResolver(container);
string json = @"{
'Logger': {
'Level':'Debug'
}
}";
// ITaskRespository and ILogger constructor parameters are injected by Autofac
TaskController controller = JsonConvert.DeserializeObject<TaskController>(json, new JsonSerializerSettings
{
ContractResolver = contractResolver
});
Console.WriteLine(controller.Repository.GetType().Name);
詳細は http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm で確認できます。
それが価値があることのために、私はこれをほとんど自分で処理しなければならなくなりました。各オブジェクトにはDeserialize(string jsonStream)メソッドがあります。いくつかのスニペット:
JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));
この場合、new Thingy(string)は、適切な具象型のDeserialize(string jsonStream)メソッドを呼び出すコンストラクターです。このスキームは、json.NETが処理できるベースポイントに到達するまで、下に向かって進み続けます。
this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);
などなど。このセットアップにより、ライブラリ自体の大部分をリファクタリングしたり、関係するオブジェクトの数が原因でライブラリ全体を動かなくなってしまう扱いにくいtry/parseモデルを使用したりすることなく、json.NETセットアップを処理できます。また、特定のオブジェクトに対するjsonの変更を効果的に処理でき、オブジェクトが触れるすべてのことを心配する必要がありません。それは決して理想的なソリューションではありませんが、単体テストと統合テストからは非常にうまく機能します。
数年後、私は同様の問題を抱えていました。私の場合、インターフェイスが大きくネストされていて、実行時に具象クラスを生成する設定があり、汎用クラスで動作するようになっています。
Newtonsoftから返されたオブジェクトをラップするプロキシクラスを実行時に作成することにしました。
このアプローチの利点は、クラスの具体的な実装を必要とせず、ネストされたインターフェイスの深さを自動的に処理できることです。詳しくは、私の blog をご覧ください。
using Castle.DynamicProxy;
using Newtonsoft.Json.Linq;
using System;
using System.Reflection;
namespace LL.Utilities.Std.Json
{
public static class JObjectExtension
{
private static ProxyGenerator _generator = new ProxyGenerator();
public static dynamic toProxy(this JObject targetObject, Type interfaceType)
{
return _generator.CreateInterfaceProxyWithoutTarget(interfaceType, new JObjectInterceptor(targetObject));
}
public static InterfaceType toProxy<InterfaceType>(this JObject targetObject)
{
return toProxy(targetObject, typeof(InterfaceType));
}
}
[Serializable]
public class JObjectInterceptor : IInterceptor
{
private JObject _target;
public JObjectInterceptor(JObject target)
{
_target = target;
}
public void Intercept(IInvocation invocation)
{
var methodName = invocation.Method.Name;
if(invocation.Method.IsSpecialName && methodName.StartsWith("get_"))
{
var returnType = invocation.Method.ReturnType;
methodName = methodName.Substring(4);
if (_target == null || _target[methodName] == null)
{
if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
{
invocation.ReturnValue = null;
return;
}
}
if (returnType.GetTypeInfo().IsPrimitive || returnType.Equals(typeof(string)))
{
invocation.ReturnValue = _target[methodName].ToObject(returnType);
}
else
{
invocation.ReturnValue = ((JObject)_target[methodName]).toProxy(returnType);
}
}
else
{
throw new NotImplementedException("Only get accessors are implemented in proxy");
}
}
}
}
使用法:
var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
インターフェースはすべて定義上抽象的であるため、オブジェクトはeverbeIThingyにはなりません。
最初にシリアライズされたオブジェクトは、concreteタイプで、abstractインターフェイスを実装していました。これと同じconcreteクラスでシリアル化されたデータを復活させる必要があります。
結果のオブジェクトは、探しているabstractインターフェイスをimplementsするタイプになります。
documentation から、使用できることになります
(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));
シリアル化を解除してJSON.NETに具象型を通知する場合。
これに対する私の解決策は、非常に一般的であるため、私は好きです。
/// <summary>
/// Automagically convert known interfaces to (specific) concrete classes on deserialisation
/// </summary>
public class WithMocksJsonConverter : JsonConverter
{
/// <summary>
/// The interfaces I know how to instantiate mapped to the classes with which I shall instantiate them, as a Dictionary.
/// </summary>
private readonly Dictionary<Type,Type> conversions = new Dictionary<Type,Type>() {
{ typeof(IOne), typeof(MockOne) },
{ typeof(ITwo), typeof(MockTwo) },
{ typeof(IThree), typeof(MockThree) },
{ typeof(IFour), typeof(MockFour) }
};
/// <summary>
/// Can I convert an object of this type?
/// </summary>
/// <param name="objectType">The type under consideration</param>
/// <returns>True if I can convert the type under consideration, else false.</returns>
public override bool CanConvert(Type objectType)
{
return conversions.Keys.Contains(objectType);
}
/// <summary>
/// Attempt to read an object of the specified type from this reader.
/// </summary>
/// <param name="reader">The reader from which I read.</param>
/// <param name="objectType">The type of object I'm trying to read, anticipated to be one I can convert.</param>
/// <param name="existingValue">The existing value of the object being read.</param>
/// <param name="serializer">The serializer invoking this request.</param>
/// <returns>An object of the type into which I convert the specified objectType.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
try
{
return serializer.Deserialize(reader, this.conversions[objectType]);
}
catch (Exception)
{
throw new NotSupportedException(string.Format("Type {0} unexpected.", objectType));
}
}
/// <summary>
/// Not yet implemented.
/// </summary>
/// <param name="writer">The writer to which I would write.</param>
/// <param name="value">The value I am attempting to write.</param>
/// <param name="serializer">the serializer invoking this request.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}
変換インスタンス変数をインスタンス化するために、Dictionary <Type、Type>型の引数をとるコンストラクターを追加することにより、明らかに一般的なコンバーターに変換できます。
私のソリューションは、コンストラクターにインターフェース要素を追加しました。
public class Customer: ICustomer{
public Customer(Details details){
Details = details;
}
[JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
public IDetails Details {get; set;}
}