Mentransmisikan antarmuka untuk deserialisasi di JSON.NET

129

Saya mencoba menyiapkan pembaca yang akan mengambil objek JSON dari berbagai situs web (pikirkan informasi scraping) dan menerjemahkannya ke dalam objek C #. Saya saat ini menggunakan JSON.NET untuk proses deserialization. Masalah yang saya hadapi adalah bahwa ia tidak tahu bagaimana menangani properti tingkat antarmuka di kelas. Jadi sesuatu yang alami:

public IThingy Thing

Akan menghasilkan kesalahan:

Tidak dapat membuat instance berjenis IThingy. Type adalah antarmuka atau kelas abstrak dan tidak dapat dipakai.

Relatif penting untuk menjadikannya sebagai IThingy dibandingkan dengan Thingy karena kode yang saya kerjakan dianggap sensitif dan pengujian unit sangat penting. Mocking objek untuk skrip pengujian atom tidak dimungkinkan dengan objek yang lengkap seperti Thingy. Mereka harus menjadi antarmuka.

Saya telah mempelajari dokumentasi JSON.NET untuk sementara waktu sekarang, dan pertanyaan yang dapat saya temukan di situs ini terkait dengan ini semuanya berasal lebih dari setahun yang lalu. Ada bantuan?

Selain itu, jika penting, aplikasi saya ditulis dalam .NET 4.0.

tmesser
sumber

Jawaban:

115

@SamualDavis memberikan solusi bagus untuk pertanyaan terkait , yang akan saya rangkum di sini.

Jika Anda harus deserialisasi aliran JSON menjadi kelas beton yang memiliki properti antarmuka, Anda dapat menyertakan kelas konkret sebagai parameter ke konstruktor untuk kelas tersebut! NewtonSoft deserializer cukup pintar untuk mengetahui bahwa ia perlu menggunakan kelas konkret tersebut untuk deserialisasi properti.

Berikut ini contohnya:

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; }
}
Mark Meuer
sumber
15
Bagaimana cara kerjanya dengan ICollection? ICollection <IGuest> Tamu {get; set;}
DrSammyD
12
Ia bekerja dengan ICollection <ConcreteClass>, sehingga ICollection <Guest> bekerja. Sama seperti FYI, Anda dapat meletakkan atribut [JsonConstructor] pada konstruktor Anda sehingga atribut tersebut akan digunakan secara default jika Anda kebetulan memiliki beberapa konstruktor
DrSammyD
6
Saya terjebak pada masalah yang sama, dalam kasus saya, saya memiliki beberapa implementasi antarmuka (dalam contoh Anda antarmuka adalah ILocation) jadi bagaimana jika ada kelas seperti MyLocation, VIPLocation, OrdinaryLocation. Bagaimana cara memetakannya ke properti Lokasi? Jika Anda hanya memiliki satu implementasi seperti MyLocation itu mudah, tetapi bagaimana melakukannya jika ada beberapa implementasi ILocation?
ATHER
10
Jika Anda memiliki lebih dari satu konstruktor, Anda dapat menandai konstruktor khusus Anda dengan [JsonConstructor]atribut.
Dr Rob Lang
26
Ini sama sekali tidak baik. Tujuan menggunakan antarmuka adalah menggunakan injeksi ketergantungan, tetapi melakukan ini dengan parameter jenis objek yang diperlukan oleh konstruktor Anda, Anda benar-benar mengacaukan titik memiliki antarmuka sebagai properti.
Jérôme MEVEL
57

(Disalin dari pertanyaan ini )

Dalam kasus di mana saya tidak memiliki kendali atas JSON yang masuk (sehingga tidak dapat memastikan bahwa itu termasuk properti $ type) saya telah menulis konverter khusus yang hanya memungkinkan Anda untuk secara eksplisit menentukan jenis konkret:

public class Model
{
    [JsonConverter(typeof(ConcreteTypeConverter<Something>))]
    public ISomething TheThing { get; set; }
}

Ini hanya menggunakan implementasi serializer default dari Json.Net sementara secara eksplisit menentukan tipe konkret.

Ringkasan tersedia di posting blog ini . Kode sumber di bawah ini:

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);
    }
}
Steve Greatrex
sumber
11
Saya sangat menyukai pendekatan ini dan menerapkannya pada proyek kami sendiri. Saya bahkan menambahkan ConcreteListTypeConverter<TInterface, TImplementation>untuk menangani anggota kelas tipe IList<TInterface>.
Oliver
3
Itu kode yang bagus. Mungkin lebih baik untuk memiliki kode sebenarnya untuk concreteTypeConverterpertanyaan tersebut.
Chris
2
@Oliver - Dapatkah Anda memposting ConcreteListTypeConverter<TInterface, TImplementation>implementasi Anda ?
Michael
2
Dan jika Anda memiliki dua pelaksana ISomething?
bdaniel7
56

Mengapa menggunakan konverter? Ada fungsi asli Newtonsoft.Jsonuntuk menyelesaikan masalah ini dengan tepat:

Set TypeNameHandlingdi JsonSerializerSettingskeTypeNameHandling.Auto

JsonConvert.SerializeObject(
  toSerialize,
  new JsonSerializerSettings()
  {
    TypeNameHandling = TypeNameHandling.Auto
  });

Ini akan menempatkan setiap tipe ke dalam json, yang tidak diadakan sebagai contoh konkret dari sebuah tipe tetapi sebagai antarmuka atau kelas abstrak.

Pastikan Anda menggunakan pengaturan yang sama untuk serialisasi dan deserialisasi .

Saya mengujinya, dan itu bekerja seperti pesona, bahkan dengan daftar.

Hasil Pencarian Hasil Web dengan link situs

⚠️ PERINGATAN :

Gunakan ini hanya untuk json dari sumber yang dikenal dan tepercaya. Snipsnipsnip pengguna dengan benar menyebutkan bahwa ini memang vunerabilitas.

Lihat CA2328 dan SCS0028 untuk informasi lebih lanjut.


Sumber dan implementasi manual alternatif: Code Inside Blog

Mafii
sumber
3
Sempurna, ini membantu saya untuk klon cepat & kotor yang dalam ( stackoverflow.com/questions/78536/deep-cloning-objects )
Compufreak
1
@Shimmy Objects: "Sertakan nama jenis .NET saat membuat serial ke dalam struktur objek JSON." Otomatis: Sertakan nama tipe .NET ketika tipe objek yang diserialisasi tidak sama dengan tipe yang dideklarasikan. Perhatikan bahwa ini tidak menyertakan objek berseri root secara default. Untuk memasukkan nama tipe objek root di JSON Anda harus menentukan objek tipe root dengan SerializeObject (Object, Type, JsonSerializerSettings) atau Serialize (JsonWriter, Object, Type). "Sumber: newtonsoft.com/json/help/html/…
Mafii
4
Saya baru saja mencoba ini di Deserialization dan tidak berhasil. Baris subjek dari pertanyaan Stack Overflow ini adalah, "Mentransmisikan antarmuka untuk deserialisasi di JSON.NET"
Justin Russo
3
@JustinRusso hanya berfungsi ketika json telah diserialkan dengan pengaturan yang sama
Mafii
3
Beri suara positif untuk solusi cepat, jika tidak kotor. Jika Anda hanya membuat konfigurasi serial, ini berfungsi. Beats menghentikan pengembangan untuk membangun konverter dan tentunya mengalahkan dekorasi setiap properti yang diinjeksi. serializer.TypeNameHandling = TypeNameHandling.Auto; JsonConvert.DefaultSettings () TypeNameHandling = TypeNameHandling.Auto;
Sean Anderson
39

Untuk mengaktifkan deserialisasi beberapa implementasi antarmuka, Anda dapat menggunakan JsonConverter, tetapi tidak melalui atribut:

Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Converters.Add(new DTOJsonConverter());
Interfaces.IEntity entity = serializer.Deserialize(jsonReader);

DTOJsonConverter memetakan setiap antarmuka dengan implementasi konkret:

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 diperlukan hanya untuk deserializer. Proses serialisasi tidak berubah. Objek Json tidak perlu menyematkan nama tipe konkret.

Posting SO ini menawarkan solusi yang sama selangkah lebih maju dengan JsonConverter generik.

Eric Boumendil
sumber
Bukankah panggilan metode WriteJson ke serializer.Serialize menyebabkan stack overflow, karena memanggil serialize pada nilai yang diserialkan oleh konverter akan menyebabkan metode WriteJson konverter dipanggil lagi secara rekursif?
Triynko
Seharusnya tidak, jika metode CanConvert () mengembalikan hasil yang konsisten.
Eric Boumendil
3
Mengapa Anda membandingkan FullNamejika Anda dapat membandingkan tipe secara langsung?
Alex Zhukovskiy
Membandingkan tipe juga tidak masalah.
Eric Boumendil
23

Gunakan kelas ini, untuk memetakan tipe abstrak ke tipe nyata:

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);
}

... dan saat deserialize:

        var settings = new JsonSerializerSettings
        {
            Converters = {
                new AbstractConverter<Thing, IThingy>(),
                new AbstractConverter<Thing2, IThingy2>()
            },
        };

        JsonConvert.DeserializeObject(json, type, settings);
Gildor
sumber
1
Saya sangat menyukai jawaban singkat yang bagus yang memecahkan masalah saya. Tidak perlu autofac atau apapun!
Ben Power
3
Layak untuk memasukkan ini ke deklarasi kelas konverter: where TReal : TAbstractuntuk memastikannya dapat mentransmisikan ke tipe
Artemious
1
Lebih lengkap dimana mungkin where TReal : class, TAbstract, new().
Erik Philips
2
Saya menggunakan konverter ini dengan struct juga, saya percaya "di mana TReal: TAbstract" sudah cukup. Terima kasih semuanya.
Gildor
2
Emas! Cara apik untuk pergi.
SwissCoder
12

Nicholas Westby memberikan solusi hebat dalam artikel yang luar biasa .

Jika Anda ingin Deserializing JSON ke salah satu dari banyak kemungkinan kelas yang mengimplementasikan antarmuka seperti itu:

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#"
            }
        };
    }
}

Anda dapat menggunakan konverter JSON khusus:

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;
    }
}

Dan Anda perlu menghias properti "Profesi" dengan atribut JsonConverter agar tahu cara menggunakan konverter khusus Anda:

    public class Person
    {
        [JsonConverter(typeof(ProfessionConverter))]
        public IProfession Profession { get; set; }
    }

Dan kemudian, Anda dapat mentransmisikan kelas Anda dengan Antarmuka:

Person person = JsonConvert.DeserializeObject<Person>(jsonString);
A. Morel
sumber
8

Dua hal yang bisa Anda coba:

Menerapkan model coba / 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();
  }
}

Atau, jika Anda dapat melakukannya dalam model objek Anda, terapkan kelas dasar konkret antara IPerson dan objek daun Anda, dan deserialisasi padanya.

Yang pertama berpotensi gagal pada waktu proses, yang kedua memerlukan perubahan pada model objek Anda dan menghomogenkan keluaran ke penyebut umum terendah.

mcw
sumber
Model coba / parse tidak layak karena skala yang harus saya kerjakan. Saya harus mempertimbangkan cakupan ratusan objek dasar dengan lebih dari ratusan objek stub / helper untuk mewakili objek JSON yang disematkan yang sering terjadi. Tidak mustahil untuk mengubah model objek, tetapi bukankah menggunakan kelas dasar konkret di properti membuat kami tidak dapat meniru item untuk pengujian unit? Atau apakah saya mendapatkan itu mundur entah bagaimana?
tmesser
Anda masih bisa mengimplementasikan tiruan dari IPerson - perhatikan bahwa tipe properti Organisation.Owner masih IPerson. Tetapi untuk deserialisasi target sewenang-wenang, Anda harus mengembalikan tipe konkret. Jika Anda tidak memiliki definisi jenis dan Anda tidak dapat menentukan kumpulan minimum properti yang akan diperlukan kode Anda, maka pilihan terakhir Anda adalah sesuatu seperti tas kunci / nilai. Menggunakan komentar contoh facebook Anda - dapatkah Anda memposting dalam jawaban seperti apa (satu atau beberapa) implementasi ILocation Anda? Itu dapat membantu memajukan segalanya.
mcw
Karena harapan utamanya adalah mengejek, antarmuka ILocation sebenarnya hanyalah sebuah fasad untuk objek beton Lokasi. Contoh cepat yang baru saja saya kerjakan akan menjadi sesuatu seperti ini ( pastebin.com/mWQtqGnB ) untuk antarmuka dan ini ( pastebin.com/TdJ6cqWV ) untuk objek konkret.
tmesser
Dan untuk melanjutkan ke langkah berikutnya, ini adalah contoh tampilan IPage ( pastebin.com/iuGifQXp ) dan Halaman ( pastebin.com/ebqLxzvm ). Masalahnya, tentu saja, meskipun deserialisasi Halaman umumnya akan berfungsi dengan baik, ia akan tersedak ketika sampai ke properti ILocation.
tmesser
Oke, jadi pikirkan tentang objek yang sebenarnya Anda kikis dan deserialisasi - apakah biasanya data JSON konsisten dengan satu definisi kelas konkret? Artinya (secara hipotetis) Anda tidak akan menemukan "lokasi" dengan properti tambahan yang akan membuat Lokasi tidak cocok untuk digunakan sebagai tipe konkret untuk objek deserialisasi? Jika demikian, mengaitkan properti ILocation Halaman dengan "LocationConverter" akan berfungsi. Jika tidak, dan itu karena data JSON tidak selalu sesuai dengan struktur yang kaku atau konsisten (seperti ILocation), maka (... lanjutan)
mcw
8

Saya menemukan ini berguna. Anda mungkin juga.

Contoh Penggunaan

public class Parent
{
    [JsonConverter(typeof(InterfaceConverter<IChildModel, ChildModel>))]
    IChildModel Child { get; set; }
}

Konverter Pembuatan Kustom

public class InterfaceConverter<TInterface, TConcrete> : CustomCreationConverter<TInterface>
    where TConcrete : TInterface, new()
{
    public override TInterface Create(Type objectType)
    {
        return new TConcrete();
    }
}

Dokumentasi Json.NET

smiggleworth
sumber
1
Bukan solusi yang bisa diterapkan. Tidak membahas Daftar dan mengarah ke dekorator / anotasi yang tersebar di mana-mana.
Sean Anderson
5

Bagi mereka yang mungkin penasaran dengan ConcreteListTypeConverter yang direferensikan oleh Oliver, berikut adalah upaya saya:

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);
    }
}
Matt M.
sumber
1
Saya bingung dengan yang ditimpa CanConvert(Type objectType) { return true;}. Tampaknya hacky, bagaimana sebenarnya ini membantu? Saya mungkin salah, tetapi bukankah itu seperti memberi tahu petarung kecil yang tidak berpengalaman bahwa mereka akan memenangkan pertarungan, tidak peduli lawannya?
Chef_Code
4

Untuk apa nilainya, saya akhirnya harus menangani ini sendiri untuk sebagian besar. Setiap objek memiliki a Deserialize (string jsonStream) . Beberapa cuplikannya:

JObject parsedJson = this.ParseJson(jsonStream);
object thingyObjectJson = (object)parsedJson["thing"];
this.Thing = new Thingy(Convert.ToString(thingyObjectJson));

Dalam hal ini, Thingy baru (string) adalah konstruktor yang akan memanggil metode Deserialize (string jsonStream) dari jenis beton yang sesuai. Skema ini akan terus berlanjut ke bawah dan ke bawah sampai Anda mencapai titik dasar yang bisa ditangani oleh json.NET.

this.Name = (string)parsedJson["name"];
this.CreatedTime = DateTime.Parse((string)parsedJson["created_time"]);

Begitu seterusnya. Pengaturan ini memungkinkan saya untuk memberikan pengaturan json.NET yang dapat ditangani tanpa harus merefaktor sebagian besar pustaka itu sendiri atau menggunakan model coba / parse yang berat yang akan menghambat seluruh pustaka kami karena jumlah objek yang terlibat. Ini juga berarti bahwa saya dapat secara efektif menangani perubahan json apa pun pada objek tertentu, dan saya tidak perlu khawatir tentang semua yang disentuh objek. Ini sama sekali bukan solusi ideal, tetapi berfungsi dengan baik dari pengujian unit dan integrasi kami.

tmesser
sumber
4

Misalkan pengaturan autofac seperti berikut:

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;
    }
}

Kemudian, misalkan kelas Anda seperti ini:

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; }
    }
}

Oleh karena itu, penggunaan resolver dalam deserialization bisa seperti:

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);

Anda dapat melihat detail lebih lanjut di http://www.newtonsoft.com/json/help/html/DeserializeWithDependencyInjection.htm

Oh Tuhan
sumber
Saya akan memilih ini sebagai solusi terbaik. DI telah digunakan secara luas hari ini oleh c # web devs, dan ini cocok sebagai tempat terpusat untuk menangani jenis konversi tersebut oleh resolver.
appletdua
3

Tidak ada objek yang akan pernah menjadi sebuah IThingy sebagai antarmuka semua abstrak dengan definisi.

Objek yang Anda miliki yang pertama kali dibuat berseri adalah dari beberapa jenis konkret , menerapkan antarmuka abstrak . Anda harus memiliki beton yang sama kelas menghidupkan kembali data serial.

Objek yang dihasilkan kemudian akan dari beberapa jenis yang mengimplementasikan yang abstrak antarmuka yang Anda cari.

Dari dokumentasi berikut yang dapat Anda gunakan

(Thingy)JsonConvert.DeserializeObject(jsonString, typeof(Thingy));

saat deserializing untuk menginformasikan JSON.NET tentang jenis konkret.

Sean Kinsey
sumber
Justru itulah postingan lebih dari setahun yang lalu yang saya maksud. Satu-satunya saran utama (menulis konverter khusus) tidak terlalu layak dengan skala yang terpaksa saya pertimbangkan. JSON.NET telah banyak berubah di tahun-tahun berikutnya. Saya sangat memahami perbedaan antara kelas dan antarmuka, tetapi C # juga mendukung konversi implisit dari antarmuka ke objek yang mengimplementasikan antarmuka terkait dengan pengetikan. Saya pada dasarnya menanyakan apakah ada cara untuk memberi tahu JSON.NET objek mana yang akan mengimplementasikan antarmuka ini.
tmesser
Itu semua ada di jawaban yang saya tunjukkan. Pastikan ada _typeproperti yang menandakan tipe beton yang akan digunakan.
Sean Kinsey
Dan saya sangat meragukan bahwa C # mendukung segala jenis typecasting 'implisit' dari variabel yang dideklarasikan sebagai antarmuka ke tipe konkret tanpa petunjuk apa pun.
Sean Kinsey
Kecuali saya salah membacanya, properti _type seharusnya ada di JSON untuk diserialkan. Itu berfungsi dengan baik jika Anda hanya menghilangkan serialisasi apa yang sudah Anda buat, tetapi bukan itu yang terjadi di sini. Saya menarik JSON dari sejumlah situs yang tidak akan mengikuti standar itu.
tmesser
@YYY - Apakah Anda mengontrol serialisasi ke dan deserialisasi dari sumber JSON? Karena pada akhirnya Anda harus menyematkan jenis konkret dalam JSON serial sebagai petunjuk untuk digunakan saat deserialisasi atau Anda harus menggunakan semacam model coba / parse yang mendeteksi / mencoba mendeteksi jenis konkret pada waktu proses dan aktifkan deserializer yang sesuai.
mcw
3

Solusi saya untuk yang satu ini, yang saya suka karena cukup umum, adalah sebagai berikut:

/// <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();
    }
}

}

Anda dapat dengan jelas dan mudah mengubahnya menjadi konverter yang lebih umum dengan menambahkan konstruktor yang mengambil argumen tipe Dictionary <Type, Type> yang dapat digunakan untuk membuat instance variabel instance konversi.

Simon Brooke
sumber
3

Beberapa tahun kemudian saya mengalami masalah serupa. Dalam kasus saya ada banyak antarmuka bersarang dan preferensi untuk menghasilkan kelas beton pada waktu proses sehingga akan bekerja dengan kelas generik.

Saya memutuskan untuk membuat kelas proxy pada waktu proses yang membungkus objek yang dikembalikan oleh Newtonsoft.

Keuntungan dari pendekatan ini adalah tidak memerlukan implementasi konkret kelas dan dapat menangani kedalaman antarmuka bersarang secara otomatis. Anda dapat melihat lebih banyak tentang itu di blog saya .

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");
            }

        }
    }



}

Pemakaian:

var jObj = JObject.Parse(input);
InterfaceType proxyObject = jObj.toProxy<InterfaceType>();
Berbusa
sumber
Terima kasih! Ini adalah satu-satunya jawaban yang dengan benar mendukung pengetikan dinamis (pengetikan bebek) tanpa memaksa pembatasan pada json masuk.
Philip Pittle
Tidak masalah. Saya sedikit terkejut melihat tidak ada apa-apa di luar sana. Ini telah berpindah sedikit sejak contoh asli itu jadi saya memutuskan untuk membagikan kodenya. github.com/sudsy/JsonDuckTyper . Saya juga menerbitkannya di nuget sebagai JsonDuckTyper. Jika Anda merasa ingin meningkatkannya, kirimkan saja saya PR dan saya akan dengan senang hati membantu.
Sudsy
Ketika saya mencari solusi di bidang ini, saya juga menemukan github.com/ekonbenefits/impromptu-interface . Ini tidak berfungsi dalam kasus saya karena tidak mendukung dotnet core 1.0 tetapi mungkin berhasil untuk Anda.
Sudsy
Saya memang mencoba dengan Antarmuka Dadakan, tetapi Json.Net tidak senang melakukan PopulateObjectproksi yang dihasilkan oleh Antarmuka Dadakan. Sayangnya saya menyerah untuk Mengetik Bebek - itu hanya lebih mudah untuk membuat Serializer Kontrak Json khusus yang menggunakan refleksi untuk menemukan implementasi yang ada dari antarmuka yang diminta dan menggunakannya.
Philip Pittle
1

Gunakan JsonKnownTypes ini , cara penggunaannya sangat mirip, hanya menambahkan diskriminator ke json:

[JsonConverter(typeof(JsonKnownTypeConverter<Interface1>))]
[JsonKnownType(typeof(MyClass), "myClass")]
public interface Interface1
{  }
public class MyClass : Interface1
{
    public string Something;
}

Sekarang ketika Anda membuat serial objek di json akan ditambahkan "$type"dengan"myClass" nilai dan itu akan digunakan untuk deserialize

Json:

{"Something":"something", "$type":"derived"}
Dmitry
sumber
0

Solusi saya menambahkan elemen antarmuka di konstruktor.

public class Customer: ICustomer{
     public Customer(Details details){
          Details = details;
     }

     [JsonProperty("Details",NullValueHnadling = NullValueHandling.Ignore)]
     public IDetails Details {get; set;}
}
Jorge Santos Neill
sumber