简体   繁体   中英

How to have type-safe Mongo Object Ids in C#?

Say I have three collections in Mongo: flavor , color , and cupcake . Each collection has its own _id (obviously) and the cupcake collection references the _id s in flavor and cupcake , like so:

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

This is a toy example, of course, and there is more stuff in these collections. That's not important to this question, except that it's the moreData that I'm really looking for when I query.

I want to be able to look up cupcake objects by flavorId and by colorId (and they are appropriately indexed for such lookups). However, both fields are ObjectId , and I want to avoid somebody accidentally looking for a colorId with a flavorId . How can I design the object and a repository class such that colorId and flavorId will be different types so that the compiler will not allow interchanging them, but still store both ids as ObjectId?

My first thought was to extend ObjectId and pass the extended object around, but ObjectId is a struct which cannot be extended.

You won't be able to prevent those errors, but you can use number intervals to make it easier for "someone" to find the problem.

If I'm not mistaken you can set the ids, so you can use a "prefix" for every kind.

Colors could start with 1000, flavors with 2000 and so on...

Hmm, it is a kind of soft problems, because in most repositories ID is something common (like integers). So having this in mind we could enforce passing an extra parameter instead of changing base object, like this bulletproof solution

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

or just extend repository to be more verbose

cupcakeRepository.FindByColor(ObjectId id)

cupcakeRepository.FindByFlavor(ObjectId id)

So I ended up biting the bullet on building the Mongo-specific junk to make a custom class work for this. So here is my drop-in replacement for 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);
    }
}

The type parameter T can be anything; it is never used. It should be the type of your object, like so:

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

This way, your Flavor class has an Id of type DocumentId<Flavor> and your Color class has an Id of type DocumentId<Color> , and never shall the two be interchanged. Now I can create a CupcakeRepository with the following unambiguous methods as well:

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

This should be safe with existing data as well because the serialized representation is exactly the same, just an ObjectId("1234567890abcef123456789") .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM