Преобразование несериализуемых классов в массив байтов

#c# #.net #serialization #bytearray #hashcode

#c# #.net #сериализация #bytearray #хэш-код

Вопрос:

У меня есть сценарий, в котором я синхронизирую данные между несколькими ОЧЕНЬ разными системами. (Сами данные похожи, но таблицы в разных системах имеют совершенно разные форматы.) Для облегчения этой синхронизации у меня есть таблица базы данных, в которой хранятся хэши объектов из каждой из систем вместе с ключами элементов и другой соответствующей информацией. Когда хэш объекта из любой системы изменяется, я обновляю другую.

Моя таблица базы данных выглядит примерно так.

 CREATE TABLE [dbo].[SyncHashes](
    [SyncHashId] [int] IDENTITY(1,1) NOT NULL,
    [ObjectName] [nvarchar](50) NULL,
    [MappingTypeValue] [nvarchar](25) NULL,
    [MappingDirectionValue] [nvarchar](25) NULL,
    [SourceSystem] [nvarchar](50) NULL,
    [SourceKey] [nvarchar](200) NULL,
    [SourceHash] [nvarchar](50) NULL,
    [TargetSystem] [nvarchar](50) NULL,
    [TargetKey] [nvarchar](200) NULL,
    [TargetHash] [nvarchar](50) NULL,
    [UpdateNeededValue] [nvarchar](max) NULL,
    [CreatedOn] [datetime] NULL,
    [ModifiedOn] [datetime] NULL,
    [Version] [timestamp] NOT NULL, 
    [IsActive] [bit] NOT NULL,
PRIMARY KEY CLUSTERED 
(
    [SyncHashId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
  

Пока все хорошо. Но…

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

И…

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

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

Для другой системы все не так здорово. Мне передается контекст базы данных из модели entity framework 4 (code first), и объекты НЕ сериализуются.

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

 foreach(var dataItem in context.TableName)
{
    var byteArray = (byte[]) dataItem;
}
  

ОК. Нет проблем.

У меня есть хороший маленький метод расширения, который, как я думал, может помочь.

 public static byte[] ObjectToByteArray<T>(this T obj)
{
    if (obj == null)
        return null;
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();

    bf.Serialize(ms, obj);
    return ms.ToArray();
}
  

Но нет! Если объект (сущность) не сериализуем, эта процедура выдает мне еще одно приятное маленькое (и вполне ожидаемое) исключение.

Итак… Я изменяю процедуру и добавляю предложение where в определение метода следующим образом.

 public static byte[] ObjectToByteArray<T>(this T obj) where T : ISerializable
{
    if (obj == null)
        return null;
    BinaryFormatter bf = new BinaryFormatter();
    MemoryStream ms = new MemoryStream();

    bf.Serialize(ms, obj);
    return ms.ToArray();
}
  

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

Хммм. Нехорошо.

Итак, я собрал хак, чтобы перебрать все свойства объекта и сгенерировать строковое представление, из которого я мог бы построить массив байтов. Это было НЕКРАСИВО и НЕЭФФЕКТИВНО, но это вроде как сделало свое дело.

 public static string ComputeMD5Hash<T>(this T input)
{
    StringBuilder sb = new StringBuilder();

    Type t = input.GetType();
    PropertyInfo[] properties = t.GetProperties();

    foreach (var property in properties)
    {
        sb.Append(property.Name);
        sb.Append("|");
        object value = property.GetValue(input, null);
        if (value != null)
        {
            sb.Append(value);
        }
        sb.Append("|");
    }

    return MD5HashGenerator.GenerateKey(sb.ToString());
}
  

Но…

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

Заранее благодарю!

Комментарии:

1. Я забыл упомянуть, что MD5HashGenerator . Функция GenerateKey(byte[] ByteArray) принимает массив байтов в качестве своего. параметр.

Ответ №1:

создайте массив байтов из объекта, класс которого не помечен как сериализуемый

Для этого можно использовать protobuf-net v2. Загрузите zip-файл, затем обратитесь к protobuf-net сборке.

Рассмотрим это простое определение класса, которое мы хотим сериализовать:

 public class Person
{
    public string Firstname { get; set; }
    public string Surname { get; set; }
    public int Age { get; set; }
}
  

Затем вы можете сериализовать это как массив байтов:

 var person = new Person {Firstname = "John", Surname = "Smith", Age = 30};
var model = ProtoBuf.Meta.TypeModel.Create();
//add all properties you want to serialize. 
//in this case we just loop over all the public properties of the class
//Order by name so the properties are in a predictable order
var properties = typeof (Person).GetProperties().Select(p => p.Name).OrderBy(name => name).ToArray();
model.Add(typeof(Person), true).Add(properties);

byte[] bytes;

using (var memoryStream = new MemoryStream())
{
    model.Serialize(memoryStream, person);
    bytes = memoryStream.GetBuffer();
}
  

Сериализатор protobuf-net сериализует намного быстрее и создает меньший массив байтов [], чем BinaryFormatter

предостережение 1 Это приведет только (в его текущей форме) к сериализации общедоступных свойств вашего класса, что выглядит нормально для вашего использования.
предостережение 2 Это считается хрупким, поскольку добавление нового свойства в Person может означать, что вы не сможете десериализовать Person объект, который был сериализован с предыдущим TypeModel .

Комментарии:

1. Wal, это выглядит ФАНТАСТИЧЕСКИМ решением. Однако в настоящее время я работаю над одной проблемой. Объекты, которые я сериализую, находятся во вложенной иерархии, и у Protobuf, похоже, возникают некоторые трудности с дочерними объектами. (Похоже, он не добавляет их автоматически в модель при добавлении родительского элемента.) Далее я буду использовать отражение дочерних объектов, чтобы автоматически добавлять их в модель, а затем отчитываться. В целом, мне действительно нравится эта библиотека Protobuf. Это кажется невероятно быстрым. Большое спасибо, что поделились этим.

2. @Anthony Gatlin К сожалению, вам придется указать каждый тип в модели, которую вы хотите сериализовать. Я отправил электронное письмо создателю проекта (Марку Гравеллу), чтобы узнать, существует ли существующий способ или кто-то другой это сделал. Между тем, то, что вы предлагаете, перебирая свойства и добавляя каждое из них в модель, которая не является примитивной, звучит достижимо.

3. и да, Protobuf-net работает очень быстро. Он используется этим сайтом под капотом.

4. Важно: поскольку GetProperties() это не гарантирует порядок, вы должны убедиться, что properties он представляет собой повторяющийся порядок. Самым простым подходом было бы просто использовать либо .Select(p => p.Name).OrderBy(x => x).ToArray() , либо Array.Sort(properties); (после LINQ).

5. @Marc, просто из любопытства, почему порядок свойств имеет значение? Использует ли сериализатор позицию свойства, а не имя при выполнении преобразования?