ardalis / Specification

Base class with tests for adding specifications to a DDD model
MIT License
1.84k stars 240 forks source link

Search by value object's Value property: Translation error #399

Closed jtrose22 closed 1 month ago

jtrose22 commented 1 month ago

I have a defined value object called Title, which exposes its value through a property called "Value" (i.e. Title.Value). I am using EF Core 8.0 and have the value object defined as:

builder
    .Property(x => x.Title)
    .HasConversion(x => x.Value, x => Title.Create(x).Value)
    .HasMaxLength(Title.MaxLength)
    .IsRequired();

When I try to use the Query.Search() function, I am receiving an error that it could not be translated. Examples:

Query.Search(x => x.Title.Value, filter);
Query.Search(x => x.Title.ToString(), filter);
Query.Search(x => EF.Property<string>(x, "Title"), filter);
Query.Where(x => x.Title.Value.Contains(searchFilter));

Error:

The LINQ expression 'DbSet<Books>()
    .Where(s => __Functions_0
        .Like(
            matchExpression: s.Title.Value, 
            pattern: __criteria_SearchTerm_1))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'. See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. 

I know this isn't directly related to the specification package, but I'm looking for some help and maybe a new feature. Maybe something along the lines of:

Query.Search(x => x.Title, filter, translation: t => t.Title.Value);

Is there another approach I could take to use this value object in the search?

enrij commented 1 month ago

If i got it right your Book object is something like

public sealed class Book
{
    // ...
    public Title Title { get; set; }
}

and your Title object is like

public struct Title
{
    private string _value;

    public string Value => _value;

    public short MaxLength => 200;

    public static Title Create(string title)
    {
        // checks on input ...

        _value = title;
    }
}

in this case your conversion for the Title property should be

builder
    .Property(x => x.Title)
    .HasConversion(x => x.Value, x => Title.Create(x)) // <-- Remove the final .Value from Title.Create()
    .HasMaxLength(Title.MaxLength)
    .IsRequired() // This can be omitted if title is not null in Book 
jtrose22 commented 1 month ago

@enrij That's almost exactly what I have, but my Title.Create() function returns either errors or the Title object. So the Title.Create(x).Value is needed in the EF Core configuration. I am using the ErrorOr library so the actual method looks like:

public static ErrorOr<Title> Create(string title);

Do you think because I have it configured that way, it's the cause of the issue with translation? I'll change the code a bit and see if that is indeed the issue.

I did find a work around to my issue, but it's not pretty.

Query.Where(x => ((string)(object)x.Title).Contains(filter)); <--- Works
Query.Search(x => x.Title.Value, filter); <--- Does not work
jtrose22 commented 1 month ago

I found my issue. I misconfigured the EF configuration. My original configuration worked in almost every case until I tried to use it in the Search. I then realized that some of my objects worked fine in search, such as Author.Email. The Author object is setup as a complex property, while my Title is not.

I changed:

builder
    .Property(x => x.Title)
    .HasConversion(x => x.Value, x => Title.Create(x).Value)
    .HasMaxLength(Title.MaxLength)
    .IsRequired();

to

builder
    .ComplexProperty(x => x.Title, b =>
    {
        b.Property(x => x.Value)
        .HasColumnName("Title")
        .HasMaxLength(Title.MaxLength)
        .IsRequired();
    });

And now I can use the following:

Query.Search(x => x.Title.Value, filter);

Thanks @enrij, you set my mind in the right direction.