Sieve icon indicating copy to clipboard operation
Sieve copied to clipboard

Filtering / sorting complex /nested objects

Open radeanurazvan opened this issue 5 years ago • 2 comments

Hello,

I started using Sieve library a few days ago but I encountered some problems when working with objects / nested values as filter subjects. For instance, consider the following configuration in the FluentApi:

public override void Configure(SievePropertyMapper mapper)
        {
            mapper.Property<BackOfficeUser>(u => u.Email)
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.UserName)
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.FirstName)
                .HasName(nameof(BackOfficeUser.FirstName).PascalToCamelCase())
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.LastName)
                .HasName(nameof(BackOfficeUser.LastName).PascalToCamelCase())
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.PhoneNumber)
                .HasName(nameof(BackOfficeUser.PhoneNumber).PascalToCamelCase())
                .CanFilter();
        }

The names are being specified for the FullName and PhoneNumber just for consistency when querying from the client. There are no custom filter methods defined for that entity. Here is how the FirstName, LastName and PhoneNumber properties are defined:


        public Name FirstName { get; private set; }

        public Name LastName { get; private set; }

        public new PhoneNumber PhoneNumber { get; private set; }

The Name value object:

public class Name : ValueObject
    {
        public static int MaxLength = 50;

        private Name() {}

        public string Value { get; private set; }

        public static Result<Name> Create(Maybe<string> name)
        {
            return name
                .ToResult(KernelDomainMessages.InvalidName)
                .Ensure(x => !string.IsNullOrWhiteSpace(x), KernelDomainMessages.InvalidName)
                .Ensure(HasValidLength, KernelDomainMessages.InvalidName)
                .Map(x => new Name {Value = x});
        }

        public static implicit operator string(Name name)
        {
            return name.Value;
        }

        public static Name Empty => new Name { Value = "" };

        private static bool HasValidLength(string value)
        {
            EnsureArg.IsNotEmpty(value);
            return value.Trim().Length <= MaxLength;
        }

        protected override IEnumerable<object> GetEqualityComponents()
        {
            yield return Value;
        }

        public override string ToString()
        {
            return Value;
        }
    }

The PhoneNumber is just another ValueObject which contains phone numbers specific domain logic. The point here is that my domain entity (BackOfficeUser) is modeled using value objects, which are concrete types defined by me, yet not primitive types like string (as Sieve is expecting). So the problems that I encounter are:

  1. Sieve does not register the configuration for FirstName. Why? Because the configuration is stored as a dictionary which uses types as a key. Since FirstName and LastName are both of type "Name", the LastName configuration overrides the FirstName configuration.

2.Sieve fails when converting string value from SieveModel to my Name type. This could be solved if I define a cast operator in my Name class, but I really don't want to do that since I want to enforce this Value Object to always have a valid state, hence a cast from any string would violate this.

But as you have seen, the value objects described above have a "Value" property which is a string. So I may use it for filtering, right? So my second though was this:

public override void Configure(SievePropertyMapper mapper)
        {
            mapper.Property<BackOfficeUser>(u => u.Email)
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.UserName)
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.FirstName.Value)
                .HasName(nameof(BackOfficeUser.FirstName).PascalToCamelCase())
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.LastName.Value)
                .HasName(nameof(BackOfficeUser.LastName).PascalToCamelCase())
                .CanFilter();

            mapper.Property<BackOfficeUser>(u => u.PhoneNumber.Value)
                .HasName(nameof(BackOfficeUser.PhoneNumber).PascalToCamelCase())
                .CanFilter();
        }

But this fails when Sieve tries to evaluate the property given in the expression at filter time. It fails with an error like "Property 'Value' is not part of type {NameSpace}.BackOfficeUser". It fails because I'm using nested values instead of direct usage.

What I've considered: I considered creating separate filter methods for these value objects, but it doesn't work since they are part of the model and not entities as a whole. I tried to create generic custom filter methods, like :

        public IQueryable<TEntity> Name<TEntity>(IQueryable<TEntity> entities, string op, string value)
            where TEntity : IOwnsFirstName, IOwnsLastName
        {
           //// filter specific code here
            return entities;
        }

But this doesn't get recognized by Sieve when looking for custom filter methods for the fiven

Possible solutions:

  1. Support nested values. At least the second approach should be doable. Getting the nested value at filter time shouldn't be hard.
  2. Expose a new FluentApi method. What about this:
            mapper.Property<BackOfficeUser>(u => u.LastName)
                .HasValue(ln => ln.Value) // Or .HasValue<Name>(ln => ln.Value)
                .HasName(nameof(BackOfficeUser.LastName).PascalToCamelCase())
                .CanFilter();


I think this problem also occurs when trying to sort complex objects / nested values. It would be nice if Sieve could support that.

radeanurazvan avatar Oct 16 '18 20:10 radeanurazvan

` mapper.Property<BackOfficeUser>(u => u.FirstName) .HasName(nameof(BackOfficeUser.FirstName).PascalToCamelCase()) .CanFilter();

mapper.Property<BackOfficeUser>(u => u.LastName) .HasName(nameof(BackOfficeUser.LastName).PascalToCamelCase()) .CanFilter();`

You can try this,

Just don't configure the FirstName and LastName properties for sieve..

Create two (first/last name) or one (full name) custom filters, sieve will call that as it won't find that property on entity definition.

mfahadi avatar Oct 17 '18 14:10 mfahadi

https://github.com/Biarity/Sieve/pull/51

jakoss avatar Jan 18 '19 06:01 jakoss