6
\$\begingroup\$

My question, REST API and lazy loading objects, didn't receive any answers (or many views), so here is my attempt at the solution.

Problem overview:
My mobile app requests a list of objects through a HTTP REST API but only required a small subset of each item to display items in a list. Other larger properties should not be returned unless requested i.e. in an an object detailed view.

My attempt:
API will act as normal unless a querystring is appended with the fields that are required.

I know that I need to fix the requiredFields.Contains(property.Name) call to ignore case during comparison.

My other concern is that this code is redundant due to my use of Automapper that may include functionality similar to this.

Also, could it be more efficient?

public static Object FilterFields(Object target, Uri requestUri)
    {
        NameValueCollection mapQuery = UriExtensions.ParseQueryString(requestUri);

        IEnumerable<string> requiredFields = null;

        if (mapQuery != null && mapQuery.Get("fields") != null)
            requiredFields = mapQuery.Get("fields").Split(new char[] { ',' });

        if (requiredFields != null)
        {
            foreach (PropertyInfo property in target.GetType().GetProperties())
            {
                if (!requiredFields.Contains(property.Name))
                {
                    property.SetValue(target, null);
                }
            }
        }

        return target;
    }
\$\endgroup\$
  • 1
    \$\begingroup\$ Maybe I'm paranoid, but this solution seems insecure. I am always leery about reflecting direct from user input. Someone with domain knowledge or someone who is of nefarious intent could cause properties which they may or may not be allowed to access to be filled with data. While flexible, this is potentially problematic. Can you give an example of some of the URLs / QueryString parameters? \$\endgroup\$ – xDaevax Nov 12 '14 at 14:54
  • \$\begingroup\$ How does this work with nested objects? \$\endgroup\$ – David Kassa Aug 14 '17 at 15:45
3
\$\begingroup\$

Consider this a thoughtreview instead of a codereview.

A lot of this will depend on your scenario; both of them are viable options. How extensive are your Book objects? Do you have a few but with many fields or do you have many but there are only a few fields? Have you considered pagination?

There are a few approaches here.

1. Thin overview, fat detail

You can have your api setup like this:

class Book
{
    public string Title { get; set; }
    public string Thumbnail { get; set; }
    public DateTime ReleaseDate { get; set; }
    public Author Author { get; set; }
}

class BookOverviewViewmodel
{
    public string Title { get; set; }
    public string Thumbnail { get; set; }
}

class BookViewmodel
{
    public string Title { get; set; }
    public string Thumbnail { get; set; }
    public DateTime ReleaseDate { get; set; }
    public Author Author { get; set; }
}

[HttpGet]
[Route("/api/books")]
[ResponseType(typeof(BookOverviewViewmodel))]
public IHttpActionResult GetBooks()
{
    var books = repo.GetBooks();
    return Ok(books.Select(Mapper.Map<Book, BookOverviewViewmodel>));
}

[HttpGet]
[Route("/api/books/{id:int}")]
[ResponseType(typeof(BookViewmodel))]
public IHttpActionResult GetBooks(int id)
{
    var book = repo.GetBook(id);
    return Ok(Mapper.Map<Book, BookViewmodel>(book));
}

Granted, this will duplicate some fields across types but it is a fairly flexible way of allowing you to define exactly what you want returned for each request. In this scenario the Book type is what you use in the backend and *Viewmodel are responses for the specific situation they're responding to.

Personally I like this because it allows me to easily define what should be returned when but on the other hand it does introduce quite a bit of duplication. If you feel really compelled though you could look into generating types based off annotations but that might be going down overengineeringlane perhaps.

2. Select fields based on request

I see three options here:

2.1. Either you pass in a list of strings in the request which indicate which fields you are interested in and then you build a JSON response that only contains those fields.

2.2. You define a few configurations beforehand and allow the user to pick either of those which will return a response based on this configuration. This can be done with hardcoded types or through some sort of lookup service which uses reflection like your example in the question.

2.3. You don't pass in any query since it isn't really needed anyway if you're adhering to REST principles: if you want the overview of books you should query /api/books/ and when you want a specific book's information you query /api/books/{id}. This will already tell you what kind of information you should return.

3. Work with pagination

This is also a common approach: simply return x-amount of objects and indicate in the response what "page" you're on. When you allow your API endpoint to take in this page as a parameter, you can gradually return data which solves your underlying problem: too many objects for one response.

Do note that you should define some sort of order somewhere. Either the user does this or you have a default ordering, there's a lot you can customize here.

A sample implementation could look like this:

class BookRequestParameters
{
    public int GenreId { get; set; }
    public int Page { get; set; }
    public int PageSize { get; set; }
}

class BookViewmodel
{
    public string Title { get; set; }
    public string Thumbnail { get; set; }
    public DateTime ReleaseDate { get; set; }
    public Author Author { get; set; }
}

class BookPaginationResponse
{
    public BookRequestParameters Parameters { get; set; }
    public List<BookViewmodel> Books { get; set; }
}

[HttpGet]
[Route("/api/books")]
[ResponseType(typeof(BookPaginationResponse))]
public IHttpActionResult GetBooks([FromUri] BookRequestParameters parameters)
{
    var books = repo.GetBooks()
                    .Where(x => x.Genre.Id == parameters.GenreId)
                    .Skip(parameters.PageSize * (parameters.Page - 1))
                    .Take(parameters.PageSize);

    var response = new BookPaginationResponse 
                   { 
                     Parameters = parameters, 
                     Books = books.Select(Mapper.Map<Book, BookOverviewViewmodel>) 
                   };
    return Ok(response);
}
\$\endgroup\$
5
\$\begingroup\$

Style

Be consistent in your style. If you use braces {} for single if statements then you should use them always.

Using guard conditions help you to save horizontal space.

NameValueCollection mapQuery = UriExtensions.ParseQueryString(requestUri);
if (mapQuery == null) { return target; }

string fields = mapQuery.Get("fields");
if (String.IsNullOrEmpty(fields)) { return target; }

IEnumerable<string> requiredFields = fields.Split(new char[] { ',' });
if (requiredFields == null) { return target; }  

your loop here  

otherwise your code looks good.

\$\endgroup\$
4
\$\begingroup\$

I know this is an old post but I tried doing something similar recently so thought I should share it here. The code looks great but it still returns unwanted properties with 'Null' values. What I have tried to do is serialize only those fields that are needed.

 public class ShouldSerializeContractResolver : DefaultContractResolver
{

    protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, Newtonsoft.Json.MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (property.DeclaringType == typeof(BaseEntity) || property.DeclaringType.BaseType == typeof(BaseEntity))
        {
            if (property.PropertyName == "serializableProperties")
            {
                property.ShouldSerialize = instance => { return false; };
            }
            else
            {
                property.ShouldSerialize = instance =>
                {
                    var p = (Product)instance;
                    return p.serializableProperties.Contains(property.PropertyName);
                };
            }
        }
        return property;
    }
}

// GET api/products/5
    public JsonResult<Product> Get(int id, string fields="")
    {
        var product = _productsRepository.Find(x => x.Id == id);
        product.SetSerializableProperties(fields);
        return Json(product, new Newtonsoft.Json.JsonSerializerSettings()
        {
            ContractResolver = new ShouldSerializeContractResolver()
        });
    }

If you want detailed explanation check my Blog

\$\endgroup\$
  • 2
    \$\begingroup\$ Please include the relevant code and explanation in the answer. \$\endgroup\$ – ZeroOne May 29 '15 at 6:43
  • \$\begingroup\$ Added code in the answer. \$\endgroup\$ – shwetaOnStack May 29 '15 at 8:44
  • 1
    \$\begingroup\$ I see some concerns from your code. Your implementation has to use JSON response, however WebApi has content negotiation feature to let client decide the response should be JSON or XML. \$\endgroup\$ – hardywang Feb 23 '17 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.