在 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() 呼叫没有contentTypeapplication/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&lt;Person&gt;(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.ServiceMyNamespace.MyProject 现在可以独立测试,甚至可以包含在其他项目中,而无需携带 WebAPI 依赖项。我们已经实现了关注点的明确分离。


类命名注意事项

相同的类名可能会让某些团队感到困惑。您可以选择实施某种命名约定以使名称更清晰,例如将 DTOModel 附加到类在您的服务层中。我更喜欢将它们放在不同的命名空间中并根据需要对其进行限定。


引用的第三方库

    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 控制器中自动反序列化为类似字符串的类的主要内容,如果未能解决你的问题,请参考以下文章

在 Web Api 控制器中将 JSON 反序列化为字典

Web API JSON 属性长度限制?

从 .NET Core Web API 调用 SalesForce API 并将响应反序列化为 c# 对象?

在 Azure 移动应用服务后端将字符串属性反序列化为 Json 对象

C# - Web API - 将枚举序列化为带空格的字符串

Java序列化对象为字符串并将字符串反序列化为对象