Как получить безопасные идентификаторы объектов Mongo в С#?

Скажем, у меня есть три коллекции в Mongo: flavor, color и cupcake. Каждая коллекция имеет свой собственный _id (очевидно), а коллекция cupcake ссылается на _id в flavor и cupcake, например:

{
  "_id": ObjectId("123"),
  "flavorId": ObjectId("234"),
  "colorId": ObjectId("345"),
  "moreData": {}
}

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

Я хочу иметь возможность искать cupcake объектов по flavorId и по colorId (и они должным образом индексируются для таких поисков). Однако оба поля ObjectId, и я хочу, чтобы кто-то случайно не искал colorId с flavorId. Как спроектировать объект и класс репозитория таким образом, чтобы colorId и flavorId были разными типами, чтобы компилятор не позволял менять их местами, но при этом сохранял оба идентификатора как ObjectId?

Моей первой мыслью было расширить ObjectId и передать расширенный объект, но ObjectId — это struct, который не может быть расширен.


person meustrus    schedule 05.07.2016    source источник
comment
Это кажется немного излишним? и я хочу, чтобы кто-то случайно не искал colorId с flavorId. Вы не можете допустить, чтобы кто-то вводил ошибку   -  person Liam    schedule 05.07.2016
comment
Легче, чем можно подумать, ввести эту конкретную ошибку. Допустим, у вас есть объект репозитория, и вы думаете, что я получу все cupcake объекты к flavorId. Таким образом, вы вызываете cupcakeRepository.Find(ObjectId flavorId), потому что IntelliSense очень любезно предложил это, и вы не прочитали имя аргумента. Эй, он компилируется! Даже ваши модульные тесты проходят успешно, потому что вы смоделировали метод в соответствии с тем, что вы думали, он делал.   -  person meustrus    schedule 05.07.2016
comment
Или cupcakeRepository.Find(ObjectId colorId). Я на самом деле забыл, какой из них я пытался использовать между вводом этих двух предложений. Видите ли, это не просто о каком-то другом разработчике, это чтобы защитить меня от самого себя.   -  person meustrus    schedule 05.07.2016


Ответы (3)


Вы не сможете предотвратить эти ошибки, но вы можете использовать числовые интервалы, чтобы «кому-то» было легче найти проблему.

Если я не ошибаюсь, вы можете установить идентификаторы, чтобы вы могли использовать «префикс» для каждого типа.

Цвета могут начинаться с 1000, вкусы с 2000 и так далее...

person Woozar    schedule 05.07.2016
comment
К сожалению, это не помогает решить проблему. Это может гарантировать, что неправильный вызов всегда ничего не вернет, но теперь программист должен знать даже о большем количестве подводных камней, а не думать о том, является ли его ObjectId colorId или flavorId. - person meustrus; 05.07.2016
comment
Им не нужно это знать. Знать это необязательно, но если он знает, ему становится легче. Как только он начнет жонглировать этими идентификаторами, он, скорее всего, найдет шаблон без какой-либо документации. - person Woozar; 06.07.2016

Хм, это своего рода программные проблемы, потому что в большинстве репозиториев ID является чем-то общим (например, целыми числами). Имея это в виду, мы могли бы принудительно передать дополнительный параметр вместо изменения базового объекта, как это пуленепробиваемое решение

cupcakeRepository.Find(ObjectId flavorId, ÒbjectType ÒbjectType.Flavor)

или просто расширить репозиторий, чтобы он был более подробным

cupcakeRepository.FindByColor(ObjectId id)

cupcakeRepository.FindByFlavor(ObjectId id)
person profesor79    schedule 06.07.2016

Так что я закончил тем, что укусил пулю за создание специфичного для Mongo мусора, чтобы заставить собственный класс работать для этого. Итак, вот моя замена ObjectId:

public struct DocumentId<T> : IEquatable<DocumentId<T>>
{
    static DocumentId()
    {
        BsonSerializer.RegisterSerializer(typeof(DocumentId<T>), DocumentIdSerializer<T>.Instance);
        BsonSerializer.RegisterIdGenerator(typeof(DocumentId<T>), DocumentIdGenerator<T>.Instance);
    }

    public static readonly DocumentId<T> Empty = new DocumentId<T>(ObjectId.Empty);
    public readonly ObjectId Value;

    public DocumentId(ObjectId value)
    {
        Value = value;
    }

    public static DocumentId<T> GenerateNewId()
    {
        return new DocumentId<T>(ObjectId.GenerateNewId());
    }

    public static DocumentId<T> Parse(string value)
    {
        return new DocumentId<T>(ObjectId.Parse(value));
    }

    public bool Equals(DocumentId<T> other)
    {
        return Value.Equals(other.Value);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        return obj is DocumentId<T> && Equals((DocumentId<T>)obj);
    }

    public static bool operator ==(DocumentId<T> left, DocumentId<T> right)
    {
        return left.Value == right.Value;
    }

    public static bool operator !=(DocumentId<T> left, DocumentId<T> right)
    {
        return left.Value != right.Value;
    }

    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }

    public override string ToString()
    {
        return Value.ToString();
    }
}

public class DocumentIdSerializer<T> : StructSerializerBase<DocumentId<T>>
{
    public static readonly DocumentIdSerializer<T> Instance = new DocumentIdSerializer<T>();

    public override DocumentId<T> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        return new DocumentId<T>(context.Reader.ReadObjectId());
    }

    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DocumentId<T> value)
    {
        context.Writer.WriteObjectId(value.Value);
    }
}

public class DocumentIdGenerator<T> : IIdGenerator
{
    public static readonly DocumentIdGenerator<T> Instance = new DocumentIdGenerator<T>();

    public object GenerateId(object container, object document)
    {
        return DocumentId<T>.GenerateNewId();
    }

    public bool IsEmpty(object id)
    {
        var docId = id as DocumentId<T>? ?? DocumentId<T>.Empty;
        return docId.Equals(DocumentId<T>.Empty);
    }
}

Параметр типа T может быть любым; он никогда не используется. Это должен быть тип вашего объекта, например:

public class Cupcake {
    [BsonId]
    public DocumentId<Cupcake> Id { get; set; }
    // ...
}

Таким образом, ваш класс Flavor имеет идентификатор типа DocumentId<Flavor>, а ваш класс Color имеет идентификатор типа DocumentId<Color>, и они никогда не будут взаимозаменяемы. Теперь я также могу создать CupcakeRepository с помощью следующих однозначных методов:

public interface ICupcakeRepository {
    IEnumerable<Cupcake> Find(DocumentId<Flavor> flavorId);
    IEnumerable<Cupcake> Find(DocumentId<Color> colorId);
}

Это также должно быть безопасным с существующими данными, потому что сериализованное представление точно такое же, просто ObjectId("1234567890abcef123456789").

person meustrus    schedule 07.07.2016