c# - .Net Core 3.0 JsonSerializer füllt vorhandene Objekte auf




asp.net-core razor-pages (5)

Die Problemumgehung kann auch so einfach sein:

    private static T ParseWithTemplate<T>(T template, string input) 
    {
        var ignoreNulls = new JsonSerializerOptions() { IgnoreNullValues = true };
        var templateJson = JsonSerializer.ToString(template, ignoreNulls);
        var combinedData = templateJson.TrimEnd('}') + "," + input.TrimStart().TrimStart('{');
        return JsonSerializer.Parse<T>(combinedData);
    }

Ausgabe:

Ich bereite eine Migration von ASP.NET Core 2.2 auf 3.0 vor.

Da ich keine fortgeschritteneren JSON-Funktionen verwende (aber vielleicht eine wie unten beschrieben) und 3.0 jetzt mit einem eingebauten Namespace / Klassen für JSON, System.Text.Json , System.Text.Json , habe ich mich entschlossen, die zu System.Text.Json vorherige Standardeinstellung Newtonsoft.Json . Beachten Sie, dass System.Text.Json nicht vollständig ersetzt.

Ich habe das überall hin geschafft, z

var obj = JsonSerializer.Parse<T>(jsonstring);

var jsonstring = JsonSerializer.ToString(obj);

aber an einem Ort, an dem ich ein vorhandenes Objekt auffülle.

Mit Newtonsoft.Json ist das möglich

JsonConvert.PopulateObject(jsonstring, obj);

Der integrierte System.Text.Json Namespace enthält einige zusätzliche Klassen wie JsonDocumnet , JsonElement und Utf8JsonReader , obwohl ich keine finden kann, die ein vorhandenes Objekt als Parameter verwenden.

Ich bin auch nicht erfahren genug, um zu sehen, wie man das Vorhandene nutzt.

Möglicherweise wird es in .Net Core eine neue Funktion geben (danke an Mustafa Gursel für den Link), aber in der Zwischenzeit (und wenn nicht) ...

... Ich frage mich jetzt, ob es möglich ist, etwas Ähnliches zu erreichen wie mit PopulateObject ?

Ich meine, ist es mit einer der anderen System.Text.Json Klassen möglich, System.Text.Json zu erreichen und nur die eingestellten Eigenschaften zu aktualisieren / zu ersetzen?, ... oder eine andere clevere Problemumgehung?

Hier ist ein Beispiel, wie es aussehen könnte (und es muss generisch sein, da das an die Deserialisierungsmethode übergebene Objekt vom Typ <T> ). Ich habe 2 Json-Strings, die in ein Objekt geparst werden sollen, wobei für den ersten einige Standardeigenschaften und für den zweiten einige festgelegt sind, z

Beachten Sie, dass ein Eigenschaftswert von jedem anderen Typ als eine string .

Json String 1:

{
  "Title": "Startpage",
  "Link": "/index",
}

Json String 2:

{
  "Head": "Latest news"
  "Link": "/news"
}

Unter Verwendung der beiden obigen Json-Zeichenfolgen möchte ich ein Objekt erhalten, das Folgendes bewirkt:

{
  "Title": "Startpage",
  "Head": "Latest news",
  "Link": "/news"
}

Wie im obigen Beispiel zu sehen ist, werden bei Eigenschaften in der 2. Klasse die Werte in der 1. Klasse ersetzt (wie bei "Kopf" und "Link"). Wenn dies nicht der Fall ist, bleiben die vorhandenen Werte erhalten (wie bei "Titel").


Hier ist ein Beispielcode, der dies tut. Es verwendet die neue Utf8JsonReader-Struktur, damit das Objekt beim Parsen aufgefüllt wird . Es unterstützt JSON / CLR-Äquivalenztypen, verschachtelte Objekte (erstellt, wenn sie nicht vorhanden sind), Listen und Arrays.

var populator = new JsonPopulator();
var obj = new MyClass();
populator.PopulateObject(obj, "{\"Title\":\"Startpage\",\"Link\":\"/index\"}");
populator.PopulateObject(obj, "{\"Head\":\"Latest news\",\"Link\":\"/news\"}");

public class MyClass
{
    public string Title { get; set; }
    public string Head { get; set; }
    public string Link { get; set; }
}

Beachten Sie, dass es nicht alle Funktionen unterstützt, die Sie wahrscheinlich erwarten, Sie können es jedoch überschreiben oder anpassen. Dinge, die hinzugefügt werden könnten: 1) Namenskonvention. Sie müssten die GetProperty-Methode überschreiben. 2) Wörterbücher oder expando Objekte. 3) Die Leistung kann verbessert werden, da anstelle der MemberAccessor / Delegate-Techniken Reflection verwendet wird

public class JsonPopulator
{
    public void PopulateObject(object obj, string jsonString, JsonSerializerOptions options = null) => PopulateObject(obj, jsonString != null ? Encoding.UTF8.GetBytes(jsonString) : null, options);
    public virtual void PopulateObject(object obj, ReadOnlySpan<byte> jsonData, JsonSerializerOptions options = null)
    {
        options ??= new JsonSerializerOptions();
        var state = new JsonReaderState(new JsonReaderOptions { AllowTrailingCommas = options.AllowTrailingCommas, CommentHandling = options.ReadCommentHandling, MaxDepth = options.MaxDepth });
        var reader = new Utf8JsonReader(jsonData, isFinalBlock: true, state);
        new Worker(this, reader, obj, options);
    }

    protected virtual PropertyInfo GetProperty(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));

        if (propertyName == null)
            throw new ArgumentNullException(nameof(propertyName));

        var prop = obj.GetType().GetProperty(propertyName);
        return prop;
    }

    protected virtual bool SetPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, object obj, string propertyName)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));

        if (propertyName == null)
            throw new ArgumentNullException(nameof(propertyName));

        var prop = GetProperty(ref reader, options, obj, propertyName);
        if (prop == null)
            return false;

        if (!TryReadPropertyValue(ref reader, options, prop.PropertyType, out var value))
            return false;

        prop.SetValue(obj, value);
        return true;
    }

    protected virtual bool TryReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions options, Type propertyType, out object value)
    {
        if (propertyType == null)
            throw new ArgumentNullException(nameof(reader));

        if (reader.TokenType == JsonTokenType.Null)
        {
            value = null;
            return !propertyType.IsValueType || Nullable.GetUnderlyingType(propertyType) != null;
        }

        if (propertyType == typeof(object)) { value = ReadValue(ref reader); return true; }
        if (propertyType == typeof(string)) { value = JsonSerializer.ReadValue<JsonElement>(ref reader, options).GetRawText(); return true; }
        if (propertyType == typeof(int) && reader.TryGetInt32(out var i32)) { value = i32; return true; }
        if (propertyType == typeof(long) && reader.TryGetInt64(out var i64)) { value = i64; return true; }
        if (propertyType == typeof(DateTime) && reader.TryGetDateTime(out var dt)) { value = dt; return true; }
        if (propertyType == typeof(DateTimeOffset) && reader.TryGetDateTimeOffset(out var dto)) { value = dto; return true; }
        if (propertyType == typeof(Guid) && reader.TryGetGuid(out var guid)) { value = guid; return true; }
        if (propertyType == typeof(decimal) && reader.TryGetDecimal(out var dec)) { value = dec; return true; }
        if (propertyType == typeof(double) && reader.TryGetDouble(out var dbl)) { value = dbl; return true; }
        if (propertyType == typeof(float) && reader.TryGetSingle(out var sgl)) { value = sgl; return true; }
        if (propertyType == typeof(uint) && reader.TryGetUInt32(out var ui32)) { value = ui32; return true; }
        if (propertyType == typeof(ulong) && reader.TryGetUInt64(out var ui64)) { value = ui64; return true; }
        if (propertyType == typeof(byte[]) && reader.TryGetBytesFromBase64(out var bytes)) { value = bytes; return true; }

        if (propertyType == typeof(bool))
        {
            if (reader.TokenType == JsonTokenType.False || reader.TokenType == JsonTokenType.True)
            {
                value = reader.GetBoolean();
                return true;
            }
        }

        // fallback here
        return TryConvertValue(ref reader, propertyType, out value);
    }

    protected virtual object ReadValue(ref Utf8JsonReader reader)
    {
        switch (reader.TokenType)
        {
            case JsonTokenType.False: return false;
            case JsonTokenType.True: return true;
            case JsonTokenType.Null: return null;
            case JsonTokenType.String: return reader.GetString();

            case JsonTokenType.Number: // is there a better way?
                if (reader.TryGetInt32(out var i32))
                    return i32;

                if (reader.TryGetInt64(out var i64))
                    return i64;

                if (reader.TryGetUInt64(out var ui64)) // uint is already handled by i64
                    return ui64;

                if (reader.TryGetSingle(out var sgl))
                    return sgl;

                if (reader.TryGetDouble(out var dbl))
                    return dbl;

                if (reader.TryGetDecimal(out var dec))
                    return dec;

                break;
        }
        throw new NotSupportedException();
    }

    // we're here when json types & property types don't match exactly
    protected virtual bool TryConvertValue(ref Utf8JsonReader reader, Type propertyType, out object value)
    {
        if (propertyType == null)
            throw new ArgumentNullException(nameof(reader));

        if (propertyType == typeof(bool))
        {
            if (reader.TryGetInt64(out var i64)) // one size fits all
            {
                value = i64 != 0;
                return true;
            }
        }

        // TODO: add other conversions

        value = null;
        return false;
    }

    protected virtual object CreateInstance(ref Utf8JsonReader reader, Type propertyType)
    {
        if (propertyType.GetConstructor(Type.EmptyTypes) == null)
            return null;

        // TODO: handle custom instance creation
        try
        {
            return Activator.CreateInstance(propertyType);
        }
        catch
        {
            // swallow
            return null;
        }
    }

    private class Worker
    {
        private readonly Stack<WorkerProperty> _properties = new Stack<WorkerProperty>();
        private readonly Stack<object> _objects = new Stack<object>();

        public Worker(JsonPopulator populator, Utf8JsonReader reader, object obj, JsonSerializerOptions options)
        {
            _objects.Push(obj);
            WorkerProperty prop;
            WorkerProperty peek;
            while (reader.Read())
            {
                switch (reader.TokenType)
                {
                    case JsonTokenType.PropertyName:
                        prop = new WorkerProperty();
                        prop.PropertyName = Encoding.UTF8.GetString(reader.ValueSpan);
                        _properties.Push(prop);
                        break;

                    case JsonTokenType.StartObject:
                    case JsonTokenType.StartArray:
                        if (_properties.Count > 0)
                        {
                            object child = null;
                            var parent = _objects.Peek();
                            PropertyInfo pi = null;
                            if (parent != null)
                            {
                                pi = populator.GetProperty(ref reader, options, parent, _properties.Peek().PropertyName);
                                if (pi != null)
                                {
                                    child = pi.GetValue(parent); // mimic ObjectCreationHandling.Auto
                                    if (child == null && pi.CanWrite)
                                    {
                                        if (reader.TokenType == JsonTokenType.StartArray)
                                        {
                                            if (!typeof(IList).IsAssignableFrom(pi.PropertyType))
                                                break;  // don't create if we can't handle it
                                        }

                                        if (reader.TokenType == JsonTokenType.StartArray && pi.PropertyType.IsArray)
                                        {
                                            child = Activator.CreateInstance(typeof(List<>).MakeGenericType(pi.PropertyType.GetElementType())); // we can't add to arrays...
                                        }
                                        else
                                        {
                                            child = populator.CreateInstance(ref reader, pi.PropertyType);
                                            if (child != null)
                                            {
                                                pi.SetValue(parent, child);
                                            }
                                        }
                                    }
                                }
                            }

                            if (reader.TokenType == JsonTokenType.StartObject)
                            {
                                _objects.Push(child);
                            }
                            else if (child != null) // StartArray
                            {
                                peek = _properties.Peek();
                                peek.IsArray = pi.PropertyType.IsArray;
                                peek.List = (IList)child;
                                peek.ListPropertyType = GetListElementType(child.GetType());
                                peek.ArrayPropertyInfo = pi;
                            }
                        }
                        break;

                    case JsonTokenType.EndObject:
                        _objects.Pop();
                        if (_properties.Count > 0)
                        {
                            _properties.Pop();
                        }
                        break;

                    case JsonTokenType.EndArray:
                        if (_properties.Count > 0)
                        {
                            prop = _properties.Pop();
                            if (prop.IsArray)
                            {
                                var array = Array.CreateInstance(GetListElementType(prop.ArrayPropertyInfo.PropertyType), prop.List.Count); // array is finished, convert list into a real array
                                prop.List.CopyTo(array, 0);
                                prop.ArrayPropertyInfo.SetValue(_objects.Peek(), array);
                            }
                        }
                        break;

                    case JsonTokenType.False:
                    case JsonTokenType.Null:
                    case JsonTokenType.Number:
                    case JsonTokenType.String:
                    case JsonTokenType.True:
                        peek = _properties.Peek();
                        if (peek.List != null)
                        {
                            if (populator.TryReadPropertyValue(ref reader, options, peek.ListPropertyType, out var item))
                            {
                                peek.List.Add(item);
                            }
                            break;
                        }

                        prop = _properties.Pop();
                        var current = _objects.Peek();
                        if (current != null)
                        {
                            populator.SetPropertyValue(ref reader, options, current, prop.PropertyName);
                        }
                        break;
                }
            }
        }

        private static Type GetListElementType(Type type)
        {
            if (type.IsArray)
                return type.GetElementType();

            foreach (Type iface in type.GetInterfaces())
            {
                if (!iface.IsGenericType) continue;
                if (iface.GetGenericTypeDefinition() == typeof(IDictionary<,>)) return iface.GetGenericArguments()[1];
                if (iface.GetGenericTypeDefinition() == typeof(IList<>)) return iface.GetGenericArguments()[0];
                if (iface.GetGenericTypeDefinition() == typeof(ICollection<>)) return iface.GetGenericArguments()[0];
                if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) return iface.GetGenericArguments()[0];
            }
            return typeof(object);
        }
    }

    private class WorkerProperty
    {
        public string PropertyName;
        public IList List;
        public Type ListPropertyType;
        public bool IsArray;
        public PropertyInfo ArrayPropertyInfo;

        public override string ToString() => PropertyName;
    }
}

Ich weiß nicht viel über diese neue Version des Plug-Ins, habe jedoch ein Tutorial gefunden, dem einige Beispiele folgen können

Basierend auf ihm habe ich über diese Methode nachgedacht und ich stelle mir vor, dass er in der Lage ist, sein Problem zu lösen

//To populate an existing variable we will do so, we will create a variable with the pre existing data
object PrevData = YourVariableData;

//After this we will map the json received
var NewObj = JsonSerializer.Parse<T>(jsonstring);

CopyValues(NewObj, PrevData)

//I found a function that does what you need, you can use it
//source: https://.com/questions/8702603/merging-two-objects-in-c-sharp
public void CopyValues<T>(T target, T source)
{

    if (target == null) throw new ArgumentNullException(nameof(target));
    if (source== null) throw new ArgumentNullException(nameof(source));

    Type t = typeof(T);

    var properties = t.GetProperties(
          BindingFlags.Instance | BindingFlags.Public).Where(prop => 
              prop.CanRead 
           && prop.CanWrite 
           && prop.GetIndexParameters().Length == 0);

    foreach (var prop in properties)
    {
        var value = prop.GetValue(source, null);
        prop.SetValue(target, value, null);
    }
}

Nehmen wir also an, dass Core 3 dies nicht standardmäßig unterstützt, und versuchen wir, dieses Problem zu umgehen. Also, was ist unser Problem?

Wir wollen eine Methode, die einige Eigenschaften eines vorhandenen Objekts mit denen eines JSON-Strings überschreibt. Unsere Methode hat also eine Signatur von:

void PopulateObject<T>(T target, string jsonSource) where T : class

Wir wollen kein benutzerdefiniertes Parsing, da es umständlich ist. jsonSource versuchen wir den offensichtlichen Ansatz: Deserialisieren Sie jsonSource und kopieren Sie die Ergebniseigenschaften in unser Objekt. Wir können jedoch nicht einfach gehen

T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);

Das liegt an einem Typ

class Example
{
    int Id { get; set; }
    int Value { get; set; }
}

und ein JSON

{
    "Id": 42
}

Wir erhalten updateObject.Value == 0 . Jetzt wissen wir nicht, ob 0 der neue aktualisierte Wert ist oder ob er nur nicht aktualisiert wurde. jsonSource müssen wir genau wissen, welche Eigenschaften jsonSource enthält.

Glücklicherweise können wir mit der System.Text.Json API die Struktur des analysierten JSON untersuchen.

var json = JsonDocument.Parse(jsonSource).RootElement;

Wir können jetzt alle Eigenschaften auflisten und kopieren.

foreach (var property in json.EnumerateObject())
{
    OverwriteProperty(target, property);
}

Wir werden den Wert mit Reflection kopieren:

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
    var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    var parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType);

    propertyInfo.SetValue(target, parsedValue);
} 

Wir können hier sehen, dass wir ein flaches Update machen. Wenn das Objekt ein anderes komplexes Objekt als Eigenschaft enthält, wird dieses Objekt als Ganzes kopiert und überschrieben und nicht aktualisiert. Wenn Sie umfangreiche Aktualisierungen benötigen, muss diese Methode geändert werden, um den aktuellen Wert der Eigenschaft zu extrahieren. Rufen Sie dann das PopulateObject rekursiv auf, wenn der Typ der Eigenschaft ein Referenztyp ist (für den auch Type als Parameter in PopulateObject akzeptiert werden muss).

Wenn wir alles zusammenfügen, erhalten wir:

void PopulateObject<T>(T target, string jsonSource) where T : class
{
    var json = JsonDocument.Parse(jsonSource).RootElement;

    foreach (var property in json.EnumerateObject())
    {
        OverwriteProperty(target, property);
    }
}

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
    var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    var parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType);

    propertyInfo.SetValue(target, parsedValue);
} 

Wie robust ist das? Nun, für ein JSON-Array ist dies sicherlich nicht sinnvoll, aber ich bin mir nicht sicher, wie die PopulateObject Methode PopulateObject auf einem Array funktionieren soll. Ich weiß nicht, wie es in der Leistung mit der Json.Net Version verglichen wird, das Json.Net Sie selbst testen. Eigenschaften, die nicht zum Zieltyp gehören, werden automatisch ignoriert. Ich dachte, es wäre der sinnvollste Ansatz, aber Sie könnten auch anders denken, in diesem Fall muss die Eigenschaft null-check durch einen Ausnahmewurf ersetzt werden.

BEARBEITEN:

Ich ging voran und implementierte eine Deep Copy:

void PopulateObject<T>(T target, string jsonSource) where T : class => 
    PopulateObject(target, jsonSource, typeof(T));

void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
    OverwriteProperty(target, updatedProperty, typeof(T));

void PopulateObject(object target, string jsonSource, Type type)
{
    var json = JsonDocument.Parse(jsonSource).RootElement;

    foreach (var property in json.EnumerateObject())
    {
        OverwriteProperty(target, property, type);
    }
}

void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
    var propertyInfo = type.GetProperty(updatedProperty.Name);

    if (propertyInfo == null)
    {
        return;
    }

    var propertyType = propertyInfo.PropertyType;
    object parsedValue;

    if (propertyType.IsValueType)
    {
        parsedValue = JsonSerializer.Parse(updatedProperty.Value, propertyType);
    }
    else
    {
        parsedValue = propertyInfo.GetValue(target);
        PopulateObject(parsedValue, updatedProperty.Value, propertyType);
    }

    propertyInfo.SetValue(target, parsedValue);
}

Um dies robuster zu machen, müssten Sie entweder eine separate PopulateObjectDeep Methode haben oder PopulateObjectOptions oder etwas Ähnliches mit einem deep / shallow-Flag übergeben.

EDIT 2:

Der Punkt des tiefen Kopierens ist, dass wir ein Objekt haben

{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 32
    },
    "Value": 128
}

und bevölkere es mit

{
    "Child":
    {
        "Value": 64
    }
}

wir würden bekommen

{
    "Id": 42,
    "Child":
    {
        "Id": 43,
        "Value": 64
    },
    "Value": 128
}

Im Falle einer flachen Kopie erhalten wir im kopierten Kind Id = 0 .


Wenn es nur eine Verwendung ist und Sie keine zusätzlichen Abhängigkeiten / viel Code hinzufügen möchten, macht es Ihnen nichts aus, ein bisschen ineffizient zu sein, und ich habe etwas Offensichtliches nicht verpasst. Sie können einfach Folgendes verwenden:

using System;
using System.Linq;
using System.Text.Json.Serialization;

namespace ConsoleApp
{
    public class Model
    {
        public string Title { get; set; }
        public string Head { get; set; }
        public string Link { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var model = new Model();

            Console.WriteLine(JsonSerializer.ToString(model));

            var json1 = "{ \"Title\": \"Startpage\", \"Link\": \"/index\" }";

            Map<Model>(model, json1);

            Console.WriteLine(JsonSerializer.ToString(model));

            var json2 = "{ \"Head\": \"Latest news\", \"Link\": \"/news\" }";

            Map<Model>(model, json2);

            Console.WriteLine(JsonSerializer.ToString(model));

            Console.ReadKey();
        }

        private static T Map<T>(T obj, string jsonString) where T : class
        {
            var newObj = JsonSerializer.Parse<T>(jsonString);

            foreach (var property in newObj.GetType().GetProperties())
            {
                if (obj.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(newObj) != null))
                {
                    property.SetValue(obj, property.GetValue(newObj));
                }
            }

            return obj;
        }
    }
}






asp.net-core-3.0