I have a class hierarchy like this:
class Rule { }
class Condition { List<Rule> Rules { get; set; } }
Forget about the remaining properties. I need to deserialize from a JSON string, using a custom JsonConverter. The problem is, I have code for each specific case, but I cannot have it ran recursively, for taking care of the Rules property, each of its elements can be a Condition too.
My code looks like this (ReadJson method):
var jo = JObject.Load(reader);
Rule rule = null;
if (jo["condition"] == null)
{
rule = new Rule();
//fill the properties for rule
}
else
{
rule = new Condition();
//I now want the converter to go through all the values in jo["rules"] and turn them into Rules or Conditions
}
What is the best way to achieve this? I tried to get the JSON for the remaining part, if the object is found to be a Condition:
var json = jo.GetValue("rule").ToString();
But I cannot deserialize it like this, it throws an exception:
var rules = JsonConvert.DeserializeObject<Rule[]>(json, this);
The exception is: JsonReaderException : Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray. Path '', line 1, position 1.
Any ideas?
You're not far from having it working. After you instantiate correct type of object based on the presence or absence of the condition property in the JSON, you can populate the instance using the serializer.Populate method. This should take care of the recursion automatically. You do need to pass a new JsonReader instance to Populate, which you can create using jo.CreateReader().
Here is what the converter should look like:
public class RuleConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(Rule).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jo = JObject.Load(reader);
Rule rule = null;
if (jo["condition"] == null)
{
rule = new Rule();
}
else
{
rule = new Condition();
}
serializer.Populate(jo.CreateReader(), rule);
return rule;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is a working example: https://dotnetfiddle.net/SHctMo
Related
I have the following interface
public interface IApiResult<TResult> : IApiResult
{
TResult Result { get; set; }
}
with a concrete class like this
public class ApiResult<TResult> : ApiResult, IApiResult<TResult>
{
public ApiResult( TResult result ) : base() {
Result = result;
}
public TResult Result { get; set; }
}
When I used Newtonsoft json library I used a JsonConverter to manage polimorphic serialization and deserialization this way
public override object ReadJson( JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer ) {
var obj = JObject.Load( reader );
var apiResult = new ApiResult.ApiResult().As<IApiResult>();
//Check if is an IApiResult<T>
if (obj.TryGetValue( nameof( ApiResult<object>.Result ), StringComparison.InvariantCultureIgnoreCase, out var jResult )) {
//Retrieve the result property type in order to create the proper apiResult object
var prop = objectType.GetProperty( nameof( ApiResult<object>.Result ) );
var value = jResult.ToObject( prop.PropertyType, serializer );
var rType = typeof( ApiResult<> ).MakeGenericType( prop.PropertyType );
apiResult = Activator.CreateInstance( rType ).As<IApiResult>();
prop.SetValue( apiResult, value );
}
//Set the messages
var jMessages = obj.GetValue( nameof( ApiResult.ApiResult.Messages ), StringComparison.InvariantCultureIgnoreCase ).ToObject<JObject[]>();
apiResult.Messages = DeserializeReasons( jMessages );
return apiResult;
}
How can I migrate this code to System.Text.Json?
UPDATE
My biggest problem is with the JObject.TryGetvalue function. This function would have returned a deserialized object that let me understood the type. Since that, I was only using some reflection to understand the type of ApiResult<T>.
With the actual UTF8JsonReader class I am only able to read token by token so I can not replicate the previous behavior.
Your question boils down to, Inside JsonConverter<T>.Read(), how can I scan forward in, or load the contents of, a Utf8JsonReader to determine the polymorphic type of object to deserialize without having to manually deserialize token by token as shown in the documentation example?
As of .NET 6 you have a couple options to accomplish this.
Firstly you can copy the Utf8JsonReader struct and scan forward in the copy until you find the property or properties you want. The original, incoming Utf8JsonReader will be unchanged and still point to the beginning of the incoming JSON value. System.Text.Json will always preload the entire JSON object, array or primitive to be deserialized before calling JsonConverter<T>.Read() so you can be certain the require values are present.
To do this, introduce the following extension methods:
public static partial class JsonExtensions
{
public delegate TValue? DeserializeValue<TValue>(ref Utf8JsonReader reader, JsonSerializerOptions options);
///Scan forward in a copy of the Utf8JsonReader to find a property with the specified name at the current depth, and return its value.
///The Utf8JsonReader is not passed by reference so the state of the caller's reader is unchanged.
///This method should only be called inside JsonConverter<T>.Read(), at which point the entire JSON for the object being read should have been pre-loaded
public static bool TryGetPropertyValue<TValue>(this Utf8JsonReader reader, string name, StringComparison comparison, JsonSerializerOptions options, out TValue? value) =>
reader.TryGetPropertyValue<TValue>(name, comparison, options, (ref Utf8JsonReader r, JsonSerializerOptions o) => JsonSerializer.Deserialize<TValue>(ref r, o), out value);
public static bool TryGetPropertyValue<TValue>(this Utf8JsonReader reader, string name, StringComparison comparison, JsonSerializerOptions options, DeserializeValue<TValue> deserialize, out TValue? value)
{
if (reader.TokenType == JsonTokenType.Null)
goto fail;
else if (reader.TokenType == JsonTokenType.StartObject)
reader.ReadAndAssert();
do
{
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
var currentName = reader.GetString();
reader.ReadAndAssert();
if (String.Equals(name, currentName, comparison))
{
value = deserialize(ref reader, options);
return true;
}
else
{
reader.Skip();
}
}
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject);
fail:
value = default;
return false;
}
static void ReadAndAssert(ref this Utf8JsonReader reader)
{
if (!reader.Read())
throw new JsonException();
}
}
public static partial class ObjectExtensions
{
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
}
And now your Newtonsoft converter might be rewritten to look something like:
public class ApiResultConverter : System.Text.Json.Serialization.JsonConverter<IApiResult>
{
record MessagesDTO(Message [] Messages); // Message is the presumed type the array elements of ApiResult.ApiResult.Messages, which is not shown in your question.
public override IApiResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
IApiResult? apiResult = null;
if (reader.TryGetPropertyValue(nameof( ApiResult<object>.Result ), StringComparison.OrdinalIgnoreCase, options,
(ref Utf8JsonReader r, JsonSerializerOptions o) =>
{
var prop = typeToConvert.GetProperty( nameof( ApiResult<object>.Result ) ).ThrowOnNull();
return (Value : JsonSerializer.Deserialize(ref r, prop.PropertyType, o), Property : prop);
},
out var tuple))
{
var rType = typeof( ApiResult<> ).MakeGenericType( tuple.Property.PropertyType );
apiResult = Activator.CreateInstance( rType ).As<IApiResult>().ThrowOnNull();
tuple.Property.SetValue( apiResult, tuple.Value );
}
if (apiResult == null)
apiResult = new ApiResult.ApiResult().As<IApiResult>();
// Now consume the contents of the Utf8JsonReader by deserializing to MessagesDTO.
var dto = JsonSerializer.Deserialize<MessagesDTO>(ref reader, options);
apiResult.Messages = dto?.Messages ?? Array.Empty<Message>();
return apiResult;
}
public override void Write(Utf8JsonWriter writer, IApiResult value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
Notes:
This approach works well when scanning forward for a single simple property, e.g. a type discriminator string. If the type discriminator string is likely to be at the beginning of the JSON object it will be quite efficient. (This does not seem to apply in your case.)
JsonConverter<T>.Read() must completely consume the incoming token. E.g. if the incoming token is of type JsonTokenType.StartObject then, when exiting, the reader must be positioned on a token of type JsonTokenType.EndObject at the same depth. Thus if you only scan forward in copies of the incoming Utf8JsonWriter you must advance the incoming reader to the end of the current token by calling reader.Skip() before exiting.
Both Json.NET and System.Text.Json use StringComparison.OrdinalIgnoreCase for case-invariant property name matching, so I recommend doing so as well.
Secondly, you can load the contents of your Utf8JsonReader into a JsonDocument or JsonNode, query its properties, then deserialize to your final desired type with one of the JsonSerializer.Deserialzie() overloads that accepts a JSON document or node.
Using this approach with JsonObject in place of JObject, your converter might look something like:
public class ApiResultConverter : System.Text.Json.Serialization.JsonConverter<IApiResult>
{
public override IApiResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var obj = JsonNode.Parse(ref reader, new JsonNodeOptions { PropertyNameCaseInsensitive = true })?.AsObject();
if (obj == null)
return null; // Or throw JsonException() if you prefer
IApiResult? apiResult = null;
if (obj.TryGetPropertyValue( nameof( ApiResult<object>.Result ), out var jResult ))
{
//Retrieve the result property type in order to create the proper apiResult object
var prop = typeToConvert.GetProperty( nameof( ApiResult<object>.Result ) ).ThrowOnNull();
var value = JsonSerializer.Deserialize(jResult, prop.PropertyType, options);
var rType = typeof( ApiResult<> ).MakeGenericType( prop.PropertyType );
apiResult = Activator.CreateInstance( rType ).As<IApiResult>();
prop.SetValue( apiResult, value );
}
if (apiResult == null)
apiResult = new ApiResult.ApiResult().As<IApiResult>();
//Set the messages
JsonObject? []? messages = obj[nameof( ApiResult.Messages )]?.AsArray()?.Select(i => i?.AsObject())?.ToArray();
apiResult.Messages = DeserializeReasons(messages); // Not shown in your question
return apiResult;
}
static JsonObject? [] DeserializeReasons(JsonObject? []? messages) => messages == null ? Array.Empty<JsonObject>() : messages;
public override void Write(Utf8JsonWriter writer, IApiResult value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
public static partial class ObjectExtensions
{
public static T ThrowOnNull<T>(this T? value) where T : class => value ?? throw new ArgumentNullException();
}
Notes:
This approach works well when you have multiple properties (possibly with complex values) to search for and load during the conversion process.
By loading the JsonObject with JsonNodeOptions.PropertyNameCaseInsensitive = true, all property name lookups in the deserialized JsonNode hierarchy will be case-insensitive (using StringComparer.OrdinalIgnoreCase matching as shown in the source).
Since your question does not include a compilable example, the above converters are untested.
I am reading in a list of objects from JSON using this call:
Rootobject userInfo = JsonConvert.DeserializeObject<Rootobject>(File.ReadAllText(strFileName));
But I get an exception Cannot deserialize the current JSON array. If one of the arrays within one of the class objects is empty. As long as there is data everything works.
Here is an example of JSON that is tripping up the Deserializer:
This is normal type of data for the Venue object:
"venue": {
"venue_id": 696895,
"venue_name": "Blackfinn Ameripub",
"venue_slug": "blackfinn-ameripub",
"primary_category": "Food",
"parent_category_id": "4d4b7105d754a06374d81259",
"categories": {
"count": 1,
"items": [
{
"category_name": "American Restaurant",
"category_id": "4bf58dd8d48988d14e941735",
"is_primary": true
}
]
},
"is_verified": false
},
And here is what is causing the exception, an empty array:
"venue": [
],
I have tried using the JsonSerializerSettings options including DefaultValueHandling, NullValueHandling and MissingMemberHandling but none of them seem to prevent the error.
Any idea how to deserialize the JSON and just ignore any empty arrays within the data? I'd like this to handle any empty arrays not just the example above for the object Venue.
New issue was found - 03/17/2018 <<
Hi, the converter below has been working perfectly but the server I am getting my json responses from threw another challenge. JSON.NET has had no problem retrieving this type of data:
"toasts": {
"total_count": 1,
"count": 1,
"auth_toast": false,
"items": [
{
"uid": 3250810,
"user": {
"uid": 3250810,
"account_type": "user",
"venue_details": [
],
"brewery_details": [
]
},
"like_id": 485242625,
"like_owner": false,
"created_at": "Wed, 07 Mar 2018 07:54:38 +0000"
}
]
},
Specifically the section that has venue_details. 99% of the responses come back with venue_details in this format:
"venue_details": [
],
But then I get this format suddenly:
"toasts": {
"total_count": 1,
"count": 1,
"auth_toast": false,
"items": [
{
"uid": 4765742,
"user": {
"uid": 4765742,
"account_type": "venue",
"venue_details": {
"venue_id": 4759473
},
"brewery_details": [
],
"user_link": "https://untappd.com/venue/4759473"
},
"like_id": 488655942,
"like_owner": false,
"created_at": "Fri, 16 Mar 2018 16:47:10 +0000"
}
]
},
Notice how venue_details now has a value and includes a venue_id.
So instead venue_details ends up looking like an object instead of an array. This ends up giving this exception:
JsonSerializationException: Cannot deserialize the current JSON object (e.g. {"name":"value"}) into type 'System.Collections.Generic.List`1[System.Object]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly.
In the converter code provided, that exception happens in this line with *s next to it:
public abstract class IgnoreUnexpectedArraysConverterBase : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is JsonObjectContract))
{
throw new JsonSerializationException(string.Format("{0} is not a JSON object", objectType));
}
do
{
if (reader.TokenType == JsonToken.Null)
return null;
else if (reader.TokenType == JsonToken.Comment)
continue;
else if (reader.TokenType == JsonToken.StartArray)
{
var array = JArray.Load(reader);
if (array.Count > 0)
throw new JsonSerializationException(string.Format("Array was not empty."));
return existingValue ?? contract.DefaultCreator();
}
else if (reader.TokenType == JsonToken.StartObject)
{
// Prevent infinite recursion by using Populate()
existingValue = existingValue ?? contract.DefaultCreator();
*** serializer.Populate(reader, existingValue); ***
return existingValue;
Any ideas how to add this additional handling to account for a flip like this between the JSON returning an object instead of an array?
Thanks,
Rick
Your problem is not that you need to ignore empty arrays. If the "items" array were empty, there would be no problem:
"items": [],
Instead your problem is as follows. The JSON standard supports two types of container:
The array, which is an ordered collection of values. An array begins with [ (left bracket) and ends with ] (right bracket). Values are separated by , (comma).
The object, which is an unordered set of name/value pairs. An object begins with { (left brace) and ends with } (right brace).
For some reason the server is returning an empty array in place of a null object. If Json.NET expects to encounter a JSON object but instead encounters a JSON array, it will throw the Cannot deserialize the current JSON array exception you are seeing.
You might consider asking whoever generated the JSON to fix their JSON output, but in the meantime, you can use the following converters to skip unexpected arrays when deserializing objects:
public class IgnoreUnexpectedArraysConverter<T> : IgnoreUnexpectedArraysConverterBase
{
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
}
public class IgnoreUnexpectedArraysConverter : IgnoreUnexpectedArraysConverterBase
{
readonly IContractResolver resolver;
public IgnoreUnexpectedArraysConverter(IContractResolver resolver)
{
if (resolver == null)
throw new ArgumentNullException();
this.resolver = resolver;
}
public override bool CanConvert(Type objectType)
{
if (objectType.IsPrimitive || objectType == typeof(string))
return false;
return resolver.ResolveContract(objectType) is JsonObjectContract;
}
}
public abstract class IgnoreUnexpectedArraysConverterBase : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var contract = serializer.ContractResolver.ResolveContract(objectType);
if (!(contract is JsonObjectContract))
{
throw new JsonSerializationException(string.Format("{0} is not a JSON object", objectType));
}
do
{
if (reader.TokenType == JsonToken.Null)
return null;
else if (reader.TokenType == JsonToken.Comment)
continue;
else if (reader.TokenType == JsonToken.StartArray)
{
var array = JArray.Load(reader);
if (array.Count > 0)
throw new JsonSerializationException(string.Format("Array was not empty."));
return null;
}
else if (reader.TokenType == JsonToken.StartObject)
{
// Prevent infinite recursion by using Populate()
existingValue = existingValue ?? contract.DefaultCreator();
serializer.Populate(reader, existingValue);
return existingValue;
}
else
{
throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
}
}
while (reader.Read());
throw new JsonSerializationException("Unexpected end of JSON.");
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then, if empty arrays can appear in only one place in the object graph, you can add the converter to your model as follows:
public class Rootobject
{
[JsonConverter(typeof(IgnoreUnexpectedArraysConverter<Venue>))]
public Venue venue { get; set; }
}
But if, as you say, any object might be replaced with an empty array, you can use the non-generic IgnoreUnexpectedArraysConverter for all object types:
var resolver = new DefaultContractResolver(); // Cache for performance
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
Converters = { new IgnoreUnexpectedArraysConverter(resolver) },
};
var userInfo = JsonConvert.DeserializeObject<Rootobject>(jsonString, settings);
Notes:
The converter does not work with the TypeNameHandling or PreserveReferencesHandling settings.
The converter assumes that the object being deserialized has a default constructor. It the object has a parameterized constructor you will need to create a hardcoded converter to allocate and populate the object.
The converter throws an exception if the array is not empty, to ensure there is no data loss in the event of incorrect assumptions about the structure of the JSON. Sometimes servers will write a single object in place of a one-object array, and an array when there are zero, two or more objects. If you are also in that situation (e.g. for the "items" array) see How to handle both a single item and an array for the same property using JSON.net.
If you want the converter to return a default object instead of null when encountering an array, change it as follows:
else if (reader.TokenType == JsonToken.StartArray)
{
var array = JArray.Load(reader);
if (array.Count > 0)
throw new JsonSerializationException(string.Format("Array was not empty."));
return existingValue ?? contract.DefaultCreator();
}
Working sample .Net fiddle.
Is it possible to ignore ExpandoObject properties, in particular those of Delegate type, when using JsonConvert(expando, formatting, serializerSettings)?
Essentially I'm trying to ignore all parsing of the func property in this example expando object:
//{
// func: () => {}
//}
Action func = () => {};
dynamic expando = new ExpandoObject();
expando.func = func;
// should be empty object {}
string json = JsonConvert(expando, formatting, serializerSettings);
The first thing I tried was overriding the converter. Unfortunately this doesn't work, as I see CanConvert called recursively for Action -> DelegateEntry -> some generic type -> RuntimeMethodInfo.
private class ExpandoObjectIgnoreConverter : ExpandoObjectConverter
{
public override bool CanConvert(Type objectType)
{
if (typeof(Delegate).IsAssignableFrom(objectType))
{
return false;
}
return base.CanConvert(objectType);
}
}
A method that works is using an error handler in serialization settings and a contract resolver. When I throw the error, all further processing of the property is ignored, i.e. Action -> DelegateEntry -> some generic type -> RuntimeMethodInfo. However, I'd like to do this more elegantly than throwing an exception if possible.
Error handler:
serializationSettings.Error = (sender, args) =>
{
if (args.ErrorContext.Error is InvalidCastException)
{
args.ErrorContext.Handled = true;
}
}
Contract resolver:
private class ExpandoObjectContractResolver : DefaultContractResolver
{
public override JsonContract ResolveContract(Type type)
{
if (typeof(Delegate).IsAssignableFrom(type))
{
throw new InvalidCastException();
}
else
{
return base.ResolveContract(type);
}
}
}
I'm using the edge library to script nodejs from within a C# process. I'm trying to remove functions from the returned javascript objects from within C#, as they are assigned a Delegate type that doesn't play nicely with JsonConvert.
ExpandoObjectConverter does not have any custom code to write an ExpandoObject. Instead it overrides JsonConverter.CanWrite to return false thereby allowing the expando to be serialized generically as an IDictionary<string, object>.
Thus you can override CanWrite and WriteJson() yourself to filter undesired key/value pairs before serialization:
public class FilteredExpandoObjectConverter : ExpandoObjectConverter
{
public override bool CanWrite { get { return true; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var expando = (IDictionary<string, object>)value;
var dictionary = expando
.Where(p => !(p.Value is System.Delegate))
.ToDictionary(p => p.Key, p => p.Value);
serializer.Serialize(writer, dictionary);
}
}
Then use the converter in settings as follows:
var formatting = Formatting.Indented;
var serializerSettings = new JsonSerializerSettings
{
Converters = { new FilteredExpandoObjectConverter() },
};
var json = JsonConvert.SerializeObject(expando, formatting, serializerSettings);
Note this will only filter delegates values directly owned by an ExpandoObject. If you have a collection containing some delegates, or delegate-valued members in some POCO, they will not be filtered by this code.
Sample fiddle.
This question already has answers here:
Serialize dictionary as array (of key value pairs)
(6 answers)
Closed 7 years ago.
How can I make Json.NET serializer to serialize IDictionary<,> instance into array of objects with key/value properties?
By default it serializes the value of Key into JSON object's property name.
Basically I need something like this:
[{"key":"some key","value":1},{"key":"another key","value":5}]
instead of:
{{"some key":1},{"another key":5}}
I tried to add KeyValuePairConverter to serializer settings but it has no effect. (I found this converter is ignored for type of IDictionary<> but I cannot easily change the type of my objects as they are received from other libraries, so changing from IDictionary<> to ICollection<KeyValuePair<>> is not option for me.)
I was able to get this converter to work.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public class CustomDictionaryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (typeof(IDictionary).IsAssignableFrom(objectType) ||
TypeImplementsGenericInterface(objectType, typeof(IDictionary<,>)));
}
private static bool TypeImplementsGenericInterface(Type concreteType, Type interfaceType)
{
return concreteType.GetInterfaces()
.Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Type type = value.GetType();
IEnumerable keys = (IEnumerable)type.GetProperty("Keys").GetValue(value, null);
IEnumerable values = (IEnumerable)type.GetProperty("Values").GetValue(value, null);
IEnumerator valueEnumerator = values.GetEnumerator();
writer.WriteStartArray();
foreach (object key in keys)
{
valueEnumerator.MoveNext();
writer.WriteStartObject();
writer.WritePropertyName("key");
writer.WriteValue(key);
writer.WritePropertyName("value");
serializer.Serialize(writer, valueEnumerator.Current);
writer.WriteEndObject();
}
writer.WriteEndArray();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Here is an example of using the converter:
IDictionary<string, int> dict = new Dictionary<string, int>();
dict.Add("some key", 1);
dict.Add("another key", 5);
string json = JsonConvert.SerializeObject(dict, new CustomDictionaryConverter());
Console.WriteLine(json);
And here is the output of the above:
[{"key":"some key","value":1},{"key":"another key","value":5}]
Figured out another way - you can create custom ContractResolver and set it to JsonSerializerSettings before (de)serialization. The one below is derived from built-in CamelCasePropertyNamesContractResolver to convert serialized property names to camel case but it could be derived from DefaultContractResolver if you prefer not to modify the names.
public class DictionaryFriendlyContractResolver : CamelCasePropertyNamesContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
if (objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
return new JsonArrayContract(objectType);
if (objectType.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
return new JsonArrayContract(objectType);
return base.CreateContract(objectType);
}
}
Usage:
var cfg = new JsonSerializerSettings();
cfg.ContractResolver = new DictionaryFriendlyContractResolver();
string json = JsonConvert.SerializeObject(myModel, cfg);
Instead of this ..
public string Text
{
get { return ViewState["Text"] as string; }
set { ViewState["Text"] = value; }
}
I would like this ..
[ViewState]
public String Text { get; set; }
Can it be done?
Like this:
public class BasePage: Page {
protected override Object SaveViewState() {
object baseState = base.SaveViewState();
IDictionary<string, object> pageState = new Dictionary<string, object>();
pageState.Add("base", baseState);
// Use reflection to iterate attributed properties, add
// each to pageState with the property name as the key
return pageState;
}
protected override void LoadViewState(Object savedState) {
if (savedState != null) {
var pageState = (IDictionary<string, object>)savedState;
if (pageState.Contains("base")) {
base.LoadViewState(pageState["base"]);
}
// Iterate attributed properties. If pageState contains an
// item with the appropriate key, set the property value.
}
}
}
Pages that inherit from this class could use the attribute-driven syntax you've proposed.
Well, this is what i got so far, TY Jeff for pointing me in the right direction:
TestPage:
public partial class Pages_Test : BasePage {
[ViewState]
public String Name { get; set; }
BasePage:
#region Support ViewState Attribute
BindingFlags _flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
protected override Object SaveViewState()
{
object _baseState = base.SaveViewState();
IDictionary<string, object> _pageState = new Dictionary<string, object> { { "base", _baseState } };
//Use reflection to get properties marked for viewstate
foreach (PropertyInfo _property in GetType().GetProperties(_flags))
{
if (_property.HasAttribute<ViewState>())
{
object _value = _property.GetValue(this, _flags , null, null, null);
_pageState.Add(new KeyValuePair<string, object>(_property.Name, _value));
}
}
return _pageState;
}
protected override void LoadViewState(Object savedState)
{
if (savedState != null)
{
var _pageState = (IDictionary<string, object>)savedState;
if (_pageState.ContainsKey("base"))
{
base.LoadViewState(_pageState["base"]);
}
//use reflection to set properties
foreach (PropertyInfo _property in GetType().GetProperties(_flags ))
{
if (_property.HasAttribute<ViewState>() && _pageState.ContainsKey(_property.Name))
{
object _value = _pageState[_property.Name];
_property.SetValue(this, _value, _flags , null, null, null);
}
}
}
}
#endregion
Attribute:
/// <summary>
/// This attribute is used by the BasePage to identify properties that should be persisted to ViewState
/// Note: Private properties are not supported
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class ViewState : Attribute
{
//Marker
}
Helpers:
public static class PropertyExtension
{
public static Boolean HasAttribute<T>(this PropertyInfo property)
{
object[] attrs = property.GetCustomAttributes(typeof(T), false);
return attrs != null && attrs.Length == 1;
}
}
EDIT
Jan has a valid point about performance, I did some profiling with the following results:
Without Attribute With Attribute Increase Slower %
One Property
First Load 0,004897899 0,010734255 0,005836356 219
Save, postback 0,002353861 0,010478008 0,008124147 445
Load, Postback 0,001488807 0,00627482 0,004786013 421
10 properties
First Load 0,006184096 0,015288675 0,009104579 247
Save, postback 0,004061759 0,015052262 0,010990503 371
Load, Postback 0,0015708 0,005833074 0,004262274 371
% increase
Avg Page. 0,902215714567075 0,00648
On a Empty page the increase is considerable, but on an average page with a load of 1s this increase amounts to 0,01%.
Update : Using PostSharp, PostSharp4ViewState
Step 1 : Make sure your website is precompiled
Step 2 : Install PostSharp and PostSharp4ViewState
Step 3 : Reference PostSharp.Public And PostSharp4ViewState
Step 4 : Following is Code is now valid.
[Persist(Mode=PersistMode.ViewState)]
private string _name;
public String Name {
get { return _name; }
set { _name = value; }
}
BBorg's solution is actually incredibly slow because of the heavy use of reflection.
Using PostSharp.Laos, by letting your attribute inherit from OnMethodBoundaryAspect, you can easily override public override void OnInvocation(MethodInvocationEventArgs eventArgs) and do all the magic in there. This will be way faster. Check for example the CacheAttribute example on the PostSharp homepage.
If you are really wanting bare speed, you can write a PostSharp plugin that weaves MSIL (GetFromViewState, SetInViewState methods or something) into your properties, that won't even have a performance penalty.
This functionality is built into NHibernate Burrow. If you don't happen to use NHibernate in your application, the source code for NHibernate Burrow is available here. Feel free to dig in, see how they did it, and rip out any parts that our useful to you (as long as you comply with the LGPL license).
The most relevant code seems to be in StatefulFieldProcessor.cs lines 51 - 72.
/// <summary>
/// Get the FieldInfo - Attribute pairs that have the customer attribute of type <typeparamref name="AT"/>
/// </summary>
/// <typeparam name="AT"></typeparam>
/// <returns></returns>
protected IDictionary<FieldInfo, AT> GetFieldInfo<AT>() where AT : Attribute {
IDictionary<FieldInfo, AT> retVal = new Dictionary<FieldInfo, AT>();
foreach (FieldInfo fi in GetFields())
foreach (AT a in Attribute.GetCustomAttributes(fi, typeof (AT)))
retVal.Add(fi, a);
return retVal;
}
protected IDictionary<FieldInfo, StatefulField> GetStatefulFields() {
IDictionary<FieldInfo, StatefulField> retVal;
Type controlType = Control.GetType();
if (controlType.Assembly == webAssembly)
return null;
if (!fieldInfoCache.TryGetValue(controlType, out retVal))
fieldInfoCache[controlType] = retVal = GetFieldInfo<StatefulField>();
return retVal;
}