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

7

我正在返回一个对象数组,以便渲染基于相册的幻灯片。

我从数据库获取图片。

在将此数组作为结果返回之前,我想添加一个“缩略图”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分钟,只找到了依赖于第三方库的低劣解决方案。我对当前的“最佳实践”感兴趣。
可能是重复的:如何从控制器动态地将更多属性添加到Json响应中。但是,该答案会使动态添加的字段成为AlbumPicture属性在生成的JSON中的叔叔而不是兄弟。

4
返回一个包含对象和附加属性的匿名对象集合。 - user3559349
是的!那正是我想做的。有没有一种好的方法可以从一个非匿名对象创建一个匿名对象? - John Shedletsky
4
这真的是个问题吗?(你仍然可以在ajax方法中访问它),但你总是可以使用var data = pics.Select(p => new { ID = p.ID, Name = p.Name, AnotherProperty = p.AnotherProperty, ....., Thumbnail = ThumbManager.GetThumb(p.ID) }); - user3559349
返回嵌套的 JSON 对象让表示层知道并处理它的嵌套方式似乎很粗糙。应该有一种更 JavaScript 风格的响应对象,你可以在 C# 中像哈希表一样添加字段,然后在服务器上将其序列化为 JSON。 - John Shedletsky
@JohnShedletsky,你的问题和每个评论都由主观术语组成:例如“好看”,“不太好的解决方案”,“优雅”,“看起来很糟糕”...如果有一个候选问题被标记为主要是基于观点的,那么你的问题就是其中之一。而且没有人认为node.js很酷(或者这听起来像是基于观点的?!) - Brett Caswell
显示剩余4条评论
9个回答

8
目前,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);

首先,感谢您的回答。如果这是我想要实现的最优雅的代码,微软确实需要在语言设计方面进行改进。我开始理解为什么人们认为node.js很酷了。 - John Shedletsky
我认为将对象转换为动态对象是解决您的问题的最佳方法。如果这个解决方案对您有用,请将我的答案标记为正确答案。 - Huy Hoang Pham
3
@JohnShedletsky,你谈论的是基本上设计不同的语言。动态类型与强类型语言之争可以持续数日。 - Oliver
@Oliver,很容易想象扩展var关键字的方式,使其像JavaScript中的变量一样运行。仅因为C#可以强类型化并不意味着涉及JSON的代码必须看起来很糟糕。 - John Shedletsky

5

我同意上面的大部分回答,使用动态对象或扩展对象可以添加其他属性等。然而,这项任务很简单,在我的看法中,您不应该将实体对象公开给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);

如果您正在使用MVVM、MVC、MVP等方法论,那么像qamar建议的那样使用viewmodel来返回添加的属性,并在需要时使用automapper将属性映射到它是正确的方式。 - Andrej Kikelj
这是我在代码中所做的,但感觉写了太多的管道连接。 - John Shedletsky
即使这涉及一些管道工作,创建额外的类型等,你的控制器、视图或消费者也不会意识到你的领域对象。如果你认为这是答案,请将其标记为答案。 - qamar
这就是我最终做的事情。从语言设计的角度来看,我认为微软应该远离这种针对Web的学究式代码风格。但这只是我的个人观点。 - John Shedletsky

2
你可以使用Json.NET将它们转换为其对象,并动态地添加在创建的JObject上的属性。
从那里,你可以将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");

2
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,或者愿意安装它,这是另一种可以用来实现相同结果的解决方法。

首先,您需要创建一个Album的扩展类,并添加属性。

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

然后,在选择语句中创建扩展类的新实例,使用自动映射(如果属性不多)或手动复制属性值到新类,并设置缩略图属性。

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


与使用node.js的方式相比,这种方法丑陋不堪。 - John Shedletsky
Andrej 展示的第一种方法不就是你在问题中举的例子吗?虽然有一个辅助函数,但只在一个地方使用。然后在选择中使用的代码只是 p.AppendProperty,无需使用 foreach。 - eol

2
使用投影和匿名对象将动态属性与域属性分离。
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();

0
你可以使用匿名类型来创建一个具有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"}]

希望对您有所帮助。


0
我建议根据你的AlbumPicture对象创建一个数据传输对象(DTO)。
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 对象添加任意数量的属性,并使用此方法将这些属性返回给客户端。


0

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

在我看来,实现您想要的结果的最佳方法是为客户端创建新类。通常直接使用域模型与客户端通信是一个坏主意。如果您想要尽可能少的代码重复,最好的方法是简单地创建继承您的域模型的ViewModels。但是,正如所说,您可能不想将模型的所有属性都暴露给客户端。

如果打算完全公开模型,则可以像@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;

0

虽然这不是直接回答您的问题,但我认为您正在使用类似于实体框架的东西,以“数据库优先”方式处理数据库连接。现在您不想在数据库中添加“缩略图”属性,但也不想更改动态创建的模型。

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

我会在数据库项目中添加一个名为ExtendedModels.cs的类,并添加以下内容:

/// <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注释来确保属性不会在数据库中创建。有关不同版本的详细信息,请参阅忽略Entity Framework中的类属性。因此,在您的情况下,可以像以下示例一样进行更改get/set以满足您的需求。

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

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

}

我正在使用Code First的Entity Framework。如果我创建一个没有setter的模型属性,它是否仍会在底层数据库中创建一列? - John Shedletsky
如果我的模型在许多不同的上下文中使用,每个上下文都有自己的一组附加属性,那么似乎我会向我的模型添加很多未映射的属性?而且响应将变得臃肿,其中包含了与上下文无关的信息。 - John Shedletsky
如果您不想发送带有不必要信息的响应,则应像@qamar所说的那样使用视图模型。在我看来,将信息存储在其所属对象中只是常识。 - Hugo Delsing

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接