Reputation: 75
I am trying to find a way for multiple types to be passed into an api put request via a collection which accepts a base type. I am using Asp.Net Core 2.1.0 I can't seem to find an easy way to do this? Do you know of a way to do this?
public abstract class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
public class GoodPerson :Person
{
public int NumberOfGoodDeeds { get; set; }
}
public class BadPerson : Person
{
public int NumberOfBadDeeds { get; set; }
}
[HttpPut]
public IActionResult Update([FromBody]Collection<Person> persons)
{
//do stuff with concrete classes
return Ok();
}
I'm passing in the arguments using JSON in the body of an api call.
[
{
"Age": 12,
"Name": "Bob",
"NumberOfGoodDeeds": 2
},
{
"Age": 18,
"Name": "Andy",
"NumberOfBadDeeds": 5
}
]
Upvotes: 1
Views: 1354
Reputation: 239430
You cannot. An action param isn't the same thing as just a normal method param. With a normal method, you would instantiate a copy of GoodPerson
, for example and pass it in, where it would be upcast to Person
. Then, in your method code, you could use pattern matching, casting, etc. to get back to the "real" type. That works because ultimately, it's GoodPerson
in memory.
With an action method, however, all you have is some string(s) or a bit of JSON representing a class. To activate the action, the modelbinder inspects the param types of the action method, and then attempts to initialize an instance of that type(s) and map the data onto it. (In the case of JSON, it's technically a deserialization process running, but importantly, it's still going to attempt to deserialize into the param type.) Any extraneous data is simply dropped. As a result, inside your action method, the Person
instance is literally just a Person
instance. It will never be a GoodPerson
nor BadPerson
.
The best you can do here is to create a class that encompasses all possible classes with all their properties. For example:
public class PersonViewModel
{
public string Name { get; set; }
public int Age { get; set; }
// Note the use of nullables here
public int? NumberOfGoodDeeds { get; set; }
public int? NumberOfBadDeeds { get; set; }
}
Then, you bind to this view model class instead of Person
. Inside your action, you can branch on known differences between the types and map the data from your view model to the correct class type:
Person person;
if (model.NumberOfGoodDeeds.HasValue)
{
person = new GoodPerson { NumberOfGoodDeeds = model.NumberOfGoodDeeds.Value };
}
else if (model.NumberOfBadDeeds.HasValue)
{
person = new BadPerson { NumberOfBadDeeds = model.NumberOfBadDeeds.Value };
}
else
{
person = new Person();
}
person.Name = model.Name;
person.Age = model.Age;
Using a mapping library like AutoMapper can help minimize some of that code, but you get the basic idea. Now, you're essentially at the point you would have been directly calling a method with an instance in memory. As a result, if you need to do something with a specific type, just use something like pattern matching:
switch (person)
{
case GoodPerson goodPerson:
// do something with `goodPerson`
break;
case BadPerson badPerson:
// do something with `badPerson`
break;
}
Upvotes: 1