Search code examples
mongodbmongodb-.net-driver

Filtering collection by _t on property


I already implemented with mongo/c# a scenario where I store on the same collection different types of classes. Everything works great and I can do this in the end:

var collection = _mongoDb.GetCollection<TLogType>(CollectionNames.Logs).OfType<TLogType>();

Now I needed a similar functionality but for properties. Basically, I have collection (normal one, not polymorphic) which has an array of contacts. I wanted each item in the array to be of a specific contact type. The document I came up with and it is working looks like this (smaller for brevity):

Contacts" : [ 
    {
        "_t" : [ 
            "Contact", 
            "ExternalContact"
        ],
        "Role" : "Product Emergency",
        "Name" : "Fred",
        "Email" : "fred@gmail",
        "Phone" : "1231",
        "Availability" : "",
    }, 
    {
        "_t" : [ 
            "Contact", 
            "InternalContact"
        ],
        "Role" : "Manager",
        "Name" : "Mickey"
    }
]

What I want now is to retrieve contacts by type. We have the OfType<TResult> but it's only available to MongoQueryable. So, something like this:

public async Task<IEnumerable<string>> GetContacts<TContactType>(CancellationToken token) where TContactType : Contact
{

    var collection = _mongoDb.GetCollection<Person>(CollectionNames.Person)();

    var projectedListOfContacts =
        await collection.Find(
            s => s.Contacts != null && 
            s.Contacts.OfType<TContactType> // how to filter by type on a property?
        )
            .Project(dto => dto.Contacts).
            .ToListAsync(token);

    ...
} 

Any way to do this without having to get all contacts and work with them in memory?

The relevant classes are:

public class Person
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public IEnumerable<Contact> Contacts {get; set;}
}

public abstract class Contact : IContact
{
    public string Role { get; private set; }
    public string Name { get; private set; }

    protected Contact(string role, string name)
    {
        Role = role ?? throw new ArgumentNullException(nameof(role));
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }
}

public class ExternalContact : Contact
{

    public string Email { get; private set; }   
    public string Phone { get; private set; }   
    public string Availability { get; private set; }

    public ExternalContact(string role, string name, string email, string phone, string availability)
        : base(role, name)
    {
        Email = email ?? throw new ArgumentNullException(nameof(email));
        Phone = phone ?? throw new ArgumentNullException(nameof(phone));
        Availability = availability;
    }
}

Update: The answer helped me figure it out.. so in the end I could do this:

var contacts =
await Collection.Find(s => s.Contacts != null)
    .Project(dto => dto.Contacts.OfType<TContactType>())
    .ToListAsync(token);

Turned out that the requirements changed.. and the Contact array will always have at least one of each type. So there was no reason to filter.. but I took advantage of the Projection that only returned the type I wanted.


Solution

  • Just taking a wild shot here but I believe this may be what you are looking for, but it's hard to be sure without a mcve.

            var nullFilter = Builders<Person<TContactType>>.Filter.Ne(person => person.Contacts, null);
            var typeFilter = Builders<Person<TContactType>>.Filter.OfType<Contact, TContactType>(person => person.Contacts);
            var combinedFilter = Builders<Person<TContactType>>.Filter.And(nullFilter, typeFilter);
    
            var projectedListOfContact = collection.Find(combinedFilter).Project(dto => dto.Contacts).ToListAsync(token);      
    

    Updated Builder

    var query = Builders<Person>.Filter.ElemMatch(x => x.Contacts, Builders<Contact>.Filter.OfType<ExternalContact>());
    collection.Find(query);
    

    Which of course can also be used with your generic T.

    This should return find({ "Contacts" : { "$elemMatch" : { "_t" : "ExternalContact" } } })