提取Select表达式到变量致集合序列化失败的原因排查
问题:提取Select表达式后JSON序列化出现自引用循环错误
现象
将Patient集合通过.Select()转换为匿名类型集合时:
- 内联定义Select表达式:可以正常序列化为JSON
- 提取为变量或方法返回表达式:序列化失败,报错:
Self referencing loop detected for property 'ManifestModule' with type 'System.Reflection.RuntimeModule'
已尝试调整序列化设置(如ReferenceLoopHandling.Serialize、PreserveReferencesHandling.Objects),但无效。
相关代码示例
定义的表达式与方法
// 可正常序列化 readonly Expression<Func<Appointment, object>> appointmentSelect = data => new { id = data.id.ToString(), patientId = data.patientId, calendarId = data.calendarId, date = Convert.ToDateTime(data.date).ToString("yyyy-MM-dd HH:mm"), typeId = data.typeId, locationId = data.locationId, summary = data.summary, details = data.details, cancelled = data.cancelled, duration = data.duration, blocked = false }; // 序列化失败 readonly Expression<Func<Patient, object>> patientSelect2 = x => new { id = x.patient_id.ToString(), internalId = x.patient_id.ToString(), firstName = x.first_name, middleName = x.middle_name, lastName = x.last_name, emailAddress = x.email, homePhone = x.HomePhone, mobileNumber = x.OtherPhone, communicationConsent = GetCommConsent(x) }; // 序列化失败 private Expression<Func<Patient, object>> patientSelect(Patient p) { return x => new { id = p.patient_id.ToString(), internalId = p.patient_id.ToString(), firstName = p.first_name, middleName = p.middle_name, lastName = p.last_name, emailAddress = p.email, homePhone = p.HomePhone, mobileNumber = p.OtherPhone, communicationConsent = GetCommConsent(p) }; } private static object[] GetCommConsent(Patient p) { var list = new List<object>(); if (p.TextConsent) list.Add(new { method = "TEXT", consent = true }); if (p.VoiceConsent) list.Add(new { method = "VOICE", consent = true }); if (p.EmailConsent) list.Add(new { method = "EMAIL", consent = true }); return list.ToArray(); }
调用代码
public async Task SyncData(ProcessType processType) { var companies = await databaseHelper.GetCompanies(processType); foreach (var company in companies) { var manager = new ContentManager(company, appConfiguration); // 内联调用:可正常序列化 await ProcessResponses(company, await manager.PostEntityData("patients", appConfiguration.PatientUrl, company.Patients.Select(x => new { id = x.patient_id.ToString(), internalId = x.patient_id.ToString(), firstName = x.first_name, middleName = x.middle_name, lastName = x.last_name, emailAddress = x.email, homePhone = x.HomePhone, mobileNumber = x.OtherPhone, communicationConsent = ((Func<Array>)(() => { var list = new List<object>(); if (x.TextConsent) list.Add(new { method = "TEXT", consent = true }); if (x.VoiceConsent) list.Add(new { method = "VOICE", consent = true }); if (x.EmailConsent) list.Add(new { method = "EMAIL", consent = true }); return list.ToArray(); }))() })), databaseHelper.SavePatientLastUpdatedDate); // 调用方法返回表达式:序列化失败 await ProcessResponses(company, await manager.PostEntityData("patients", appConfiguration.PatientUrl, company.Patients.AsQueryable().Select(p => patientSelect(p))), databaseHelper.SavePatientLastUpdatedDate); // 使用提取的变量表达式:序列化失败 await ProcessResponses(company, await manager.PostEntityData("patients", appConfiguration.PatientUrl, company.Patients.AsQueryable().Select(patientSelect2)), databaseHelper.SavePatientLastUpdatedDate); // Appointment的提取表达式:可正常序列化(无嵌套方法调用) await ProcessResponses(company, await manager.PostEntityData("appointments", appConfiguration.AppointmentUrl, company.Appointments.AsQueryable().Select(appointmentSelect)), databaseHelper.SaveAppointmentLastUpdatedDate); } } private async Task ProcessResponses(Company company, HttpResponseMessage[] responses, Func<Company, Task<int>> func) { if (responses.Any(x => !x.IsSuccessStatusCode)) { foreach (var response in responses.Where(x => !x.IsSuccessStatusCode)) { var request = await response.RequestMessage.Content.ReadAsStringAsync(); } } await func(company); }
ContentManager核心序列化代码
public async Task<HttpResponseMessage[]> PostEntityData<T>(string collectionName, string url, IEnumerable<T> data) { var batches = SplitCollectionIntoSmallerCollections(data.ToList()); var content = new List<StringContent>(batches.Count); foreach (var batch in batches) { content.Add(await CreateContent(collectionName, batch)); } List<HttpResponseMessage> responses = new List<HttpResponseMessage>(batches.Count()); foreach (var item in content) { var result = await PostHttpContent(company, url, item); responses.Add(result); } return responses.ToArray(); } public async Task<StringContent> CreateContent(string collectionName, object data) { try { var auth = await BuildUpdoxAuth(databaseHelper, company.DatabaseName); var request = new DataRequest(auth, data); // 报错发生在此处 var content = new StringContent(JsonConvert.SerializeObject(request).Replace("\"data\":", $"\"{collectionName}\":"), Encoding.UTF8, "application/json"); return content; } catch (Exception ex) { throw; } }
原因分析
1. 表达式与委托的核心混淆
你犯了一个关键错误:把Expression<Func<Patient, object>>当成普通委托Func<Patient, object>来使用。
- 内联
Select调用中,IEnumerable.Select接收的是委托,直接执行代码生成匿名类型实例,最终传递给PostEntityData的是匿名类型集合。 - 而
IQueryable.Select接收的是表达式树对象,此时并没有执行映射逻辑生成匿名类型,而是把表达式树本身传递给了PostEntityData。Json.NET序列化表达式树时,会遍历其内部的反射属性(比如ManifestModule),这些属性存在自引用循环,触发报错。
2. Appointment表达式正常的原因
appointmentSelect能正常工作,是因为它的表达式中没有调用外部方法,LINQ提供者可以将其完整转换为查询逻辑,最终生成的是匿名类型集合,而非表达式树对象。而patientSelect2中调用了GetCommConsent,这个方法无法被LINQ提供者解析为SQL,导致表达式树没有被正确执行,最终传递的是未解析的表达式对象。
3. patientSelect(Patient p)的额外错误
这个方法返回的表达式捕获了外部变量p,表达式树会持有对p的引用,进一步增加序列化复杂度,同时完全偏离了Select的设计意图——Select需要的是针对集合中每个元素的映射逻辑,而非绑定到单个实例的表达式。
解决方案
方案1:使用委托而非表达式(内存集合场景)
如果只是处理内存中的IEnumerable<Patient>,直接定义委托而非表达式:
// 定义委托,而非表达式 readonly Func<Patient, object> patientSelect2 = x => new { id = x.patient_id.ToString(), internalId = x.patient_id.ToString(), firstName = x.first_name, middleName = x.middle_name, lastName = x.last_name, emailAddress = x.email, homePhone = x.HomePhone, mobileNumber = x.OtherPhone, communicationConsent = GetCommConsent(x) }; // 调用时直接用IEnumerable.Select,无需AsQueryable() await ProcessResponses(company, await manager.PostEntityData("patients", appConfiguration.PatientUrl, company.Patients.Select(patientSelect2)), databaseHelper.SavePatientLastUpdatedDate);
方案2:支持IQueryable(数据库查询场景)
如果company.Patients是数据库查询的IQueryable<Patient>,需要确保映射逻辑能被LINQ提供者解析,可使用LINQKit扩展表达式支持:
// 使用LINQKit的Expandable()扩展 readonly Expression<Func<Patient, object>> patientSelect2 = x => new { id = x.patient_id.ToString(), internalId = x.patient_id.ToString(), firstName = x.first_name, middleName = x.middle_name, lastName = x.last_name, emailAddress = x.email, homePhone = x.HomePhone, mobileNumber = x.OtherPhone, communicationConsent = GetCommConsentExpr().Invoke(x) }; // 将GetCommConsent改为可解析的表达式 private static Expression<Func<Patient, object[]>> GetCommConsentExpr() { return p => new[] { p.TextConsent ? new { method = "TEXT", consent = true } : null, p.VoiceConsent ? new { method = "VOICE", consent = true } : null, p.EmailConsent ? new { method = "EMAIL", consent = true } : null }.Where(item => item != null).ToArray(); } // 调用时使用AsExpandable()解析表达式 await ProcessResponses(company, await manager.PostEntityData("patients", appConfiguration.PatientUrl, company.Patients.AsQueryable().AsExpandable().Select(patientSelect2)), databaseHelper.SavePatientLastUpdatedDate);
方案3:删除错误的patientSelect(Patient p)方法
该方法设计完全偏离Select的用法,直接删除即可。
内容的提问来源于stack exchange,提问作者jrandomuser
相关产品推荐
相关产品推荐

