I'm working on building a WebAPI based OData service and I’m having issues with navigation links. Basically I have two classes where one has a reference to another. When I request either atom or verbose JSON I can see that I have a link between the two. However, I’d like to customize the uri to have it point to a different location rather than the default assumed by the OData library.
Using a simple example, assume that I have two entity sets called ‘entity1’ and ‘entity2’. These are exposed as OData services located at: /api/entities1 and /api/entities2 respectively.
Here’s my sample model code:
public class Entity1 {
public int ID { get; set; }
public string Name { get; set; }
public virtual Entity2 OtherEntity { get; set; }
}
public class Entity2 {
public int ID { get; set; }
public string Value { get; set; }
}
I’m using the ODataConventionModelBuilder to register these as follows:
ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
builder.EntitySet<Entity1>("entities1");
builder.EntitySet<Entity2>("entities2");
IEdmModel model = builder.GetEdmModel();
config.Routes.MapODataRoute(routeName: "OData", routePrefix: "api", model: model);
I've implemented the controller as an EntitySetController. All of this works as expected and I get the following response when I request verbose JSON:
{
"d": {
"results": [{
"__metadata": {
"id": "http://localhost:37826/api/entities1(1)",
"uri": "http://localhost:37826/api/entities1(1)",
"type": "ODataSample.Models.Entity1"
},
"OtherEntity": {
"__deferred": {
"uri": "http://localhost:37826/api/entities1(1)/OtherEntity"
}
},
"ID": 1,
"Name": "First Entity"
}]
}
}
What I’d like to do is to have the ‘OtherEntity’ field in an Entity1 instance refer to the associated Entity2 instance under /api/entities2 so that the link appears something like /api/entities2(2) (assuming the ID of the Entity2 instance is '2').
I know that I could just make the type of ‘OtherEntity’ a Uri and insert the appropriate value in my controller but that seems a bit of a hack (but I could be wrong). From what understand about OData, I believe the right way to do this is to modify the navigation property but I’m not sure how or where.
Any help is appreciated. Thanks in advance.
Cheers,
Steve
You could do something like the following:
var entities1 = builder.EntitySet<Entity1>("Entities1");
entities1.HasNavigationPropertyLink(entities1.EntityType.NavigationProperties.First(np => np.Name == "OtherEntity"),
(context, navigation) =>
{
return new Uri(context.Url.ODataLink(new EntitySetPathSegment("Entities2"), new KeyValuePathSegment(context.EntityInstance.OtherEntity.Id.ToString())));
}, followsConventions: false);
Related
I have an ASP.NET Web API 2 project to which I have added Swagger - Swashbuckle v5.6.0. Everything works fine. Swagger UI renders test endpoints for my API as expected.
I added a new Controller to my API. There is a GET action with a complex type parameter. For complex types, Web API tries to read the value from the message body. This is the default behaviour.
Here is my GET action:
[HttpGet]
[Route("search")]
[ResponseType(typeof(List<SearchModel>))]
public IHttpActionResult Search(SearchModel searchOptions)
{
//....
return Ok();
}
And her is my complex type:
public class SearchModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
[DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }
public string AddressLine1 { get; set; }
public string City { get; set; }
public string Telephone { get; set; }
public string MobilePhone { get; set; }
}
The problem:
But Swagger UI doesn't render body parameter field for my complex type in the GET action. For POST and PUT actions Swagger UI renders body parameter fields as expected but not for the complex type in my GET action.
As can be seen in the screenshot Swagger UI renders query parameters fields for attributes in my complex type instead of rendering a body parameter field for my type as it does in the case of POST and PUT.
My GET action is working fine when testing from Postman and filling the json in the body of the request. By setting breakpoint in the action inside Visual Studio I can see the values are bound to my object in the action parameter.
I have tried to decorate the parameter in my action with [FromBody] (which is the default for complex type) but same result.
Is this a bug in Swagger? Or am I missing something?
Sadly, you can't do what you want with Swagger. You can't send a request model in an HTTP GET method. You can however change the swagger UI to look like this:
but you won't be able to receive the model in your controller.
This is a known issue within the Swagger developers and it was discussed in 2016 and the final decision is that swagger won't support a request body in an HTTP GET method. Here is the link to the already closed issue.
You have three options here:
Leave the method as it is, and test it in Postman, but not in Swagger.
Follow the below steps to achieve the picture above, but please note, that it will only fix the UI part and you will always end up with null SearchModel in the controller when you press Try it out! in swagger.
Make it a [HttpPost method instead of [HttpGet].
How to make swagger UI display GET method with request body:
First, create one Attribute class:
public class ModelInBodyAttribute : Attribute
{
public ModelInBodyAttribute(string modelName, string description, bool isRequired)
{
this.ModelName = modelName;
this.Description = description;
this.IsRequired = IsRequired;
}
public string ModelName { get; set; }
public bool IsRequired { get; set; }
public string Description { get; set; }
}
Then you can decorate your method in the controller:
[ModelInBody(modelName: nameof(SearchModel), description: "My model description", isRequired: true)]
[HttpGet]
[Route("search")]
[ResponseType(typeof(List<SearchModel>))]
public IHttpActionResult Search(SearchModel searchOptions)
{
//....
return Ok(new List<SearchModel>());
}
After that create IOperationFilter class (ModelInBodyOperationFilter):
public class ModelInBodyOperationFilter : IOperationFilter
{
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var attribute = apiDescription.GetControllerAndActionAttributes<ModelInBodyAttribute>().FirstOrDefault();
if (attribute == null)
{
return;
}
operation.parameters.Clear();
operation.parameters.Add(new Parameter
{
name = attribute.ModelName,
description = attribute.Description,
#in = "body",
required = attribute.IsRequired,
schema = new Schema { #ref = $"#/definitions/{attribute.ModelName}" }
});
}
}
Lastly, don't forget to register the IOperationFilter in SwaggerConfig:
c.OperationFilter<ModelInBodyOperationFilter>();
When you send the request via swagger, you will notice that the Curl part is absolutely correct, but still, in your controller there is nothing.
There are endless discussions on whether you should have a PAYLOAD "Body content" in a GET request. As you mentioned it's supported by HTTP but you will find in the internet that many people suggest not to do it. I guess that swagger team also expect you not to use it.
I am trying to send multiple files along with some data for every file. This is my model:
public class FileDTO
{
[Required]
public IFormFile File { get; set; }
[Required]
public string CategoryName { get; set; }
[Required]
public string CategoryDescription { get; set; }
public string Detail { get; set; }
}
This is my controller:
[HttpPost("Upload/{id:int}")]
public async Task<IActionResult> Upload(int id, IEnumerable<FileDTO> appFileDTOs)
{
...
}
Is this even a correct way to do so? How do I send such a request in Postman to simulate it?
Thanks in advance!
Edit
I tried it like this in Postman:
Everything submits correctly besides the image. For some reason the image is always null...
[] represents collection/dictionary index while dot(.) represents there's a property.
So you should rename all the field names with the dot representation.
For example, change
appFileDTOs[0][File]
to
appFileDTOs[0].File
Demo
try this it may help you,
send from formData.
in model key send value as
[
{
"CategoryName":"Category1",
"CategoryDescription ":"Category1 Description",
"Detail":"Details "
},
{
"CategoryName":"Category2",
"CategoryDescription ":"Category2 Description",
"Detail":"Details2"
}
]
and for file send first file as file1 and second file as file2;
In server side , remove IEnumerable of FileDTO appFileDTOs from method name.
get value of model as
var data = JsonConvert.DeserializeObject<List<FileDTO>>(Request.Form["model"]);
simillary for file
var fileUpload1 = Request.Form.Files["file1"];
var fileUpload2 = Request.Form.Files["file2"];
I'm using JsonPatchDocument with ASP.NET 4.5 and Web Api. My controller looks like this:
[HttpPatch]
[Route("MyRoute/{PersonItem1}/{PersonItem2}/")]
public IHttpActionResult ChangePerson([FromHeader]Headers, [FromBody]JsonPatchDocument<PersonDto> person)
{
// Do some stuff with "person"
}
And PersonDto:
public class PersonDto
{
public string Name { get; set; }
public string Email { get; set; }
}
Now, I may send a PATCH request that is something like:
{
"op": "op": "replace", "path": "/email", "value": "new.email#example.org"
}
Now let's say I add some data annotations:
public class PersonDto
{
public string Name { get; set; }
[MaxLength(30)]
public string Email { get; set; }
}
What is the best way to ensure this validation is honored without writing additional validation. Is it even possible?
There is the simple method:
Get your object from your repository.
Deep copy the object so you have object A and B.
Apply the change with person.ApplyUpdatesTo(objB).
Create an extension method to validate the difference between object A and B.
If the validation is good proceede, if not throw an error.
This would catch if the client was attempting to modify immutable fields or if the new information in object B violates your constraints.
Note that this is not a great solution in that you would have to change your code in two places if you happen to change your constraints.
In a GET request, I can create a mapping from my back-end model to a customized DTO with AutoMapper with ease. However, I have some concerns when using AutoMapper with POST requests.
Suppose a user orders a product online, he sends the server a POST request with some required data. The fact is, not every piece of data in the back-end model is sent by the user. Let's say the ID of the Order is a GUID which is generated automatically when the entry is inserted into the database; or maybe there are other properties which are auto-incremented. All of these cannot-be-mapped properties lead to a lot of .ForMember(dest => dest.myProperty, opt => opt.Ignore()) chains, and extra handling on the mapped instance after var mappedInstance = Mapper.Map<PostDTO, BackEndModel>(postDTO).
Is AutoMapper not designed for the aforementioned scenario? What is the practice for handling the model-mapping process if the back-end model is much more complex than the DTO?
Update
public class MultipleChoiceQuestion
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid MultipleChoiceQuestionId { get; set; }
[Required]
public string Question { get; set; }
[Required]
public ICollection<PossibleChoice> PossibleChoices { get; set; }
}
public class PossibleChoice
{
[Key, Column(Order = 1), ForeignKey("MultipleChoiceQuestion")]
public Guid MultipleChoiceQuestionId { get; set; }
[Key, Column(Order = 2)]
public int ChoiceIndex { get; set; }
[Required]
public string AnswerText { get; set; }
public MultipleChoiceQuestion MultipleChoiceQuestion { get; set; }
}
The user sends a request to create a new question. Only 2 fields are sent.
{
"Question": "How are you?",
"Answers": [
{ "Text": "I am fine." },
{ "Text": "Feeling bad." }
]
}
Properties that are missing at this stage:
MultipleChoiceQuestionId
Generated after the insertion
ChoiceIndex
Auto-incremented from 1 up to the number of answers available
Without manual mapping, how to handle this situation with AutoMapper?
1- Define your DTOs to be something like this:
public class MultipleChoiceQuestionDto
{
// This property could stay here, because you may need to use the same DTO for update (PUT),
// which means you need the Id to distinguish and validate the DTO data against the URL id
//public Guid MultipleChoiceQuestionId { get; set; }
public string Question { get; set; }
public ICollection<PossibleChoiceDto> PossibleChoices { get; set; }
}
public class PossibleChoiceDto
{
// This can go from this dto, because this DTO is a child dto for its parent.
//public Guid MultipleChoiceQuestionId { get; set; }
// This property could stay here, because you may need to use the same DTO for update (PUT),
// which means you need the Id to know which Choice was updated.
//public int ChoiceIndex { get; set; }
public string AnswerText { get; set; }
}
2- You create a mapping between the entity and the corresponding Dto like this, make sure you call this code from the global.asax file.
Mapper.CreateMap<MultipleChoiceQuestion, MultipleChoiceQuestionDto>();
Mapper.CreateMap<MultipleChoiceQuestionDto, MultipleChoiceQuestion>()
.ForMember(m => m.MultipleChoiceQuestionId, e => e.Ignore()); // you force automapper to ignore this property
Mapper.CreateMap<PossibleChoice, PossibleChoiceDto>();
Mapper.CreateMap<PossibleChoiceDto, PossibleChoice>()
.ForMember(m => m.MultipleChoiceQuestion, e => e.Ignore()) //
.ForMember(m => m.MultipleChoiceQuestionId, e => e.Ignore())
.ForMember(m => m.ChoiceIndex, e => e.Ignore());
3- In your controller.Post you need to map from the DTO to the entity and save the mapped entity to the database.
Now, the above solution will work for you for POST, however, you need to think about the PUT scenario and soon you will realize that you need the Ids to be included in the DTOs, and if you decided to do that then you need to revisit the mapping in point 2 and remove the Ignore code for the properties that you decided to include in the DTO.
Hope that helps.
I'm not sure where in your architecture you're using AutoMapper, but you could conceptually whitelist the properties before doing the automapping. For example, if you're in MVC and you're doing modelbinding, there are techniques (e.g. in the UpdateModel method) to include or exclude a list of properties.
My Controller is able to create the model object but all the properties related to model and assigned to null values
Environment : VS 2010, ASP.NET MVC RC latest, jQuery 1.7.1
Following is the Web API Controller code
public class Customer
{
public string Name { get; set; }
public string City { get; set; }
}
public class UserController : ApiController
{
public Customer Post(Customer user)
{
return user;
}
}
Following is the ajax calling code
$.ajax('/api/user',
{
ContentType: "application/x-www-form-urlencoded; charset=UTF-8",
dataType: 'json',
type: 'POST',
data: JSON.stringify({ "Name": "Scott", "City": "SC" })
});
Controller does create the model "Customer" object but both "Name" and "City" properties are null.
What's wrong here?
I have read many similar issues on this site but could not find the solution.
This blog here gives a good idea about how Model Binding differs in ASP.NET Web project and a ASP.NET Web API project.
I was facing a similar issue in the project I was working on and adding a explicit ModelBinding attribute made the property values stick
Requested Data :
var customer = { Name : "customer Name", City : "custome City" }
$.ajax({
url : ...
type: ...
data : customer
});
Request Class:
public class Customer
{
public string Name { get; set; }
public string City { get; set; }
}
Controller :
public class UserController : ApiController
{
[HttpGet]
public Customer Get([ModelBinder] Customer user)
{
// fetch from whereever
}
}
I'm going through the same issue right now. I'm not 100% sure of the answer, but below is my javascript and I've added to the class [DataModel] and to the Properties [DataMember]. That is:
[DataModel]
public class Customer
{
[DataMember] public string Name { get; set; }
[DataMember] public string City { get; set; }
}
And My JavaScript
$(document).ready(function () {
// Send an AJAX request
$.getJSON("api/session/GetAll",
function (data) {
// On success, 'data' contains a list of products.
$.each(data, function (key, val) {
//debugger;
// Format the text to display.
//var str = val.Name + ': $' + val.Price;
var str = 'abcd';
// Add a list item for the product.
$('<li/>', { text: str })
.appendTo($('#products'));
});
});
});
Faced a similar issue and my problem turned out to be invalid JSON in the body of the request. There was a comma missing after one of the fields. So it seems like the default model binder just binds null if there are any syntax errors in the JSON.
Next time this happens, ensure that there is no "internal" keyword on the property setter. That is:
instead of public string Comment {get; internal set;}
use public string Comment {get; set;}
This fixed it for me.