Working example of AngularJS $resource searching items in Web Api
Asked Answered
O

1

6

I'm learning how to use AngularJS's $resource to call a Web Api backend. I want to pass an object hierarchy in as criteria and get back an IEnumerable<Program>. Here's an example of the criteria:

$scope.criteria = {
    Categories:[
        {
            Name: "Cat1",
            Options: [
                {Text: "Opt1", Value: true},
                {Text: "Opt2", Value: false}
            ]
        },
        {
            Name: "Cat2",
            Options: [
                {Text: "Opt3", Value: true},
                {Text: "Opt4", Value: false}
            ]
        }
    ]
}

I have the same objects defined on the server in C#.

public class CriteriaModel
{
   public IEnumerable<CriteriaCategory> Categories { get; set; }
}

public class CriteriaCategory
{
    public string Name { get; set; }
    public IEnumerable<CriteriaOption> Options { get; set; }
}

public class CriteriaOption
{
    public string Text { get; set; }
    public bool Value { get; set; }
}

Here's how I am configuring $resource:

angular.module('my.services')
    .factory('api', [
        '$resource',
        function ($resource) {
            return {
                Profile: $resource('/api/profile/:id', { id: '@id' }),
                Settings: $resource('/api/settings/:id', { id: '@id' }),
                Program: $resource('/api/program/:id', { id: '@id' })
            };
        }
    ]);

And I call it like this:

api.Program.query({ criteria: $scope.criteria }, function (response) {
    $scope.programs = response;
});

No matter what I try I either get null as the criteria parameter or the action doesn't execute at all. I don't know if the problem is in angular, web api, or both. Here is the action:

public class ProgramController : ApiController
{
    public IEnumerable<Program> GetByCriteria([FromUri]CriteriaModel criteria)
    {
        // Either criteria is null or this action doesn't even get
        // executed depending on what I try.
    }
}

Can someone help me get a working example going for searching and returning items using AngularJS $resource and Web Api?

Organ answered 14/4, 2014 at 15:23 Comment(0)
R
9

You're going to need a custom model binder. From what I understand FromUri will not handle complex nested types or json which $resource will put in the query string.

Model binder:

public class CriteriaModelBinder : IModelBinder
{
    public bool BindModel(
        HttpActionContext actionContext,
        ModelBindingContext bindingContext
    )
    {
        if (bindingContext.ModelType != typeof (CriteriaModel))
        {
            return false;
        }

        var value = bindingContext.ValueProvider.GetValue("Categories");

        if (value == null)
        {
            return false;
        }

        var categoryJson = value.RawValue as IEnumerable<string>;

        if (categoryJson == null)
        {
            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Categories cannot be null.");
            return false;
        }
        
        var categories = categoryJson
            .Select(JsonConvert.DeserializeObject<CriteriaCategory>)
            .ToList();

        bindingContext.Model = new CriteriaModel {Categories = categories};
        return true;
    }
}

Controller:

[RoutePrefix("api/program")]
public class ProgramController : ApiController
{
    [Route("getbycriteria")]
    [HttpGet]
    public HttpResponseMessage GetByCriteria(
        [ModelBinder(typeof(CriteriaModelBinder))]CriteriaModel criteria
    )
    {
        return new HttpResponseMessage(HttpStatusCode.OK);
    }
}

Angular Controller:

angular.module('myApp').
    controller('HomeController', function($scope, $resource) {
        var Program = $resource('/api/program/:id', {}, {
            getByCriteria: {
                url: '/api/program/getbycriteria',
                method: 'GET',
                isArray: true
            }
        });

        var program = new Program();
        var criteria = {
            Categories: [
                {
                    Name: "Cat1",
                    Options: [
                        { Text: "Opt1", Value: true },
                        { Text: "Opt2", Value: false }
                    ]
                },
                {
                    Name: "Cat2",
                    Options: [
                        { Text: "Opt3", Value: true },
                        { Text: "Opt4", Value: false }
                    ]
                }
            ]
        };

        $scope.submit = function () {
            console.log(program);
            program.$getByCriteria(criteria);
        };
    });

Edit:

Here is for POST:

Controller:

[RoutePrefix("api/program")]
public class ProgramController : ApiController
{
    [Route("getbycriteria")]
    [HttpPost]
    public HttpResponseMessage GetByCriteria(CriteriaModel criteria)
    {
        return new HttpResponseMessage(HttpStatusCode.OK);
    }
}

Angular:

angular.module('myApp').
    controller('HomeController', function($scope, $resource) {
        var Program = $resource('/api/program/:id', {}, {
            getByCriteria: {
                url: '/api/program/getbycriteria',
                method: 'POST',
                isArray: true
            }
        });

        var program = new Program();
        program.Categories = [
                {
                    Name: "Cat1",
                    Options: [
                        { Text: "Opt1", Value: true },
                        { Text: "Opt2", Value: false }
                    ]
                },
                {
                    Name: "Cat2",
                    Options: [
                        { Text: "Opt3", Value: true },
                        { Text: "Opt4", Value: false }
                    ]
                }
            ];

        $scope.submit = function () {
            console.log(program);
            program.$getByCriteria();
        };
    });
Ramah answered 14/4, 2014 at 17:32 Comment(6)
So if I created a new action in $resource which does a POST and took [FromUri] off the parameter, would that take care of it without me having to create a custom model binder?Organ
The default model binder looks for JSON in the body of the request. $resource is RESTful and sends JSON in the query string. The mismatch is why you need to either use a model binder or use $http.Ramah
Wait, I'm confused. You can specify a new action on $resource with a method of 'POST' according to the documentation. Are you saying it ignores that and puts it in the querystring anyway?Organ
Yes you can change the action to POST except you will need to attach the Categories directly to the program object.Ramah
I would recommend against using POST unless you are intending to do an operation with side effects.Ramah
That makes a lot of sense now. I have a large number of criteria so I may not have a choice of whether I can use GET vs POST. Thanks for your help.Organ

© 2022 - 2024 — McMap. All rights reserved.