OData / odata.net

ODataLib: Open Data Protocol - .NET Libraries and Frameworks
https://docs.microsoft.com/odata
Other
686 stars 349 forks source link

Inject ODataUriResolver's property EnableCaseInsensitive will always override to false #695

Closed VikingsFan closed 5 years ago

VikingsFan commented 8 years ago

Short summary (3-5 sentences) describing the issue.

Assemblies affected

ODataLib 7.0

https://github.com/OData/odata.net/blob/ODataV4-7.x/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs#L50 will always set the property to false after get UriResolver from DI containder : https://github.com/OData/odata.net/blob/ODataV4-7.x/src/Microsoft.OData.Core/UriParser/ODataUriParserConfiguration.cs#L37

t03apt commented 6 years ago

I think this issue is resolved and should be closed. It works fine in 7.3.1.

hidegh commented 6 years ago

@t03apt yes, that part is fixed, yet still if I do register:

services.TryAddSingleton<ODataUriResolver>(_ => new ODataUriResolver() { EnableCaseInsensitive = true });

...only PascalCase property names (like the names in .NET) will be accepted, no camelCase (like i'd have them on the Angular side).

This is a big issue, cause API's are used by JavaScript, and while .NET uses PascalCase for prop-names, JavaScript uses camelCase. Mixing those 2 is a pain. So this is really a TOP PRIORITY for using OData.

Actually what I don't get that a non Microsoft based OData implementation exists - where this works, wonder how they did it...

t03apt commented 6 years ago

@hidegh If you are using .net core and you have .EnableDependencyInjection() in your code, then try this: https://github.com/t03apt/AspNetCoreOdataTest/blob/96612ae5fd5fba5f3d38c8378f7dca62e7e59373/AspNetCoreOdataTest/Startup.cs#L54

mikepizzo commented 6 years ago

@hidegh; can you provide a repo of exactly what is not working? i.e., a specific URL that fails to parse based on a given model?

The initial issue should be fixed, and case insensitive shouldn't differentiate between camelCase, PascalCase, or any other case.

hidegh commented 6 years ago

@mikepizzo This issue was already solved/fixed. So currently everything works as expected.

image

I do have a working setup, where any action-result with IQueryable can be odata-eabled (for read), using a custom attribute - the extra thing added was the code to execute IQueryable, so that we do catch possible errors within MVC infastructure...

Startup.cs config:

            /*
            services.TryAddSingleton(_ =>
            {
                var mb = new ODataConventionModelBuilder(_, true);
                mb.EnableLowerCamelCase();
                return mb;
            });
            */

            services.TryAddSingleton<ODataUriResolver>(_ => new ODataUriResolver() { EnableCaseInsensitive = true });

            // Workaround: https://github.com/OData/WebApi/issues/1177
            services.AddMvcCore(options =>
            {
                foreach (var outputFormatter in options.OutputFormatters.OfType<ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
                {
                    outputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
                }
                foreach (var inputFormatter in options.InputFormatters.OfType<ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
                {
                    inputFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
                }
            });

---

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "error",
                    template: "Home/Error/{errorCode?}",
                    defaults: new { controller = "Home", action = "Error" });

                routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "apiDefault",
                    template: "api/{controller=Home}/{action=Index}/{id?}");

                // Odata
                routes.Count().Filter().OrderBy().Expand().Select().MaxTop(null);

                // Work - around for #1175
                routes.EnableDependencyInjection(odataContainerBuilder =>
                {
                    odataContainerBuilder.AddService(
                        Microsoft.OData.ServiceLifetime.Singleton,
                        typeof(ODataUriResolver),
                        _ => app.ApplicationServices.GetRequiredService<ODataUriResolver>());
                });
            });

Custom attribute:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Query;
using System.Linq;
using System;
using Newtonsoft.Json;
using System.Data.Entity.Infrastructure;
using System.Data.Entity;
using System.Collections;

namespace R
{
    /// <summary>
    /// Replacement for Microsoft.AspNet.OData.EnableQueryAttribute that returns a valid OData response, see: ODataPagedResult.
    /// </summary>
    public class ODataQueryable : EnableQueryAttribute
    {
        public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
        {
            // NOTE: altering OData query:
            // https://d-fens.ch/2017/02/26/modifying-odataqueryoptions-on-the-fly/
            // https://tutel.me/c/programming/questions/33660648/odata+v4+modify+filter+on+server+side
            var baseResult = base.ApplyQuery(queryable, queryOptions);
            return baseResult;
        }

        public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
        {
            // NOTE:
            // issue https://github.com/OData/WebApi/issues/159
            // workaround: https://stackoverflow.com/questions/27545207/items-count-in-odata-v4-webapi-response

            base.OnActionExecuted(actionExecutedContext);

            // Skip post processing on exceptions...
            if (actionExecutedContext.Exception != null)
                return;

            // ...and on errors
            var response = actionExecutedContext.HttpContext.Response;
            if (response == null || !response.IsSuccessStatusCode() || actionExecutedContext.Result == null)
                return;

            //
            // Post-process
            var responseContent = actionExecutedContext.Result as ObjectResult;

            var itemsQueryable = responseContent?.Value as IQueryable;
            if (itemsQueryable == null)
                throw new NotSupportedException($"Controller action result with OData queries applied has to be an IQueryable<>, returned type is: {responseContent?.Value?.GetType()}!");

            // NOTE: if below does not work, we can try: count = long.Parse(Request.Properties["System.Web.OData.TotalCount"]‌​.ToString())
            var nextLink = actionExecutedContext.HttpContext.Request.ODataFeature().NextLink;
            var count = actionExecutedContext.HttpContext.Request.ODataFeature().TotalCount;

            //
            // Execute (so we are able to catch errors with error attribute)
            object items;

            var dbQueryable = itemsQueryable as IDbAsyncEnumerable;

            if (dbQueryable != null)
            {
                // EF
                var result = itemsQueryable.ToListAsync().Result;
                items = result;
            }
            else
            {
                // Other...
                var elementType = itemsQueryable.ElementType;

                var toListMethodInfo = typeof(Enumerable).GetMethod("ToList").MakeGenericMethod(elementType);

                var result = toListMethodInfo.Invoke(null, new object[] { itemsQueryable });
                items = result;

            }

            //
            // Return
            responseContent.Value = new ODataPagedResult((IEnumerable)items, nextLink, count);
        }
    }

    /// <summary>
    /// NOTE: theres a PageResult<T> class already in .NET but it does not serialize via JSON as proper OData result...
    /// </summary>
    public class ODataPagedResult
    {
        public ODataPagedResult(IEnumerable items, Uri nextLink, long? count)
        {
            Value = items;
            NextLink = nextLink?.ToString() ?? "";
            Count = count;
        }

        [JsonProperty("value")]
        public IEnumerable Value { get; set; }
        [JsonProperty("@odata.count")]
        public long? Count { get; set; }
        [JsonProperty("@odata.nextLink")]
        public string NextLink { get; set; }
    }
}
bdebaere commented 5 years ago

@hidegh Am I reading this correctly that you no longer have issues with casing and thus this issue can be closed?

hidegh commented 5 years ago

@bdebaere correct, it's working now, from that versio 7.0.0 beta or so...

mikepizzo commented 5 years ago

Thanks @hidegh and @bdebaere; closing the issue.