.Net Core 3.0 JsonSerializer populate existing object

前端 未结 7 917
再見小時候
再見小時候 2020-12-24 10:51

I\'m preparing a migration from ASP.NET Core 2.2 to 3.0.

As I don\'t use more advanced JSON features (but maybe one as described below), and 3.0 now comes with a buil

7条回答
  •  醉梦人生
    2020-12-24 11:40

    Here is some sample code that does it. It's using the new Utf8JsonReader struct so it populates the object at the same time it parses it. It supports JSON/CLR types equivalence, nested objects (creates if they don't exist), lists and 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; }
    }
    

    Note it doesn't support all of what you would probably expect, but you can override or customize it. Things that could be added: 1) naming convention. You'd have to override the GetProperty method. 2) dictionaries or expando objects. 3) performance can be improved because it uses Reflection instead of MemberAccessor/delegate techniques

    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 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.Deserialize(ref reader, options).GetString(); 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 _properties = new Stack();
            private readonly Stack _objects = new Stack();
    
            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;
        }
    }
    
        

    提交回复
    热议问题