Unable to query child types using marten and Linq

433 views Asked by At

I am new to Marten and am having terrible difficulty with what should be an easy query. I have a Person class that has a property of type EmailAddress. I want to find any person with a specific email address.

public class EmailAddress : ValueObject
{
    // construction
    protected EmailAddress() { } // needed for deserialization

    public EmailAddress(EmailType type, string emailAddress)
    {
        Guard.Against.Null(type, nameof(type));
        Guard.Against.NullOrWhiteSpace(emailAddress, nameof(emailAddress));
        Guard.Against.NotValidEmail(emailAddress, nameof(emailAddress));

        if (!(type.HasFlag(EmailType.Personal) || type.HasFlag(EmailType.Work) || type.HasFlag(EmailType.Other))) throw new ArgumentException("Unable to craete an EmailAddress without a valid type (i.e., Personal, Work, Other).");

        Type = type;
        Address = emailAddress;
    }


    // properties
    public EmailType Type { get; private set; }
    public string Address { get; private set; }       
}


public class Person : ValueObject
{
    // fields
    List<EmailAddress> _emails = new List<EmailAddress>();

    // Construction
    protected Person() { } // needed for deserialization

    public Person(Guid id, string firstName, string lastName, EmailAddress identityAddress)
    {
        Guard.Against.Default(id, nameof(id));
        Guard.Against.NullOrWhiteSpace(firstName, nameof(firstName));
        Guard.Against.NullOrWhiteSpace(lastName, nameof(lastName));
        Guard.Against.Null(identityAddress, nameof(identityAddress));

        if (!identityAddress.Type.HasFlag(EmailType.IsIdentity))
        {
            identityAddress = new EmailAddress(identityAddress.Type | EmailType.IsIdentity, identityAddress.Address);
        }

        this.Id = id;
        this.FirstName = firstName;
        this.LastName = lastName;
        _emails.Add(identityAddress);
    }

    // properties        
    public Guid Id { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public IReadOnlyList<EmailAddress> Emails { get => _emails; }       
}

I have created a test class that is wired up to PostgreSQl, and am able to insert types using Marten. The serializer is working well and is able to serialize and deserialize these types using their non-public setters. But any queries related to the Emails property on the Person type fail.

    [TestMethod]
    public void Can_Find_Person_Manual()
    {
        // set the test values
        Guid lukeId = Guid.NewGuid();
        EmailAddress lukeEmail = new EmailAddress(EmailType.Work | EmailType.IsIdentity, "[email protected]");
        Person lukeSkywalker = new Person(lukeId, "Luke", "Skywalker", lukeEmail);

        Guid leiaId = Guid.NewGuid();
        EmailAddress leiaEmail = new EmailAddress(EmailType.Personal | EmailType.IsIdentity, "[email protected]");
        Person leiaSolo = new Person(leiaId, "Leia", "Solo", leiaEmail);

        // add a test object
        IDocumentStore database = _container.GetInstance<IDocumentStore>();
        using (var session = database.LightweightSession())
        {
            // clear prior people
            session.DeleteWhere<Person>(p => true);
            session.SaveChanges();

            // Add new people
            session.Store<Person>(lukeSkywalker);
            session.Store<Person>(leiaSolo);
            session.SaveChanges();

            // Start Testing
            IReadOnlyList<Person> people = session.LoadMany<Person>(new Guid[] { lukeId, leiaId });
            Assert.IsTrue(people.Count == 2);

            Person luke = session.Load<Person>(lukeId);
            Assert.IsTrue(luke.Id == lukeId);
            Assert.IsTrue(luke.Emails.Contains(e => e.Address == "[email protected]"));

            Person foundLuke = session.Query<Person>().Where(p => p.FirstName == "Luke").First();
            Assert.IsTrue(foundLuke.Id == lukeId);

            Person leia = session.Load<Person>(leiaId);
            Assert.IsTrue(leia.Id == leiaId);
            Assert.IsTrue(leia.Emails.Contains(e => e.Address == "[email protected]"));


            List<Person> allPeople = session.Query<Person>().ToList(); // works fine 2 items

            List<Person> allLeias = session.Query<Person>().Where(p => p.FirstName == "Leia").ToList();  // works fine 1 item

            List<Person> allBills = session.Query<Person>().Where(p => p.FirstName == "Bill").ToList();  // works fine 0 items

            // List<Person> withEmail = session.Query<Person>().Where(p => p.Emails.Count > 0).ToList(); // ERROR returns empty list

            // List<Person> withEmail = session.Query<Person>().Where(p => p.Emails.Count(e => true) > 0).ToList(); // ERROR: select d.data, d.id, d.mt_version from public.mt_doc_person as d where jsonb_array_length(CAST(d.data ->> 'Emails' as jsonb)) > :arg0$ $ 22023: cannot get array length of a non - array' 

            List<Person> allLukes = session.Query<Person>().Where(p => p.Emails.Any(e => e.Address == "[email protected]")).ToList();  // ERROR NullreferenceException

            //// should get Leia
            List<Person> withPersonalEmail = session.Query<Person>().Where(p => p.Emails.Any(e => e.Type == EmailType.Personal)).ToList(); // ERROR returns empty List

            List<Person> ranchEmail = session.Query<Person>().Where(p => p.Emails.Any(e => e.Address.Contains("ranch"))).ToList();  // ERROR 'Specific method not supported'

            // Below  is the one is need
            Person foundLeia = session.Query<Person>().Where(p => p.Emails.Any(_ => _.Address == "[email protected]")).SingleOrDefault(); // ERROR returns null
            Assert.IsTrue(foundLeia.Id == leiaId);
        }
    }
1

There are 1 answers

0
Caleb Owusu-Yianoma On

In our case, the issue was that our collection properties weren't being serialised as JSON arrays.

We solved this by setting the CollectionStorage property of the Json serialiser to CollectionStorage.AsArray, so collections are serialised as JSON arrays.

See the 'Collection Storage' section at this link: https://martendb.io/documentation/documents/json/newtonsoft/

We also needed to set TypeNameHandling to None, to prevent type metadata from being stored when our collections were serialised and saved to the database. (See the introductory section of the above link for more info.)

var serialiser = new JsonNetSerializer
{
    CollectionStorage = CollectionStorage.AsArray,
    EnumStorage = EnumStorage.AsString
};

serialiser.Customize(x =>
{
    x.TypeNameHandling = TypeNameHandling.None;
});