0

Say I have three collections in Mongo: flavor, color, and cupcake. Each collection has its own _id (obviously) and the cupcake collection references the _ids 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.

3
  • This seems a bit redundant? and I want to avoid somebody accidentally looking for a colorId with a flavorId. You can't allow for someone introducing a bug Commented Jul 5, 2016 at 15:40
  • It's easier than one might think to introduce this particular bug. Say you have a repository object and you think, "I'll get all cupcake objects by flavorId". So you call cupcakeRepository.Find(ObjectId flavorId) because IntelliSense very helpfully suggested it and you didn't read the argument name. Hey, it compiles! Even your unit tests pass, because you mocked out the method according to what you thought it did. Commented Jul 5, 2016 at 15:55
  • Or cupcakeRepository.Find(ObjectId colorId). I actually forgot which one I was trying to use between typing those two sentences. See, this isn't just about some other developer, this is to protect me from myself. Commented Jul 5, 2016 at 16:31

3 Answers 3

1

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...

Sign up to request clarification or add additional context in comments.

2 Comments

Unfortunately this doesn't work to solve the problem. It could ensure that making the wrong call always returns nothing, but now the programmer needs to know about even more gotchas, rather than not needing to think about whether the ObjectId they have is a colorId or a flavorId.
They don't need to know it. Knowing it is optional, but if he does, it makes it easier for him. As soon as he starts juggling those ids around, he will most likely find the pattern without any documentation.
0

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)

Comments

0

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").

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.