Using Swashbuckle.AspNetCore in an ASP.NET Core webapp, we have response types like:
public class DateRange
{
[JsonConverter(typeof(IsoDateConverter))]
public DateTime StartDate {get; set;}
[JsonConverter(typeof(IsoDateConverter))]
public DateTime EndDate {get; set;}
}
When using Swashbuckle to emit the swagger API JSON, this becomes:
{ ...
"DateRange": {
"type": "object",
"properties": {
"startDate": {
"format": "date-time",
"type": "string"
},
"endDate": {
"format": "date-time",
"type": "string"
}
}
}
...
}
The problem here is that DateTime is a value type, and can never be null; but the emitted Swagger API JSON doesn't tag the 2 properties as required. This behavior is the same for all other value types: int, long, byte, etc - they're all considered optional.
To complete the picture, we're feeding our Swagger API JSON to dtsgenerator to generate typescript interfaces for the JSON response schema. e.g. the class above becomes:
export interface DateRange {
startDate?: string; // date-time
endDate?: string; // date-time
}
Which is clearly incorrect. After digging into this a little bit, I've concluded that dtsgenerator is doing the right thing in making non-required properties nullable in typescript. Perhaps the swagger spec needs explicit support for nullable vs required, but for now the 2 are conflated.
I'm aware that I can add a [Required] attribute to every value-type property, but this spans multiple projects and hundreds of classes, is redundant information, and would have to be maintained. All non-nullable value type properties cannot be null, so it seems incorrect to represent them as optional.
Web API, Entity Framework, and Json.net all understand that value type properties cannot be null; so a [Required] attribute is not necessary when using these libraries.
I'm looking for a way to automatically mark all non-nullable value types as required in my swagger JSON to match this behavior.
If you're using C# 8.0+ and have Nullable Reference Types enabled, then the answer can be even easier. Assuming it is an acceptable division that all non-nullable types are required, and all other types that are explicitly defined as nullable are not then the following schema filter will work.
public class RequireNonNullablePropertiesSchemaFilter : ISchemaFilter
{
/// <summary>
/// Add to model.Required all properties where Nullable is false.
/// </summary>
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
var additionalRequiredProps = model.Properties
.Where(x => !x.Value.Nullable && !model.Required.Contains(x.Key))
.Select(x => x.Key);
foreach (var propKey in additionalRequiredProps)
{
model.Required.Add(propKey);
}
}
}
The Apply method will loop through each model property checking to see if Nullable is false and adding them to the list of required objects. From observation it appears that Swashbuckle does a fine job of setting the Nullable property based on if it a nullable type. If you don't trust it, you could always use Reflection to produce the same affect.
As with other schema filters don't forget to add this one in your Startup class as well as the appropriate Swashbuckle extensions to handle nullable objects.
services.AddSwaggerGen(c =>
{
/*...*/
c.SchemaFilter<RequireNonNullablePropertiesSchemaFilter>();
c.SupportNonNullableReferenceTypes(); // Sets Nullable flags appropriately.
c.UseAllOfToExtendReferenceSchemas(); // Allows $ref enums to be nullable
c.UseAllOfForInheritance(); // Allows $ref objects to be nullable
}
I found a solution for this: I was able to implement a Swashbuckle ISchemaFilter that does the trick. Implementation is:
/// <summary>
/// Makes all value-type properties "Required" in the schema docs, which is appropriate since they cannot be null.
/// </summary>
/// <remarks>
/// This saves effort + maintenance from having to add <c>[Required]</c> to all value type properties; Web API, EF, and Json.net already understand
/// that value type properties cannot be null.
///
/// More background on the problem solved by this type: https://stackoverflow.com/questions/46576234/swashbuckle-make-non-nullable-properties-required </remarks>
public sealed class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
private readonly CamelCasePropertyNamesContractResolver _camelCaseContractResolver;
/// <summary>
/// Initializes a new <see cref="RequireValueTypePropertiesSchemaFilter"/>.
/// </summary>
/// <param name="camelCasePropertyNames">If <c>true</c>, property names are expected to be camel-cased in the JSON schema.</param>
/// <remarks>
/// I couldn't figure out a way to determine if the swagger generator is using <see cref="CamelCaseNamingStrategy"/> or not;
/// so <paramref name="camelCasePropertyNames"/> needs to be passed in since it can't be determined.
/// </remarks>
public RequireValueTypePropertiesSchemaFilter(bool camelCasePropertyNames)
{
_camelCaseContractResolver = camelCasePropertyNames ? new CamelCasePropertyNamesContractResolver() : null;
}
/// <summary>
/// Returns the JSON property name for <paramref name="property"/>.
/// </summary>
/// <param name="property"></param>
/// <returns></returns>
private string PropertyName(PropertyInfo property)
{
return _camelCaseContractResolver?.GetResolvedPropertyName(property.Name) ?? property.Name;
}
/// <summary>
/// Adds non-nullable value type properties in a <see cref="Type"/> to the set of required properties for that type.
/// </summary>
/// <param name="model"></param>
/// <param name="context"></param>
public void Apply(Schema model, SchemaFilterContext context)
{
foreach (var property in context.SystemType.GetProperties())
{
string schemaPropertyName = PropertyName(property);
// This check ensures that properties that are not in the schema are not added as required.
// This includes properties marked with [IgnoreDataMember] or [JsonIgnore] (should not be present in schema or required).
if (model.Properties?.ContainsKey(schemaPropertyName) == true)
{
// Value type properties are required,
// except: Properties of type Nullable<T> are not required.
var propertyType = property.PropertyType;
if (propertyType.IsValueType
&& ! (propertyType.IsConstructedGenericType && (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
// Properties marked with [Required] are already required (don't require it again).
if (! property.CustomAttributes.Any(attr =>
{
var t = attr.AttributeType;
return t == typeof(RequiredAttribute);
}))
{
// Make the value type property required
if (model.Required == null)
{
model.Required = new List<string>();
}
model.Required.Add(schemaPropertyName);
}
}
}
}
}
}
To use, register it in your Startup class:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc(c_swaggerDocumentName, new Info { Title = "Upfront API", Version = "1.0" });
c.SchemaFilter<RequireValueTypePropertiesSchemaFilter>(/*camelCasePropertyNames:*/ true);
});
This results in the DateRange type above becoming:
{ ...
"DateRange": {
"required": [
"startDate",
"endDate"
],
"type": "object",
"properties": {
"startDate": {
"format": "date-time",
"type": "string"
},
"endDate": {
"format": "date-time",
"type": "string"
}
}
},
...
}
In the swagger JSON schema, and:
export interface DateRange {
startDate: string; // date-time
endDate: string; // date-time
}
in the dtsgenerator output. I hope this helps someone else.
I was able to achieve the same effect as the accepted answer using the following schema filter and Swashbuckle 5.4.1:
public class RequireValueTypePropertiesSchemaFilter : ISchemaFilter
{
private readonly HashSet<OpenApiSchema> _valueTypes = new HashSet<OpenApiSchema>();
public void Apply(OpenApiSchema model, SchemaFilterContext context)
{
if (context.Type.IsValueType)
{
_valueTypes.Add(model);
}
if (model.Properties != null)
{
foreach (var prop in model.Properties)
{
if (_valueTypes.Contains(prop.Value))
{
model.Required.Add(prop.Key);
}
}
}
}
}
This relies on the fact that the ISchemaFilter must be applied to the simple schemas of each property before it can be applied to the complex schema that contains those properties - so all we have to do is keep track of the simple schemas that relate to a ValueType, and if we later encounter a schema that has one of those ValueType schemas as a property, we can mark that property name as required.
I struggled with a similar problem for several days before I realized two important things.
A property's nullability and its requiredness are completeley orthogonal concepts and should not be conflated.
As great as C#'s new nullable feature is at helping you avoid null reference exceptions, it's still a compile-time feature. As far as the CLR is concerned, and therefore as far as the reflection API is concerned, all strings (indeed all reference types) are always nullable. Period.
The second point really caused problems for any schema filter I wrote because, regardless of whether I typed something as string or string?, the context parameter of the Apply function always had it's MemberInfo.Nullable property set to true.
So I came up with the following solution.
First, create Nullable attribute.
using System;
[AttributeUsage(AttributeTargets.Property)]
public class NullableAttribute : Attribute {
public NullableAttribute(bool Property = true, bool Items = false) {
this.Property = Property;
this.Items = Items;
}
public bool Property { get; init; }
public bool Items { get; init; }
}
Next, create NullableSchemaFilter.
using MicroSearch.G4Data.Models;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
public class NullableSchemaFilter : ISchemaFilter {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) {
var attrs = context.MemberInfo?.GetInlineAndMetadataAttributes();
if (attrs != null) {
foreach (var attr in attrs) {
var nullableAttr = attr as NullableAttribute;
if (nullableAttr != null) {
schema.Nullable = nullableAttr.Property;
if (schema.Items != null)
schema.Items.Nullable = nullableAttr.Items;
}
}
}
}
}
And, of course, you have to add the schema filter in your startup code.
services.AddSwaggerGen(config => {
config.SchemaFilter<NullableSchemaFilter>();
});
The Nullable attribute takes two optional boolean parameters:
Property controls if the property itself is nullable.
Items controls if items in an array are nullable. Obviously, this only applies to properties that are arrays.
Examples:
// these all express a nullable string
string? Name { get; set; }
[Nullable] string? Name { get; set; }
[Nullable(true)] string? Name { get; set; }
[Nullable(Property: true)] string? Name { get; set; }
// non-nullable string
[Nullable(false)] string Name { get; set; }
[Nullable(Property: false)] string Name { get; set; }
// non-nullable array of non-nullable strings
[Nullable(false)] string[] Names { get; set; }
[Nullable(Property: false, Items: false) Names { get; set; }
// nullable array of non-nullable strings
[Nullable(Property: true, Items: false)] string[]? Names { get; set; }
// non-nullable array of nullable strings
[Nullable(Property: false, Items: true)] string?[] Names { get; set; }
// nullable array of nullable strings
[Nullable(Property: true, Items: true)] string?[]? Names { get; set; }
The [Required] attribute can be freely used together with the [Nullable] attribute when necessary. i.e. this does what you would expect.
[Nullable][Required] string? Name { get; set; }
I am using .NET 5 and Swashbuckle.AspNetCore 6.2.3.
Let me suggest solution based on json schema.
This scheme was described in RFC, so it should works like common solution https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
public class AssignPropertyRequiredFilter : ISchemaFilter
{
public void Apply(Schema schema, SchemaFilterContext context)
{
if (schema.Properties == null || schema.Properties.Count == 0)
{
return;
}
var typeProperties = context.SystemType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var property in schema.Properties)
{
if (IsSourceTypePropertyNullable(typeProperties, property.Key))
{
continue;
}
// "null", "boolean", "object", "array", "number", or "string"), or "integer" which matches any number with a zero fractional part.
// see also: https://json-schema.org/latest/json-schema-validation.html#rfc.section.6.1.1
switch (property.Value.Type)
{
case "boolean":
case "integer":
case "number":
AddPropertyToRequired(schema, property.Key);
break;
case "string":
switch (property.Value.Format)
{
case "date-time":
case "uuid":
AddPropertyToRequired(schema, property.Key);
break;
}
break;
}
}
}
private bool IsNullable(Type type)
{
return Nullable.GetUnderlyingType(type) != null;
}
private bool IsSourceTypePropertyNullable(PropertyInfo[] typeProperties, string propertyName)
{
return typeProperties.Any(info => info.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)
&& IsNullable(info.PropertyType));
}
private void AddPropertyToRequired(Schema schema, string propertyName)
{
if (schema.Required == null)
{
schema.Required = new List<string>();
}
if (!schema.Required.Contains(propertyName))
{
schema.Required.Add(propertyName);
}
}
}
Or you can try this one
public class AssignPropertyRequiredFilter : ISchemaFilter {
public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type) {
var requiredProperties = type.GetProperties()
.Where(x => x.PropertyType.IsValueType)
.Select(t => char.ToLowerInvariant(t.Name[0]) + t.Name.Substring(1));
if (schema.required == null) {
schema.required = new List<string>();
}
schema.required = schema.required.Union(requiredProperties).ToList();
}
}
and use
services.AddSwaggerGen(c =>
{
...
c.SchemaFilter<AssignPropertyRequiredFilter>();
});
Related
I'm using Swashbuckle.AspNetCore.Swagger to generate a swagger document and then using NSwag to generate a C# SDK.
I have a couple classes where I use a Dictionary<SomeEnum, string> to hold miscellaneous properties.
namespace Sample
{
/// <summary>Possible properties for MyClass1 objects</summary>
[JsonConverter(typeof(StringEnumConverter))]
public enum PropEnum1
{
/// <summary>OpenAPI doesn't see this description</summary>
PropName1,
PropName2,
PropName3,
// and more property names
}
/// <summary>The first class...</summary>
public class MyClass1
{
public string Name { get; set; }
/// <summary>Properties that vary from instance to instance.</summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public IDictionary<PropEnum1, string> Props { get; set; }
}
// There is also a PropEnum2 and MyClass2, but let's stay simple
}
Using Swashbuckle 6.14 I end up getting swagger that looks like this:
"MyClass1": {
"type": "object",
"properties": {
"name": {
"type": "string",
"nullable": true
},
"props": {
"type": "object",
"properties": {
"PropName1": {
"type": "string"
},
"PropName2": {
"type": "string"
},
"PropName3": {
"type": "string"
}
},
"additionalProperties": false,
"description": "Properties that vary from instance to instance.",
"nullable": true
}
},
"additionalProperties": false,
"description": "The first class..."
}
and then NSwag generates a C# interface that looks like this:
/// <summary>The first class...</summary>
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class MyClass1
{
[Newtonsoft.Json.JsonProperty("name", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Name { get; set; }
/// <summary>Properties that vary from instance to instance.</summary>
[Newtonsoft.Json.JsonProperty("props", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public Props Props { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "10.4.3.0 (Newtonsoft.Json v12.0.0.0)")]
public partial class Props
{
[Newtonsoft.Json.JsonProperty("PropName1", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName1 { get; set; }
[Newtonsoft.Json.JsonProperty("PropName2", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName2 { get; set; }
[Newtonsoft.Json.JsonProperty("PropName3", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PropName3 { get; set; }
}
I find the generation of the Props class with a member for every possible enum value a little surprising, but I could live with it.
The problem is that instances of MyClass1 may not have all the properties. I have an API that returns an instance of MyClass1. It works from the swagger page, but using the NSwag SDK gives me an error "Newtonsoft.Json.JsonSerializationException: Required property 'PropName2' expects a non-null value."
The immediate issue is the DisallowNull's in the generated Props class. If I hand-edit them from DisallowNull to Default, the SDK works as expected. But hand-editing generated code is not good.
The DisallowNull's come from the PropName properties in the swagger not having "nullable" set to true. If I pause my generation process and hand-edit them into the swagger, I get Required.Default in the generated C# code. But hand-editing generated swagger is also not good.
So one way to fix it would be to get a "nullable" into the properties in the swagger, but I don't know how to do that.
Or is there a way to get Swashbuckle to treat the Props dictionary more like a Dictionary<string, string>?
From this post it seems like a Dictionary<int, string> gets treated this way, but that may be old.
Here's what I ended up with. It turns the type into a generic object (where the properties have to be of the valueType. Note that I already have enums serialize as strings, so this may not be appropriate for other people.
/// <summary>
/// For properties that are Dictionary[SomeEnum, valueType] alter the schema
/// so the generated SDK code will be IDictionary[string, valueType].
/// </summary>
public class EnumDictionaryToStringDictionarySchemaFilter : ISchemaFilter
{
/// <summary>
/// Apply the schema changes
/// </summary>
/// <param name="schema">The schema model</param>
/// <param name="context">The schema filter context</param>
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
// Only for fields that are Dictionary<Enum, TValue>
//
if (!context.Type.IsGenericType)
return;
if (!context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(Dictionary<,>))
&& !context.Type.GetGenericTypeDefinition().IsAssignableFrom(typeof(IDictionary<,>)))
return;
var keyType = context.Type.GetGenericArguments()[0];
if (!keyType.IsEnum)
return;
var valueType = context.Type.GetGenericArguments()[1];
var valueTypeSchema = context.SchemaGenerator.GenerateSchema(valueType, context.SchemaRepository);
schema.Type = "object";
schema.Properties.Clear();
schema.AdditionalPropertiesAllowed = true;
schema.AdditionalProperties = valueTypeSchema;
}
}
public class InputModel
{
public string Thing { get; set; }
public DateTime AnotherThing { get; set; }
}
public ThingController : ControllerBase
{
public Task DoTheThing([FromQuery] int foo, [FromQuery] InputModel input)
{
// Elided.
}
}
The problem is that when the Swagger documentation is generated for this controller, the following inputs are listed for DoTheThing:
foo: int
Thing: string
AnotherThing: DateTime
Note how the last two inputs start with uppercase, because that's how they are defined in their model. I want them to start with lowercase to be consistent with the non-complex parameters passed to the controller method (remember, ASP.NET model binding doesn't care about casing).
The easy way to do this is to either have those properties be named starting with lowercase on the model, or apply the FromQuery and/or FromBody attribute on them. I don't want to do either of these things because the former is just nasty, and the latter is applying behaviour to properties, when I need that behaviour to be applied on a case-by-case basis.
Ideally I'd like to be able to write something like the following (which currently doesn't work because Swashbuckle doesn't seem to know/care about the DisplayName or Display attributes):
public class InputModel
{
[DisplayName("thing")]
public string Thing { get; set; }
[DisplayName("anotherThing")]
public DateTime AnotherThing { get; set; }
}
However, I'd be happy with any solution that allows me to "rename" the model properties without changing their names.
I have looked at Swashbuckle.AspNetCore.Annotations but it doesn't appear to provide this functionality.
To force all parameters to be lowercase, use the obviously-named but poorly-documented DescribeAllParametersInCamelCase method:
services.AddSwaggerGen(o =>
{
...
o.DescribeAllParametersInCamelCase();
...
});
(This ended up being somewhat of an XY question, because what I wanted was a way to force all params to be described as lowercase, but couldn't find a way to do it, so I asked for a general way to modify param names.)
You can achieve that with a IDocumentFilter in Asp.Net Core, i am not sure that this will work in normal Asp.Net but the solution must be similar.
The DocumentFilter iterates all parameters and lowers the first letter.
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
/// <summary>
/// A DocumentFilter that lowers the first letter of the query parameters.
/// </summary>
public class NameDocumentFilter : IDocumentFilter
{
#region explicit interfaces
/// <inheritdoc />
public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)
{
if (swaggerDoc.Paths.Count <= 0)
{
return;
}
foreach (var path in swaggerDoc.Paths.Values)
{
ToLower(path.Parameters);
// Edit this list if you want other operations.
var operations = new List<Operation>
{
path.Get,
path.Post,
path.Put
};
operations.FindAll(x => x != null)
.ForEach(x => ToLower(x.Parameters));
}
}
#endregion
#region methods
/// <summary>
/// Lowers the first letter of a parameter name.
/// </summary>
private static void ToLower(IList<IParameter> parameters)
{
if (parameters == null)
{
return;
}
foreach (var param in parameters)
{
// limit the renaming only to query parameters
if (!param.In.Equals("query", StringComparison.OrdinalIgnoreCase))
{
continue;
}
// shouldn't happen, just to make sure
if (string.IsNullOrWhiteSpace(param.Name))
{
continue;
}
param.Name = param.Name[0]
.ToString()
.ToLower() + param.Name.Substring(1);
}
}
#endregion
}
Then register die DocumentFilter in the swagger configuration:
services.AddSwaggerGen(
c =>
{
c.SwaggerDoc(
"v1",
new Info
{
Title = "My WebSite",
Version = "v1"
});
c.DocumentFilter<NameDocumentFilter>();
});
I adjusted this code from a sample that describes enum parameters, but the same idea works for renaming.
At work, we're using EFCore on our data layer and graphql-dotnet to manage APIs requests, I'm having a problem updating some of our big objects using GraphQL mutations. When the user sends a partial update on the model, we would like to update on our database only the fields that actually were changed by the mutation. The problem we're having is that as we directly map the input to the entity, wheather some field was purposefully passed as null, or the field was not specified on the mutation at all, we get the property value as null. This way we can't send the changes to the database otherwise we would incorrectly update a bunch of fields to null.
So, we need a way to identify which fields are sent in a mutation and only update those. In JS this is achieved by checking if the property value is undefined, if the value is null we know that it was passed as null on purpose.
Some workarounds we've been thinking were using reflection on a Dictionary to update only the specified fields. But we would need to spread reflection to every single mutation. Another solution was to have a isChanged property to every nullable property on our model and change ir on the refered property setter, but... cmon...
I'm providing some code as example of this situation bellow:
Human class:
public class Human
{
public Id { get; set; }
public string Name { get; set; }
public string HomePlanet { get; set; }
}
GraphQL Type:
public class HumanType : ObjectGraphType<Human>
{
public HumanType()
{
Name = "Human";
Field(h => h.Id).Description("The id of the human.");
Field(h => h.Name, nullable: true).Description("The name of the human.");
Field(h => h.HomePlanet, nullable: true).Description("The home planet of the human.");
}
}
Input Type:
public class HumanInputType : InputObjectGraphType
{
public HumanInputType()
{
Name = "HumanInput";
Field<NonNullGraphType<StringGraphType>>("name");
//The problematic field
Field<StringGraphType>("homePlanet");
}
}
Human Mutation:
/// Example JSON request for an update mutation without HomePlanet
/// {
/// "query": "mutation ($human:HumanInput!){ createHuman(human: $human) { id name } }",
/// "variables": {
/// "human": {
/// "name": "Boba Fett"
/// }
/// }
/// }
///
public class StarWarsMutation : ObjectGraphType<object>
{
public StarWarsMutation(StarWarsRepository data)
{
Name = "Mutation";
Field<HumanType>(
"createOrUpdateHuman",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<HumanInputType>> {Name = "human"}
),
resolve: context =>
{
//After conversion human.HomePlanet is null. But it was not informed, we should keep what is on the database at the moment
var human = context.GetArgument<Human>("human");
//On EFCore the Update method is equivalent to an InsertOrUpdate method
return data.Update(human);
});
}
}
You could use JsonConvert.PopulateObject from the Newtonsoft Json library. On the mutation resolver instead of using GetArgument with my type, I'm using GetArgument<dynamic> and serializing it using JsonConvert.SerializeObject then by calling JsonConvert.PopulateObject I'm able to update only the fields that were informed.
public StarWarsMutation(StarWarsRepository data)
{
Name = "Mutation";
Field<HumanType>(
"createOrUpdateHuman",
arguments: new QueryArguments(
new QueryArgument<NonNullGraphType<HumanInputType>> {Name = "human"}
),
resolve: context =>
{
//After conversion human.HomePlanet is null. But it was not informed, we should keep what is on the database at the moment
var human = context.GetArgument<dynamic>("human");
var humanDb = data.GetHuman(human["id"]);
var json = JsonConvert.SerializeObject(human);
JsonConvert.PopulateObject(json, humanDb);
//On EFCore the Update method is equivalent to an InsertOrUpdate method
return data.Update(humanDb);
});
}
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
Can I automatically validate complex child objects when validating a parent object and include the results in the populated ICollection<ValidationResult>?
If I run the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ConsoleApplication1
{
public class Person
{
[Required]
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
[Required]
public string Street { get; set; }
[Required]
public string City { get; set; }
[Required]
public string State { get; set; }
}
class Program
{
static void Main(string[] args)
{
Person person = new Person
{
Name = null,
Address = new Address
{
Street = "123 Any St",
City = "New York",
State = null
}
};
var validationContext = new ValidationContext(person, null, null);
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(person, validationContext, validationResults);
Console.WriteLine(isValid);
validationResults.ForEach(r => Console.WriteLine(r.ErrorMessage));
Console.ReadKey(true);
}
}
}
I get the following output:
False
The Name field is required.
But I was expecting something similar to:
False
The Name field is required.
The State field is required.
I offered a bounty for a better child object validation solution but didn't get any takers, ideally
validating child objects to an arbitrary depth
handling multiple errors per object
correctly identifying the validation errors on the child object fields.
I'm still surprised the framework doesn't support this.
Issue - Model Binder Order
This is, unfortunately, the standard behavior of Validator.TryValidateObject which
does not recursively validate the property values of the object
As pointed out in Jeff Handley's article on Validating Object and Properties with the Validator, by default, the validator will validate in order:
Property-Level Attributes
Object-Level Attributes
Model-Level implementation IValidatableObject
The problem is, at each step of the way...
If any validators are invalid, Validator.ValidateObject will abort validation and return the failure(s)
Issue - Model Binder Fields
Another possible issue is that the model binder will only run validation on objects that it has decided to bind. For example, if you don't provide inputs for fields within complex types on your model, the model binder won't need to check those properties at all because it hasn't called the constructor on those objects. According to Brad Wilson's great article on Input Validation vs. Model Validation in ASP.NET MVC:
The reason we don't "dive" into the Address object recursively is that there was nothing in the form that bound any values inside of Address.
Solution - Validate Object at the same time as Properties
One way to solve this problem is to convert object-level validations to property level validation by adding a custom validation attribute to the property that will return with the validation result of the object itself.
Josh Carroll's article on Recursive Validation Using DataAnnotations provides an implementation of one such strategy (originally in this SO question). If we want to validate a complex type (like Address), we can add a custom ValidateObject attribute to the property, so it is evaluated on the first step
public class Person {
[Required]
public String Name { get; set; }
[Required, ValidateObject]
public Address Address { get; set; }
}
You'll need to add the following ValidateObjectAttribute implementation:
public class ValidateObjectAttribute: ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var results = new List<ValidationResult>();
var context = new ValidationContext(value, null, null);
Validator.TryValidateObject(value, context, results, true);
if (results.Count != 0) {
var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName));
results.ForEach(compositeResults.AddResult);
return compositeResults;
}
return ValidationResult.Success;
}
}
public class CompositeValidationResult: ValidationResult {
private readonly List<ValidationResult> _results = new List<ValidationResult>();
public IEnumerable<ValidationResult> Results {
get {
return _results;
}
}
public CompositeValidationResult(string errorMessage) : base(errorMessage) {}
public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames) {}
protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {}
public void AddResult(ValidationResult validationResult) {
_results.Add(validationResult);
}
}
Solution - Validate Model at the Same time as Properties
For objects that implement IValidatableObject, when we check the ModelState, we can also check to see if the model itself is valid before returning the list of errors. We can add any errors we want by calling ModelState.AddModelError(field, error). As specified in How to force MVC to Validate IValidatableObject, we can do it like this:
[HttpPost]
public ActionResult Create(Model model) {
if (!ModelState.IsValid) {
var errors = model.Validate(new ValidationContext(model, null, null));
foreach (var error in errors)
foreach (var memberName in error.MemberNames)
ModelState.AddModelError(memberName, error.ErrorMessage);
return View(post);
}
}
Also, if you want a more elegant solution, you can write the code once by providing your own custom model binder implementation in Application_Start() with ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());. There are good implementations here and here
I also ran into this, and found this thread. Here's a first pass:
namespace Foo
{
using System.ComponentModel.DataAnnotations;
using System.Linq;
/// <summary>
/// Attribute class used to validate child properties.
/// </summary>
/// <remarks>
/// See: http://stackoverflow.com/questions/2493800/how-can-i-tell-the-data-annotations-validator-to-also-validate-complex-child-pro
/// Apparently the Data Annotations validator does not validate complex child properties.
/// To do so, slap this attribute on a your property (probably a nested view model)
/// whose type has validation attributes on its properties.
/// This will validate until a nested <see cref="System.ComponentModel.DataAnnotations.ValidationAttribute" />
/// fails. The failed validation result will be returned. In other words, it will fail one at a time.
/// </remarks>
public class HasNestedValidationAttribute : ValidationAttribute
{
/// <summary>
/// Validates the specified value with respect to the current validation attribute.
/// </summary>
/// <param name="value">The value to validate.</param>
/// <param name="validationContext">The context information about the validation operation.</param>
/// <returns>
/// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult"/> class.
/// </returns>
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var isValid = true;
var result = ValidationResult.Success;
var nestedValidationProperties = value.GetType().GetProperties()
.Where(p => IsDefined(p, typeof(ValidationAttribute)))
.OrderBy(p => p.Name);//Not the best order, but at least known and repeatable.
foreach (var property in nestedValidationProperties)
{
var validators = GetCustomAttributes(property, typeof(ValidationAttribute)) as ValidationAttribute[];
if (validators == null || validators.Length == 0) continue;
foreach (var validator in validators)
{
var propertyValue = property.GetValue(value, null);
result = validator.GetValidationResult(propertyValue, new ValidationContext(value, null, null));
if (result == ValidationResult.Success) continue;
isValid = false;
break;
}
if (!isValid)
{
break;
}
}
return result;
}
}
}
You will need to make your own validator attribute (eg, [CompositeField]) that validates the child properties.