0

So I have a code first EF 6 layer which has a Contact class of:

public class Contact
{
    [Key]
    public int Id { get; set; }

    [MaxLength(50)]
    public string Prefix { get; set; }

    [MaxLength(50)]
    public string Suffix { get; set; }

    [MaxLength(50)]
    public string FirstName { get; set; }

    [MaxLength(50)]
    public string MiddleName { get; set; }

    [MaxLength(50)]
    public string LastName { get; set; }

    [NotMapped]
    [DisplayName("Full Name")]
    public string FullName
    {
        get
        {
            string tempName =
                (!string.IsNullOrEmpty(Prefix) ? Prefix + " " : "") +
                (!string.IsNullOrEmpty(FirstName) ? FirstName + " " : "") +
                (!string.IsNullOrEmpty(MiddleName) ? MiddleName + " " : "") +
                (!string.IsNullOrEmpty(LastName) ? LastName + " " : "") +
                (!string.IsNullOrEmpty(Suffix) ? Suffix + " " : "");
            return tempName.Trim();
        }
    }

    [MaxLength(50)]
    public string JobTitle { get; set; }

    public bool? Primary { get; set; }
    public bool? Inactive { get; set; }

    public int? Customer_Id { get; set; }

    [ForeignKey("Customer_Id")]
    public virtual Customer Customer { get; set; }

    public virtual ICollection<Email> Emails { get; set; }
    public virtual ICollection<Address> Addresses { get; set; }
    public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
    public virtual ICollection<Note> Notes { get; set; }
}

I have an ASP.NET Web API 2 service running that offers up a list of contacts, but when I perform an OData query of $filter=contains(tolower(FullName), tolower('smith')) I get a BadRequest response. I verified in the WebAPI get method that it is successfully getting results from the database, but it sends back a BadRequest error.

It definitely has something to do with the FullName field either being a calculated field or because it has the NotMapped attribute. When I change the OData query to $filter=contains(tolower(LastName), tolower('smith')) it works fine. I also tried using the display name of "Full Name" in the query instead of "FullName" and that too did not work.

Is there something I need to do to make OData play nice with a calculated or notmapped field?

lencharest
  • 2,825
  • 2
  • 15
  • 22
Xipooo
  • 1,496
  • 1
  • 14
  • 13
  • Is your action method decorated with the `EnableQuery` attribute? – lencharest Jan 07 '16 at 19:27
  • @lencharest no, but I have the following command in my WebApiConfig which enables OData queries on all my controllers: System.Web.Http.OData.Extensions.HttpConfigurationExtensions.AddODataQueryFilter(config); – Xipooo Jan 07 '16 at 19:40
  • Ah, you're using OData 3.0. Any reason you're not using version 4.0? – lencharest Jan 07 '16 at 19:43
  • @lencharest Actually in fact it's Microsoft.AspNet.WebApi.OData 5.7.0. – Xipooo Jan 07 '16 at 19:48
  • @lencharest I have no assembly and therefore no reference for System.Web.Odata. I've searched the framework and Extensions for a reference, as well as Nuget. Nothing. Is it under a different Nuget package name? I have the Microsoft.AspNet.WebApi.OData package and dependencies installed. – Xipooo Jan 07 '16 at 20:01
  • Nevermind.. I found it. Microsoft.AspNet.Odata Found via http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/odata-v4/create-an-odata-v4-endpoint – Xipooo Jan 07 '16 at 20:08
  • Sorry for the confusion. I misspoke regarding 2 implementations in 1 library. If you can move to OData v4, I can give some possible solutions to your issue. – lencharest Jan 07 '16 at 20:14
  • @lencharest I've decommissioned my V3 implementation and now verified V4 is working. I've also switched from the substring method to the contains method for my OData query. However, the problem still persists. Trying to do a contains against a calculated/notmapped field returns a bad request response. I will edit my OP to reflect my changes. – Xipooo Jan 07 '16 at 20:27

2 Answers2

1

Implement an OData function on your ContactsController that takes a string for comparison and returns the filtered set of Contacts. Something like:

    [HttpGet]
    [ODataRoute("Contacts/Default.FullNameContains(value={value})")]
    public IHttpActionResult FullNameContains(string value)
    {
        value = value.ToLower();
        return Ok(db.Contacts.ToList().Where(c => c.FullName.Contains(value)));
    }

Because FullName is computed, the function must perform the filtering in memory.

lencharest
  • 2,825
  • 2
  • 15
  • 22
  • just keep in mind, if you Contacts database has a large number of rows, you will pulling them all into memory to perform the filter. – snow_FFFFFF Jan 07 '16 at 22:28
  • OData Functions feel clunky, but I guess there really isn't any other way with OData. This feels like a glaring bug. I'm sure there are plenty of calculated fields on classes people would want to perform an OData query on. – Xipooo Jan 07 '16 at 22:55
  • Another approach: Add an `ODataQueryOptions` parameter to your `Get` method; scan the `opts.Filter.FilterClause.Expression` for computed properties; if the filter is "dirty", save the original filter, rewrite the request URI and regenerate query options as described in [modify $filter on server side](http://stackoverflow.com/questions/33660648/odata-v4-modify-filter-on-server-side); execute the query; finally, apply the original filter to the query results (again, in-memory). This approach will let you continue using the `$filter` syntax. Now what feels clunky? ;-) – lencharest Jan 07 '16 at 23:14
  • @lencharest, i understand your answer, the problem is the word "must". It must be pulled into memory to do it your way. However, if you have a million contacts, your solution is terrible and will not scale. – snow_FFFFFF Jan 08 '16 at 01:02
1

@lencharest's answer will give you the correct result, but keep in mind that you will be pulling all of the data into memory in order to perform the filter. Not a big deal if you have 100 contacts, but what if you have a large number?

I would create a view in the DB that includes the FullName logic. Then, expose this view as your entity. Then, the filtering can occur in the database and you can have a fully queryable entity.

UPDATE: With a little more consideration, an even better approach would be a computed column in the table for FullName. Assuming you may eventually need to support other verbs (POST, PATCH, etc...), having the full entity definition in the table will keep things straight-forward. By implementing the OData function, you've essentially given up on a queryable odata model. They have their place, but are typically used for complex, multi-entity operations, not to implement a filter on a single attribute for a basic entity.

snow_FFFFFF
  • 3,235
  • 17
  • 29
  • Moving the calculated fields to the DB is OK I suppose, but feels more like a work around than a solution. Especially if you start getting into the discussion of whether or not coders should be concerned with DB implementations. – Xipooo Jan 07 '16 at 22:47
  • maybe, but what you are doing is making the database expose a complete implementation of the entity. On additional thought, I might even make FullName a computed column in the table (as opposed to a view). That would simplify the implementation of other operations, etc... – snow_FFFFFF Jan 08 '16 at 01:04