在 Web.API 控制器中自动反序列化为类似字符串的 class
Automatically deserialize to string-like class in Web.API controller
我有一个 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
class 是一个对象,它定义了 string
的隐式运算符,因此我们在整个应用程序中都像对待 string
一样对待它。
不幸的是,Web.API 不会自动知道如何将相应的 JavaScript Person
对象反序列化为 C# Person
对象 - 反序列化的 C# Person
对象始终为空。例如,这里是我如何从我的 JavaScript 前端调用这个端点,使用 jQuery:
$.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
class?
这是我的 UserName
class 的样子:
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();
}
}
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 请求中创建的内容相匹配 name/property 则它将可用于映射。
外壳注意事项。我遵循 camelCasing 模式,所以第一个字符总是小写。在您的 dot net 类型中,您不需要这样做 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;
}
您需要为您的 UserName
class 编写自定义 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);
}
}
我建议努力实现更多的关注点分离。
你有两个问题:
- 处理 HTTP 请求和响应。
- 执行域逻辑。
WebAPI 关注处理 HTTP 请求和响应。它为消费者提供了一份合同,指定他们如何使用其端点和操作。它不应该关心做任何其他事情。
项目管理
考虑使用多个项目来更清楚地分离关注点。
MyNamespace.MyProject
- Class 将包含您的域逻辑的库项目。
MyNamespace.MyProject.Service
- 仅包含您的 Web 服务的 WebAPI 项目。
在 MyNamespace.MyProject.Service
上添加对 MyNamespace.MyProject
的引用。这将帮助您保持关注点的清晰分离。
不同Class有
现在,重要的是要了解您将有两个同名但不同的 classes。完全合格,他们的区别就很明显了:
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
的域逻辑移动到您的域逻辑 Class 库中。 这也允许在可能超出 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。您首先会在应用程序启动时的某个配置 class 中设置您的映射。这些映射将告诉 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 依赖项。我们已经实现了明确的关注点分离。
关于 Class 命名的注意事项
相同的 class 名称可能会让一些团队感到困惑。 您可以选择实施某种类型的命名约定以使名称更清晰,例如附加 DTO
或 Model
到服务层中的 classes。我更喜欢将它们放在不同的命名空间中并根据需要对它们进行限定。
引用的第三方库
- AutoMapper - 用于减少将服务对象映射到域对象的样板,反之亦然。
- Ninject - 用于将依赖项注入控制器(记住也要获取 WebAPI 或 OWIN 包)。可以使用任何 IoC。或者,也可以使用 Singleton 或 Service Locator 模式,但可能会使测试变得困难。
这些库都不需要遵循这个答案的想法,但可以让生活更轻松。
我有一个 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
class 是一个对象,它定义了 string
的隐式运算符,因此我们在整个应用程序中都像对待 string
一样对待它。
不幸的是,Web.API 不会自动知道如何将相应的 JavaScript Person
对象反序列化为 C# Person
对象 - 反序列化的 C# Person
对象始终为空。例如,这里是我如何从我的 JavaScript 前端调用这个端点,使用 jQuery:
$.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
class?
这是我的 UserName
class 的样子:
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();
}
}
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 请求中创建的内容相匹配 name/property 则它将可用于映射。
外壳注意事项。我遵循 camelCasing 模式,所以第一个字符总是小写。在您的 dot net 类型中,您不需要这样做 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;
}
您需要为您的 UserName
class 编写自定义 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);
}
}
我建议努力实现更多的关注点分离。
你有两个问题:
- 处理 HTTP 请求和响应。
- 执行域逻辑。
WebAPI 关注处理 HTTP 请求和响应。它为消费者提供了一份合同,指定他们如何使用其端点和操作。它不应该关心做任何其他事情。
项目管理
考虑使用多个项目来更清楚地分离关注点。
MyNamespace.MyProject
- Class 将包含您的域逻辑的库项目。MyNamespace.MyProject.Service
- 仅包含您的 Web 服务的 WebAPI 项目。
在 MyNamespace.MyProject.Service
上添加对 MyNamespace.MyProject
的引用。这将帮助您保持关注点的清晰分离。
不同Class有
现在,重要的是要了解您将有两个同名但不同的 classes。完全合格,他们的区别就很明显了:
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
的域逻辑移动到您的域逻辑 Class 库中。 这也允许在可能超出 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。您首先会在应用程序启动时的某个配置 class 中设置您的映射。这些映射将告诉 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 依赖项。我们已经实现了明确的关注点分离。
关于 Class 命名的注意事项
相同的 class 名称可能会让一些团队感到困惑。 您可以选择实施某种类型的命名约定以使名称更清晰,例如附加 DTO
或 Model
到服务层中的 classes。我更喜欢将它们放在不同的命名空间中并根据需要对它们进行限定。
引用的第三方库
- AutoMapper - 用于减少将服务对象映射到域对象的样板,反之亦然。
- Ninject - 用于将依赖项注入控制器(记住也要获取 WebAPI 或 OWIN 包)。可以使用任何 IoC。或者,也可以使用 Singleton 或 Service Locator 模式,但可能会使测试变得困难。
这些库都不需要遵循这个答案的想法,但可以让生活更轻松。