Webapi: [feature/netcore] "/$count" not work

Created on 4 Jan 2018  路  16Comments  路  Source: OData/WebApi

I did not understand the reason, but when I try to return the count of the records does not come and not the error.

image

It seems duplicate: #1160 #1153

I tried several ways, I did not understand why it does not work, putting in and taking out the "? $ Count = true" return is always the same.

Package

Microsoft.AspNetCore.All _2.0.3_
Microsoft.AspNetCore.OData _7.0.0-beta1_

P3 featurnetcore question

Most helpful comment

I was unable to identify for sure, but test the URL: http://host:port/api/OrganizationTest
Do not forget the capital and tiny that must be the same.

All 16 comments

Source code:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    app.UseAuthentication();

    var builder = new ODataConventionModelBuilder(app.ApplicationServices);
    builder.EntitySet<Conta>(nameof(Conta));

    app.UseMvc(routebuilder =>
    {
        routebuilder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
        routebuilder.MapODataServiceRoute("odata", "api", builder.GetEdmModel());
        routebuilder.EnableDependencyInjection();
    });
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddOData();
    services.AddMvc(options => 
    {
        ////https://github.com/OData/WebApi/issues/597
        ////https://q-a-assistant.info/computer-internet-technology/exception-connecting-excel-to-net-core-v1-1-odata-v4-add-at-least-one-media-type/3883239
        //// loop on each OData formatter to find the one without a supported media type
        foreach (var outputFormatter in options.OutputFormatters.OfType<Microsoft.AspNet.OData.Formatter.ODataOutputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
        {
            // to comply with the media type specifications, I'm using the prs prefix, for personal usage
            outputFormatter.SupportedMediaTypes.Add(new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
        }
        foreach (var inputFormatter in options.InputFormatters.OfType<Microsoft.AspNet.OData.Formatter.ODataInputFormatter>().Where(_ => _.SupportedMediaTypes.Count == 0))
        {
            // to comply with the media type specifications, I'm using the prs prefix, for personal usage
            inputFormatter.SupportedMediaTypes.Add(new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("application/prs.odatatestxx-odata"));
        }
    }).AddJsonOptions(opt =>
    {
        opt.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.None;
        opt.SerializerSettings.Converters.Clear();
        opt.SerializerSettings.Converters.Add(new DtoJsonConverter());
        opt.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
        opt.SerializerSettings.ContractResolver = new DtoContractResolver();

    });

    services.AddDbContext<ErpContext>(options => 
    {
        options.UseMySql(Session.Connection.GetStringConexao());
    });   
}

Controller:

    [Produces("application/json")]
    [Route("api/[controller]")]
    public class ContaController : ODataController
    {
        private ErpContext _erpContext;

        /// <summary>
        /// Inicializa uma nova inst芒ncia da classe <see cref="ContaController"/>.
        /// </summary>
        /// <param name="erpContext"></param>
        public ContaController(ErpContext erpContext)
        {
            _erpContext = erpContext;
        }

        /// <summary>
        /// Retorna todas as contas cadastradas baseado na requisi莽茫o OData
        /// </summary>
        /// <returns>Lista de todos os registros</returns>
        [HttpGet]
        [ProducesResponseType(typeof(void), 401)] //Unauthorized
        [ProducesResponseType(typeof(void), 500)] //Internal Server Error
        [ProducesResponseType(typeof(IQueryable<Conta>), 200)] //OK
        [EnableQuery]
        public IActionResult Get() //Microsoft.AspNet.OData.Query.ODataQueryOptions<Conta> queryOptions
        {
            return Ok(_erpContext.Conta.AsQueryable());
        }

        /// <summary>
        /// Retorna uma conta especifica
        /// </summary>
        /// <para name="codigo">C贸digo da conta</para>
        /// <returns>Registro do c贸digo informado</returns>
        [HttpGet("{codigo}")]
        [ProducesResponseType(typeof(Conta), 200)] //OK
        [ProducesResponseType(typeof(void), 401)] //Unauthorized
        [ProducesResponseType(typeof(void), 404)] //Not Found
        [ProducesResponseType(typeof(void), 500)] //Internal Server Error
        [EnableQuery]
        public IActionResult Get([FromODataUri]int codigo)
        {
            var item = new ContaDal().Where(dto => dto.Codigo == codigo).GetFirst();
            if (item == null)
            {
                return NotFound();
            }
            return Ok(item);
        }

        /// <summary>
        /// Insere uma nova conta
        /// </summary>
        /// <para name="value">Conta para ser inserida</para>
        /// <returns>Conta criada preenchida com o c贸digo de chave prim谩ria</returns>
        [HttpPost]
        [ProducesResponseType(typeof(void), 400)] //Bad Request
        [ProducesResponseType(typeof(void), 401)] //Unauthorized
        [ProducesResponseType(typeof(void), 500)] //Internal Server Error
        [ProducesResponseType(typeof(Dto.Core.ValidationResult), 422)] //Unprocessable Entity
        [ProducesResponseType(typeof(ContaInsertUpdate), 201)] //Created
        public IActionResult Post([FromBody]ContaInsertUpdate value)
        {
            if (value == null)
            {
                return BadRequest();
            }
            var dto = value.ToDto();
            var resultValidation = Bll.Helpers.Validation.Validade(dto);
            if (resultValidation != null && resultValidation.HasError)
            {
                return UnprocessableEntity(resultValidation);
            }
            if (new ContaDal().Insert(dto) == false)
            {
                return StatusCode(500);
            }
            //TODO Retornar o objeto inserido (Com c贸digo)
            return Created(GetAbsoluteUriWithOutQuery(Request) + "/" + value.Codigo, value);
        }

        /// <summary>
        /// Atualiza conta existente
        /// </summary>
        /// <para name="value">Conta para ser atualizada</para>
        [HttpPut("{codigo}")]
        [ProducesResponseType(typeof(void), 200)] //OK
        [ProducesResponseType(typeof(void), 400)] //Bad Request
        [ProducesResponseType(typeof(void), 401)] //Unauthorized
        [ProducesResponseType(typeof(void), 404)] //Not Found
        [ProducesResponseType(typeof(Dto.Core.ValidationResult), 422)] //Unprocessable Entity
        [ProducesResponseType(typeof(void), 500)] //Internal Server Error
        public IActionResult Put(int codigo, [FromBody]ContaInsertUpdate value)
        {
            if (value == null || value.Codigo != codigo)
            {
                return BadRequest();
            }
            var dto = value.ToDto();
            var resultValidation = Bll.Helpers.Validation.Validade(dto);
            if (resultValidation != null && resultValidation.HasError)
            {
                return UnprocessableEntity(resultValidation);
            }
            using (var dal = new ContaDal())
            {
                var item = dal.Where(dtoQ => dtoQ.Codigo == codigo).GetFirst();
                if (item == null)
                {
                    return NotFound();
                }
                dal.Update(value.ToDto());
            }
            return Ok();
        }

        /// <summary>
        /// Apaga uma conta existente
        /// </summary>
        /// <para name="codigo">C贸digo da conta para ser apagada</para>
        [HttpDelete("{codigo}")]
        [ProducesResponseType(typeof(void), 200)] //OK
        [ProducesResponseType(typeof(void), 401)] //Unauthorized
        [ProducesResponseType(typeof(void), 404)] //Not Found
        [ProducesResponseType(typeof(Dto.Core.ValidationResult), 422)] //Unprocessable Entity
        [ProducesResponseType(typeof(void), 500)] //Internal Server Error
        public IActionResult Delete(int codigo)
        {

            using (var dal = new ContaDal())
            {
                var dto = dal.Where(dtoQ => dtoQ.Codigo == codigo).GetFirst();
                if (dto == null)
                {
                    return NotFound();
                }
                dto.Deleted = true;
                var resultValidation = Bll.Helpers.Validation.Validade(dto);
                if (resultValidation != null && resultValidation.HasError)
                {
                    return UnprocessableEntity(resultValidation);
                }
                dal.Delete(dto);
            }

            return Ok();
        }

        public class ContaInsertUpdate
        {
            public ContaInsertUpdate()
            {
                EmpresafuncionarioCodigo = 0;
                CdLimite = 0.00000;
                CdUtilizado = 0.00000;
                CdSaldo = 0.00000;
                PermitirQuitacao = true;
            }
            public int Codigo { get; set; }

            public int? LojaCodigo { get; set; }

            public int? SetorCodigo { get; set; }
            public string Codigocontabel { get; set; }

            public int? FuncionarioCodigo { get; set; }

            public int? ContacontabelCodigo { get; set; }

            public int? PlanodespesaCodigo { get; set; }

            public int? SubplanodespesaCodigo { get; set; }

            public int? PlanoreceitaCodigo { get; set; }

            public int? SubplanoreceitaCodigo { get; set; }

            public int? EmpresafuncionarioCodigo { get; set; }

            public double? CdLimite { get; set; }

            public double? CdUtilizado { get; set; }

            public double? CdSaldo { get; set; }

            public string NBanco { get; set; }

            public int? Tipo { get; set; }

            public int? ContaCP { get; set; }

            public string NumeroConta { get; set; }

            public string Agencia { get; set; }

            public string Descricao { get; set; }

            public string Autor { get; set; }

            public double? Saldo { get; set; }

            public double? SaldoDisponivel { get; set; }

            public bool? Imprimir { get; set; }

            public int? Numeracaocheque { get; set; }

            public int? Relatoriocheque { get; set; }

            public int? Relatoriorelacaocheque { get; set; }

            public bool? PermitirQuitacao { get; set; }

            public Dto.Entities.Conta ToDto()
            {
                return new Dto.Entities.Conta
                {
                    Agencia = Agencia,
                    EmpresafuncionarioCodigo = EmpresafuncionarioCodigo,
                    Autor = Autor,
                    CdLimite = CdLimite,
                    CdSaldo = CdSaldo,
                    CdUtilizado = CdUtilizado,
                    Codigo = Codigo,
                    Codigocontabel = Codigocontabel,
                    ContacontabelCodigo = ContacontabelCodigo,
                    ContaCP = ContaCP,
                    Descricao = Descricao,
                    FuncionarioCodigo = FuncionarioCodigo,
                    Imprimir = Imprimir,
                    LojaCodigo = LojaCodigo,
                    NBanco = NBanco,
                    Numeracaocheque = Numeracaocheque,
                    NumeroConta = NumeroConta,
                    PermitirQuitacao = PermitirQuitacao,
                    PlanodespesaCodigo = PlanodespesaCodigo,
                    PlanoreceitaCodigo = PlanoreceitaCodigo,
                    Relatoriocheque = Relatoriocheque,
                    Relatoriorelacaocheque = Relatoriorelacaocheque,
                    Saldo = Saldo,
                    SaldoDisponivel = SaldoDisponivel,
                    SetorCodigo = SetorCodigo,
                    SubplanodespesaCodigo = SubplanodespesaCodigo,
                    SubplanoreceitaCodigo = SubplanoreceitaCodigo,
                    Tipo = Tipo
                };
            }
        }

        protected static Uri GetAbsoluteUriWithOutQuery(HttpRequest request)
        {
            UriBuilder uriBuilder = new UriBuilder();
            uriBuilder.Scheme = request.Scheme;
            uriBuilder.Path = request.Path.ToString();
            uriBuilder.Host = request.Host.Host;
            if (request.Host.Port.HasValue)
            {
                uriBuilder.Port = request.Host.Port.Value;
            }
            return uriBuilder.Uri;
        }

        /// <summary>
        /// 422 UNPROCESSABLE ENTITY
        /// </summary>
        /// <param name="value">Description error</param>
        /// <remarks>
        /// The server understands the content type of the request entity (hence a 415 Unsupported Media Type status code is inappropriate), and the syntax of the request entity is correct (thus a 400 Bad Request status code is inappropriate) but was unable to process the contained instructions.
        /// </remarks>
        protected ObjectResult UnprocessableEntity(object value)
        {
            return StatusCode(422, value);
        }
    }

I was debugging to see if I could find any solution, and I realized I was not calling ODataPathRouteConstraint.Match (...), so I decided to remove the controller attribute [Route ("api / [controller]")] and started calling, but even that's not how it worked.

When debugging happened this error in this line:

"Microsoft.OData.UriParser.ODataUnrecognizedPathException: Resource not found for the segment 'conta'.\r\n at Microsoft.OData.UriParser.ODataPathParser.CreateDynamicPathSegment(ODataPathSegment previous, String identifier, String parenthesisExpression)\r\n at Microsoft.OData.UriParser.ODataPathParser.CreateFirstSegment(String segmentText)\r\n at Microsoft.OData.UriParser.ODataPathParser.ParsePath(ICollection1 segments)\r\n at Microsoft.OData.UriParser.ODataPathFactory.BindPath(ICollection1 segments, ODataUriParserConfiguration configuration)\r\n at Microsoft.OData.UriParser.ODataUriParser.Initialize()\r\n at Microsoft.OData.UriParser.ODataUriParser.ParsePath()\r\n at Microsoft.AspNet.OData.Routing.DefaultODataPathHandler.Parse(String serviceRoot, String odataPath, IServiceProvider requestContainer, Boolean template)"

URL: http://localhost:8080/api/conta?$count=true

Does anyone have any suggestions for what it might be?

After hours is a cause of the problem, the integrity name can not be the same as the driver name and service URL.

It does not work:

builder.EntitySet <Conta> ("Conta");
public class ContaController : Core.ODataController

It works:

builder.EntitySet <Conta> ("ContaTest");
public class ContaTestController : Core.ODataController

Is this a limitation of the library or was it to work?

+1 @ErliSoares. I am also able to get the plain response using [Route()] but it does not contain any of the "@odata" properties, similar to your GET Portman response. However, I was unable to get it to work even by changing the names.

Can confirm issue using:

  • out-of-the-box WebAPI project in VS
  • targeting .NET Core 2.0
  • Microsoft.AspNetCore.OData 7.0.0-beta1

Show the controller and the configuration, maybe I'll find something to help you

Startup:

namespace WebApiPoc
{
    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.AddOData();
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            IEdmModel model = GetEdmModel(app.ApplicationServices);
            app.UseMvc(routeBuilder =>
            {
                routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(100).Count();
                routeBuilder.MapODataServiceRoute("odata", "api", model);
                routeBuilder.EnableDependencyInjection();
            });
        }

        private static IEdmModel GetEdmModel(IServiceProvider serviceProvider)
        {
            var builder = new ODataConventionModelBuilder(serviceProvider);
            builder.EntitySet<Models.Organization>("OrganizationTest");

            return builder.GetEdmModel();
        }
    }
}

Controller:

namespace WebApiPoc.Controllers
{
    public class OrganizationTestController : ODataController
    {
        // GET api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

Thanks, @ErliSoares!

I was unable to identify for sure, but test the URL: http://host:port/api/OrganizationTest
Do not forget the capital and tiny that must be the same.

If it does not work, I'll give you some changes to see if it works.

@ErliSoares - that did it! Thanks! If I remember correctly, there's a way to make routes case insensitive. I'll look, and if I find anything I'll report back.

Thanks, if I find a way I'll report back too.

@ErliSoares - Form the comments on Jan 6/7, the Resource not found for the segment 'conta' is expected if you query http://.../contra instead of http://.../Contra, i.e. it is case sensitive. I've tried querying with $count=true for both cases where the entity name does not match the clr type name and when they do, both work as expected but it is case sensitive.

I do notice than in your initial bug, you are using "conta" in the Url and "nameof(Contra)" in the model. nameof("Contra") will produce "Contra" so it's unclear why you received a response when you should have received a 404.

Are you still having issues with $count?

1231 filed for documenting how to configure case insensitivity.

@ErliSoares did you try to use http://host:port/api/OrganizationTest(1)? Has a very similar configuration, but routed to public IEnumerable<string> Get() instead of public string Get(int id).

@ErliSoares we're currently reviewing open issues in preparation for our upcoming WebAPI 7.x release. Is the behavior that you're seeing still applicable? Please review the posts from Rob and Igor above. Thank you.

PR #1409 makes case-insensitive (and unqualified operation names) the default for WebAPI OData 7.x. This is documented in PR #1426, including how to disable this default if you want the old behavior.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

christiannagel picture christiannagel  路  4Comments

rafeeqg picture rafeeqg  路  4Comments

ilya-chumakov picture ilya-chumakov  路  5Comments

suadev picture suadev  路  3Comments

NetTecture picture NetTecture  路  4Comments