在 MVC 5 中构建动态 JSON 响应 - 如何将属性附加到结果集?

Building a dynamic JSON response in MVC 5 - How to tack on properties to a result set?

我正在 return 将一组对象添加到一个页面,该页面根据相册呈现幻灯片。

我从数据库中获取我的照片。

在我return这个数组之前,我想加上一个"Thumbnail"url属性。此 属性 在 AlbumPicture 上不存在,但我希望它出现在响应中。

这说明了这个想法:

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);

将此 JSON 字段添加到我的结果集中的最优雅方法是什么?

这个问题太基础了,可能是重复的。然而,我用谷歌搜索了 10 分钟,只能找到依赖于 3rd 方库的 janky 解决方案。我对当前的 "best practice".

感兴趣

可能重复:How to add dynamically more properties to Json response from the controller。但是,该答案将使动态添加的字段成为我的 AlbumPicture 属性的叔叔而不是兄弟姐妹 JSON.

目前,C# 4.0 支持动态对象,因此您可以使用动态对象来添加您的属性作为 运行 时间。

首先,将此扩展方法添加到您的代码中:

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;
    }
}

然后,将您的代码更改为如下内容

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);

conanak99 提供的答案在我看来是正确的做法。无论如何,像这样的扩展通常隐藏在您的核心实用程序中,因此他的答案具有可重用性。如果你确定你只会将它用于这个目的,你可以将它重构为

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;
    }

这将依次产生以下代码来执行您想要的操作:

 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();

更新----------------------------

如果您正在使用 automapper,或者愿意安装它,这是另一个可以用来达到相同结果的解决方法。

首先,您创建相册的扩展 class,添加 属性

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

然后在 select 语句中创建扩展 class 的新实例,使用自动映射器复制 属性 值(如果没有太多,则手动复制) 进入新的 class,并设置缩略图 属性.

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 基本上只是使用默认约定将 属性 值从一个 class 复制到另一个 属性,如果需要可以覆盖这些默认约定。

我同意上面的大部分答案,使用动态对象或 expando 对象你可以添加额外的 属性 等。然而任务很简单,在我看来你永远不应该将你的实体对象暴露给 UI 或消费者。我会创建一个简单的 ViewModel 并将其传回给调用者,如下所示:

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);

不是您问题的直接答案,但在我看来,您正在使用类似 Entity Framework 的方法通过 "database first" 方法处理数据库连接。现在您不想在数据库中添加 "Thumbnail" 属性,也不想更改动态创建的模型。

现在我认为最好的方法是将 属性 添加到模型中,而不仅仅是添加到 JSON 中。在我看来,这将使它更易于使用,并且还可以在需要时直接在视图中使用缩略图。

我会在名为 ExtendedModels.cs 的数据库项目中添加一个 class 并添加如下内容

/// <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);
        }
    }

}

显然它也会被添加到 JSON 输出中。

更新

如果您先使用代码,则可以使用 NotMapped 注释来确保未在数据库中创建 属性。有关不同版本的更多详细信息,请参阅 Ignoring a class property in Entity Framework。因此,在您的情况下,它将类似于以下内容。您可以将 get/set 更改为任何您想要的。

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

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

}

使用投影和匿名对象将动态属性与域属性分开。

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();

您可以使用匿名类型创建具有 AlbumPicture 和 Thumbnail 属性的新类型。

这是修改后的代码。

        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);

你会得到 json 这样的结果

[{"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"}]

希望对你有所帮助

您可以使用 Json.NET 将它们转换为其对象,并在创建的 JObject 上动态添加属性。

从那里您可以return json 字符串作为操作的内容。

        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");

我建议根据您的 AlbumPicture 对象创建一个数据传输对象。

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)
};
}

//控制器代码

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

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

您可以向 Dto 对象添加任意数量的属性,并使用此方法将这些属性 return 提供给客户端。

并不是说 .NET 在 JSON 方面不好,只是它是一种强类型语言,这意味着在处理动态时你不能将它与 javascript 这样的动态语言进行比较类型操作。

在我看来,获得所需结果的最佳方法是为客户端创建新的 类。直接使用域模型与客户端通信通常不是一个好主意。如果您希望尽可能减少代码重复,您充其量可以简单地创建继承域模型的 ViewModel。但正如所说,您可能不想将模型的所有属性公开给您的客户。

如果打算进行完整的模型展示,那么您可以像@conanak99 在他的出色回答中指出的那样做。使用 LINQ 形式的代码可能会更优雅:

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;