7

I am returning an array of objects to a page that renders a slideshow based on a photo album.

I fetch my pictures from the database.

Before I return this array as a result, I would like to tack on a "Thumbnail" url property. This property does not exist on the AlbumPicture, but I want it in the response.

This illustrates the idea:

List<AlbumPicture> pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID).OrderBy(p => p.RankOrder).ToList();
foreach(AlbumPicture p in pics)
{
    p.AddPropertyThatDoesntExist("Thumbnail", ThumbManager.GetThumb(p.ID));
}

return Json(pics, JsonRequestBehavior.AllowGet);

What is the most elegant way to add this JSON field to my result set?

This question is so basic that it is probably a duplicate. However, I googled for 10 minutes and could only find janky solutions that depend on 3rd party libraries. I'm interested in the current "best practice".

Possible duplicate of: How to add dynamically more properties to Json response from the controller. However, that answer will make the dynamically added fields uncles instead of siblings to my AlbumPicture properties in the resulting JSON.

Community
  • 1
  • 1
John Shedletsky
  • 7,110
  • 12
  • 38
  • 63
  • 4
    Return a collection of anonymous objects that contains the object and the additional property –  Jan 15 '15 at 01:03
  • Yes! That is exactly what I want to do. Is there a nice way to create an anonymous object from a nonymous object? – John Shedletsky Jan 15 '15 at 01:09
  • Something like `var data = pics.Select(p => new { image = p, thumbnail = ThumbManager.GetThumb(p.ID) }); return (data);` - you have not given enough info know exactly what you need to return) –  Jan 15 '15 at 01:13
  • AlbumPictures is a Model with fields. I want the thumbnail property to be a sibling to these fields - what you suggest will make it an uncle (sibling of the parent) in the emitted JSON. – John Shedletsky Jan 15 '15 at 01:19
  • 4
    Is that really a problem (you can still access it in the ajax method) but you can always use `var data = pics.Select(p => new { ID = p.ID, Name = p.Name, AnotherProperty = p.AnotherProperty, ....., Thumbnail = ThumbManager.GetThumb(p.ID) });` –  Jan 15 '15 at 01:28
  • It seems gross to return a nested json object that the presentation layer then has to know about and deal with how it is nested. There should be a more javascripty response object that you can add fields to willy-nilly in C# like its a hashtable and then serialize to json on the server. – John Shedletsky Jan 15 '15 at 01:36
  • What about using json.net objects and then returning the json string that is generated? http://james.newtonking.com/json – Brian Jan 19 '15 at 02:36
  • @JohnShedletsky, your question and every comment you've made are composed of subjective terms: i.e. 'nice', 'jankly solutions', 'elegant', 'look awful'.. if ever there was a candidate for flagging a question for being primarily opinated, yours would be it. And no one thinks node.js is cool (or did that come off as opiniated?!) – Brett Caswell Jan 21 '15 at 22:17
  • @Brian - best answer so far. – John Shedletsky Jan 22 '15 at 02:06

9 Answers9

8

Currently, C# 4.0 support dynamic object, so you can use dynamic object to add your properties as run time.

First, add this extension method to your code:

public static class DynamicExtensions
{
    public static dynamic ToDynamic(this object value)
    {
        IDictionary<string, object> expando = new ExpandoObject();

        foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(value.GetType()))
            expando.Add(property.Name, property.GetValue(value));

        return expando as ExpandoObject;
    }
}

And then, change your code to something like this

var pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID)
          .OrderBy(p => p.RankOrder)
          .Select(p=> 
                  { dynamic copy = p.ToDynamic();
                    copy.Thumbnail = ThumbManager.GetThumb(p.ID); //You can add more dynamic properties here
                    return copy;
                  })
          .ToList();

return Json(pics, JsonRequestBehavior.AllowGet);
Huy Hoang Pham
  • 4,107
  • 1
  • 16
  • 28
  • First, thanks for the answer. If this is the most elegant code to do what I want, Microsoft really needs to get on this, language design-wise. I'm beginning to understand why people think node.js is cool. – John Shedletsky Jan 15 '15 at 21:24
  • I think converting the object to dynamic object is the best way to solve your problem. If the solution works for you, please mark my answer as the right answer. – Huy Hoang Pham Jan 16 '15 at 01:35
  • 3
    @JohnShedletsky you are talking about fundamentally differently designed languages. The argument of dynamic vs strongly typed languages could rage on for days. – Oliver Jan 20 '15 at 08:53
  • @Oliver it's not hard to imagine an extension to the var keyword that could make it function like variables in javascript do. Just because C# can be strongly typed doesn't mean code that touches JSON has to look awful. – John Shedletsky Jan 21 '15 at 01:18
5

I agree with most of the answers above, using dynamic object or expando object you can add the additional property etc. However the task is simple and in my opinion you should never expose your entity object to UI or consumers. I would just create a simple ViewModel and pass it back to the caller like following :

Public Class Album{
   string PhotoUrl {get; set;}
   string ThumbnailUrl {get; set;}
}


List<AlbumPicture> pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID).OrderBy(p => p.RankOrder).ToList();
List<Album> albums = new List<Album>();
foreach(AlbumPicture p in pics)
{
    albums.add(new Album(){
      PhotoUrl = p.PhotoUrl,//your photo url
      ThumbnailUrl = p.ThumbnailUrl
    });
}
 return Json(albums, JsonRequestBehavior.AllowGet);
qamar
  • 1,437
  • 1
  • 9
  • 12
  • If you are using a methodology like MVVM, MVC, MVP, then using a viewmodel, as qamar suggested, to return the added property along with automapper to map the properties to it when needed is the way to go. – Andrej Kikelj Jan 19 '15 at 08:56
  • This is what I have done in my code, but it feels like I am writing too much plumbing. – John Shedletsky Jan 21 '15 at 00:17
  • Even it involves a bit of plumbing creating extra type etc. your controller therefore view or consumer are not aware of your domain objects. If you think that this is the answer please mark it as answer – qamar Jan 21 '15 at 02:37
  • This is what I eventually ended up doing. Language design-wise I think Microsoft should move away from this pedantic code style for the web. But that is just my opinion. – John Shedletsky Jan 26 '15 at 20:53
2

The answer conanak99 provided is in my opinion the correct way to do this. Extensions like these are usually hidden in your core utilities anyway, and thus the reusable nature of his answer. If you are sure you will only use it for this purpose, you can refactor it to

public static dynamic AppendProperty<T>(this object value, string propertyName, T newValue)
    {
        IDictionary<string, object> expando = new ExpandoObject();

        foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(value.GetType()))
            expando.Add(property.Name, property.GetValue(value));

        expando.Add(propertyName, newValue);

        return expando as ExpandoObject;
    }

which will in turn yield the following code to do what you want:

 var pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID)
                  .OrderBy(p => p.RankOrder)
                  .Select(p =>
                  {
                      return p.AppendProperty("Thumbnail", ThumbManager.GetThumb(p.ID));
                  })
                  .ToList();

Update ------------------------------

If you are using automapper, or are willing to install it, this is another workaround that can be used in order to achieve the same result.

First, you create an extension class of the Album, with the added property

class AlbumExt : Album
    {
        public string Thumbnail { get; set; }
    }

Then you create a new instance of the extended class in the select statement, copy the property values using automapper(or manually, if you don't have too many) into the new class, and set the thumbnail property.

var pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID)
                  .OrderBy(p => p.RankOrder)
                  .Select(p =>
                  {
                      AlbumExt tmp = new AlbumExt();
                      AutoMapper.Mapper.DynamicMap(p, tmp);
                      tmp.Thumbnail = "new prop value";
                      return tmp;
                  })
                  .ToList();

Automapper basically just copies property values from one class to another using default conventions which are can be overridden if needed.

Andrej Kikelj
  • 800
  • 7
  • 11
  • Compared to the node.js way of doing it, this is ugly as sin. – John Shedletsky Jan 19 '15 at 22:23
  • Isn't the first way Andrej shows pretty much exactly what you gave as an example in your question? There is a helper function yes, but that's just in one place. Then the code to use is just p.AppendProperty in the select and no need for a foreach. – eol Jan 21 '15 at 16:24
2

Separate the dynamic properties from the domain ones by using a projection and an anonymous object.

var pics = db.AlbumPictures
 .Where(p => p.AlbumID == album.ID)
 .OrderBy(p => p.RankOrder)
 .Select(p => new { album = p, Thumbnail = ThumbManager.GetThumb(p.ID))
 .ToList();
Travis J
  • 81,153
  • 41
  • 202
  • 273
2

You could use Json.NET to convert them to its objects and dynamically add the properties on the JObject that is created.

From there you can return the json string as content from the action.

        var pics = db.AlbumPictures.Where(x => x.ID == albumID);

        //Will be used to hold each picture in the sequence
        JArray dynamicPics = new JArray();
        foreach (var pic in pics)
        {
            //Convert your object to JObject
            JObject dynamicPic = JObject.FromObject(pic);

            //Create your dynamic properties here.
            dynamicPic["Thumbnail"] = ThumbManager.GetThumb(pic.ID);

            dynamicPics.Add(dynamicPic);
        }

        //Convert the dynamic pics to the json string.
        string json = dynamicPics.ToString();

        //Return the json as content, with the type specified.
        return this.Content(json, "application/json");
Brian
  • 1,164
  • 1
  • 9
  • 27
0

Not directly an answer to your question, but it seems to me that you are using something like the Entity Framework to handle your database connection with a "database first" approach. Now you don't want to add the "Thumbnail" property in the database, but also don't want to alter the dynamically created models.

Now I think the best approach would be to add the property to the model and not just to the JSON. In my opionion it will make it much easier to use and will also enable to possibility to use the thumbnail directly in a view if you ever need it.

I would add a class to the database project called ExtendedModels.cs and add something like the following

/// <summary>
/// This class is used to extend the AlbumPicture object from the database
/// </summary>
/// <remarks>
/// 2015-01-20: Created
/// </remarks>
public partial class AlbumPicture
{
    /// <summary>An URL to the thumbnail.</summary> 
    public string Thumbnail
    {
        get
        {
            //ImageName is the actual image from the AlbumPicture object in the database
            if (string.IsNullOrEmpty(ImageName))
            {
                return null;
            }
            return string.Format("/Thumbnail/{0}", ImageName);
        }
    }

}

Obviously it will also be added to the JSON output.

UPDATE

If you use code first you can use the NotMapped Annotation to make sure a property is not created in the database. See Ignoring a class property in Entity Framework for more details about different versions. So In your case it would be something like the following. You can change the get/set to what ever you want.

public class AlbumPicture
{
    public string ImageName { get; set; } 

    [NotMapped]
    public string Thumbnail{ get; set; } 

}
Community
  • 1
  • 1
Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72
  • I am using the Entity Framework with code first. If I create a model property with no setter, does it still create a column in the underlying database? – John Shedletsky Jan 22 '15 at 23:29
  • If my model is used in many contexts, each with its own set of additional properties, it seems like I would add a lot of unmapped properties to my models? And the responses would become bloated with information that is not contextually relevant. – John Shedletsky Jan 23 '15 at 19:45
  • If you dont wan't to send out responses with unnecessary information, you should use viewmodels als @qamar said. To have the information stored at the object it belongs to, is just common sence in my opnion. – Hugo Delsing Jan 24 '15 at 10:17
0

You can use anonymous type to create a new type with properties AlbumPicture and Thumbnail.

Here is the modified code.

        List<AlbumPicture> pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID).OrderBy(p => p.RankOrder).ToList();
        var lstPics = new List<object>();
        foreach (AlbumPicture p in pics)
        {
            lstPics.Add(new { AlbumPicture = p, Thumbnail = ThumbManager.GetThumb(p.ID) });
        }

        return Json(lstPics, JsonRequestBehavior.AllowGet);

And you will get json result something like this

[{"AlbumPicture":{"ID":1,"AlbumID":1,"RankOrder":1,,,,},"Thumbnail":"Image_1"},{"AlbumPicture":{"ID":2,"AlbumID":2,"RankOrder":2,,,,},"Thumbnail":"Image_2"},{"AlbumPicture":{"ID":3,"AlbumID":3,"RankOrder":3,,,,},"Thumbnail":"Image_3"}]

I hope it helps you.

sandip patil
  • 191
  • 4
0

I would recommend creating a Data Transfer Object based on your AlbumPicture object.

public class AlbumPictureDto
{
    public int id { get; set; }
    public string url{ get; set; }
    public string thumbnailUrl{ get; set; } //create new properties

    public new static readonly Func<AlbumPicture, AlbumPictureDto> AsDto = p => new AlbumPictureDto()
    {
        id = p.id,
        url= p.url,
        thumbnailUrl= ThumbManager.GetThumb(p.id)
};
}

//Controller code

List<AlbumPicture> pics = db.AlbumPictures.Where(p => p.AlbumID == album.ID).OrderBy(p => p.RankOrder).ToList();

return Json(pics.Select(AlbumPicturesDto.AsDto),JsonRequestBehavior.AllowGet);

You can add as many properties you like to the Dto object and use this method to return those properties to the client.

0

It's not that .NET is bad at JSON, it's just that it is a strongly typed language which means you can't compare it with a dynamic language like javascript when dealing with dynamic operations on types.

In my opinion, the best way to achieve the result you want is to create new classes for the client-side. It is in general a bad idea to use your domain models directly to communicate with the client. At best you could simply create ViewModels inheriting your domain models if you want the less code duplication possible. But like said, you probably don't want to expose all the properties of your model to your clients.

If full model exposition is intended, then you could do like @conanak99 pointed out in his excellent answer. The code could be much more elegant using it's LINQ form:

var pics =  from album in db.AlbumPictures
        where album.AlbumID == album.ID
        orderby album.RankOrder
        let dynamicAlbum = album.ToDynamic()
        dynamicAlbum.Thumbnail = ThumbManager.GetThumb(p.ID)
        select dynamicAlbum;
slvnperron
  • 1,323
  • 10
  • 13