Dawood Awan Dawood Awan - 3 months ago 36
AngularJS Question

Angular JS MVC Web API Model/ Parameter not binding .NET Core

I am using Angular JS with TypeScript and ASP.NET Core MVC/API.

I have an

apiService
which deals with all
POST
and
GET
requests to the server, which looks like this:

module TBApp {

export class apiService {

static $inject = ['$http', 'notificationService'];

constructor(private $http, private notificationService: notificationService) {

}

get(url, config, success, failure) {

return this.$http.get(url, config)

.then(result => { this.handleResponse(result, success); }, result => { this.handleError(result, failure) });
}

post(url, data, success, failure) {

return this.$http.post(url,data)
.then(result => { this.handleResponse(result, success); }, result => { this.handleError(result, failure) });
}

handleResponse(result, success) {

alert('success');
success(result);

}

handleError(result, failure) {

if (result.status === '401') {

this.notificationService.displayError('Authentication required.');
//this.$rootScope.previousState = this.$location.path();
//this.$location.path('/login');

}
else if (failure !== null) {
failure(result);
}
}
}
}


Now when I send this request:

onCompanyChanged(selectedCompany, model, companyName) {

this.apiService.post('/api/Dashboard/GetAssetListByCompany', { companyId: selectedCompany.id },

response => {

this.assetListViewModel = response.data.data;


}, response => {
this.notificationService.displayError(response.data.message);
});
}


It is not binding the
companyId
in the controller

Here is the controller:

[Route("api/[controller]")]
public class DashboardController : BaseController
{
[HttpPost]
[Route("GetAssetListByCompany")]
public IActionResult GetAssetListByCompany([FromBody]int companyId)
{
return CreateJsonResult(() =>
{
if (companyId == 0) { return new xPTJsonResult(null, xPTStatusCodesEnum.Success, "Company Id is 0"); }

//var treeModel = _dashboardProvider.GetTreeModelByCompany(companyId, userModel);

return new xPTJsonResult(null, xPTStatusCodesEnum.Success, "Loaded assets successfully");

});
}

}


even though when I check the Request in the Browser, in shows that the companyId is in the Payload.

enter image description here


NOTE: The same function works when I post a ViewModel


EDIT

In the above scenario I am only passing one Parameter to the controller, but in some cases I want to be able to pass 2 or 3 parameters without using a ViewModel.

e.g.

public IActionResult GetAssetListByCompany([FromBody]int companyId, [FromBody]int assetId)
{....


OR

public IActionResult GetAssetListByCompany([FromBody]int companyId, [FromBody]int assetId, [FromBody]bool canEdit = false)
{.....


and then on the client side I can do this:

this.apiService.post('/api/Dashboard/GetAssetListByCompany', { companyId: selectedCompany.id, assetId: 123 }.....


OR

this.apiService.post('/api/Dashboard/GetAssetListByCompany', { companyId: selectedCompany.id, canEdit: true, assetId: 22 }....

Answer

The best approach here is to follow the HTTP guidelines and change your action from POST to GET as you are not modifying any data. This is fairly simple to do and still be able to send data with your request using the URI.

MVC changes

See Model Binding for the various options, the best approach here is to bind based on the query string as you want only a single primitive type. If you had an array of primitive types you could still bind to the query string, the query string variable name would be repeated once for each value.

So the only changes we make are to specify that the parameter is coming from the Query string and that it is associated with an Http Get request instead of Post.

[Route("api/[controller]")]
public class DashboardController : BaseController
{
    [HttpGet] // change to HttpGet
    [Route("GetAssetListByCompany")]
    public IActionResult GetAssetListByCompany([FromQuery]int companyId) // use FromQuery
    {
        return CreateJsonResult(() =>
        {
            if (companyId == 0) { return new xPTJsonResult(null, xPTStatusCodesEnum.Success, "Company Id is 0"); }

           //var treeModel = _dashboardProvider.GetTreeModelByCompany(companyId, userModel);

            return new xPTJsonResult(null, xPTStatusCodesEnum.Success, "Loaded assets successfully");

        });
    }
}

AngularJS changes

We extend the apiService to allow passing for data for calls using HttpGet. This can be done using the params on the $http call, it will dynamically create the URL based on the passed in data using the name as the query string value name and the value as the value part.

export class apiService {
    /* all other code is left as is, just change the get method to also accept data via the params. If null is passed in then it is ignored. */
    get(url, config, data, success, failure) {
        return this.$http({
            url: url,
            config: config,
            params: data,
            method: "GET"
            })
            .then(result => { this.handleResponse(result, success); }, result => { this.handleError(result, failure) });
    }
}

On the call we just need to change from post to get and it should work.

// only change from post to get
onCompanyChanged(selectedCompany, model, companyName) {
    this.apiService.get('/api/Dashboard/GetAssetListByCompany', { companyId: selectedCompany.id },
        response => {
            this.assetListViewModel = response.data.data;
        }, response => {
        this.notificationService.displayError(response.data.message);
    });
}

Edit - This is flexible

One more important note, this design is flexible on the angular side. If you extend your MVC Action or have various actions that take additional parameters it works without having to implement any other changes. Example:

[HttpGet]
[Route("GetSomethingElseFromServer")]
public IActionResult GetSomethingElseFromServer([FromQuery]int companyId, [FromQuery]string assetName, [FromQuery]string companyModelNumber) // use FromQuery

the call to your angular api would be

this.apiService.get('/api/Dashboard/GetSomethingElseFromServer', { companyId: companyId, assetName: somePassedInAssetNameVar, companyModelNumber: somePassedInModelNumber }

Edit - You can also send arrays

To answer the question on how to send multiple primitive types as an array you can do that this way. Again this assumes that its not a complex type that you are sending but, for example, a list of company ids.

c# code

[HttpGet]
[Route("GetAssetListByCompany")]
public IActionResult GetAssetListByCompany([FromQuery]int[] companyIds) // use an array of int ie. int[]. i changed the variable name to make it clear there can be more than 1

Angular call, note there is no need to change the service

onCompanyChanged(selectedCompany, model, companyName) {
    this.apiService.get('/api/Dashboard/GetAssetListByCompany', { "companyIds[]": [id1, id2, id3] }, // note the name is now enclosed in quotes, made plural, and includes []. The value is an array
        response => {
            this.assetListViewModel = response.data.data;
        }, response => {
        this.notificationService.displayError(response.data.message);
    });
}

Edit - If you want to POST anyways

You are curretly only sending a single primitive field so this will not be correctly deserialized by the MVC framework in a POST. You need to either wrap the parameter in a view model, send it as a query string part, or send it as a form field value. Here is the POST with a query string part which works just fine.

Option 1

Append it to the URL

[HttpPost] // change to HttpGet
[Route("GetAssetListByCompany")]
public IActionResult GetAssetListByCompany([FromQuery] int companyId) // use FromQuery

Angular call

this.apiService.post('/api/Dashboard/GetAssetListByCompany/?companyId=' + selectedCompany.id + , null, // the rest of the code remains unchanged so I did not include it

Option 2

Extend the apiService to also take the params object so it can build up your query. Either way you are stuck with the caller having to know a little bit about the http call being made.

this.apiService.post('/api/Dashboard/GetAssetListByCompany', null, {companyId: selectedCompany.id}, null, // the rest of the code remains unchanged so I did not include it

post(url, config, data, params, success, failure) {
    return this.$http({
        url: url,
        config: config,
        data: data,
        params: params,
        method: "POST"
        })
        .then(result => { this.handleResponse(result, success); }, result => { this.handleError(result, failure) });
}

Option 3

Update your view model to take a complex type, this requires no changes to your angular code.

public class ListByCompanyModel {
    public int CompanyId {get;set;}
}

[HttpPost] // change to HttpGet
[Route("GetAssetListByCompany")]
public IActionResult GetAssetListByCompany([FromBody] ListByCompanyModel model) // use FromQuery