如何处理同一个 c# 的编码和解码版本 class

How to handle encoded and decoded version of the same c# class

场景

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

解决方案

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

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

(1) 初次尝试

下面是一个需要支持编码和解码状态的 class 示例,其中 Utils.Encode()Utils.Decode() 是将字段在 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));

(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 

(3) 下一次尝试

我的下一次尝试是将解码 class 的缺失映射添加到服务模型中,并撤消尝试 #2 中的更改。这创建了大量重复的映射代码,我仍然坚持在两个 classes 中使用重复的属性,没有明确指示哪些字段获得 decoded/encoded,而且感觉比必要的要麻烦得多。

感谢任何建议!

这是其中一个并没有真正直接回答您的问题的答案,而是一种解决手头问题的不同方法。根据我上面的评论。

我不会尝试在 "hardcoded" 转换中烘焙,或者使别名成为对象生命周期的某些固有部分。这里的想法是标识符的转换应该是明显的、显式的和可插入的。

让我们从界面开始:

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

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

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

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

您需要将 IObscuredIDProvider 的实例注入到将 "outside/untrusted" 数据转换为 "trusted/domain" 数据的任何层中。这是您将实体 ID 从隐藏版本分配给内部版本的地方,反之亦然。

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

经过大量尝试,我最终选择了不使用自动映射器的路线,并且通过使用自定义 getters/setters 来控制 getters/setters 状态的两个 encoded/unencoded 状态只有一个 DTO基于只读 属性 isEncoded.

返回

我在使用自动映射器和拥有多个 DTO 时遇到的问题是,要添加新的可解码 DTO 需要编写太多的重复代码和太多的代码。此外,有太多方法可以打破 encodedDTO 和 unencodedDTO 之间的关系,特别是因为团队中还有其他开发人员(更不用说未来的雇员)可能会忘记创建编码 DTO 或创建映射以正确编码或解码ID 值。

虽然我仍然有单独的 util 方法来执行值的编码,但我将所有自动映射器 "logic" 移动到基 class EncodableDTO 中,这将允许用户到 DTO 上的 运行 Decode()Encode() 以切换其编码状态,包括通过反射为其所有可编码属性的编码状态。让 DTO 继承 EncodableDTO 也可以清楚地指示开发人员了解正在发生的事情,而自定义 getters/setters 清楚地表明我正在尝试为特定领域做什么。

这是一个示例:

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() 的替代方法是在继承 class:

中显式 decode/encode 子属性
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) 覆盖方法的任何新的可解码属性。话虽这么说,我确信我正在通过遍历我的 class 属性及其子项的每个 class 来对性能造成某种影响,所以我可能不得不及时迁移到这个解决方案。

无论我选择 decode/encode 属性的方法如何,控制器中的最终结果如下:

// 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>();

虽然我不认为它是最干净的解决方案,但它隐藏了许多必要的逻辑,使 encoding/decoding 以最少的工作量为未来的开发人员工作