Снятие отпечатков объектов: сериализация + неприкосновенный устаревший код + автоматические свойства только для геттеров = загнали в угол?

Я оказался загнанным в угол, так что поехали.

Контекст

Мне нужно создать хэш-код отпечатка пальца для сравнения объектов. Сравнение хэшей двух наборов объектов должно будет сказать мне, есть ли идентичные объекты с одинаковым хешем.

Отпечаток хэш должен быть независимым от платформы. Поэтому я выбрал хеширование MD5.

Я работаю с большой базой кода объектной модели, которая находится вне моего контроля. Все типы, которые будут переданы мне для этого снятия отпечатков пальцев, не могут быть изменены мной. Я не могу добавить атрибут или конструкторы или что-либо изменить. Что не исключает, что в будущем типы изменятся. Так что любой подход должен быть программным — я не могу просто создать суррогатный класс, чтобы избежать проблемы; по крайней мере, не вручную.

Однако производительность не имеет значения, поэтому отражение полностью разрешено.

Кроме того, мне нужно будет иметь возможность управлять исключением свойств из хеширования. Если я исключаю определенное свойство, два объекта, которые имеют все свойства, идентичные друг другу, за исключением того, что все равно нужно будет получить один и тот же хэш.

Проблема: сериализация в Byte[] со связанными руками над устаревшим кодом

Хэширование MD5 требует, чтобы объект был сериализован в Byte[].

Сериализация требует, чтобы класс был помечен как [Serializable]. Который я не могу добавить в устаревший код, и, естественно, его нельзя добавить и во время выполнения.

Поэтому я выбрал protobuf-net.

Protobuf справедливо терпит неудачу при обнаружении типов, которые реализуют интерфейс с автоматическими свойствами только для Getter:

public interface ISomeInterface
{
        double Vpy { get; }
        double Vy { get; }
        double Vpz { get; }
        ...
}

Поскольку этот интерфейс реализован многими типами, использование Surrogates также кажется неприемлемым (непрактичным, необслуживаемым).

Мне просто нужно сериализовать, а не десериализовать, поэтому я не понимаю, почему в этом случае ограничение protobuf-net. Я понимаю, что protobuf-net не сможет выполнить обмен данными, если это необходимо, но мне это не нужно!

Вопрос

Я действительно загнан в угол? Есть ли альтернатива?

Мой код

Как я уже сказал, это прекрасно работает, но только в том случае, если объекты не имеют какого-либо свойства (или вложенного свойства), которое является типом с автоматическим свойством только для Getter.

public static byte[] ToByteArray(this object obj, List<PropertyInfo> exclusionsProps = null)
{
    if (exclusionsProps == null)
        exclusionsProps = new List<PropertyInfo>();

    // Protobuf-net implementation
    ProtoBuf.Meta.RuntimeTypeModel model = ProtoBuf.Meta.TypeModel.Create();

    AddPropsToModel(model, obj.GetType(), exclusionsProps);

    byte[] bytes;
    using (var memoryStream = new MemoryStream())
    {
        model.Serialize(memoryStream, obj);
        bytes = memoryStream.GetBuffer();
    }

    return bytes;
}

public static void AddPropsToModel(ProtoBuf.Meta.RuntimeTypeModel model, Type objType, List<PropertyInfo> exclusionsProps = null)
{
    List<PropertyInfo> props = new List<PropertyInfo>();

    if (exclusionsProps != null)
        props.RemoveAll(pr => exclusionsProps.Exists(t => t.DeclaringType == pr.DeclaringType && t.Name == pr.Name));

    props
        .Where(prop => prop.PropertyType.IsClass || prop.PropertyType.IsInterface).ToList()
        .ForEach(prop =>
        {
            AddPropsToModel(model, prop.PropertyType, exclusionsProps); //recursive call
        }
        );

    var propsNames = props.Select(p => p.Name).OrderBy(name => name).ToList();

    model.Add(objType, true).Add(propsNames.ToArray());
}

Который я затем буду использовать как таковой:

  foreach (var obj in objs)
            {
                byte[] objByte = obj.ToByteArray(exclusionTypes);

                using (MD5 md5Hash = MD5.Create())
                {
                    string hash = GetMd5Hash(md5Hash, objByte);
                    Console.WriteLine(obj.GetType().Name + ": " + hash);
                }
            }

person alexlomba87    schedule 19.07.2019    source источник
comment
Если два объекта имеют одинаковый хэш, вы считаете их точными копиями друг друга?   -  person    schedule 20.07.2019
comment
Я считаю, что ваш вопрос не содержит достаточно информации, чтобы дать вам правильный ответ. Вы должны создать рабочую демонстрацию, показывающую проблемы, с которыми вы столкнулись.   -  person t3chb0t    schedule 20.07.2019
comment
@dfhwze да, это правильно.   -  person alexlomba87    schedule 20.07.2019


Ответы (2)


Простое решение здесь — полностью обойти основную причину вашей проблемы.

Если вы не можете изменить существующие классы, но вам нужны некоторые изменения для них, самый простой способ сделать это — создать новый и улучшенный подкласс, в котором доступны необходимые вам изменения.

Учитывая, что унаследованная кодовая база, по-видимому, будет изменяться вне вашего контроля, единственный способ справиться с этими изменениями — генерировать эти типы во время выполнения. К счастью, C# позволяет создавать промежуточный язык, который может решить именно эту проблему.

Вы должны начать с DefineType метод, доступный из класса ModuleBuilder. В частности, вы хотите использовать перегрузку, принимая String, TypeAttributes и Type (представляющий класс, который вы расширяете)

person Vogel612    schedule 20.07.2019
comment
Я думал об этом, согласен, это может быть решением, но как бы вы это сделали программно? Я думал об использовании отражения для перестроения класса, если он имеет вложенный тип с автоматическим свойством только для Getter, но это кажется слишком сложным, но я могу ошибаться. - person alexlomba87; 20.07.2019
comment
Учтите, что я не могу просто вручную создать новый и улучшенный подкласс для каждого из таких классов — их слишком много, и устаревший код будет изменен (вне моего контроля), поэтому в противном случае ремонтопригодность будет проблемой. - person alexlomba87; 20.07.2019
comment
Для создания классов во время выполнения вы, вероятно, захотите использовать генераторы IL из пространства имен System.Reflection.Emit. Чтобы сгенерировать новый тип, вам нужен метод ModuleBuilder.DefineType(String, TypeAttributes, Type). - person Vogel612; 20.07.2019

Вы указали, что

Если два объекта имеют одинаковый хэш, вы считаете их точными копиями друг друга

Пожалуйста, поймите, что хэш имеет конечную энтропию, а исходные объекты имеют бесконечную энтропию. Хэш-коллизии обязательно произойдут. Давайте посмотрим на некоторые примеры:

public class Point 
{
    public int X;
    public int Y;
}

public class Coordinate
{
    public int X;
    public int Y;
}

Допустим, мы вычисляем хэш как X ^ Y. Экземпляры обоих классов могут иметь один и тот же хэш, даже если они представляют разные классы. Даже если взять только один из этих классов, если мы возьмем один экземпляр с X = 1, Y = 2, а другой — с X = 2, Y = 1, у них будет один и тот же хэш. Конечно, вы можете оптимизировать алгоритм хеширования, чтобы снизить риск коллизий, но вы не можете гарантировать, что таких коллизий можно избежать в любое время.

Вместо этого я бы реализовал метод DeepEquals. Это требует больше усилий (если вы пишете это самостоятельно). Но при правильной реализации он может гарантировать, что два объекта будут копиями.

person Community    schedule 20.07.2019
comment
Это отличное наблюдение, спасибо. Мне нужно было бы также включить имя класса и пространство имен в сериализацию, чтобы избежать конфликтов, правда; Я думаю, этого должно быть достаточно. Предложение DeepEquals также превосходно. Я посмотрю на это. - person alexlomba87; 20.07.2019