IEnumerable<T>
时,如果存在循环引用,则会从JavascriptSerializer
生成异常。这是因为MVC Grid使用JsonResult
,后者又使用JavaScriptSerializer
,不支持序列化循环引用。我的解决方法是使用LINQ创建没有相关实体的视图对象。这适用于所有情况,但需要创建新对象,并将数据从实体对象复制到/从这些视图对象。虽然不需要做太多的工作,但还是需要做一些工作。
我最终找到了如何通用地使网格不序列化循环引用(忽略它们),我想分享我的解决方案给大众,因为我认为它是通用的,并且可以很好地插入到环境中。 解决方案由几个部分组成:
- 用自定义序列化器替换默认网格序列化器
- 安装Newtonsoft提供的Json.Net插件(这是一个很棒的库)
- 使用Json.Net实现网格序列化器
- 修改Model.tt文件,在导航属性前面插入[JsonIgnore]属性
- 覆盖Json.Net的
DefaultContractResolver
,查找_entityWrapper
属性名称以确保它也被忽略(由POCO类或实体框架注入的包装器)
正确实现后,我现在可以轻松地将任何实体框架对象直接发送到客户端,而无需创建新的视图对象。我不建议为每个对象使用此方法,但有时它是最好的选择。还要注意,任何相关实体在客户端上不可用,因此请勿使用它们。 以下是所需步骤:
Create the following class in your application somewhere. This class is a factory object that the grid uses to obtain JSON results. This will be added to the telerik library in the global.asax file shortly.
public class CustomGridActionResultFactory : IGridActionResultFactory { public System.Web.Mvc.ActionResult Create(object model) { //return a custom JSON result which will use the Json.Net library return new CustomJsonResult { Data = model }; } }
Implement the Custom
ActionResult
. This code is boilerplate for the most part. The only interesting part is at the bottom where it callsJsonConvert.SerilaizeObject
passing in aContractResolver
. TheContactResolver
looks for properties called_entityWrapper
by name and sets them to be ignored. I am not exactly sure who injects this property, but it is part of the entity wrapper objects and it has circular references.public class CustomJsonResult : ActionResult { const string JsonRequest_GetNotAllowed = "This request has been blocked because sensitive information could be disclosed to third party web sites when this is used in a GET request. To allow GET requests, set JsonRequestBehavior to AllowGet."; public string ContentType { get; set; } public System.Text.Encoding ContentEncoding { get; set; } public object Data { get; set; } public JsonRequestBehavior JsonRequestBehavior { get; set; } public int MaxJsonLength { get; set; } public CustomJsonResult() { JsonRequestBehavior = JsonRequestBehavior.DenyGet; MaxJsonLength = int.MaxValue; // by default limit is set to int.maxValue } public override void ExecuteResult(ControllerContext context) { if (context == null) { throw new ArgumentNullException("context"); } if ((JsonRequestBehavior == JsonRequestBehavior.DenyGet) && string.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException(JsonRequest_GetNotAllowed); } var response = context.HttpContext.Response; if (!string.IsNullOrEmpty(ContentType)) { response.ContentType = ContentType; } else { response.ContentType = "application/json"; } if (ContentEncoding != null) { response.ContentEncoding = ContentEncoding; } if (Data != null) { response.Write(JsonConvert.SerializeObject(Data, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, ContractResolver = new PropertyNameIgnoreContractResolver() })); } } }
Add the factory object to the telerik grid. I do this in the global.asax
Application_Start()
method, but realistically it can be done anywhere that makes sense.DI.Current.Register<IGridActionResultFactory>(() => new CustomGridActionResultFactory());
Create the
DefaultContractResolver
class that checks for_entityWrapper
and ignores that attribute. The resolver is passed into theSerializeObject()
call in step 2.public class PropertyNameIgnoreContractResolver : DefaultContractResolver { protected override JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) { var property = base.CreateProperty(member, memberSerialization); if (member.Name == "_entityWrapper") property.Ignored = true; return property; } }
Modify the Model1.tt file to inject attributes that ignore the related entity properties of the POCO Objects. The attribute that must be injected is [JsonIgnore]. This is the hardest part to add to this post but not hard to do in the Model1.tt (or whatever filename it is in your project). Also if you are using code first then you can manually place the [JsonIgnore] attributes in front of any attribute that creates a circular reference.
Search for the
region.Begin("Navigation Properties")
in the .tt file. This is where all of the navigation properties are code generated. There are two cases that have to be taken care of the many to XXX and the Singular reference. There is an if statement that checks if the property isRelationshipMultiplicity.Many
Just after that code block you need to insert the [JSonIgnore] attribute prior to the line
<#=PropertyVirtualModifier(Accessibility.ForReadOnlyProperty(navProperty))#> ICollection<<#=code.Escape(navProperty.ToEndMember.GetEntityType())#>> <#=code.Escape(navProperty)#>
Which injects the property name into the generated code file.
Now look for this line which handles the
Relationship.One
andRelationship.ZeroOrOne
relationships.<#=PropertyVirtualModifier(Accessibility.ForProperty(navProperty))#> <#=code.Escape(navProperty.ToEndMember.GetEntityType())#> <#=code.Escape(navProperty)#>
Add the [JsonIgnore] attribute just before this line.
Now the only thing left is to make sure the NewtonSoft.Json library is "Used" at the top of each generated file. Search for the call to
WriteHeader()
in the Model.tt file. This method takes a string array parameter that adds extra usings (extraUsings
). Instead of passing null, construct an array of strings and send in the "Newtonsoft.Json" string as the first element of the array. The call should now look like:WriteHeader(fileManager, new [] {"Newtonsoft.Json"});
做的事情就是这么简单,一切都能为每个对象开始工作。
免责声明
- 我从未使用过Json.Net,所以我的实现可能不是最优的。
- 我已经测试了大约两天,没有发现这种技术失败的情况。
- 我也没有发现
JavascriptSerializer
和JSon.Net序列化器之间存在任何不兼容性,但这并不意味着没有。 - 唯一的注意事项是我通过名称测试一个名为“
_entityWrapper
”的属性,将其忽略的属性设置为true。这显然不是最优的。
我希望对如何改进这个解决方案提出任何反馈意见。希望能够帮助到其他人。