2

设想

我正在更新我的 .NET API 以对所有数据库键字段进行编码,这样顺序键就不会暴露给最终用户。我为此使用hashids.org并构建了辅助方法来快速解码/编码我的自动映射器映射中的属性。但是,API 有多个版本,只有最新版本应该使用此功能进行更新,这意味着我不能简单地覆盖现有的类。我已经实现了一些可行的解决方案,但它们都有我希望清除的不良代码气味。

解决方案

我目前正在控制器层执行编码。我也可以在数据访问层看到这样做的好处,但我觉得在该层存在更多泄漏/错过转换的风险,特别是因为 API 有许多不同的数据源。另外,隐藏密钥是外部世界的一个问题,控制器是看门人,所以在那里感觉很合适。

应用程序目前有以下模型模式,无法更改:Model(DB 中存在的模型)> ValueObject(服务模型,VO)> DTO(API 模型)。

(1) 初步尝试

下面是一个需要支持编码和解码状态的类的示例,其中Utils.Encode()Utils.Decode()是辅助方法,它们将使用 Hashids 在 int 和 string 之间转换字段。

//EquipmentDTO.cs
public class EquipmentDTO //encoded class
{
  public string Id {get; set;}
  public string Name {get; set;}
}

public class EquipmentUnencodedDTO //decoded class
{
  public int Id {get; set;}
  public string Name {get; set;}
}

//Automapper.cs
CreateMap<EquipmentUnencodedDTO, EquipmentDTO>()
  .ForMember(dst => dst.Id, opt => opt.MapFrom(src => Utils.Encode(src.Id)));

CreateMap<EquipmentDTO, EquipmentUnencodedDTO>()
  .ForMember(dst => dst.Id, opt => opt.MapFrom(src => Utils.Decode(src.Id)));

CreateMap<EquipmentVO, EquipmentDTO>() //mapping from service model to controller model
  .ForMember(dst => dst.Id, opt => opt.MapFrom(src => Utils.Encode(src.Id)));
CreateMap<EquipmentDTO, EquipmentVO>()
  .ForMember(dst => dst.Id, opt => opt.MapFrom(src => Utils.Decode(src.Id)));

CreateMap<Equipment, EquipmentVO>() //mapping from DB model to service model
  .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id));
  • 我选择制作现有EquipmentDTO的编码版本,因为我希望它成为新标准,这最终会导致 EquipmentUnencodedDTO旧控制器最终得到更新而被弃用和删除。
  • 我选择复制CreateMap<EquipmentVO, EquipmentDTO>CreateMap<EquipmentVO, EquipmentUnencodedDTO>反之亦然),因为它会导致 AutoMapper 文件中出现大量重复,这已经很大(尽管这可能不是真正的问题?)
  • 我不喜欢这个解决方案,因为在我的旧控制器中,映射现在很混乱。例如,在 POST 中,未编码的输入 DTO 必须通过以下方式转换为服务模型:Mapper.Map<EquipmentVO>(Mapper.Map<EquipmentDTO>(unencodedEquipmentInput))这非常难看。
    • 话虽如此,这应该是一个暂时的问题,那么这是一个真正的问题吗?
    • 如果我创建了这个问题就会消失CreateMap<EquipmentVO, EquipmentUnencodedDTO>
  • 我不喜欢这个解决方案,因为我的类有很多重复的字段在编码和解码版本之间没有变化

(2) 第二次尝试

上面的两个要点使我对此进行了重构:

public class EquipmentDTO
{
  public string Id {get; set;}
  public string Name {get; set;}
  public Decoded Decode(){
    return Mapper.Map<Decoded>(this);
  }
  public class Decoded: EquipmentDTO {
    public new int Id {get; set;}
    public EquipmentDTO Encode(){
      return Mapper.Map<EquipmentDTO>(this);
    }
  }
}

// Automappers are the same, except EquipmentUnencodedDTO is now EquipmentDTO.Decoded 
  • 我喜欢现在在编码和解码状态之间切换是多么简单,将我上面的双重映射减少到:Mapper.Map<EquipmentVO>(unencodedEquipmentInput.Encode());
  • 我喜欢嵌套类,因为它编码了两个类之间的关系,并且在识别哪些字段被编码/解码方面做得更好
  • 我觉得这闻起来更糟

(3) 下一次尝试

我的下一个尝试是将已解码类的缺失映射添加到服务模型中,并撤消尝试 #2 中的更改。这创建了大量重复的映射代码,我仍然被两个类中的重复属性所困扰,没有明确指示哪些字段被解码/编码,而且感觉比必要的要麻烦得多。

感谢您的任何建议!

4

2 回答 2

0

这是那些并不能真正直接回答您的问题的答案之一,而是一种解决手头问题的不同方法。根据我上面的评论。

我不会尝试进行“硬编码”转换,或者使别名成为对象生命周期的某些内在部分。这里的想法是标识符的转换应该是显而易见的、明确的和可插入的。

让我们从一个界面开始:

public interface IObscuredIDProvider
{
    public string GetObscuredID(int id);
    public void SetObscuredID(int id, string obscuredID);
}

然后,对于我们的测试,一个非常简单的映射器只返回 int 作为字符串。您的生产版本可以由 hashids.org 项目或任何您喜欢的支持:

public class NonObscuredIDProvider : IObscuredIDProvider
{
    public string GetObscuredID(int id)
    {
        return id.ToString();
    }

    public void SetObscuredID(int id, string obscuredID)
    {
        // noop
    }
}

您需要将 IObscuredIDProvider 的实例注入将“外部/不受信任”数据转换为“受信任/域”数据的任何层。这是您将实体 ID 从隐藏版本分配到内部版本的地方,反之亦然。

那有意义吗?希望这是一个比在复杂的嵌套转换中烘焙更容易理解和实现的解决方案......

于 2019-12-31T22:23:33.173 回答
0

在玩了很多之后,我最终选择了不使用 automapper 并且只有一个 DTO 用于编码/未编码状态的路线,方法是使用自定义 getter/setter 来控制基于 readonly 属性返回的值isEncoded

我对 automapper 和拥有多个 DTO 的问题是有太多的重复和太多的代码要编写来添加一个新的可解码 DTO。此外,有太多方法可以打破编码 DTO 和未编码 DTO 之间的关系,特别是因为团队中的其他开发人员(更不用说未来的员工)可能忘记创建编码 DTO 或创建映射以正确编码或解码ID 值。

虽然我仍然有单独的 util 方法来执行值的编码,但我将所有自动映射器“逻辑”移动到一个基类EncodableDTO中,这将允许用户运行Decode()Encode()在 DTO 上切换其编码状态,包括编码状态通过反射获得其所有可编码属性。拥有 DTO 继承EncodableDTO也可以作为开发人员清楚指示正在发生的事情,而自定义 getter/setter 清楚地表明我正在尝试对特定字段执行什么操作。

这是一个示例:

public class EquipmentDTO: EncodableDTO
{
  private int id;
  public string Id {
    get
    {
      return GetIdValue(id);
    }
    set
    {
      id = SetIdValue(value);
    }
  }

  public List<PartDTO> Parts {get; set;}

  public string Name {get; set;}
}

public class PartDTO: EncodableDTO
{
  private int id;
  public string Id {
    get
    {
      return GetIdValue(id);
    }
    set
    {
      id = SetIdValue(value);
    }
  }

  public string Name {get; set;}
}

public class EncodableDTO
{
    public EncodableDTO()
    {
        // encode models by default
        isEncoded = true;
    }

    public bool isEncoded { get; private set; }

    public void Decode()
    {
        isEncoded = false;
        RunEncodableMethodOnProperties(MethodBase.GetCurrentMethod().Name);
    }

    public void Encode()
    {
        isEncoded = true;
        RunEncodableMethodOnProperties(MethodBase.GetCurrentMethod().Name);
    }

    protected string GetIdValue(int id)
    {
        return isEncoded ? Utils.EncodeParam(id) : id.ToString();
    }

    // TryParseInt() is a custom string extension method that does an int.TryParse and outputs the parameter if the string is not an int
    protected int SetIdValue(string id)
    {
        // check to see if the input is an encoded value, otherwise try to parse it.
        // the added logic to test if the 'id' is an encoded value allows the inheriting DTO to be received both in
        // unencoded and encoded forms (unencoded/encoded http request) and still populate the correct numerical value for the ID
        return id.TryParseInt(-1) == -1 ? Utils.DecodeParam(id) : id.TryParseInt(-1);
    }

    private void RunEncodableMethodOnProperties(string methodName)
    {
        var self = this;
        var selfType = self.GetType();
        // Loop through properties and check to see if any of them should be encoded/decoded
        foreach (PropertyInfo property in selfType.GetProperties())
        {
            var test = property;
            // if the property is a list, check the children to see if they are decodable
            if (property is IList || (
                    property.PropertyType.IsGenericType
                    && (property.PropertyType.GetGenericTypeDefinition() == typeof(List<>)
                    || property.PropertyType.GetGenericTypeDefinition() == typeof(IList<>))
                    )
                )
            {
                var propertyInstance = (IList)property.GetValue(self);
                if (propertyInstance == null || propertyInstance.Count == 0)
                {
                    continue;
                }
                foreach (object childInstance in propertyInstance)
                {
                    CheckIfObjectEncodable(childInstance, methodName);
                }
                continue;
            }

            CheckIfObjectEncodable(property.GetValue(self), methodName);
        }
    }

    private void CheckIfObjectEncodable(object instance, string methodName)
    {
        if (instance != null && instance.GetType().BaseType == typeof(EncodableDTO))
        {
            // child instance is encodable. Run the same decode/encode method we're running now on the child
            var method = instance.GetType().GetMethod(methodName);
            method.Invoke(instance, new object[] { });
        }
    }
}

另一种方法RunEncodableMethodOnProperties()是在继承类中显式解码/编码子属性:

public class EquipmentDTO: EncodableDTO
{
  private int id;
  public string Id {
    get
    {
      return GetIdValue(id);
    }
    set
    {
      id = SetIdValue(value);
    }
  }

  public List<PartDTO> Parts {get; set;}

  public string Name {get; set;}

  public new void Decode() {
    base.Decode();
    // explicitly decode child properties
    Parts.ForEach(p => p.Decode());
  }
}

我选择不执行上述操作,因为它为 DTO 创建者创建了更多工作,必须记住显式添加 (1) 覆盖方法,以及 (2) 任何新的可解码属性到覆盖方法。话虽如此,我确信通过循环遍历我的类属性及其子类的每个类都会对性能造成某种影响,因此我可能不得不及时迁移到这个解决方案。

无论我选择哪种方法来解码/编码属性,控制器中的最终结果如下:

// Sample controller method that does not support encoded output
[HttpPost]
public async Task<IHttpActionResult> AddEquipment([FromBody] EquipmentDTO equipment)
{
    // EquipmentDTO is 'isEncoded=true' by default
    equipment.Decode();
    // send automapper the interger IDs (stored in a string)
    var serviceModel = Mapper.Map<EquipmentVO>(equipment);
    var addedServiceModel = myService.AddEquipment(serviceModel);
    var resultValue = Mapper.Map<EquipmentDTO>(addedServiceModel);
    resultValue.Decode();
    return Created("", resultValue);
}


// automapper
CreateMap<EquipmentVO, EquipmentDTO>().ReverseMap();
CreateMap<Equipment, EquipmentVO>();

虽然我不认为它是最干净的解决方案,但它隐藏了许多必要的逻辑,让未来的开发人员能够以最少的工作量进行编码/解码工作

于 2020-01-17T18:39:42.203 回答