How to add metadata to describe which properties are dates in JSON.Net

前端 未结 1 1592
小鲜肉
小鲜肉 2020-12-07 06:21

I would like to add a metadata property to my json so that the client side can know what properties are dates.

For example if I had an object like this:



        
相关标签:
1条回答
  • 2020-12-07 06:40

    You could create a custom ContractResolver that inserts a synthetic "_date_properties_" property into the contract of every object that is serialized.

    To do this, first subclass DefaultContractResolver to allow contracts to be fluently customized after they have been created by application-added event handlers:

    public class ConfigurableContractResolver : DefaultContractResolver
    {
        readonly object contractCreatedPadlock = new object();
        event EventHandler<ContractCreatedEventArgs> contractCreated;
        int contractCount = 0;
    
        void OnContractCreated(JsonContract contract, Type objectType)
        {
            EventHandler<ContractCreatedEventArgs> created;
            lock (contractCreatedPadlock)
            {
                contractCount++;
                created = contractCreated;
            }
            if (created != null)
            {
                created(this, new ContractCreatedEventArgs(contract, objectType));
            }
        }
    
        public event EventHandler<ContractCreatedEventArgs> ContractCreated
        {
            add
            {
                lock (contractCreatedPadlock)
                {
                    if (contractCount > 0)
                    {
                        throw new InvalidOperationException("ContractCreated events cannot be added after the first contract is generated.");
                    }
                    contractCreated += value;
                }
            }
            remove
            {
                lock (contractCreatedPadlock)
                {
                    if (contractCount > 0)
                    {
                        throw new InvalidOperationException("ContractCreated events cannot be removed after the first contract is generated.");
                    }
                    contractCreated -= value;
                }
            }  
        }
    
        protected override JsonContract CreateContract(Type objectType)
        {
            var contract = base.CreateContract(objectType);
            OnContractCreated(contract, objectType);
            return contract;
        }
    }
    
    public class ContractCreatedEventArgs : EventArgs
    {
        public JsonContract Contract { get; private set; }
        public Type ObjectType { get; private set; }
    
        public ContractCreatedEventArgs(JsonContract contract, Type objectType)
        {
            this.Contract = contract;
            this.ObjectType = objectType;
        }
    }
    
    public static class ConfigurableContractResolverExtensions
    {
        public static ConfigurableContractResolver Configure(this ConfigurableContractResolver resolver, EventHandler<ContractCreatedEventArgs> handler)
        {
            if (resolver == null || handler == null)
                throw new ArgumentNullException();
            resolver.ContractCreated += handler;
            return resolver;
        }
    }
    

    Next, create an extension method to add the desired property to a JsonObjectContract:

    public static class JsonContractExtensions
    {
        const string DatePropertiesName = "_date_properties_";
    
        public static void AddDateProperties(this JsonContract contract)
        {
            var objectContract = contract as JsonObjectContract;
            if (objectContract == null)
                return;
            var properties = objectContract.Properties.Where(p => p.PropertyType == typeof(DateTime) || p.PropertyType == typeof(DateTime?)).ToList();
            if (properties.Count > 0)
            {
                var property = new JsonProperty
                {
                    DeclaringType = contract.UnderlyingType,
                    PropertyName = DatePropertiesName,
                    UnderlyingName = DatePropertiesName,
                    PropertyType = typeof(string[]),
                    ValueProvider = new FixedValueProvider(properties.Select(p => p.PropertyName).ToArray()),
                    AttributeProvider = new NoAttributeProvider(),
                    Readable = true,
                    Writable = false,
                    // Ensure // Ensure PreserveReferencesHandling and TypeNameHandling do not apply to the synthetic property.
                    ItemIsReference = false, 
                    TypeNameHandling = TypeNameHandling.None,
                };
                objectContract.Properties.Insert(0, property);
            }
        }
    
        class FixedValueProvider : IValueProvider
        {
            readonly object properties;
    
            public FixedValueProvider(object value)
            {
                this.properties = value;
            }
    
            #region IValueProvider Members
    
            public object GetValue(object target)
            {
                return properties;
            }
    
            public void SetValue(object target, object value)
            {
                throw new NotImplementedException("SetValue not implemented for fixed properties; set JsonProperty.Writable = false.");
            }
    
            #endregion
        }
    
        class NoAttributeProvider : IAttributeProvider
        {
            #region IAttributeProvider Members
    
            public IList<Attribute> GetAttributes(Type attributeType, bool inherit) { return new Attribute[0]; }
    
            public IList<Attribute> GetAttributes(bool inherit) { return new Attribute[0]; }
    
            #endregion
        }
    }
    

    Finally, serialize your example type as follows:

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new ConfigurableContractResolver
        {
            // Here I am using CamelCaseNamingStrategy as is shown in your JSON.
            // If you don't want camel case, leave NamingStrategy null.
            NamingStrategy = new CamelCaseNamingStrategy(),
        }.Configure((s, e) => { e.Contract.AddDateProperties(); }),
    };
    
    var json = JsonConvert.SerializeObject(example, Formatting.Indented, settings);
    

    This solution only handles statically typed DateTime and DateTime? properties. If you have object-valued properties that sometimes have DateTime values, or a Dictionary<string, DateTime>, or extension data containing DateTime values, you will need a more complex solution.

    (As an alternative implementation, you could instead subclass DefaultContractResolver.CreateObjectContract and hardcode the required properties there using JsonContractExtensions.AddDateProperties(), however I thought it would be more interesting to create a general-purpose, fluently configurable contract resolver in case it becomes necessary to plug in different customizations later.)

    You may want to cache the contract resolver for best performance.

    Sample .Net fiddle.

    0 讨论(0)
提交回复
热议问题