Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$filter doesnt work with Enums type - dotnet core beta1 #1186

Closed
HaroonSaid opened this issue Dec 30, 2017 · 32 comments
Closed

$filter doesnt work with Enums type - dotnet core beta1 #1186

HaroonSaid opened this issue Dec 30, 2017 · 32 comments

Comments

@HaroonSaid
Copy link

HaroonSaid commented Dec 30, 2017

We have some tests that fails on syntax as follows, what do we need to do to make Enums work
http://localhost:5000/api/projects?$filter=Status eq 'Active'
http://localhost:5000/api/projects?$filter=Status eq 1
The Errors are:
"error": "The method or operation is not implemented."
or
A binary operator with incompatible types was detected. Found operand types 'Bluebeam.SecurityCore.Enum.Status' and 'Edm.Int32' for operator kind 'Equal'."
}
The bugs seems to be in the method when a Filter is a constant and code needs to look thru all the assemblies

internal static IEnumerable<Type> GetLoadedTypes(IWebApiAssembliesResolver assembliesResolver)

The config is as follows:

            app.UseMvc(routes =>
            {
            app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
                routes.MapODataServiceRoute("odata", "api", ODataConfig.GetEdmModel(app.ApplicationServices));
                routes.SetDefaultODataOptions(new Microsoft.AspNet.OData.ODataOptions { NullDynamicPropertyIsEnabled = true });
                routes.Filter();
                routes.MaxTop(null);
                routes.Select();
                routes.OrderBy();
                routes.Count();
                routes.SetDefaultQuerySettings(new Microsoft.AspNet.OData.Query.DefaultQuerySettings { EnableCount = true, EnableFilter = true, MaxTop = null, EnableOrderBy = true, EnableExpand = true, EnableSelect = true });
                routes.EnableDependencyInjection(b => b.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), typeof(UnqualifiedCallAndEnumPrefixFreeResolver)));
            });

The model looks as follows

public class Project
    {
        [JsonConverter(typeof(ShortGuidConverter))]
        public Guid OrganizationId { get; set; }

        [JsonConverter(typeof(ShortGuidConverter))]
        public Guid ProjectId { get; set; }

        public string Name { get; set; }
        public Status Status { get; set; }
        public ProjectType ProjectType { get; set; }
        public DateTimeOffset CreatedOn { get; set; }
        public DateTimeOffset ModifiedOn { get; set; }
        public string CreatedBy { get; set; }
        public string ModifiedBy { get; set; }
        public string Owner { get; set; }
        public int? Version { get; set; }
    }

and Status is, the Enum is in a different assembly (netstandard1.1) project

public enum Status
    {
        Active = 1,
        Inactive = 2
    }

The controller is:

        [Route("Projects")]
        public async Task<IActionResult> GetAllProjects(ODataQueryOptions<Project> queryOptions)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            queryOptions.Validate(ValidationSettings);  // We allow $filter 

            var projects = await _repository.GetProjectsAsync();

            var data = projects.Select(p =>_mapper.Map<Project>(p));
            var items = queryOptions.ApplyTo(data.AsQueryable(), OdataSettings) as IQueryable<Project>;
            var pageResult = new PageResult<Project>(items, Request.GetNextPageLink(PageSize), null);
            return Ok(pageResult);
        }

The exception seems to be occurring on the following lines of code:

      internal static IEnumerable<Type> GetLoadedTypes(IWebApiAssembliesResolver assembliesResolver)
        {
            List<Type> result = new List<Type>();

            if (assembliesResolver == null)
            {
                return result;
            }

            // Go through all assemblies referenced by the application and search for types matching a predicate
            IEnumerable<Assembly> assemblies = assembliesResolver.Assemblies; // HERE
            foreach (Assembly assembly in assemblies)
            {
                Type[] exportedTypes = null;
                if (assembly == null || assembly.IsDynamic)
                {
                    // can't call GetTypes on a null (or dynamic?) assembly
                    continue;
                }
 ...
 ...
@HaroonSaid HaroonSaid changed the title $filter doesnt work with Enums type $filter doesnt work with Enums type - dotnet core beta1 Dec 30, 2017
@biaol-odata
Copy link
Contributor

@robward-ms: netstandard1.1 is mentioned above. Should it be supported?

@robward-ms
Copy link
Contributor

@biaol-odata - Yes, netstandard 1.1 for a model class should be fine.

@HaroonSaid - There is an unresolved issue that does not get hit in the UT or E2E tests but the assembly resolver is not fully implemented in a few cases, this appears to have hit that.

Can you provide the full callstack?

@HaroonSaid
Copy link
Author

The call stack

System.NotImplementedException: The method or operation is not implemented.
   at Microsoft.AspNet.OData.Adapters.WebApiAssembliesResolver.get_Assemblies()
   at Microsoft.AspNet.OData.TypeHelper.GetLoadedTypes(IWebApiAssembliesResolver assembliesResolver)
   at Microsoft.AspNet.OData.Formatter.EdmLibHelpers.GetMatchingTypes(String edmFullName, IWebApiAssembliesResolver assembliesResolver)
   at Microsoft.AspNet.OData.Formatter.EdmLibHelpers.GetClrType(IEdmType edmType, IEdmModel edmModel, IWebApiAssembliesResolver assembliesResolver)
   at Microsoft.AspNet.OData.Formatter.EdmLibHelpers.GetClrType(IEdmTypeReference edmTypeReference, IEdmModel edmModel, IWebApiAssembliesResolver assembliesResolver)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindConstantNode(ConstantNode constantNode)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(QueryNode node)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindBinaryOperatorNode(BinaryOperatorNode binaryOperatorNode)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(QueryNode node)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindExpression(SingleValueNode expression, RangeVariable rangeVariable, Type elementType)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.BindFilterClause(FilterBinder binder, FilterClause filterClause, Type filterType)
   at Microsoft.AspNet.OData.Query.Expressions.FilterBinder.Bind(IQueryable baseQuery, FilterClause filterClause, Type filterType, IServiceProvider requestContainer)
   at Microsoft.AspNet.OData.Query.FilterQueryOption.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNet.OData.Query.ODataQueryOptions`1.ApplyTo(IQueryable query, ODataQuerySettings querySettings)
   at Microsoft.AspNet.OData.Query.ODataQueryOptions.ApplyTo(IQueryable query, ODataQuerySettings querySettings, AllowedQueryOptions ignoreQueryOptions)
   at Api.Controllers.OrganizationController.<Get>d__4.MoveNext() in Z:\Code\acme\Source\api\Controllers\OrganizationController.cs:line 85

@skarpovru
Copy link

Have the same issue. Would be nice to have support of Enums in filters

@DJHightower
Copy link

Is somebody able to provide a hotfix of some sort?

@robward-ms robward-ms added the P2 label Feb 5, 2018
@robward-ms
Copy link
Contributor

I attempted a repo today and did not get this callstack. I did merge with master recently but I'm unsure how that has impacted the issue.

I was able to get the scenario working. However, it involved using the StringAsEnumResolver for which we have little documentation. There is a SO discussion here: https://stackoverflow.com/questions/40997153/how-to-enablecaseinsensitive-enableenumprefixfree-and-enableunqualifiednamecall

The change I made as applied to your sample looks like:

original: routes.MapODataServiceRoute("odata", "api", ODataConfig.GetEdmModel(app.ApplicationServices));

new: routes.MapODataServiceRoute("odata", "api", builder =>
builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => edmModel)
.AddService<IEnumerable>(Microsoft.OData.ServiceLifetime.Singleton, sp =>
ODataRoutingConventions.CreateDefaultWithAttributeRouting("OData", routes))
.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => new StringAsEnumResolver()));

Once the StringAsEnumResolver was added the config, I was able to filter by enum using:

http://localhost:5000/api/projects?$filter=Status eq 'Active'

Can one of you try this configuration?

@robward-ms
Copy link
Contributor

#1231 filed for documenting how to configure prefix-free enums.

@HaroonSaid
Copy link
Author

HaroonSaid commented Feb 7, 2018 via email

@HaroonSaid
Copy link
Author

HaroonSaid commented Feb 19, 2018

@robward-ms - Hi Rob, I tried your suggestions, but I still can't couldn't get the code to work.
My code is a follows:

app.UseMvc(routes =>
            {
                routes.MapRoute(name: "default", template: "{controller=Home}/{action=Index}/{id?}");
                routes.MapODataServiceRoute("odata", "api", builder => 
                        builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataConfig.GetEdmModel(app.ApplicationServices))
                               .AddService<IEnumerable>(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("api", routes))
                               .AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => new StringAsEnumResolver()));
                routes.SetDefaultODataOptions(new Microsoft.AspNet.OData.ODataOptions { NullDynamicPropertyIsEnabled = true });
                routes.Filter().MaxTop(null).Select().OrderBy().Count();
                routes.EnableDependencyInjection(); //b => b.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), typeof(UnqualifiedCallAndEnumPrefixFreeResolver)));

            });

The error that I keep getting is, different then before.

Microsoft.OData.ODataException: A binary operator with incompatible types was detected. Found operand types 'Bluebeam.SecurityCore.Enum.Status' and 'Edm.String' for operator kind 'Equal'.
   at Microsoft.OData.UriParser.BinaryOperatorBinder.PromoteOperandTypes(BinaryOperatorKind binaryOperatorKind, SingleValueNode& left, SingleValueNode& right, TypeFacetsPromotionRules facetsPromotionRules)
   at Microsoft.OData.UriParser.BinaryOperatorBinder.BindBinaryOperator(BinaryOperatorToken binaryOperatorToken)
   at Microsoft.OData.UriParser.MetadataBinder.Bind(QueryToken token)
   at Microsoft.OData.UriParser.FilterBinder.BindFilter(QueryToken filter)
   at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilterImplementation(String filter, ODataUriParserConfiguration configuration, ODataPathInfo odataPathInfo)
   at Microsoft.OData.UriParser.ODataQueryOptionParser.ParseFilter()
   at Microsoft.AspNet.OData.Query.FilterQueryOption.get_FilterClause()
   at Microsoft.Extensions.Internal.PropertyHelper.CallNullSafePropertyGetter[TDeclaringType,TValue](Func 2 getter, Object target)
   at Microsoft.AspNetCore.Mvc.Internal.DefaultComplexObjectValidationStrategy.Enumerator.GetModel(Object container, ModelMetadata property)
   at Microsoft.AspNetCore.Mvc.Internal.DefaultComplexObjectValidationStrategy.Enumerator.<>c__DisplayClass10_0.<MoveNext>b__1()
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationEntry.get_Model()
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitChildren(IValidationStrategy strategy)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.VisitComplexType(IValidationStrategy defaultStrategy)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Visit(ModelMetadata metadata, String key, Object model)
   at Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationVisitor.Validate(ModelMetadata metadata, String key, Object model)
   at Microsoft.AspNetCore.Mvc.Internal.DefaultObjectValidator.Validate(ActionContext actionContext, ValidationStateDictionary validationState, String prefix, Object model)
   at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.<BindModelAsync>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter 1.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<<CreateBinderDelegate>g__Bind|0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextResourceFilter>d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeFilterPipelineAsync>d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeAsync>d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.<Invoke>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.Elm.ElmPageMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.Elm.ElmCaptureMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()

I have a 'UnqualifiedCallAndEnumPrefixFreeResolver class' that I want to injected. The code is as follows:

public class UnqualifiedCallAndEnumPrefixFreeResolver : ODataUriResolver
    {
        private readonly StringAsEnumResolver _stringAsEnum;
        private readonly UnqualifiedODataUriResolver _unqualified;

        private bool _enableCaseInsensitive = true;
        public UnqualifiedCallAndEnumPrefixFreeResolver()
        {
            _stringAsEnum = new StringAsEnumResolver { EnableCaseInsensitive = true };
            _unqualified = new UnqualifiedODataUriResolver { EnableCaseInsensitive = true };
        }
        
        public override bool EnableCaseInsensitive
        {
            get { return _enableCaseInsensitive; }
            set
            {
                _enableCaseInsensitive = value;
                _stringAsEnum.EnableCaseInsensitive = _enableCaseInsensitive;
                _unqualified.EnableCaseInsensitive = _enableCaseInsensitive;
            }
        }

        #region UnqualifiedODataUriResolver

        public override IEnumerable<IEdmOperation> ResolveBoundOperations(IEdmModel model, string identifier,
            IEdmType bindingType)
        {
            return _unqualified.ResolveBoundOperations(model, identifier, bindingType);
        }

        public override IEnumerable<IEdmOperation> ResolveUnboundOperations(IEdmModel model, string identifier)
        {
            return _unqualified.ResolveUnboundOperations(model, identifier);
        }

        #endregion

        #region StringAsEnumResolver

        public override void PromoteBinaryOperandTypes(BinaryOperatorKind binaryOperatorKind,
            ref SingleValueNode leftNode, ref SingleValueNode rightNode, out IEdmTypeReference typeReference)
        {
            _stringAsEnum.PromoteBinaryOperandTypes(binaryOperatorKind, ref leftNode, ref rightNode, out typeReference);
        }

        public override IEnumerable<KeyValuePair<string, object>> ResolveKeys(IEdmEntityType type,
            IDictionary<string, string> namedValues, Func<IEdmTypeReference, string, object> convertFunc)
        {
            return _stringAsEnum.ResolveKeys(type, namedValues, convertFunc);
        }

        public override IEnumerable<KeyValuePair<string, object>> ResolveKeys(IEdmEntityType type,
            IList<string> positionalValues, Func<IEdmTypeReference, string, object> convertFunc)
        {
            return _stringAsEnum.ResolveKeys(type, positionalValues, convertFunc);
        }

        public override IDictionary<IEdmOperationParameter, SingleValueNode> ResolveOperationParameters(
            IEdmOperation operation, IDictionary<string, SingleValueNode> input)
        {
            return _stringAsEnum.ResolveOperationParameters(operation, input);
        }

        #endregion
    }

What am I doing wrong?

@gallivantor
Copy link

I'm also having this problem with .net core beta1, trying to get enums to work in the $filter clause, even when fully qualifying the enum name I get the same error:

System.NotImplementedException: The method or operation is not implemented.

Is it correct to say that with the current beta, enums are not supported in filters?

@robward-ms
Copy link
Contributor

The merge with master, which was only available in the nightly builds, may have an impact here. Can you try it with the just-released Beta 2?

@DJHightower
Copy link

DJHightower commented Mar 8, 2018

Exception changed in comparision to beta1 - but still not working with beta2.

status eq 'Finished'
=>Microsoft.OData.ODataException: A binary operator with incompatible types was detected. Found operand types 'Oppia.Model.MatchStatus' and 'Edm.String' for operator kind 'Equal'.

Status eq '2'
=> Microsoft.OData.ODataException: "A binary operator with incompatible types was detected. Found operand types 'Oppia.Model.MatchStatus' and 'Edm.String' for operator kind 'Equal'."

status eq 2
=> Microsoft.OData.ODataException: "A binary operator with incompatible types was detected. Found operand types 'Oppia.Model.MatchStatus' and 'Edm.Int32' for operator kind 'Equal'."

@DJHightower
Copy link

Ok, got it working now - at least with Strings adding the following to Startup:
app.UseMvc(routes =>
{
routes.EnableDependencyInjection(builder =>
{
builder.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), sp => new StringAsEnumResolver());
});
});

@vmandic
Copy link

vmandic commented Mar 21, 2018

Confirming the issue and the fix with StringAsEnumResolver approach that works. Thanks for hinting. EDIT: tested with: 7.0.0-Nightly201803071313

@robward-ms
Copy link
Contributor

@HaroonSaid - The merge with master, which was only available in the nightly builds, may have an impact here. Can you try it with the just-released Beta 2?

@vmandic
Copy link

vmandic commented Mar 21, 2018

Confirming the issue and the fix with StringAsEnumResolver approach that works tested with Beta 2.

@HaroonSaid
Copy link
Author

I will give it a try and let you know. Thanks for the update.

@HaroonSaid
Copy link
Author

Looks like StringAsEnumResolver is fixed with Beta 2

@Misiu
Copy link

Misiu commented Apr 16, 2018

@DJHightower I've tried Your solution with beta 2 and I still get this error:

{"@odata.context":"http://localhost:62439/odata/$metadata#Edm.String","value":"The query specified in the URI is not valid. A binary operator with incompatible types was detected. Found operand types 'CoreTest1.Models.Category' and 'Edm.String' for operator kind 'Equal'."}

My Startup class looks like this:

public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddMvc();
		services.AddOData();
		services.AddODataQueryFilter();
	}

	// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
	{
		if (env.IsDevelopment())
		{
			app.UseDeveloperExceptionPage();
		}

		var modelBuilder = new ODataConventionModelBuilder(app.ApplicationServices);
		modelBuilder.EntitySet<Product>("Products");

		app.UseMvc(builder =>
		{
			builder.EnableDependencyInjection(b =>
			{
				b.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), sp => new StringAsEnumResolver());
			});
			builder.MapODataServiceRoute("ODataRoute", "odata", modelBuilder.GetEdmModel());
			builder.Filter();
			builder.MaxTop(10);
		});

	}
}

Any hints are more than welcome.
@robward-ms could You take a look at this?

@DJHightower
Copy link

Can you show me your class "Product" and the enum "CoreTest1.Models.Category" and the URL resulting in this exception pls?

@Misiu
Copy link

Misiu commented Apr 16, 2018

@DJHightower I've created new project in VS 2017, added those classes:

public class Product
{
	public int ID { get; set; }
	public string Name { get; set; }
	public Category Category { get; set; }
}

public enum Category
{
	Toys = 1,
	Food = 2,
	Cars = 3
}

and this as a controller:

public class ProductsController : ODataController
{
	private readonly List<Product> products = new List<Product>()
	{
		new Product()
		{
			ID = 1,
			Name = "Bread",
			Category = Category.Food
		},
		new Product()
		{
			ID = 2,
			Name = "Handbreak",
			Category = Category.Cars
		},
		new Product()
		{
			ID = 3,
			Name = "Tire",
			Category = Category.Cars
		},
		new Product()
		{
			ID = 4,
			Name = "Toy Car",
			Category = Category.Toys
		}
	};

	[EnableQuery]
	public IQueryable<Product> Get()
	{
		return products.AsQueryable();
	}
}

I'm calling this url: http://localhost:62439/odata/Products?$filter=Category%20eq%20%27Cars%27

In Chrome Dev Tools I can see that Query String Parameters are:

$filter: Category eq 'Cars'

but I get this as a response:

{"@odata.context":"http://localhost:62439/odata/$metadata#Edm.String","value":"The query specified in the URI is not valid. A binary operator with incompatible types was detected. Found operand types 'CoreTest1.Models.Category' and 'Edm.String' for operator kind 'Equal'."}

@DJHightower
Copy link

Cant tell why your version is not working - it looks like it should - but:

if you change your controller to look like:

[Produces("application/json")]
[Route("api/Products")]
public class ProductsController : Controller //ODataController

and use the URL given as Attribute, it works. Maybe you like to experiment from that point.

@Misiu
Copy link

Misiu commented Apr 16, 2018

@DJHightower thanks for looking into this.
Maybe it is something with Enum filter? Where I should put code from Your previous comment?

@DJHightower
Copy link

The code is on the right place in your sample. Just looks like the StringAsEnumResolver is ignored in your odata-Route. But it seems to be correctly handled if the route is given in the controller as a class-attribute ("api/Products" in my sample).

@robward-ms
Copy link
Contributor

@Misiu - Can you post a sample project that shows the issue to github?

@Misiu
Copy link

Misiu commented Apr 16, 2018

@robward-ms not so good with git, but I think all files are included: https://github.com/Misiu/ODataNetCoreTests

@robward-ms
Copy link
Contributor

@Misiu - Project looks good and compiles, thanks. Can you tell me which Uri you are using for the test?

@Misiu
Copy link

Misiu commented Apr 18, 2018

@robward-ms I'm trying to get products in specific category - http://localhost:62439/odata/Products?$filter=Category%20eq%20%27Cars%27

In Dev Tools in Chrome I see thisQuery String Parameters:

$filter: Category eq 'Cars'

but I get error instead of data.

@robward-ms
Copy link
Contributor

@Misiu - Drop the following code in your Configure method. Here are the key differences:

1.) With the EnableDependencyInjection method, you were inserting a StringAsEnumResolver on the default (non-OData) route. You want one on the OData route instead.
2.) The addition of the routing conventions into the service container needs a <IEnumerable> type added. In my earlier example, I only added IEnumerable and I'm not sure why that worked, it is wrong.
3.) My earlier example also inserted StringAsEnumResolver without, which is also wrong. It needs as well.
4.) The Microsoft.Data.Edm namespace is old, the correct one is Microsoft.OData.Edm.


using System.Collections.Generic;
using Microsoft.AspNet.OData.Routing.Conventions;
using Microsoft.Data.Edm; // Remove this namespace
using Microsoft.OData.Edm;

...
			app.UseMvc(builder =>
			{
				//builder.EnableDependencyInjection(b =>
				//{
				//	b.AddService(Microsoft.OData.ServiceLifetime.Singleton, typeof(ODataUriResolver), sp => new StringAsEnumResolver());
				//});

				//builder.MapODataServiceRoute("ODataRoute", "odata", modelBuilder.GetEdmModel());

				//builder.MapODataServiceRoute("ODataRoute", "odata", containerBuilder =>
				//  containerBuilder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => modelBuilder.GetEdmModel())
				//			   //.AddService(ServiceLifetime.Singleton, sp => DefaultRouteConventions(routeName, builder))
				//			   .AddService<ODataUriResolver>(Microsoft.OData.ServiceLifetime.Singleton, sp => new UnqualifiedODataUriResolver { EnableCaseInsensitive = true })
				//			   );

				builder.MapODataServiceRoute("ODataRoute", "odata", containerBuilder =>
					containerBuilder.AddService(Microsoft.OData.ServiceLifetime.Singleton, sp => modelBuilder.GetEdmModel())
						.AddService<IEnumerable<IODataRoutingConvention>>(Microsoft.OData.ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefaultWithAttributeRouting("ODataRoute", builder))
						.AddService<ODataUriResolver>(Microsoft.OData.ServiceLifetime.Singleton, sp => new StringAsEnumResolver()));

				//routeBuilder.SetDefaultODataOptions(new Microsoft.AspNet.OData.ODataOptions { NullDynamicPropertyIsEnabled = true });
				builder.Filter();
				builder.MaxTop(10);
				//routeBuilder.Select();
				//routeBuilder.OrderBy();
				//routeBuilder.Count();
				//routeBuilder.Expand();
				//routeBuilder.SetDefaultQuerySettings(new Microsoft.AspNet.OData.Query.DefaultQuerySettings { EnableCount = true, EnableFilter = true, MaxTop = null, EnableOrderBy = true, EnableExpand = true, EnableSelect = true });
			});

@Misiu
Copy link

Misiu commented Apr 23, 2018

@robward-ms Thank You, not it works great :)
Is it somewhere documented? Probably I'm not the only one that had problems with getting things working.

It would be awesome to have this as part of README - would make life easier.

P.S. Can this be combined with case insensitive routes?
This one works: http://localhost:62439/odata/Products?$filter=Category%20eq%20%27Cars%27

Those two don't:
http://localhost:62439/odata/Products?$filter=category%20eq%20%27Cars%27
http://localhost:62439/odata/products?$filter=category%20eq%20%27cars%27

First one gives me:

{"@odata.context":"http://localhost:62439/odata/$metadata#Edm.String","value":"The query specified in the URI is not valid. Could not find a property named 'category' on type 'CoreTest1.Models.Product'."}

second gives not found.

I've tried adding another service AddService<ODataUriResolver>(Microsoft.OData.ServiceLifetime.Singleton, sp => new UnqualifiedODataUriResolver { EnableCaseInsensitive = true })); but without luck.

Misiu added a commit to Misiu/ODataNetCoreTests that referenced this issue Apr 23, 2018
@robward-ms
Copy link
Contributor

#1231 filed for documenting how to configure prefix-free enums.

Take a look at UnqualifiedCallAndEnumPrefixFreeResolver.cs. I think we only support a single ODataUriResolver so adding two won't perform as expected.

@Misiu
Copy link

Misiu commented Apr 27, 2018

@robward-ms I've replaced
AddService<ODataUriResolver>(Microsoft.OData.ServiceLifetime.Singleton, sp => new StringAsEnumResolver()));
with
AddService<ODataUriResolver>(Microsoft.OData.ServiceLifetime.Singleton, sp => new UnqualifiedCallAndEnumPrefixFreeResolver { EnableCaseInsensitive = true }));

but I get errors when accessing lowercase url's.

I can access standard API controllers via http://localhost:62439/api/Values and http://localhost:62439/api/values. I want to have same for odata endpoint. Is this possible?

I've searched over the internet and I only found case insensitive filters, but nothing about case insensitive routes and properties names for odata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants