0

I know that there is a way but what is the right way to convert(serialize) .NET Core Model Class (C# class properties) from this:

[Required]
[DataType(DataType.Text)]

public string Name {get;set;}
[Required, DataType(DataType.EmailAddress)]

public string Email {get;set;}
[Required]
[DataType(DataType.Text)]


public string Subject{get;set;}
[Required]
[MaxLength(500)]
[Required, DataType(DataType.MultilineText)]

public string Message{get;set;}
[Required]
[MaxLength(500)]
[Required, DataType(DataType.DateTime)]

public DateTime DateOfBirth{get;set;}

public int AnyNumber{get;set;}

To this:


[
    {
        "name": 
        {
            "value": "",
            "type": "text",
            "validations": 
            {
                "required": true
            } 
        }
    },
    {
        "email": 
        {
            "value": "",
            "type": "email",
            "validations": 
            {
                "required": true
            } 
        },
        "message": 
        {
            "value": "",
            "type": "multilineText",
            "validations": 
            {
                "required": true,
                "maxLength": 500
            } 
        },
        "anyNumber": 
        {
            "value": 0,
            "type": "number",
            "validations": 
            {
                "required": false
            } 
        }

    },
]

So any class that I provide structured like in the provided snipped image to get JSON serialized response.
I have found and tried some solutions but things for me get very complicated and I know that reflection is expensive so I have to use it smartly.
Thank you in advance.

11

1 Answer 1

1

Here is a rough implementation of what you need, using the .NET Core System.Text.Json

How to write custom converters for JSON serialization (marshalling) in .NET

public static class SerializationTester
{
    public static void TestSerialize()
    {
        TestClass test = new TestClass()
        {
            AnyNumber = 3,
            DateOfBirth = DateTime.UtcNow,
            Email = "[email protected]",
            Message = "hello world!",
            Name = "john smith",
            Subject = "some subject"
        };
        var options = new JsonSerializerOptions()
        {
            WriteIndented = true,
            IgnoreNullValues = true
        };
        options.Converters.Add(new MetadataConverter());
        var serialized = JsonSerializer.Serialize(test, options);
        Console.WriteLine(serialized);
    }
}

public class MetadataConverter : JsonConverterFactory
{
    /// <summary>
    /// contain nul metadata for types that don't have metdata attributes and dont' need custom converters
    /// each type will only be parsed once with reflections, obviously the attribute values are identical for all class instances
    /// </summary>
    private Dictionary<Type, Dictionary<PropertyInfo, Metadata>> typesMetadataCache = new Dictionary<Type, Dictionary<PropertyInfo, Metadata>>();
    public override bool CanConvert(Type typeToConvert)
    {
        Dictionary<PropertyInfo, Metadata> typeMeta;
        if (!typesMetadataCache.TryGetValue(typeToConvert, out typeMeta))
        {
            typesMetadataCache[typeToConvert] = typeMeta = GetTypeMeta(typeToConvert);
        }
        return typeMeta != null;
    }

    private Dictionary<PropertyInfo, Metadata> GetTypeMeta(Type typeToConvert)
    {
        Dictionary<PropertyInfo, Metadata> theReturn = new Dictionary<PropertyInfo, Metadata>();
        bool metadataSpecified = false;
        foreach (var currentProperty in typeToConvert.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            var required = currentProperty.GetCustomAttributes<RequiredAttribute>()?.FirstOrDefault();
            var dataType = currentProperty.GetCustomAttributes<DataTypeAttribute>()?.FirstOrDefault();
            var maxLength = currentProperty.GetCustomAttributes<MaxLengthAttribute>()?.FirstOrDefault();
            if (required != null || dataType != null || maxLength != null)
            {
                metadataSpecified = true;
            }

            var currentMeta = theReturn[currentProperty] = new Metadata()
            {
                type = dataType?.DataType.ToString() ?? currentProperty.PropertyType.Name,
                validations = new Validations()
                {
                    maxLength = maxLength?.MaxLength,
                    required = (required != null)
                }
            };
        }
        if (metadataSpecified)
        {
            return theReturn;
        }
        // metadata not specified for any property, don't use custom converter
        return null;
    }

    public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
    {
        return (JsonConverter)Activator.CreateInstance(typeof(MetadataTypeConverter<>).MakeGenericType(typeToConvert), options, typesMetadataCache[typeToConvert]);
    }

    private class MetadataTypeConverter<TValue> : JsonConverter<TValue>
    {
        private Dictionary<PropertyInfo, JsonConverter> propertyConverters = new Dictionary<PropertyInfo, JsonConverter>();
        public MetadataTypeConverter(JsonSerializerOptions options, Dictionary<PropertyInfo, Metadata> typeMetadata)
        {
            foreach (var currentMeta in typeMetadata)
            {
                if (currentMeta.Value == null)
                {
                    propertyConverters[currentMeta.Key] = options.GetConverter(currentMeta.Key.PropertyType);
                }
                else
                {
                    propertyConverters[currentMeta.Key] = (JsonConverter)Activator.CreateInstance(typeof(MetadataValueConverter<>).MakeGenericType(currentMeta.Key.PropertyType), options, currentMeta.Value);
                }
            }
        }
        public override TValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            throw new NotImplementedException();
        }

        public override void Write(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options)
        {
            writer.WriteStartObject();
            foreach (var currentConverter in propertyConverters)
            {
                var currentPropertyValue = currentConverter.Key.GetValue(value);
                if (currentConverter.Value is IMetadataValueConverter currentMetadataValueConverter)
                {
                    currentMetadataValueConverter.Write(writer, currentPropertyValue, options);
                }
                else
                {
                    var currentWriteMethod = currentConverter.Value.GetType().GetMethod("Write", BindingFlags.Public | BindingFlags.Instance);
                    currentWriteMethod.Invoke(currentConverter.Value, new object[] { writer, currentPropertyValue, options });
                }
            }
            writer.WriteEndObject();
        }
    }
    private class MetadataValueConverter<TValue> : JsonConverter<TValue>, IMetadataValueConverter
    {
        private Metadata metadata;
        public MetadataValueConverter(JsonSerializerOptions options, Metadata metadata)
        {
            this.metadata = metadata;
        }
        public override TValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = JsonSerializer.Deserialize<MetadataWithValue>(ref reader, options);
            return value.value;
        }

        public override void Write(Utf8JsonWriter writer, TValue value, JsonSerializerOptions options)
        {
            JsonSerializer.Serialize(writer, new MetadataWithValue(metadata, value), options);
        }
        void IMetadataValueConverter.Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) => Write(writer, (TValue)value, options);
        public class MetadataWithValue : Metadata
        {
            public MetadataWithValue() { }
            public MetadataWithValue(Metadata metadata, TValue value)
            {
                type = metadata.type;
                validations = metadata.validations;
                this.value = value;
            }
            public TValue value { get; set; }
        }
    }

    private interface IMetadataValueConverter
    {
        void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options);
    }
    private class Metadata
    {
        public string type { get; set; }
        public Validations validations { get; set; }
    }
    private class Validations
    {
        public bool required { get; set; }
        public int? maxLength { get; set; }

    }
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class RequiredAttribute : Attribute { }
public enum DataType
{
    Text,
    EmailAddress,
    MultilineText,
    DateTime
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class DataTypeAttribute : Attribute
{
    public DataTypeAttribute(DataType dataType)
    {
        DataType = dataType;
    }

    public DataType DataType { get; private set; }
}
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class MaxLengthAttribute : Attribute
{
    public MaxLengthAttribute(int maxLength)
    {
        MaxLength = maxLength;
    }

    public int MaxLength { get; private set; }
}
public class TestClass
{
    [Required]
    [DataType(DataType.Text)]
    public string Name { get; set; }

    [Required, DataType(DataType.EmailAddress)]
    public string Email { get; set; }

    [Required]
    [DataType(DataType.Text)]
    public string Subject { get; set; }

    [Required]
    [MaxLength(500)]
    [Required, DataType(DataType.MultilineText)]
    public string Message { get; set; }

    [Required]
    [MaxLength(500)]
    [Required, DataType(DataType.DateTime)]
    public DateTime DateOfBirth { get; set; }

    public int AnyNumber { get; set; }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.