【问题标题】:Return only a subset of properties of an object from an API从 API 中仅返回对象属性的子集
【发布时间】:2019-01-31 15:10:08
【问题描述】:

假设我有一个数据库,我在其中存储此结构的用户详细信息:

public class User
{
    public string UserId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
}

我有一个与之配合使用的数据访问层,它包含诸如 GetById() 之类的方法并返回给我一个用户对象。

然后说我有一个 API 需要返回用户详细信息,而不是像 PasswordHash 这样的敏感部分。我可以从数据库中获取用户,但我需要删除某些字段。这样做的“正确”方法是什么?

我已经想到了几种方法来处理这个问题,其中大部分涉及将 User 类拆分为具有非敏感数据的 BaseClass 和包含我希望保密的属性的派生类,然后转换或映射在返回 BaseClass 之前将其对象,但是这感觉笨重和肮脏。

感觉这应该是一个相对常见的情况,所以我错过了一种简单的方法来处理它吗?我正在专门使用 ASP.Net 核心和 MongoDB,但我想这是一个更普遍的问题。

【问题讨论】:

  • 看看视图模型模式。
  • @DanielA.White 我不确定我是否理解;我不想返回视图模型,这是一个返回纯数据的 API
  • @DanielA.White 我明白什么是视图模型,我只是不认为这是我想要的。如果唯一需要的数据是我已经拥有的对象的 2 个字段,我不想创建 ViewModel 类并手动填充这些字段?
  • [JsonIgnore] public string PasswordHash { get;放; . 序列化模型时会忽略

标签: c# asp.net-core .net-core


【解决方案1】:

有两种方法可以做到这一点:

  1. 使用相同的类并仅填充您要发送的属性。这样做的问题是值类型将具有默认值(int 属性将作为0 发送,这可能不准确)。
  2. 为要发送到客户端的数据使用不同的类。这基本上就是 Daniel 在 cmets 中的意思 - 您有一个不同的模型被客户“查看”。

第二个选项是最常见的。如果您使用的是 Linq,则可以使用 Select() 映射值:

users.Select(u => new UserModel { Name = u.Name, Email = u.Email });

基本类型不会按您希望的方式工作。如果将派生类型强制转换为它的父类型并对其进行序列化,它仍然会序列化派生类型的属性。

以此为例:

public class UserBase {
    public string Name { get; set; }
    public string Email { get; set; }
}

public class User : UserBase {
    public string UserId { get; set; }
    public string PasswordHash { get; set; }
}

var user = new User() {
    UserId = "Secret",
    PasswordHash = "Secret",
    Name = "Me",
    Email = "something"
};

var serialized = JsonConvert.SerializeObject((UserBase) user);

注意在序列化时进行转换。即便如此,结果是:

{
    "UserId": "Secret",
    "PasswordHash": "Secret",
    "Name": "Me",
    "Email": "something"
}

它仍然序列化来自 User 类型的属性,即使它被强制转换为 UserBase

【讨论】:

  • 将派生类型转换为其基类型不会导致错误(我测试了这段代码)。但是,是的,无论您映射什么类(基类与否),您都必须映射值。我认为使用基类/派生类型没有任何好处(无论如何都是为了性能)。
  • 抱歉,您对选角的看法是正确的。我猜现在手动映射它。
【解决方案2】:

就我的目的而言,最简洁的解决方案似乎是这样的:

将User类拆分为基类和派生类,并添加构造函数复制所需字段:

public class User
{
    public User() { }

    public User(UserDetails user)
    {
        this.UserId = user.UserId;
        this.Name = user.Name;
        this.Email = user.Email;
    }

    public string UserId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class UserDetails : User
{
    public string PasswordHash { get; set; }
}

数据访问类将返回一个 UserDetails 对象,然后可以在返回之前对其进行转换:

UserDetails userDetails = _dataAccess.GetUser();
User userToReturn = new User(userDetails);

也可以按照 Daniel 的建议使用 AutoMapper 而不是构造函数方法来完成。不喜欢这样做,因此我问这个问题,但这似乎是最简洁的解决方案,并且需要最少的重复。

【讨论】:

    【解决方案3】:

    如果你想忽略该属性,只需像这样在你的模型中添加 ignore annotation,它会在模型​​序列化时跳过该属性。

    [JsonIgnore] 
    public string PasswordHash { get; set; }
    

    如果你想在运行时忽略(这意味着动态)。Newtonsoft.Json

    中有构建函数可用
    public class User
    {
        public string UserId { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
        public string PasswordHash { get; set; }
        //FYI ShouldSerialize_PROPERTY_NAME_HERE()
       public bool ShouldSerializePasswordHash()
        {
            // use the condtion when it will be serlized
            return (PasswordHash != this);
        }
    }
    

    它被称为“条件属性序列化”和文档can be found here.希望这有帮助

    【讨论】:

    • 这是迄今为止最简单的解决方案,只要您永远不需要序列化PasswordHash
    • 是的,这实际上是一个好主意,但我没有考虑过,但就我而言,有时我确实需要对其进行序列化。
    【解决方案4】:

    问题是你看错了。 API,即使它直接与特定的数据库实体一起工作,也不是在处理实体。这里有一个关注点分离问题。您的 API 正在处理您的用户实体的表示。实体类本身是您的数据库的功能。它上面有只对数据库重要的东西,重要的是,上面有对你的 API 无关的东西。试图让一个类满足多个不同的应用程序是愚蠢的,而且只会导致具有嵌套依赖关系的脆弱代码。

    更重要的是,您将如何与此 API 进行交互?也就是说,如果您的 API 直接公开了您的 User 实体,那么使用此 API 的任何代码都必须依赖于您的数据层以便它可以访问 User 或者它必须实现自己的代表 User 的类并希望它与 API 实际想要的匹配。

    现在想象一下替代方案。您创建了一个“通用”类库,它将在您的 API 和任何客户端之间共享。在该库中,您定义了类似UserResource 的内容。您的 API 仅与 UserResource 绑定/从 UserResource 绑定,并将其映射到 User。现在,您已经完全隔离了数据层。客户只知道UserResource,而唯一触及您的数据层的是您的 API。当然,现在您可以限制 User 上的哪些信息会暴露给 API 的客户端,只需通过构建 UserResource 的方式即可。更好的是,如果您的应用程序需求发生变化,User 可以更改,而不会因为每个消费客户端的 API 冲突而螺旋式上升。您只需修复您的 API,客户就会在不知不觉中继续前进。如果您确实需要进行重大更改,您可以创建一个 UserResource2 类以及新版本的 API。你不能创建一个User2 而不导致创建一个全新的表,这会导致身份冲突。

    总而言之,使用 API 的正确方法是始终使用单独的 DTO 类,甚至多个 DTO 类。 API 永远不应该直接使用实体类,否则您将一无所获。

    【讨论】:

    • 就我而言,唯一使用此 API 的是 Angular 前端网站,所以无论如何我都必须在 typescript 中创建一个单独的类模型,这就是为什么我只是想快速完成它简单的方法,但你在更大的范围内提出了很好的观点。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-19
    • 1970-01-01
    • 2016-04-07
    • 1970-01-01
    • 2021-04-11
    相关资源
    最近更新 更多