在 Web.API 控制器中自动反序列化为类似字符串的类
Posted
技术标签:
【中文标题】在 Web.API 控制器中自动反序列化为类似字符串的类【英文标题】:Automatically deserialize to string-like class in Web.API controller 【发布时间】:2016-03-27 19:03:02 【问题描述】:我有一个 Web.API 端点,它将像这样的对象作为参数:
public class Person
public string FirstName get; set;
public string LastName get; set;
public int Age get; set;
public UserName UserName get; set;
例如:
[Route("api/person")]
[AcceptVerbs("POST")]
public void UpdatePerson(Person person)
// etc.
(这只是一个例子——我们实际上并没有通过我们的 Web.API 端点接受用户名)
我们的UserName
类是一个对象,它为string
定义了隐式运算符,因此我们在整个应用程序中将其视为string
。
不幸的是,Web.API 不会自动知道如何将相应的 javascript Person
对象反序列化为 C# Person
对象 - 反序列化的 C# Person
对象始终为空。例如,下面是我如何使用 jQuery 从我的 JavaScript 前端调用此端点:
$.ajax(
type: 'POST',
url: 'api/test',
data: FirstName: 'First', LastName: 'Last', Age: 110, UserName: 'UserName'
);
如果我不使用 UserName
属性,data
参数将正确反序列化为 C# Person
对象(UserName
属性设置为 null
)。
如何使 Web.API 正确地将 JavaScript 对象上的 UserName
属性反序列化为我们的自定义 UserName
类?
这是我的UserName
类的样子:
public class UserName
private readonly string value;
public UserName(string value)
this.value = value;
public static implicit operator string (UserName d)
return d != null ? d.ToString() : null;
public static implicit operator UserName(string d)
return new UserName(d);
public override string ToString()
return value != null ? value.ToUpper().ToString() : null;
public static bool operator ==(UserName a, UserName b)
// If both are null, or both are same instance, return true.
if (System.Object.ReferenceEquals(a, b))
return true;
// If one is null, but not both, return false.
if (((object)a == null) || ((object)b == null))
return false;
return a.Equals(b);
public static bool operator !=(UserName a, UserName b)
return !(a == b);
public override bool Equals(object obj)
if ((obj as UserName) == null)
return false;
return string.Equals(this, (UserName)obj);
public override int GetHashCode()
string stringValue = this.ToString();
return stringValue != null ? stringValue.GetHashCode() : base.GetHashCode();
【问题讨论】:
您是否发送了示例 Json,因为该对象应该包装在另一个
那不是有效的 json。属性名称应该用“-characters. "FirstName": "First", "LastName": "Last", "Age": "110", "UserName": "UserName"
问题出在您的class UserName
中,它没有任何可访问的属性设置器和无参数构造器。
为什么要将一个简单的字符串映射到这样一个复杂的类?将其定义为字符串并在您的 c# 代码中处理自己的 UserName 类会不会容易得多?
正如@jpgrassi 建议的那样,您可以使用 ViewModel 将值接收到您的控制器中,然后将它们转换为您的域模型。您可以使用 AutoMapper 之类的工具来设置映射并减少样板。
【参考方案1】:
您需要为您的UserName
类编写一个自定义Json.NET Converter。创建自定义转换器后,您需要告诉 Json.NET。在我的一个项目中,我们将以下代码行添加到您的 Global.asax.cs
文件中的 Application_Start
方法中,以让 Json.NET 了解转换器:
// Global Json.Net config settings.
JsonConvert.DefaultSettings = () =>
var settings = new JsonSerializerSettings();
// replace UserNameConverter with whatever the name is for your converter below
settings.Converters.Add(new UserNameConverter());
return settings;
;
这是一个应该可以工作(未经测试)的快速和基本的实现。几乎可以肯定它可以改进:
public class UserNameConverter : JsonConverter
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
var username = (UserName)value;
writer.WriteStartObject();
writer.WritePropertyName("UserName");
serializer.Serialize(writer, username.ToString());
writer.WriteEndObject();
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
// Variables to be set along with sensing variables
string username = null;
var gotName = false;
// Read the properties
while (reader.Read())
if (reader.TokenType != JsonToken.PropertyName)
break;
var propertyName = (string)reader.Value;
if (!reader.Read())
continue;
// Set the group
if (propertyName.Equals("UserName", StringComparison.OrdinalIgnoreCase))
username = serializer.Deserialize<string>(reader);
gotName = true;
if (!gotName)
throw new InvalidDataException("A username must be present.");
return new UserName(username);
public override bool CanConvert(Type objectType)
return objectType == typeof(UserName);
【讨论】:
我想这就是我要找的 - 你有UserNameConverter
类的实现示例吗?我尝试过类似的事情,但没有成功——转换方法永远不会被触发。
@NathanFriend 我需要更多信息才能知道为什么没有触发转换方法。我的猜测是你的 CanConvert
方法匹配了错误的类型。
是的,这个过程对我有用!我正在使用JsonConverter
的不同实现,但想法是一样的。我遇到的问题是我的.ajax()
呼叫没有contentType
的application/json; charset=utf-8
。我在这里记录了这个解决方案:***.com/q/34404269/1063392【参考方案2】:
我建议争取更多的关注点分离。
你有两个顾虑:
-
处理 HTTP 请求和响应。
执行域逻辑。
WebAPI 关注处理 HTTP 请求和响应。它为消费者提供了一个合同,指定他们如何使用它的端点和操作。它不应该关心做任何其他事情。
项目管理
考虑使用多个项目来更清楚地分离关注点。
MyNamespace.MyProject
- 将保存您的域逻辑的类库项目。
MyNamespace.MyProject.Service
- 仅包含您的 Web 服务的 WebAPI 项目。
在MyNamespace.MyProject.Service
上添加对MyNamespace.MyProject
的引用。这将帮助您保持关注点的清晰分离。
不同的类
现在,重要的是要了解您将拥有两个同名但不同的类。完全合格,他们的区别变得清晰:
MyNamespace.MyProject.Person
- 一个人的域层表示。
MyNamespace.MyProject.Service.Models.Person
- 个人的 WebAPI 合同表示。
你的领域层对象:
namespace MyNamespace.MyProject
public class Person
public string FirstName get; set;
public string LastName get; set;
public int Age get; set;
public UserName UserName get; set;
您的服务层对象:
namespace MyNamespace.MyProject.Service.Models
public class Person
public string FirstName get; set;
public string LastName get; set;
public int Age get; set;
//The service contract expects username to be a string.
public string UserName get; set;
这里的好处是域层表示可以独立于 WebAPI 合约而改变。 因此,您的消费者合同不会改变。
将域逻辑与服务逻辑隔离
我还建议将任何作用于传入Person
的域逻辑移动到您的域逻辑类库中。 这也允许在可能超出 WebAPI 范围的其他应用程序和库中重用此逻辑。 此外,为了继续将我们的域逻辑与我们的服务逻辑分离,我将实现存储库模式, 并创建 MyNamespace.MyProject.PersonRepository
定义如何处理您的域级别 Person
对象的存储库。
您的控制器现在可能看起来像这样:
[Route("api/person")]
[HttpPost]
public void UpdatePerson(Models.Person person)
var mappedPerson = Mapper.Map<Person>(person);
personRepository.Update(mappedPerson);
//I'd suggest returning some type of IHttpActionResult here, even if it's just a status code.
Mapper.Map<Person>(person)
的魔力来自AutoMapper。您首先在应用程序启动时的某个配置类中设置您的映射。这些映射会告诉 AutoMapper 如何将 MyNamespace.MyProject.Service.Models.Person
转换为 MyNamespace.MyProject.Person
。
//This gets called once somewhere when the application is starting.
public static void Configure()
//<Source, Destination>
Mapper.Create<Models.Person, Person>()
//Additional mappings.
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => new UserName(src.UserName)))
此外,您可能需要使用单例、服务定位器或控制反转 (IoC) 容器(如 Ninject)来获取对您的 personRepository
的引用。我强烈建议使用 IoC。 Ninject 有一个包可以接管 WebAPI 控制器的创建,注入您已配置的依赖项。
我们在这里完成的是我们将所有域逻辑移出MyNamespace.MyProject.Service
。 MyNamespace.MyProject
现在可以独立测试,甚至可以包含在其他项目中,而无需携带 WebAPI 依赖项。我们已经实现了关注点的明确分离。
类命名注意事项
相同的类名可能会让某些团队感到困惑。您可以选择实施某种命名约定以使名称更清晰,例如将 DTO
或 Model
附加到类在您的服务层中。我更喜欢将它们放在不同的命名空间中并根据需要对其进行限定。
引用的第三方库
-
AutoMapper - 用于减少将服务对象映射到域对象的样板,反之亦然。
Ninject - 用于将依赖项注入控制器(记住也要获取 WebAPI 或 OWIN 包)。可以使用任何 IoC。或者,也可以使用单例模式或服务定位器模式,但可能会使测试变得困难。
这些库都不需要遵循这个答案的想法,但可以让生活更轻松。
【讨论】:
【参考方案3】:WebAPI 可以序列化和序列化类型化的结构。但是,您要做的是遵循键入的模式。例如,在 Javacsript 中,我可以创建一个像 Person
这样的对象var person =
userName: 'bob123',
firstName: 'Bobby',
lastName: 'Doe'
然后将其作为对象作为我对 webAPI 的请求的一部分传递
在 webAPI 中将类型定义为:
[Route("api/membershipinfo/getuserdata")]
[HttpPost]
public IHttpActionResult DoSomething([FromBody]Person p)
try
...rest of your code here
如果您有 .net 类型 Person
并且它与您在 javascript 请求名称/属性中创建的内容相匹配,那么它将可用于映射。
注意外壳。我遵循 camelCasing 模式,所以第一个字符总是小写。在您的点网类型中,您不需要执行此 WebAPI 将允许您通过配置来解决此问题。
我是如何在我的 webapi.config 中使用自定义配置格式化程序来完成它的,它有助于在序列化期间转换类型
//source: http://www.asp.net/web-api/overview/formats-and-model-binding/json-and-xml-serialization
// Replace the default JsonFormatter with our custom one
ConfigJsonFormatter(config.Formatters);
private static void ConfigJsonFormatter(MediaTypeFormatterCollection formatters)
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
settings.Formatting = Formatting.Indented;
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
settings.TypeNameHandling = TypeNameHandling.Auto;
【讨论】:
我认为问题在于他的Person
对象已将UserName
属性声明为UserName
类型,但他却给了它string
。
如果是这种情况,可以通过引入一个轻量级 DTO 来解决,该 DTO 可以让他将更轻量级的对象传递给 API 调用。然后在 WebAPI 或辅助 DTO 层中将映射处理为更复杂的类型
这也是我的建议。使用 ViewModel 将值接收到您的控制器中,然后使用 AutoMapper 之类的映射器将它们转换为您的域逻辑所需的对象。毕竟,WebAPI 只是用于接受和响应通过 HTTP 提供的请求。
听起来这可能是我需要做的。我希望我可以自定义反序列化过程来完成此操作,但我可能会扩展 Web.API 的设计目的。以上是关于在 Web.API 控制器中自动反序列化为类似字符串的类的主要内容,如果未能解决你的问题,请参考以下文章
从 .NET Core Web API 调用 SalesForce API 并将响应反序列化为 c# 对象?