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