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);
}
}
In our case, the issue was that our collection properties weren't being serialised as JSON arrays.
We solved this by setting the
CollectionStorageproperty of the Json serialiser toCollectionStorage.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
TypeNameHandlingtoNone, 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.)