I need to import a file, and also the Person model that I have shown below.
I am able to upload the file, However, I am not able to retrieve the Person model data in the importFileAndOtherInfo method that I have written.
Note: I am testing this web API via Postman. How can I upload a file, and also send Person Model data via Postman?
Person
int pId
string PName
School schoolAttended
My implementation:
[HttpPost]
public async Task<int> importFileAndOtherInfo(Person person)
{
var stream = HttpContext.Current.Request.Files[0].InputStream
// HOW TO RETRIEVE THE PERSON DATA HERE.
}
What I understood from your question is, you want to pass model data and the file in stream at the same time; you can't send it directly, the way around is to send file with IFormFile and add create your own model binder as follows,
public class JsonWithFilesFormDataModelBinder: IModelBinder
{
private readonly IOptions<MvcJsonOptions> _jsonOptions;
private readonly FormFileModelBinder _formFileModelBinder;
public JsonWithFilesFormDataModelBinder(IOptions<MvcJsonOptions> jsonOptions, ILoggerFactory loggerFactory)
{
_jsonOptions = jsonOptions;
_formFileModelBinder = new FormFileModelBinder(loggerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
// Retrieve the form part containing the JSON
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
if (valueResult == ValueProviderResult.None)
{
// The JSON was not found
var message = bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(bindingContext.FieldName);
bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, message);
return;
}
var rawValue = valueResult.FirstValue;
// Deserialize the JSON
var model = JsonConvert.DeserializeObject(rawValue, bindingContext.ModelType, _jsonOptions.Value.SerializerSettings);
// Now, bind each of the IFormFile properties from the other form parts
foreach (var property in bindingContext.ModelMetadata.Properties)
{
if (property.ModelType != typeof(IFormFile))
continue;
var fieldName = property.BinderModelName ?? property.PropertyName;
var modelName = fieldName;
var propertyModel = property.PropertyGetter(bindingContext.Model);
ModelBindingResult propertyResult;
using (bindingContext.EnterNestedScope(property, fieldName, modelName, propertyModel))
{
await _formFileModelBinder.BindModelAsync(bindingContext);
propertyResult = bindingContext.Result;
}
if (propertyResult.IsModelSet)
{
// The IFormFile was successfully bound, assign it to the corresponding property of the model
property.PropertySetter(model, propertyResult.Model);
}
else if (property.IsBindingRequired)
{
var message = property.ModelBindingMessageProvider.MissingBindRequiredValueAccessor(fieldName);
bindingContext.ModelState.TryAddModelError(modelName, message);
}
}
// Set the successfully constructed model as the result of the model binding
bindingContext.Result = ModelBindingResult.Success(model);
}
}
Model
[ModelBinder(typeof(JsonWithFilesFormDataModelBinder), Name = "data")]
public class Person
{
public int pId {get; set;}
public string PName {get; set;}
public School schoolAttended {get; set;}
public IFormFile File { get; set; }
}
Postman request:
I am using the same in netcoreapp2.2. Works successfully.
Now when migrating from notecoreapp2.2 to netcoreapp3.1 I'm facing issues with private readonly IOptions<MvcJsonOptions> _jsonOptions;
As MvcJsonOptions is a breaking change from core 2.2 to 3.0.
Check this:
Migration of netcoreapp2.2 to netcoreapp3.1 - convert MvcJsonOptions to core3.1 compatable
Related
In Asp.Net you can automatically parse request data into an input model / API contract using for example the attributes FromBody, FromQuery, and FromRoute. I want to execute this behavior myself. Let me explain.
I want to have a custom policy requirement based on a combination of data passed to the requirement and the target entity which is passed inside the request data. But this target entity id can be in different locations. Usually the body, but for example the route or the query when using HttpGet. So I thought about putting this information about the location above the controller endpoint using an attribute. The following pseudo-code is based on the guess that I need the BindingSource.
I would create API contracts using an interface defining the location of the target id.
public interface ITargetEntityContract {
public string TargetEntityId { get; set; }
}
public class ExampleRequest : ITargetEntityContract {
public string TargetEntityId { get; set; } = default!;
public string SomeOtherData { get; set; } = default!;
}
Then I would create an attribute to define the location:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class TargetEntityLocationAttribute : Attribute {
public Type ContractType { get; }
public BindingSource BindingSource { get; }
public TargetEntityLocationAttribute(Type contractType, BindingSource bindingSource) {
if (!typeof(ITargetEntityContract).IsAssignableFrom(contractType))
throw new Exception("Contract has to implement the interface ITargetEntityContract");
this.ContractType = contractType;
this.BindingSource = bindingSource;
}
}
And you would apply this onto a controller endpoint the following way:
[TargetEntityLocation(typeof(ExampleRequest), BindingSource.Body)]
public async Task<IActionResult> SomeEndpointAsync([FromBody] ExampleRequest requestData) {
}
Within the IAuthorizationHandler I would use these classes the following way:
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExampleAuthorizationRequirement requirement) {
var endpoint = this._httpContextAccessor.HttpContext!.GetEndpoint();
if (endpoint is null)
throw new Exception("Some error");
var targetEntityLocation = endpoint.MetaData.GetMetaData<TargetEntityLocationAttribute>();
if (targetEntityLocation is null)
throw new Exception("some error");
var targetEntityModel = SomeAlmightyParser.ParseFromSource(this._httpContextAccessor.HttpContext!, targetEntityLocation.ContractType, targetEntityLocation.BindingSource) as ITargetEntityContract;
// do something with targetEntityModel.TargetEntityId
}
Is there a way to parse the request data of the HttpContext into the given model based on the data location?
If the source of the data is RequestBody,you could read the stream with StreamReader and deserialize the string you get;
If the source of the data is RequestQuery,you could map the key-value pairs to your target object with reflection
For example,I tried as below:
//read obj from requestbody
using var reader = new StreamReader(context.Request.Body);
var bodystr = reader.ReadToEndAsync().Result;
if (bodystr != "")
{
var obj1 = JsonSerializer.Deserialize(bodystr, typeof(ExampleRequest), new JsonSerializerOptions() { });
}
//read simple obj from query
var querycollection = context.Request.Query;
var type = typeof(ExampleRequest);
var properties = type.GetProperties();
object obj = Activator.CreateInstance(type);
foreach (var propinfo in properties)
{
var propname = propinfo.Name;
if (querycollection.ContainsKey(propname))
{
var proptype = propinfo.PropertyType;
propinfo.SetValue(obj, Convert.ChangeType(querycollection[propname].ToString(), proptype),null);
}
}
var obj2 = obj as ExampleRequest;
Result:
I know this has been asked here and at various other places but I have not seen a simple answer.
Or at least, I have not been able to find any.
In short, I have an .Net Core Web Api endpoint that accepts XML.
Using (in Startup):
services.AddControllers().AddXmlSerializerFormatters();
I want to modelbind it to a class. Example:
[Route("api/[controller]")]
[ApiController]
public class PersonController : ControllerBase
{
[HttpPost]
[Consumes("application/xml")]
[ApiConventionMethod(typeof(DefaultApiConventions), nameof(DefaultApiConventions.Post))]
public async Task<ActionResult> PostPerson([FromBody] Person person)
{
return Ok();
}
}
// Class/Model
[XmlRoot(ElementName = "Person")]
public class Person
{
[XmlElement(ElementName = "Name")]
public string Name { get; set; }
[XmlElement(ElementName = "Id")]
public int Id { get; set; }
}
Passing in:
<Person><Name>John</Name><Id>123</Id></Person>
works fine. However, as soon as namespaces comes into play it either fails to bind the model:
<Person xmlns="http://example.org"><Name>John</Name><Id>123</Id></Person>
<Person xmlns="http://example.org"><Name>John</Name><Id xmlns="http://example.org">123</Id></Person>
Or the model can be bound but the properties are not:
<Person><Name xmlns="http://example.org">John</Name><Id>123</Id></Person>
<Person><Name xmlns="http://example.org">John</Name><Id xmlns="http://example.org">123</Id></Person>
etc.
I understand namespaces. I do realize that I can set the namespaces in the XML attribute for the root and elements.
However, I (we) have a dozens of callers and they all set their namespaces how they want. And I want to avoid to have
dozens of different versions of the (in the example) Person classes (one for each caller). I would also mean that if a caller
changes their namespace(s) I would have to update that callers particular version and redeploy the code.
So, how can I modelbind incoming XML to an instance of Person without taking the namespaces into account?
I've done some tests overriding/creating an input formatter use XmlTextReader and set namespaces=false:
XmlTextReader rdr = new XmlTextReader(s);
rdr.Namespaces = false;
But Microsoft recommdes to not use XmlTextReader since .Net framework 2.0 so would rather stick to .Net Core (5 in this case).
You can use custom InputFormatter,here is a demo:
XmlSerializerInputFormatterNamespace:
public class XmlSerializerInputFormatterNamespace : InputFormatter, IInputFormatter, IApiRequestFormatMetadataProvider
{
public XmlSerializerInputFormatterNamespace()
{
SupportedMediaTypes.Add("application/xml");
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var xmlDoc = await XDocument.LoadAsync(context.HttpContext.Request.Body, LoadOptions.None, CancellationToken.None);
Dictionary<string, string> d = new Dictionary<string, string>();
foreach (var elem in xmlDoc.Descendants())
{
d[elem.Name.LocalName] = elem.Value;
}
return InputFormatterResult.Success(new Person { Id = Int32.Parse(d["Id"]), Name = d["Name"] });
}
}
Person:
public class Person
{
public string Name { get; set; }
public int Id { get; set; }
}
startup:
services.AddMvc(options =>
{
options.RespectBrowserAcceptHeader = true; // false by default
options.InputFormatters.Insert(0, new XmlSerializerInputFormatterNamespace());
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddXmlSerializerFormatters()
.AddXmlDataContractSerializerFormatters();
result:
So, in order to be able to modelbind XML to a class without taking namespaces into consideration I created new InputFormatter.
And I use XmlTextReader in order to ignore namespaces. Microsoft recommends to use XmlReader rather than XmlTextReader.
But since XmlTextReader is there still (in .Net 6.0 Preview 3) I'll use it for now.
Simply create an inputformatter that inherits from XmlSerializerInputFormatter like so:
public class XmlNoNameSpaceInputFormatter : XmlSerializerInputFormatter
{
private const string ContentType = "application/xml";
public XmlNoNameSpaceInputFormatter(MvcOptions options) : base(options)
{
SupportedMediaTypes.Add(ContentType);
}
public override bool CanRead(InputFormatterContext context)
{
var contentType = context.HttpContext.Request.ContentType;
return contentType.StartsWith(ContentType);
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var type = GetSerializableType(context.ModelType);
var request = context.HttpContext.Request;
using (var reader = new StreamReader(request.Body))
{
var content = await reader.ReadToEndAsync();
Stream s = new MemoryStream(Encoding.UTF8.GetBytes(content));
XmlTextReader rdr = new XmlTextReader(s);
rdr.Namespaces = false;
var serializer = new XmlSerializer(type);
var result = serializer.Deserialize(rdr);
return await InputFormatterResult.SuccessAsync(result);
}
}
}
Then add it to the inputformatters like so:
services.AddControllers(o =>
{
o.InputFormatters.Add(new XmlNoNameSpaceInputFormatter(o));
})
.AddXmlSerializerFormatters();
Now we can modelbind Person or any other class no matter if there is namespaces or not in the incoming XML.
Thanks to #yiyi-you
I created a custom model binder in my .NET Core 3.1 API to validate all JSON parameters (by the help of this post How to validate json request body as valid json in asp.net core).Due to security concern I am checking all the incoming request parameter with the actual model properties(entity model) to avoid parameter tempering(adding additional properties while sending the API request). This is working fine for normal json request but if I have a nested json(nested entity model)then it is validating only main properties(inside properties are not validating)
Example:
for non nested entity models the code is working fine, but if it is a nested models like bellow
public class dep
{
public int depId { get; set; }
public string depName { get; set; }
}
public class EmpModel
{
public dep DepDetails { get; set; }
public string empName { get; set; }
}
The code will validate only the main properties like DepDetails, empName but it wont check the inside properties of DepDetails
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); }
var modelName = bindingContext.BinderModelName ?? "XJson";
var modelType = bindingContext.ModelType;
// create a JsonTextReader
var req = bindingContext.HttpContext.Request;
var raw = req.Body;
if (raw == null)
{
bindingContext.ModelState.AddModelError(modelName, "invalid request body stream");
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
JsonTextReader reader = new JsonTextReader(new StreamReader(raw));
try
{
var jsonT = (JObject)JToken.Load(reader, this._loadSettings);
JObject reqeJson = JObject.Parse(jsonT.ToString());
var RequestParameters = reqeJson.Properties().ToList();
var actualParam = bindingContext.ModelMetadata.Properties.ToList();
//checking for additional parameters in input
//Here I am checking if any additional property is appended with the data and for the
//above model in both actualParam and RequestParameters I will get only 2 instead of 3
bool flag = true;
foreach (var obj in RequestParameters)
{
if(!actualParam.Any(item => item.Name.ToUpper() == obj.Name.ToUpper())){
flag = false;
break;
}
JObject tmp = JObject.Parse(obj.ToString());
};
if (!flag)
{
bindingContext.ModelState.AddModelError(modelName, "Additional Data is present in Request");
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
var o = jsonT.ToObject(modelType);
bindingContext.Result = ModelBindingResult.Success(o);
}
catch (Exception e)
{
bindingContext.ModelState.AddModelError(modelName, e.ToString());
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
The Real problem is the content of RequestParameters and actualParam is {DepDetails,empName} but I should get as {depId,depName,empName}
If this method is not correct then please help me with some alternatives.
I have a model class I created in angular 2 to track fields. This same class also exists as a model in my webapi project.
export class People {
Name: string;
Phone: string;
Date: date;}
export class Address {
street: string,
zip: string,
}
in my service I send this to my webapi controller
getPeopleData(peopleModel: People, addressmodel: Address)
{
let headers = new headers(...)
data = {
"p": peopleModel,
"a": addressModel
}
let body = JSON.stringify(data);
return this.http.post(url, body, {headers: headers})
.map((response: Response)=> ...
}
finally in my controller
public JArray GetPeopleData([FromBody]JObject models)
{
var modelPeople = models["p"].ToObject<People>();
var modelAddress = models["a"].ToObject<Address>();
}
modelPeople and modeAddress doesn't map. How can I get my model to map directly.
All I get are a bunch of null fields when there is a string of data in the JObject.
EDIT:
I created a container class that holds the objects of People and Address
public class Container
{
public People people {get; set;}
public Address addresss {get; set;}
}
I then passed in the object to the Container and my results are still null
public JArray GetPeopleData([FromBody]Container container)
{
var modelPeople = container.people;
var modelAddress = container.address;
}
both have all values of null.
I dont get it I feel like I am so close but something is missing
Hello it works for me
//This is a Gobal object
result = {
modelPeople: '',
modelAddress: ''
}
constructor( private _http: Http ) {
}
getJsonModel() {
promise = await this._http.get(URL).toPromise().then(
res => {
this.result= res.json();
}
);
}
Situation
I created the following Model classes
public class Car
{
public int Id {get;set;}
public string Name {get;set;}
public virtual ICollection<PartState> PartStates {get;set; }
}
public class PartState
{
public int Id {get;set;}
public string State {get;set;}
public int CarId {get;set;}
public virtual Car Car {get;set;}
public int PartId {get;set;}
public virtual Part Part {get;set;}
}
public class Part
{
public int Id {get;set;}
public string Name {get;set;}
}
And a matching DbContext
public class CarContext : DbContext
{
public DbSet<Car> Cars {get;set;}
public DbSet<PartState> PartStates {get;set;}
public DbSet<Part> Parts {get;set;}
}
And created a WebApplication to make this available via odata, using the scaffolding template "Web API 2 OData Controller with Actions, using Entity Framework"
also i create following webapi config:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var builder = new ODataConventionModelBuilder();
builder.EntitySet<Car>("Cars");
builder.EntitySet<PartState>("PartStates");
builder.EntitySet<Part>("Parts");
var edmModel = builder.GetEdmModel();
config.Routes.MapODataRoute("odata", "odata", edmModel);
}
}
I now want to add the following Method to my Cars Controller
// GET: odata/Cars(5)/Parts
[Queryable]
public IQueryable<Part> GetParts([FromODataUri] int key)
{
var parts = db.PartStates.Where(s => s.CarId == key).Select(s => s.Part).Distinct();
return parts;
}
And retrieve the data with this Url:
http://localhost/odata/Cars(1)/Parts
But it does not work, instead i get the following error:
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"No HTTP resource was found that matches the request URI 'http://localhost/odata/Cars(1)/Parts'."
},"innererror":{
"message":"No routing convention was found to select an action for the OData path with template '~/entityset/key/unresolved'.","type":"","stacktrace":""
}
}
}
Question
So my question is, is that even possible?!
I tried to create a Navigation property manually, and added it to the edm model, while this does resolve the issue to invoke the new method, it also introduces new Errors.
EDIT:
What id did try to add it manually in this way:
var edmModel = (EdmModel)builder.GetEdmModel();
var carType = (EdmEntityType)edmModel.FindDeclaredType("Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
var carsProperty = new EdmNavigationPropertyInfo();
carsProperty.TargetMultiplicity = EdmMultiplicity.Many;
carsProperty.Target = carType;
carsProperty.ContainsTarget = false;
carsProperty.OnDelete = EdmOnDeleteAction.None;
carsProperty.Name = "Cars";
var nav = EdmNavigationProperty.CreateNavigationPropertyWithPartner(partsProperty, carsProperty);
carType.AddProperty(nav);
config.Routes.MapODataRoute("odata", "odata", edmModel);
while this allowed me to invoke the above speciefied method trough the also above specified URL, it gave me the following error:
{
"odata.error":{
"code":"","message":{
"lang":"en-US","value":"An error has occurred."
},"innererror":{
"message":"The 'ObjectContent`1' type failed to serialize the response body for content type 'application/json; odata=fullmetadata; charset=utf-8'.","type":"System.InvalidOperationException","stacktrace":"","internalexception":{
"message":"The related entity set could not be found from the OData path. The related entity set is required to serialize the payload.","type":"System.Runtime.Serialization.SerializationException","stacktrace":" at System.Web.Http.OData.Formatter.Serialization.ODataFeedSerializer.WriteObject(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)\r\n at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStream(Type type, Object value, Stream writeStream, HttpContent content, HttpContentHeaders contentHeaders)\r\n at System.Web.Http.OData.Formatter.ODataMediaTypeFormatter.WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)\r\n--- End of stack trace from previous location where exception was thrown ---\r\n at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)\r\n at System.Runtime.CompilerServices.TaskAwaiter.GetResult()\r\n at System.Web.Http.WebHost.HttpControllerHandler.<WriteBufferedResponseContentAsync>d__1b.MoveNext()"
}
}
}
}
You have to call "AddNavigationTarget" on the EntitySet.
Assume that your namespace is "MyNamespace", then add the following code to your WebApiConfig.cs. In this way, retrieving the data with "Get: odata/Cars(1)/Parts" will work.
var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
cars.AddNavigationTarget(carType.AddUnidirectionalNavigation(partsProperty), parts);
Taking #FengZhao's answer further, in order to get the url odata/Cars working you also need to register the navigation property link builder to entity set link builder.
var cars = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Cars");
var parts = (EdmEntitySet)edmModel.EntityContainers().Single().FindEntitySet("Parts");
var carType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Car");
var partType = (EdmEntityType)edmModel.FindDeclaredType("MyNamespace.Part");
var partsProperty = new EdmNavigationPropertyInfo();
partsProperty.TargetMultiplicity = EdmMultiplicity.Many;
partsProperty.Target = partType;
partsProperty.ContainsTarget = false;
partsProperty.OnDelete = EdmOnDeleteAction.None;
partsProperty.Name = "Parts";
var navigationProperty = carType.AddUnidirectionalNavigation(partsProperty);
cars.AddNavigationTarget(navigationProperty, parts);
var linkBuilder = edmModel.GetEntitySetLinkBuilder(cars);
linkBuilder.AddNavigationPropertyLinkBuilder(navigationProperty,
new NavigationLinkBuilder((context, property) =>
context.GenerateNavigationPropertyLink(property, false), true));
Web Api cannot resolve your URL against any of registered URI templates.
Use Route Debugger to figure this out.
http://blogs.msdn.com/b/webdev/archive/2013/04/04/debugging-asp-net-web-api-with-route-debugger.aspx
I believe our problem is the id parameter that you pass via url
Try making it more explicit
config.Routes.MapHttpRoute
(
"DefaultInternalApi",
"api/{controller}/{objectType}/{Id}/{relation}",
defaults:
new
{
Id = System.Web.Http.RouteParameter.Optional,
}
);
The original question could also have solved their issue by adding Parts as a property on the Car object if it made sense for them. That is, adding the navigation property for real rather than persuading the OData model builder to register it. For example:
public class Car
{
public int Id {get;set;}
public string Name {get;set;}
public virtual ICollection<PartState> PartStates {get;set; }
public virtual ICollection<Part> Parts { get => this.PartStates?.Select(partState => partState.Part)
}