If there is any wrong property (For example if I send the payload data, Person_ instead of Person), model fully gets as null (Post([FromBody] Request data))
public class Person
{
public Guid Id { get; set; }
public string? Firstname { get; set; }
public string? Lastname { get; set; }
}
public class Request
{
public Guid Id { get; set; }
public Guid? Personid { get; set; }
public virtual Person? Person { get; set; }
}
public IActionResult Post([FromBody] Request data)
{
...
}
curl --location --request POST 'https://localhost:7124/v2/request?$expand=Person($select=Id,Firstname,Lastname)/Request&#odata.context=%27https://localhost:7124/v2/$metadata' \
--header 'Content-Type: application/json' \
--data-raw '{
"Id": "a436677a-fa4b-465e-8e70-211a1a3de8e9",
"Personid": "be9b53ad-4dfb-4db5-b269-32669f7c4e2d",
"Person_" : {
"Firstname": "JOHN",
"Lastname": "SMITH",
}
}'
I need to get the model even though some properties not correct according to model schema.
What could be the reason for it being null?
One of the main issues is that the type argument forms a strong contract that the OData subsystem tries to enforce. If the Deserializer cannot match the expected type fully, then it returns null, not a partially constructed object, or an empty object if none of the properties matched.
What you are expecting was a lazy implementation that we often took for granted in previous versions of OData and JSON.Net, but the OData Entity Serializer doesn't work this way any more.
When the argument is null, the ModelState should provide detailed information on the reason for the failure.
OData has support for allowing additional members, it is called Open-Type Support. Similar to the catch all solutions in other deserialization methods, we designate a dictionary to route all un-mapped properties so that you can inspect them after deserialization. This was a good walkthrough in .Net FX but basically we add the property:
public class Request
{
public Guid Id { get; set; }
public Guid? Personid { get; set; }
public virtual Person? Person { get; set; }
public IDictionary<string, object> DynamicProperties { get; set; }
}
Then in your model builder you need to declare the type as open:
builder.Entity<Request>("request").EntityType.IsOpen();
This is alone is still going to be hard to use though because your additional member is a complex type, so the type cannot be easily resolved automatically.
You could implement your own deserializer, but that is a lot more universal to all of your controllers and endpoints, you should take a little bit more care because it really opens a back door and cancels out a lot of functionality if you don't do it right. In your example the _person is omitted entirely, which might not be your intention.
Other solutions are a bit more permanent and messy, like adding additional properties to your model to capture the input and re-assign it internally. The best advice however is to respond to the client with an adequate error message so that they update the call.
There is another way that we can also cheat by using the JToken type, instead of the expected concrete type. This will universally ingest the payload from the request, then we can use good old JSON.Net to resolve the object:
/// <summary>
/// Inserts a new item into this collection
/// </summary>
/// <param name="item">The item to insert</param>
/// <returns>CreatedODataResult</returns>
[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Format | AllowedQueryOptions.Select)]
public virtual async Task<IActionResult> Post([FromBody] Newtonsoft.Json.Linq.JToken x)
//public virtual async Task<IActionResult> Post(TEntity item)
{
TEntity item = x.ToObject<TEntity>();
... insert custom logic to resolve the badly formed properties
// Tell the client that the request is invalid, if it is still invalid.
if (!ModelState.IsValid)
return BadRequest(ModelState);
//EvalSecurityPolicy(item, Operation.Insert);
await ApplyPost(item);
//UpdateRelationships(item, Operation.Insert);
await _db.SaveChangesAsync();
return Created(item);
}
/// <summary>
/// Inheriting classes can override this method to apply custom fields or other properties from dynamic members in the posted item, base class will apply TenantId only if it has not already been applied
/// </summary>
/// <remarks>This process is called after the usual validation overriding just this process means you do not have to replicate the existing internal logic for the afore mentioned tasks.</remarks>
/// <param name="item">The new item that has been uploaded</param>
/// <returns>Promise to add the item to the underlying table store</returns>
public virtual Task ApplyPost(TEntity item)
{
GetEntitySet().Add(item);
return Task.CompletedTask;
}
This is a base class implementation of ODataController Inheriting controller classes only override ApplyPost if needed. I've commented out some more advanced logic routines to give you other hints on how you might use this pattern.
Is a good practice? I'm undecided but it works and will allow your API to be resilient to schema changes that the client hasn't yet been updated to support, you can also inspect and handle the invalid ModelState in your controller before you return to the caller, or can easily add your own custom mapping logic if needed.
The problem is the declaration of Person in the class Request, It should be public Person Person_ { get; set; }.
You can declare it as public virtual Person? Person_ { get; set; } also if you don't want to change the declaration.
The only catch here is the suffix underscore before Person.
If you don't want to change the declaration then you can use JsonProperty
[JsonProperty("Person_")]
public virtual Person? Person { get; set; }
I have found a solution. I have used a custom ODataResourceDeserializer to handle the exception of doesn't exist properties and, included a try/catch block in the ApplyNestedProperty method's content. So the web service cannot throw an exception for not exists properties while deserialization process.
public class CustomResourceDeserializer : ODataResourceDeserializer
{
public CustomResourceDeserializer(IODataDeserializerProvider deserializerProvider) : base(deserializerProvider)
{
}
public override void ApplyNestedProperty(object resource, ODataNestedResourceInfoWrapper resourceInfoWrapper, IEdmStructuredTypeReference structuredType, ODataDeserializerContext readContext)
{
try
{
base.ApplyNestedProperty(resource, resourceInfoWrapper, structuredType, readContext);
}
catch (System.Exception)
{
}
}
}
Is there a way how to make Swashbuckle take the XML Comments for properties which hide those in the base class?
class Base {
/// <summary>
/// This comment shows in swagger UI.
/// </summary>
public int Value { get; set; }
}
class Model: Base {
/// <summary>
/// This comment is not visible in swagger UI.
/// </summary>
public new int Value { get; set; }
}
Is there any workaround, e.g. using a kind of SchemaFilter?
UPDATED
The above simple example works as expected.
Our problem is probably caused by a wrong configuration of the IncludeXmlComments option as our models come from multiple assemblies.
I found a related discussion on GitHub
I'm having this issue with ModelState validation, since I'm using a model with an attribute decorated with [Required] attribute but it never gets invalid, even on null.
Here's the controller:
[HttpPost("x/{sampleString}")]
[ProducesResponseType(StatusCodes.Status303SeeOther)]
public IActionResult Post(string sampleString, SponsorContractorFilterModel model)
{
if (!ModelState.IsValid)
return StatusCode(BadRequest("Model sent is not valid").StatusCode.GetValueOrDefault());
//do stuff, add location on header and send back the 303.
return StatusCode(StatusCodes.Status303SeeOther);
}
And the model is pretty simple:
public class SponsorContractorFilterModel
{
/// <summary>
/// Initializes a new instance of the <see cref="SponsorContractorFilterModel"/> class.
/// </summary>
public SponsorContractorFilterModel()
{
}
/// <summary>
/// Sponsor number.
/// </summary>
[Required]
public int? AnImportantNumber { get; set; }
public List<int> SomeIds { get; set; }
//few more attributes not been decorated on purpose.
}
The thing is that when I send with postman a model object without an "AnImportantNumber" or explicitly null in neither of the cases it shows that is invalid. And when I debug it I can see its actually mapped as null.
I would suggest to use [BindRequired] attribute to make sure data were present on the request. There is No source for a model property section in the docs that explains a bit.
Also there is a bit older article [Required] and [BindRequired] in ASP.NET Core MVC written by Filip W. about that subject. It may be good starting point/inspiration too.
Hope it helps
This is my model:
public class JQueryDataTableParamModel
{
/// <summary>
/// Request sequence number sent by DataTable, same value must be returned in response
/// </summary>
[Required]
public string sEcho { get; set; }
/// <summary>
/// Text used for filtering
/// </summary>
[Required]
public string sSearch { get; set; }
}
This is my ActionResult:
public ActionResult VolumeOverviewHandler([Bind(Include = "sEcho")]JQueryDataTableParamModel param)
My question is does the Required attribute on 2nd property going to create issues in this case?
Nice question, the Bind attribute will improve the performance by only bind properties which you needed.
You can check if this will cause any problem by using the ModelState entity.
Inside your controller, the first thing you do is checking the ModelState use the following instruction:
if(!ModelState.IsValid){ throw new someException(); or return BadRequest("Model Is Not Valid");}
If you're ModelState is valid. You can consider that there are no problems, and proceed with whatever you want to do.
I'm trying to get an auto decompile configuration for DelegateDecompiler to work, as shown here: http://daveaglick.com/posts/computed-properties-and-entity-framework
But it doesn't work :(
Not sure what I'm doing wrong.
Here's the class that has a computed value.
public class Person
{
public int Id { get; set; }
public string First { get; set; }
public string Last { get; set; }
[NotMapped]
[Computed]
public string Full { get { return First + " " + Last; } }
}
This is the configuration.
public class DelegateDecompilerConfiguration : DefaultConfiguration
{
public override bool ShouldDecompile(MemberInfo memberInfo)
{
// Automatically decompile all NotMapped members
return base.ShouldDecompile(memberInfo) || memberInfo.GetCustomAttributes(typeof(NotMappedAttribute), true).Length > 0;
}
}
I also tried removing the [NotMapped] and then changed typeof(NotMappedAttribute) to typeof(ComputedAttribute) in the above configuration.
Then I register it like so
DelegateDecompiler.Configuration.Configure(new DelegateDecompilerConfiguration());
In Startup.cs. I also tried putting it directly into my action.
public ActionResult Test()
{
DelegateDecompiler.Configuration.Configure(new DelegateDecompilerConfiguration());
var ctx = new ApplicationDbContext();
var result = ctx.People.Where(x => x.Full.Contains("foo bar")).ToList();
return View();
}
Neither work :(
If I put .Decompile() on the query then it works as expected. So the DelegateDecompiler is working, but not the configuration.
As you discovered in your GitHub issue, you always have to call .Decompile() within your LINQ query. The extra configuration just eliminates the need to decorate all of your computed properties with the Computed attribute, instead relying on the Entity Framework NotMapped attribute.
As Dave says it requires Decompile()
So you don't have to remember to call it you can create a mapper extension that wraps what you are doing.
public static class MapperExtensions
{
/// <summary>
/// Creates a list from automapper projection. Also wraps delegate decompiler to supprt Computed Domain properties
/// </summary>
/// <typeparam name="TDestination"></typeparam>
/// <param name="projectionExpression"></param>
/// <returns></returns>
public static List<TDestination>
ToList<TDestination>(this IProjectionExpression projectionExpression)
{
return projectionExpression.To<TDestination>().Decompile().ToList();
}
}