I am creating a new C# OData4 Web API with a class named Call that has dynamic properties, which OData 4 allows via "Open Types". I believe I've set everything up and configured it correctly but the serialized response does not include the dynamic properties.
Did I configure something wrong?
public partial class Call
{
public int Id { get; set; }
public string Email { get; set; }
public IDictionary<string, object> DynamicProperties { get; }
}
public class CallController : ODataController
{
[EnableQuery]
public IQueryable<Call> GetCall([FromODataUri] int key)
{
return _context.Call.GetAll();
}
}
public static partial class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
AllowUriOperations(config);
ODataModelBuilder builder = new ODataConventionModelBuilder();
builder.ComplexType<Call>();
var model = builder.GetEdmModel();
config.MapODataServiceRoute(RoutePrefix.OData4, RoutePrefix.OData4, model);
}
private static void AllowUriOperations(HttpConfiguration config)
{
config.Count();
config.Filter();
config.OrderBy();
config.Expand();
config.Select();
}
}
You can enable serialization of null valued dynamic properties in OData open types by adding the following line (in my code in WebApiConfig.cs in method Register(HttpConfiguration config))
config.Properties.AddOrUpdate("System.Web.OData.NullDynamicPropertyKey", val=>true, (oldVal,newVal)=>true);
Then my dynamic properties with nulls start to serialize.
Or configuration.SetSerializeNullDynamicProperty(true); can be used, which is more cleaner and self-explanatory.
Can you check in your metadata, on the type Call do you have OpenType="true"? If not, try making it an EntitySet changing this line:
builder.ComplexType<Call>();
to this
builder.EntitySet<Call>("Calls");
If you do have OpenType="true" in your metadata, check that you definitely have some entries in your DynamicProperties collection
If the value in a key pair is null the property is simply not serialized. I was expecting it to be serialized to
"key" : null
Here are some additional examples
DynamicProperties.Add("somekey", 1);
"somekey" : 1
DynamicProperties.Add("somekey", "1");
"somekey" : "1"
DynamicProperties.Add("somekey", null);
Related
In a POST call to a WebApi I am trying to return a Created(newobject) thing. But there is no signature for Created in ApiController that can only take the object and do the rest.
It works fine if I return something like:
return Created(newobject.blahid.ToString(), newobject);
or if I do a
return CreatedAtRoute("DefaultApi", new { controller = ControllerContext.ControllerDescriptor.ControllerName, id = newobject.blahid.ToString()}, newobject);
I want to simplify this to:
return Created(newobject);
I would need to implement a method in a BaseController
public class BaseController : ApiController
{
protected new CreatedNegotiatedContentResult<T> Created<T>(T content)
{
var id = GetId(content);//need help here
return base.Created(id, content);
}
}
I don't want to worry about the Unique Identifier for an object being called differently in different models e.g. myobjguid, someblahguid etc. I would just want to find it out and mark it as "id".
say if my model is
public class Model_A
{
public List<Model_A> ChildModels { get; set; }
[LookForThisAttribute]//I want something like this
public Guid Model_AGuid { set; get; }
public Guid ? ParentGuid { set; get; }
public List<SomeOtherObject> OtherObjects { set; get; }
}
Is there an attribute([LookForThisAttribute]) or something I can set on all my models to specify that this is the guy to be assumed as THE unique identifier if I ever look for it.
Just like the [Key] attribute in Entity Framework. No matter what you call it, Entity Framework know its going to be the primary key.
So the GetId(T content) method can take the object and return the value of the property that has a [LookForThisAttribute] set?
I ended up writing my own Attribute and then looking up for it in the BaseController.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class UniqueIdAttribute: Attribute
{
}
And in the BaseController Created method:
protected CreatedNegotiatedContentResult<T> Created<T>(T content)
{
var props =typeof(T).GetProperties().Where(
prop => Attribute.IsDefined(prop, typeof(UniqueIdAttribute)));
if (props.Count() == 0)
{
//log this
return base.Created(Request.RequestUri.ToString(), content);
}
var id = props.FirstOrDefault().GetValue(content).ToString();
return base.Created(new Uri(Request.RequestUri + id), content);
}
Mark Gravell's post here helped me with getting the value of the property that has my custom attribute:
How to get a list of properties with a given attribute?
Along with a corresponding unit test for the controllers works fine for me.
Now I can just call Created(anyobject); from all ApiControllers without bothering about the different names people put for their IDs as long as they decorate it with my custom attribute.
I'm trying to post a JSON to NancyFx. The JSON is the following:
{
"prop1": 1,
"entries":{
"Entry1": 1,
"entry2": 2
}
}
On server side I created a corresponding model:
public class Model
{
public int Prop1 { get; set; }
public IDictionary<string, object> Entries { get; set; }
}
entries field in JSON has a dynamic structure, and because of that IDictionary<string, object> is used in the model.
And then I bind the model:
this.Bind<Model>();
Model is created successfully but problem is that in Entries dictionary both keys are in capital case. For me case is very important and I expect second key to be entry2, not Entry2.
I also tried to use JavaScriptConverter and JavaScriptPrimitiveConverter but in Deserialize method I get already capitalized data.
Any ideas oh how to fix that?
For me this was solved by configuring JavascriptSerializer to retain casing.
Unfortunately I couldn't figure out a clean way to do this, but here's the hack I'm using for now.
public class Model
{
public IDictionary<string, object> Entries { get; set; }
}
public class CustomModelBinder : IModelBinder
{
public bool CanBind(Type modelType)
{
return modelType == typeof(Model);
}
public object Bind(NancyContext context, Type modelType, object instance, BindingConfig configuration, params string[] blackList)
{
using (var sr = new StreamReader(context.Request.Body))
{
return (new JavaScriptSerializer() { RetainCasing = true }).Deserialize<Model>(sr.ReadToEnd());
}
}
}
Nancy will pick up this binder at runtime, no need to explicitly register anything.
This solution is not ideal because it ignores some Nancy features like blacklists and possibly other binding configuration settings.
A better option is to set JsonSettings from Bootstrapper
public class MyBootstrapper : DefaultNancyBootstrapper
{
public MyBootstrapper () : base()
{
JsonSettings.RetainCasing = true;
}
}
Implementing IModelBinder works, but it messes up other default binding settings.
I've a requirement to omit the null valued fields from the response altogether.
I can do this by modifying the JsonFormatter Serialization Setting for a normal webapi response.
config.Formatters.JsonFormatter.SerializationSettings
.NullValueHandling = NullValueHandling.Ignore;
But that does not seem to work once i switch to OData.
Here are my files:
WebApi.config:
public static void Register(HttpConfiguration config)
{
var builder = new ODataConventionModelBuilder();
var workerEntitySet = builder.EntitySet<Item>("Values");
config.Routes.MapODataRoute("Default", "api", builder.GetEdmModel());
}
Item Model:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string OptionalField { get; set; }
}
ValuesController:
public class ValuesController : EntitySetController<Item, int>
{
public static List<Item> items = new List<Item>()
{
new Item { Id = 1, Name = "name1", OptionalField = "Value Present" },
new Item { Id = 3, Name = "name2" }
};
[Queryable(AllowedQueryOptions = AllowedQueryOptions.All)]
public override IQueryable<Item> Get()
{
return items.AsQueryable();
}
[Queryable]
protected override Item GetEntityByKey(int id)
{
return items.Single(i => i.Id == id);
}
}
Here is the response I get for GET: api/Values.
{
"odata.metadata":"http://localhost:28776/api/$metadata#Values",
"value":[
{
"Id":1,
"Name":"name1",
"OptionalField":"Value Present"
},
{
"Id":3,
"Name":"name2",
"OptionalField":null
}
]
}
But I do not need the elements with null values present in the response - in the response below, I need the "OptionalField" not to be present in the second item (As its value is null). I need to achieve it in my response, I do not want the users to query for non-null values only.
In ODataLib v7 things changed drastically around these sorts of customisations thanks to Depencency Injection (DI)
This advice is for anyone who has upgraded to ODataLib v7, who may have implemented the previously accepted answers.
If you have the Microsoft.OData.Core nuget package v7 or later then this applies to you :). If you are still using older versions then use the code provided by #stas-natalenko but please DO NOT stop inheriting from ODataController...
We can globally override the DefaultODataSerializer so that null values are omitted from all Entity and Complex value serialized outputs using the following steps:
Define your custom Serializer that will omit properties with null values
Inherit from Microsoft.AspNet.OData.Formatter.Serialization.ODataResourceSerializer
/// <summary>
/// OData Entity Serilizer that omits null properties from the response
/// </summary>
public class IngoreNullEntityPropertiesSerializer : ODataResourceSerializer
{
public IngoreNullEntityPropertiesSerializer(ODataSerializerProvider provider)
: base(provider) { }
/// <summary>
/// Only return properties that are not null
/// </summary>
/// <param name="structuralProperty">The EDM structural property being written.</param>
/// <param name="resourceContext">The context for the entity instance being written.</param>
/// <returns>The property be written by the serilizer, a null response will effectively skip this property.</returns>
public override Microsoft.OData.ODataProperty CreateStructuralProperty(Microsoft.OData.Edm.IEdmStructuralProperty structuralProperty, ResourceContext resourceContext)
{
var property = base.CreateStructuralProperty(structuralProperty, resourceContext);
return property.Value != null ? property : null;
}
}
Define a Provider that will determine when to use our custom Serializer
Inherit from Microsoft.AspNet.OData.Formatter.Serialization.DefaultODataSerializerProvider
/// <summary>
/// Provider that selects the IngoreNullEntityPropertiesSerializer that omits null properties on resources from the response
/// </summary>
public class IngoreNullEntityPropertiesSerializerProvider : DefaultODataSerializerProvider
{
private readonly IngoreNullEntityPropertiesSerializer _entityTypeSerializer;
public IngoreNullEntityPropertiesSerializerProvider(IServiceProvider rootContainer)
: base(rootContainer) {
_entityTypeSerializer = new IngoreNullEntityPropertiesSerializer(this);
}
public override ODataEdmTypeSerializer GetEdmTypeSerializer(Microsoft.OData.Edm.IEdmTypeReference edmType)
{
// Support for Entity types AND Complex types
if (edmType.Definition.TypeKind == EdmTypeKind.Entity || edmType.Definition.TypeKind == EdmTypeKind.Complex)
return _entityTypeSerializer;
else
return base.GetEdmTypeSerializer(edmType);
}
}
Now we need to Inject this into your Container Builder.
the specifics of this will vary depending on your version of .Net, for many older projects this will be where you are mapping the ODataServiceRoute, this will usually be located in your startup.cs or WebApiConfig.cs
builder => builder
.AddService(ServiceLifetime.Singleton, sp => model)
// Injected our custom serializer to override the current ODataSerializerProvider
// .AddService<{Type of service to Override}>({service lifetime}, sp => {return your custom implementation})
.AddService<Microsoft.AspNet.OData.Formatter.Serialization.ODataSerializerProvider>(ServiceLifetime.Singleton, sp => new IngoreNullEntityPropertiesSerializerProvider(sp));
And there you have it, re-exeecute your query and you should get the following:
{
"odata.metadata":"http://localhost:28776/api/$metadata#Values",
"value":[
{
"Id":1,
"Name":"name1",
"OptionalField":"Value Present"
},
{
"Id":3,
"Name":"name2"
}
]
}
This is a very handy solution that can significantly reduce the data consumption on many data entry applications based on OData Services
NOTE: At this point in time, this technique must be used to override any of these default services: (as defined here OData.Net - Dependency Injection Support
Service Default Implementation Lifetime Prototype?
-------------------------- -------------------------- ---------- ---------
IJsonReaderFactory DefaultJsonReaderFactory Singleton N
IJsonWriterFactory DefaultJsonWriterFactory Singleton N
ODataMediaTypeResolver ODataMediaTypeResolver Singleton N
ODataMessageReaderSettings ODataMessageReaderSettings Scoped Y
ODataMessageWriterSettings ODataMessageWriterSettings Scoped Y
ODataPayloadValueConverter ODataPayloadValueConverter Singleton N
IEdmModel EdmCoreModel.Instance Singleton N
ODataUriResolver ODataUriResolver Singleton N
UriPathParser UriPathParser Scoped N
ODataSimplifiedOptions ODataSimplifiedOptions Scoped Y
UPDATE: How to handle lists or complex types
Another common scenario is to exclude complex types from the output if all of their properties are null, especially now that we do not include the null properties. We can override the WriteObjectInline method in the IngoreNullEntityPropertiesSerializer for this:
public override void WriteObjectInline(object graph, IEdmTypeReference expectedType, Microsoft.OData.ODataWriter writer, ODataSerializerContext writeContext)
{
if (graph != null)
{
// special case, nullable Complex Types, just skip them if there is no value to write
if (expectedType.IsComplex() && graph.GetType().GetProperty("Instance")?.GetValue(graph) == null
&& (bool?)graph.GetType().GetProperty("UseInstanceForProperties")?.GetValue(graph) == true)
{
// skip properties that are null, especially if they are wrapped in generic types or explicitly requested by an expander
}
else
{
base.WriteObjectInline(graph, expectedType, writer, writeContext);
}
}
}
Q: And if we need to omit null list properties as well?
If you wanted to use the same logic to exclude all lists, if they are null, then you could remove the expectedType.IsComplex() clause:
// special case, nullable Complex Types, just skip them if there is no value to write
if (graph.GetType().GetProperty("Instance")?.GetValue(graph) == null
&& (bool?)graph.GetType().GetProperty("UseInstanceForProperties")?.GetValue(graph) == true)
{
// skip properties that are null, especially if they are wrapped in generic types or explicitly requested by an expander
}
I don't recommend this for lists that are navigation properties, navigation properties will only be included in the output if they are explicitly requested in an $expand clause, or by other convention-based logic you may have that does the same thing. An empty or null array in the output might be significant for some client side logic as a confirmation that the requested property data was loaded but that there is no data to return.
I know it does not look anyway logical, but simply adding DefaultODataSerializerProvider and DefaultODataDeserializerProvider to the list of Formatters did the trick for me:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
//...
var odataFormatters = System.Web.OData.Formatter.ODataMediaTypeFormatters.Create(
System.Web.OData.Formatter.Serialization.DefaultODataSerializerProvider.Instance,
System.Web.OData.Formatter.Deserialization.DefaultODataDeserializerProvider.Instance);
config.Formatters.AddRange(odataFormatters);
UPDATE
Since the global formatters modification didn't work correctly for me, I chose a different way.
First I stepped away from the ODataController and inherited my controller from ApiController using a custom ODataFormatting attribute:
[ODataRouting]
[CustomODataFormatting]
public class MyController : ApiController
{
...
}
public class CustomODataFormattingAttribute : ODataFormattingAttribute
{
public override IList<System.Web.OData.Formatter.ODataMediaTypeFormatter> CreateODataFormatters()
{
return System.Web.OData.Formatter.ODataMediaTypeFormatters.Create(
new CustomODataSerializerProvider(),
new System.Web.OData.Formatter.Deserialization.DefaultODataDeserializerProvider());
}
}
The formatting attribute replaces the DefaultODataSerializerProvider with a modified one:
public class CustomODataSerializerProvider : DefaultODataSerializerProvider
{
public override ODataEdmTypeSerializer GetEdmTypeSerializer(IEdmTypeReference edmType)
{
if(edmType.Definition.TypeKind == EdmTypeKind.Entity)
return new CustomODataEntityTypeSerializer(this);
else
return base.GetEdmTypeSerializer(edmType);
}
}
And the last, the custom serializer filters structural properties with null values:
public class CustomODataEntityTypeSerializer : System.Web.OData.Formatter.Serialization.ODataEntityTypeSerializer
{
public CustomODataEntityTypeSerializer(ODataSerializerProvider provider)
: base(provider) { }
public override ODataProperty CreateStructuralProperty(IEdmStructuralProperty structuralProperty, EntityInstanceContext entityInstanceContext)
{
var property = base.CreateStructuralProperty(structuralProperty, entityInstanceContext);
return property.Value != null ? property : null;
}
}
This doesn't seem to me like the best possible solution, but this is the one I found.
All the method are same I made changes to webapiconfig
var odataFormatters = ODataMediaTypeFormatters.Create(new CustomODataSerializerProvider(), new DefaultODataDeserializerProvider());
config.Formatters.InsertRange(0, odataFormatters);
This help me to solve the result
I have a simple ASP.Net Web API controller in an otherwise unchanged MVC4 Web API project that has a POST method that takes a Values class. When I do a:
POST /api/values with a body of { name: "somename" }
the Values() constructor gets called instead of the Values(string name) one. Normally, this isn't a problem because the Name property would have a public set and Web API would call it after construction. In this case it is private, so I get a default instance of Values.
If I remove the Values(int) ctor then it does call the Values(string) ctor.
Is there a reason ModelBinding isn't choosing the ctor with the name parameter?
Here's the example code:
using System.Web.Http;
namespace WebAPIPlayground.Controllers
{
public class ValuesController : ApiController
{
public void Post(Values value)
{
var a = value.ID; // == 0
var b = value.Name; // == null
}
}
public class Values
{
public int ID { get; private set; }
public string Name { get; private set; }
private Values() { }
public Values(int id) { ID = id; }
public Values(string name) { Name = name; }
}
}
I have already looked at: Routing and Action Selection and WebAPI Parameter binding under the hood among many other sites, but I do not understand this behavior.
You're talking about deserialization here, not model binding. Try adding this attribute to the constructor you want to have used:
[JsonConstructor]
public Values(string name)
That should do it for the Json.NET case, but it won't work in XML. Maybe that's all you care about.
I'm building an ASP.NET MVC 2 application in C# and i am successfully using Automapper to map values back and forth between ViewModels and business objects.
In addition to several explicit properties, my business objects wrap a dictionary as a catch all for properties that aren't explicitly defined. Something similar to:
public class MyBusinessObject {
public void SetExtraPropertyValue<T>(string key, T value) {
// ...
}
public T GetExtraPropertyValue<T>(string key, T defaultValue) {
// ...
}
}
In my ViewModel, I have the freedom to create any properties I want, but I cannot modify the business objects.
So let's say I create a view model like this:
class MyViewModel {
public string CustomProp { get; set; }
}
and the value I want to store and retrieve will need to use
businessModelInstance.SetExtraPropertyValue("CustomProp", newVal);
and
businessModelInstance.GetExtraPropertyValue("CustomProp");
I have problems going both directions.
First, when going from the MyBusinessObject to the MyViewModel, I thought it should be simple to do in my custom Automapper profile:
CreateMap<MyBusinessObject, MyViewModel>()
.ForMember(dest => dest.CustomProp,
opt => opt.MapFrom(s => s.GetExtraPropertyValue("CustomProp", "")));
However, MyBusinessObject.CustomProp is never populated, though other properties are.
Secondly, I don't know how to configure getting a value from MyViewModel.CustomProp to calling MyBusinessObject.SetExtraPropertyValue.
Is there a way to accomplish this
mapping with Automapper?
Is there a completely different attack that I
should be trying?
Do I have to resort to manual mapping in my controller? For example, MyBusinessObject.SetExtraPropertyValue("CustomProp",
MyViewModel.CustomProp)
UPDATE: Here is my solution based on #Patrick Steele's suggestions:
I added a custom attribute to the view model properties that i wanted to map to extra property keys. A custom TypeConverter uses reflection to find these attributes and map properties appropriately.
public class ItemExtraPropertyConverter : ITypeConverter<MyViewModel, MyBusinessObject>
{
public MyBusinessObject Convert(ResolutionContext context)
{
var destination = context.DestinationValue as MyBusinessObject;
if (destination == null )
throw new InvalidOperationException("Destination type is not of type MyBusinessObject");
foreach (var property in context.SourceType.GetProperties())
foreach (var attribute in property.GetCustomAttributes(true).OfType<ExtraPropertyAttribute>())
{
var key = attribute.Key;
if (string.IsNullOrEmpty(key))
key = property.Name;
destination.SetExtraPropertyValue(key, property.GetValue(context.SourceValue, null));
}
return destination;
}
}
public class ExtraPropertyAttribute : Attribute
{
public ExtraPropertyAttribute()
{
}
public ExtraPropertyAttribute(string key)
{
Key = key;
}
public string Key { get; set; }
}
public class MyViewModel
{
[ExtraProperty]
public string CustomProp { get; set; }
[ExtraProperty("OtherPropertyValue")]
public string CustomProp2 { get; set; }
}
In the custom profile class's configure method:
CreateMap<MyViewModel, MyBusinessObject>()
.ConvertUsing<ItemExtraPropertyConverter>();
My guess is that something is wrong with your GetExtraPropertyValue and SetExtraPropertyValue implementations. I threw together a quick test and the mapping you provided above worked as expected. Here's the implementation I used for the test:
public class MyBusinessObject
{
private readonly Dictionary<string, object> extraProperties = new Dictionary<string, object>();
public int Id { get; set; }
public string Name { get; set; }
public void SetExtraPropertyValue<T>(string key, T value)
{
extraProperties.Add(key, value);
}
public T GetExtraPropertyValue<T>(string key, T defaultValue)
{
if (extraProperties.ContainsKey(key))
{
return (T)extraProperties[key];
}
return defaultValue;
}
}