I would like to control which properties from my model are serialized to my WebAPI2 JSON response, based on matching a query parameter to an attribute. I mainly want to do this to reduce bandwidth on GETs without causing a proliferation of ViewModel classes. For example:
GET /books/1?format=summary
public class Book
{
[SerializeFormat("summary")]
public int Id { get; set; }
[SerializeFormat("summary")]
public string Title { get; set; }
public string Contents { get; set; }
}
or
[SerializeFormat("summary","Id","Title")]
public class Book
{ ... }
To do this myself, I could derive all of my model classes from a custom base implementing ISerializable. In ISerializable.GetObjectData(), iterate through all properties inspecting the attributes. Not sure about performance on this idea.
Don't want to reinvent this solution, though if it already exists as a package.
One possibility would be to introduce a custom attribute JsonConditionalIncludeAttribute that can be applied to properties and fields:
[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true, Inherited = true)]
public class JsonConditionalIncludeAttribute : System.Attribute
{
public JsonConditionalIncludeAttribute(string filterName)
{
this.FilterName = filterName;
}
public string FilterName { get; private set; }
}
Next, subclass DefaultContractResolver, override CreateProperty, and return null for properties that have at least one [JsonConditionalInclude] applied, none of which match the a filter supplied to the contract resolver:
public class JsonConditionalIncludeContractResolver : DefaultContractResolver
{
public JsonConditionalIncludeContractResolver(string filterName)
{
this.FilterName = filterName;
}
public string FilterName { get; set; }
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
// Properties without JsonConditionalIncludeAttribute applied are serialized unconditionally.
// Properties with JsonConditionalIncludeAttribute are serialized only if one of the attributes
// has a matching filter name.
var attrs = property.AttributeProvider.GetAttributes(typeof(JsonConditionalIncludeAttribute), true);
if (attrs.Count > 0 && !attrs.Cast<JsonConditionalIncludeAttribute>().Any(a => a.FilterName == FilterName))
return null;
return property;
}
}
Finally, when serializing your class to JSON, set JsonSerializerSettings.ContractResolver equal to your custom contract resolver, initializing the FilterName from your web request, for instance:
public class TestClass
{
public string Property1 { get; set; }
[JsonConditionalInclude("summary")]
[JsonConditionalInclude("title")]
public string Property2 { get; set; }
[JsonConditionalInclude("summary")]
public string Property3 { get; set; }
[JsonConditionalInclude("title")]
[JsonConditionalInclude("citation")]
public string Property4 { get; set; }
[JsonConditionalInclude("citation")]
public string Field1;
public static void Test()
{
var test = new TestClass { Property1 = "a", Property2 = "b", Property3 = "c", Property4 = "d", Field1 = "e" };
Test(test, "summary"); // Prints "a", "b" and "c"
Test(test, "title"); // Prints "a", "b" and "d".
Test(test, "citation");// Prints "e", "a" and "d"
Test(test, null); // Prints "e", "a", "b", "c" and "d".
}
public static string Test(TestClass test, string webRequestFormat)
{
var settings = new JsonSerializerSettings { ContractResolver = new JsonConditionalIncludeContractResolver(webRequestFormat) };
var json = JsonConvert.SerializeObject(test, Formatting.Indented, settings);
Debug.WriteLine(json);
return json;
}
}
The contract resolver will apply to all classes being serialized, not just the root class, which looks to be what you want.
Related
I am trying to use patch method in my minimal api application this is my code :
Car class
public class Car
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "producent")]
public Producent Producent { get; set; }
[JsonProperty(PropertyName = "age")]
public int Age { get; set; }
[JsonProperty(PropertyName = "yearCreated")]
public int YearCreated { get; set; }
[JsonProperty(PropertyName = "engine")]
public Engine Engine { get; set; }
public Car()
{
Id= Guid.NewGuid().ToString();
YearCreated = DateTime.Now.Year - Age;
}
}
ICarService:
public interface ICarService
{
Task<IEnumerable<Car>> GetAll();
Task<Car> GetById(string id,string partitionKey);
Task Create(Car car);
Task<bool> Update(Car car);
Task<Car> UpdatePatchAsync(string id, string partitionKey,List<PatchOperation> patchOperations);
Task<bool> Delete(string id,string partitionKey);
}
patch method in service
public async Task<Car> UpdatePatchAsync(string id, string partitionKey, List<PatchOperation> patchOperations)
{
var result = await _container.PatchItemAsync<Car>(id, new PartitionKey(partitionKey),
patchOperations:patchOperations );
return result;
}
my requests:
[HttpPatch]
public static async Task<IResult> Patch(ICarService service,string id,string partitionKey,
[FromBody]List<PatchOperation> operations)
{
var updatedCar = await service.UpdatePatchAsync(id,partitionKey,operations);
if (updatedCar == null)
{
return Results.NotFound();
}
return Results.Ok(updatedCar);
}
app.MapMethods("/cars/patch/{id}/{partitionKey}", new string[] { "PATCH" }, CarRequests.Patch);
I use cosmosDb database and when i code like this:
{
"op": "replace",
"path": "age",
"value": 22
}
i get the error
System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'Microsoft.Azure.Cosmos.PatchOperation'. Path: $[0] | LineNumber: 3 | BytePositionInLine: 3.
---> System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'Microsoft.Azure.Cosmos.PatchOperation'.
The problem is you are using [FromBody]List<PatchOperation> operations.
This is making ASP.NET attempt to deserialize a JSON raw string into PatchOperation, which when we look at our source code, has no parameterless constructor: https://github.com/Azure/azure-cosmos-dotnet-v3/blob/master/Microsoft.Azure.Cosmos/src/Patch/PatchOperation.cs, it is an abstract class.
The way it is meant to be used is through PatchOperation.XXXX, like PatchOperation.Add, you cannot use it to deserialize into it. You could have your own MyPatchOperation<Car> that has the exposed properties for deserialization, and then simply have a method in that class that based on which properties are populated, calls PatchOperation<Car>.Add (or whatever operation).
Not the full code but:
public class MyPatchOperation
{
[JsonProperty(PropertyName = "op")]
public string Operation { get; set; }
[JsonProperty(PropertyName = "path")]
public string Path { get; set; }
[JsonProperty(PropertyName = "value")]
public string Value { get; set; }
public PatchOperation<Car> ToPatchOperation()
{
switch (this.Operation)
{
case "replace":
return PatchOperation<Car>.Replace(this.Path, this.Value);
/* other operations */
}
}
}
I need to make a service that allows both next jsons as request body
{
"id": 1,
...
"foo": null
}
and
{
"id": [1, 2, 3],
...
"foo": null
}
I tried making the model like this
public class MyModel
{
public List<int> Id { get; set; }
//...
public string Foo { get; set; }
}
which works for the second case, but not for the first one, as an int is not a list of ints.
What can I do to be able to parse both bodies correctly?
You could deserialize into a dynamic object instead.
dynamic result = JsonConvert.DeserializeObject<dynamic>(json);
Another approach I'm not sure would work is to make the Id property dynamic.
public class MyModel
{
public dynamic Id { get; set; }
public string Foo { get; set; }
}
You can create Models like the following(Model includes all the properties except id):
public class MyModel
{
public List<int> id { get; set; }
public Model model { get; set; }
}
public class Model
{
//...
public string foo { get; set; }
}
And ckeck id in requestbody,if id is int type,create a new list and add id to the list.
MyModel myModel = new MyModel();
myModel.model=JsonConvert.DeserializeObject<Model>(jsonResult);
var mydata = JsonConvert.DeserializeObject<JObject>(jsonResult);
if (!mydata["id"].ToString().Contains("["))
{
myModel.id = new List<int> { Convert.ToInt32(mydata["id"]) };
}
else {
myModel.id = JsonConvert.DeserializeObject<List<int>>(mydata["id"].ToString());
}
result:
I have a class that inherits from List, this class has some additional properties on it. When I store the parent document in RavenDB, the list items get stored but the additional properties do not.
The failing test below probably explains my issue better:
[TestFixture]
public class RDBIssueTests
{
private DocumentStore _documentStore;
[TestFixtureSetUp]
public void TestFixtureSetUp()
{
_documentStore = new EmbeddableDocumentStore
{
RunInMemory = true,
UseEmbeddedHttpServer = true,
DataDirectory = "Data",
};
_documentStore.Initialize();
}
[Test]
public void StoreSimpleDataTest()
{
string id = "people/1";
string laptopName = "MacPro";
string personName = "Joe Bloggs";
string attrOne = "Screen";
string attrTwo = "Keyboard";
var person = new Person()
{
Id = id,
Name = personName,
Laptop = new Possession<string>()
};
person.Laptop.Name = laptopName;
person.Laptop.Add(attrOne);
person.Laptop.Add(attrTwo);
using (var session = _documentStore.OpenSession())
{
session.Store(person);
session.SaveChanges();
}
using (var session = _documentStore.OpenSession())
{
var loadedPerson = session.Load<Person>(id);
Assert.AreEqual(personName, loadedPerson.Name);
Assert.AreEqual(2, loadedPerson.Laptop.Count); // 2 items in the list
Assert.IsTrue(loadedPerson.Laptop.Contains(attrOne));
Assert.IsTrue(loadedPerson.Laptop.Contains(attrTwo));
Assert.AreEqual(laptopName, loadedPerson.Laptop.Name); // fails here - Person.Laptop.Name is not persisted in RBD
}
}
}
public class Person
{
public string Id { get; set; }
public string Name { get; set; }
public Possession<string> Laptop { get; set; }
}
public class Possession<TValueType> : PossessionAttribute<TValueType>
{
public string Name { get; set; } // RDB doesn't persist this value
}
public class PossessionAttribute<TValueType> : List<TValueType>
{
}
As you can see from the test, the string property 'Name' on the Possession class does not get saved.
Is there something I need to do to get this property to persist?
Many thanks!
JSON has no way of representing an object that is both a list and has properties.
That is why you cannot do that. You can have an object that contains a list property, which is a more natural way of going about it.
I'm using JSON.Net to try and deserialize some survey responses from SurveyGizmo.
Here's a snapshot of the data I'm reading in:
{"result_ok":true,
"total_count":"44",
"page":1,
"total_pages":1,
"results_per_page":50,
"data":[
{"id":"1",
"contact_id":"",
"status":"Complete",
"is_test_data":"0",
"datesubmitted":"2011-11-13 22:26:53",
"[question(59)]":"11\/12\/2011",
"[question(60)]":"06:15 pm",
"[question(62)]":"72",
"[question(63)]":"One",
"[question(69), option(10196)]":"10",
I've setup a class as far as datesubmitted but I'm not sure how to setup the class to deserialize the questions given that the amount of questions will change? I also need to capture the option if it's present.
I'm using this code to use the JSON.NET Deserialize function:
Dim responses As Responses = JsonConvert.DeserializeObject(Of Responses)(fcontents)
Classes:
Public Class Responses
Public Property result_OK As Boolean
Public Property total_count As Integer
Public Property page As Integer
Public Property total_pages As Integer
Public Property results_per_page As Integer
Public Overridable Property data As List(Of surveyresponse)
End Class
Public Class SurveyResponse
Public Property id As Integer
Public Property status As String
Public Property datesubmitted As Date
End Class
This trick to support totally crazy mappings is to use JsonConverter and completely replace the parsing for that object, (I apologize for the C#, but I'm no good at VB syntax):
class Program
{
static void Main(string[] args)
{
var result = JsonConvert.DeserializeObject<Responses>(TestData);
}
const string TestData = #"{""result_ok"":true,
""total_count"":""44"",
""page"":1,
""total_pages"":1,
""results_per_page"":50,
""data"":[
{""id"":""1"",
""contact_id"":"""",
""status"":""Complete"",
""is_test_data"":""0"",
""datesubmitted"":""2011-11-13 22:26:53"",
""[question(59)]"":""11\/12\/2011"",
""[question(60)]"":""06:15 pm"",
""[question(62)]"":""72"",
""[question(63)]"":""One"",
""[question(69), option(10196)]"":""10"",
}]}";
}
[JsonObject]
class Responses
{
public bool result_ok { get; set; }
public string total_count { get; set; }
public int page { get; set; }
public int total_pages { get; set; }
public int results_per_page { get; set; }
public SurveyResponse[] Data { get; set; }
}
[JsonObject]
// Here is the magic: When you see this type, use this class to read it.
// If you want, you can also define the JsonConverter by adding it to
// a JsonSerializer, and parsing with that.
[JsonConverter(typeof(DataItemConverter))]
class SurveyResponse
{
public string id { get; set; }
public string contact_id { get; set; }
public string status { get; set; }
public string is_test_data { get; set; }
public DateTime datesubmitted { get; set; }
public Dictionary<int, string> questions { get; set; }
}
class DataItemConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(SurveyResponse);
}
public override bool CanRead
{
get { return true; }
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var value = (SurveyResponse)existingValue;
if (value == null)
{
value = new SurveyResponse();
value.questions = new Dictionary<int, string>()
}
// Skip opening {
reader.Read();
while (reader.TokenType == JsonToken.PropertyName)
{
var name = reader.Value.ToString();
reader.Read();
// Here is where you do your magic
if (name.StartsWith("[question("))
{
int index = int.Parse(name.Substring(10, name.IndexOf(')') - 10));
value.questions[index] = serializer.Deserialize<string>(reader);
}
else
{
var property = typeof(SurveyResponse).GetProperty(name);
property.SetValue(value, serializer.Deserialize(reader, property.PropertyType), null);
}
// Skip the , or } if we are at the end
reader.Read();
}
return value;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Now obviously there's a lot more you would want to do to get this really robust, but this gives you the basics of how to do it. There are more lightweight alternatives if you simply need to change property names (either JsonPropertyAttribute or overriding DefaultContractResolver.ResolvePropertyName(), but this gives you full control.
Say I have a class like this:
public class MyClass
{
public int Id { get; set; }
public DateTime Date { get; set; }
public string String1 { get; set; }
public string String2 { get; set; }
public string String3 { get; set; }
public string String4 { get; set; }
}
Is it possible to get NHibernate to store it in the following schema?
CREATE TABLE [dbo].[MyClass](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Xml] [varchar](max) NOT NULL,
)
Where the Id maps to Id, but then all other fields get serialized into XML (or otherwise)? I don't mind if these other fields have to go on a child object like the below, if that helps:
public class MyClass
{
public int Id { get; set; }
public AllOtherOptions Options { get; set; }
}
public class AllOtherOptions
{
public DateTime Date { get; set; }
public string String1 { get; set; }
public string String2 { get; set; }
public string String3 { get; set; }
public string String4 { get; set; }
}
I am thinking about doing something similar for an upcoming project. The project requires collecting a lot of data but only a few elements need to be stored in a relational database. I haven't started experimenting but these are my thoughts so far.
You can map an XML data type by creating a type that implements IUserType. If the child class (AllOtherOptions) is serializable, you should be able to map the XML field as a private member in MyClass and serialize/deserialize AllOtherOptions as needed. You could either dynamically maintain the XML field (sounds like a lot of work) or create an interceptor to do it. My thinking is that MyClass would implement an interface such as
public interface IXmlObjectContainer
{
void SerializeChildObjects();
void DeSerializeChildObjects();
}
and the interceptor would call those methods as needed. That's a proof of concept idea. I would probably refine that by exposing pairs of xml fields and serializable objects to remove the work of serializing from IXmlObjectContainer implementers. Or maybe handle serialization through the XML field's get/set accessors.
More info:
Working with XML Fields in NHibernate
Another XML implementation of IUserType
I had the same idea to save object in XML column. My idea was other. I took code from links and changed it to generic IUserType implementation. So any field/prop which is [Serializable] can be saved in XML column.
public class XmlUserType<T> : IUserType where T : class
{
public new bool Equals(object x, object y)
{
return x == y;
}
public int GetHashCode(object x)
{
return x.GetHashCode();
}
public object NullSafeGet(IDataReader rs, string[] names, object owner)
{
if (names.Length != 1)
throw new InvalidOperationException("names array has more than one element. can't handle this!");
var val = rs[names[0]] as string;
if (string.IsNullOrWhiteSpace(val) == false)
{
return KRD.Common.GenericXmlSerialization.Deserialize<T>(val);
}
return null;
}
public void NullSafeSet(IDbCommand cmd, object value, int index)
{
var parameter = (DbParameter)cmd.Parameters[index];
T toSave = value as T;
if (toSave != null)
{
parameter.Value = KRD.Common.GenericXmlSerialization.Serialize(toSave);
}
else
{
parameter.Value = DBNull.Value;
}
}
public object DeepCopy(object value)
{
T toCopy = value as T;
if (toCopy == null)
return null;
string serialized = KRD.Common.GenericXmlSerialization.Serialize(toCopy);
return KRD.Common.GenericXmlSerialization.Deserialize<T>(serialized);
}
public object Replace(object original, object target, object owner)
{
throw new NotImplementedException();
}
public object Assemble(object cached, object owner)
{
var str = cached as string;
if (string.IsNullOrWhiteSpace(str) == false)
{
return null;
}
return KRD.Common.GenericXmlSerialization.Deserialize<T>(str);
}
public object Disassemble(object value)
{
var toCache = value as T;
if (toCache != null)
{
return KRD.Common.GenericXmlSerialization.Serialize(toCache);
}
return null;
}
public SqlType[] SqlTypes
{
get
{
return new SqlType[] { new SqlXmlType() };
}
}
public Type ReturnedType
{
get { return typeof(XmlDocument); }
}
public bool IsMutable
{
get { return true; }
}
}
public class SqlXmlType : SqlType
{
public SqlXmlType()
: base(DbType.Xml)
{
}
}
Usage with FluentNHibernate:
public class MainObject
{
public int Id { get; set; }
public ObjectAsXml Data { get; set; }
}
public class ObjectAsXml
{
public string Name { get; set; }
public int Date { get; set; }
public ObjectAsXml OtherObject { get; set; }
}
private class MainObjectMap : ClassMap<MainObject>
{
public MainObjectMap()
{
Id(id => id.Id);
Map(m => m.Data).CustomType<XmlUserType<ObjectAsXml>>().Nullable();
}
}
Maybe it will help somebody.