AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

PropertyRoutingConvention not working

Open o-glethorpe opened this issue 3 years ago • 7 comments

Hi, it seems to me that the default convention PropertyRoutingConvention does not work anymore after update to the 8.0 version. Using the UseODataRouteDebug middleware I can see my propertyref routes and then works fine but not my endpoints like GetPropertyName and PostToPropertyName. My code:

[HttpGet, EnableQuery] public IActionResult GetAddress([FromODataUri] Guid key) { ...

[HttpPost] public IActionResult PostToAddress([FromODataUri] Guid key, TenantAddress address) { ...

o-glethorpe avatar Nov 11 '21 21:11 o-glethorpe

@fernandobracaroto can you share with me your whole controller and route configuration?

xuzhg avatar Nov 15 '21 23:11 xuzhg

Shure, thanks for asking: Here is my startup configuration:

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.AddCors();
            services.AddSignalR();

            services.AddFeatures(this.Configuration);

            services
                .AddAuthentication(options =>
                {
                    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                })
                .AddJwtBearer(options =>
                {
                    options.SaveToken = true;
                    options.RequireHttpsMetadata = false;
                    options.TokenValidationParameters = new TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ValidateIssuerSigningKey = true,
                        ValidIssuer = Configuration["Jwt:Issuer"],
                        ValidAudience = Configuration["Jwt:Issuer"],
                        ClockSkew = TimeSpan.FromMinutes(5),
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"]))
                    };
                })
                .AddPolicyScheme("HubPolicyScheme", "Bearer or Cookie authorization for signalr endpoint", options =>
                {
                    options.ForwardDefaultSelector = context =>
                    {
                        return JwtBearerDefaults.AuthenticationScheme;
                    };
                });

            services.AddControllers()
            
            .AddOData(p =>
            {
                p.AddRouteComponents(this.GetEdmModel());
                p.Count().Filter().Expand().Select().OrderBy().SetMaxTop(20);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseCors(p =>
            {
                p.AllowAnyOrigin();
                p.AllowAnyMethod();
                p.AllowAnyHeader();
            });

            app.UseStaticFiles();
            app.UseMiddleware<TenantInfoMiddleware>();

            // Use odata route debug, /$odata
            app.UseODataRouteDebug();

            // Add OData /$query middleware
            app.UseODataQueryRequest();

            // Add the OData Batch middleware to support OData $Batch
            app.UseODataBatching();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();



            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }

        private IEdmModel GetEdmModel()
        {
            var builder = new ODataConventionModelBuilder();

            builder.EntitySet<Tenant>("Tenants");

            builder.EnableLowerCamelCase();
            builder.Namespace = this.Configuration.GetSection("System").GetValue<string>("Name");
            return builder.GetEdmModel();
        }
    }

Here is my controller: "baseodatacontroller" inherits from ODataController

public class TenantsController : BaseOdataController
    {
        private readonly FeaturesProvider featuresProvider;
        private readonly AccountManagerProvider accountManagerProvider;
        private readonly IConfiguration configuration;

        public TenantsController(
            FeaturesProvider featuresProvider,
            AccountManagerProvider accountManagerProvider,
            IConfiguration configuration)
        {
            this.featuresProvider = featuresProvider;
            this.accountManagerProvider = accountManagerProvider;
            this.configuration = configuration;
        }

        [HttpGet, EnableQuery]
        public IActionResult Get()
        {
            var useCaseResult = new FetchTenantsUseCase(this.featuresProvider).Execute();
            return useCaseResult.Succeeded ? Ok(useCaseResult.Output) : FailureContent(useCaseResult.Failure);
        }

        [EnableQuery]
        public IActionResult Get([FromODataUri] Guid key)
        {
            var useCaseResult = new FetchTenantUseCase(key, this.featuresProvider).Execute();
            return useCaseResult.Succeeded ? Ok(SingleResult.Create<Tenant>(useCaseResult.Output)) : FailureContent(useCaseResult.Failure);
        }

        [HttpPost, CustomEnableQueryAttribute]
        public async Task<IActionResult> Post(
            [FromBody] CreateTenantInputModel model)
        {
            if (!ModelState.IsValid) return FailureContent(400, ModelState);

            var useCaseResult = await new CreateTenantUseCase(
                model,
                this.featuresProvider,
                this.accountManagerProvider)
            .Execute();

            if (useCaseResult.Succeeded)
            {
                return Ok(useCaseResult.Output);
            }
            else
            {
                return FailureContent(useCaseResult.Failure);
            }
        }

        [HttpPut]
        public async Task<IActionResult> Put([FromODataUri] Guid key, [FromBody] Tenant model)
        {
            if (!ModelState.IsValid) return FailureContent(400, ModelState);

            var useCaseResult = await new UpdateTenantUseCase(key, model, this.featuresProvider).Execute();
            return useCaseResult.Succeeded ? Ok(useCaseResult.Output) : FailureContent(useCaseResult.Failure);
        }

        [HttpPatch]
        public async Task<IActionResult> Patch([FromODataUri] Guid key, [FromBody] Delta<Tenant> model)
        {
            if (!ModelState.IsValid) return FailureContent(400, ModelState);

            var fetchAccountResult = new FetchTenantUseCase(key, this.featuresProvider).Execute();
            if (fetchAccountResult.Succeeded)
            {
                var tenant = fetchAccountResult.Output.FirstOrDefault();
                if (tenant == null) return NotFound();

                model.Patch(tenant);
                var updateCategoryResult = await new UpdateTenantUseCase(key, tenant, this.featuresProvider).Execute();

                return updateCategoryResult.Succeeded ? Ok(updateCategoryResult.Output) : FailureContent(updateCategoryResult.Failure);
            }
            else
            {
                return FailureContent(fetchAccountResult.Failure);
            }
        }

        [HttpDelete]
        public async Task<IActionResult> Delete([FromODataUri] Guid key)
        {
            var useCaseResult = await new DeleteTenantUseCase(key, this.featuresProvider).Execute();
            return useCaseResult.Succeeded ? Ok(useCaseResult.Output) : FailureContent(useCaseResult.Failure);
        }

        [EnableQuery]
        public IActionResult GetAddress([FromODataUri] Guid key)
        {
            var query = this.featuresProvider.dbContext.Tenants.AsNoTracking().Where(p => p.Id == key).Where(p => p.Address != null).Select(p => p.Address);

            return Ok(SingleResult.Create<StreetAddress>(query));
        }

        [EnableQuery]
        public IActionResult PostToAddress([FromODataUri] Guid key, TenantAddress address)
        {
            return Ok($"GetRef - {key}");
        }

        [HttpGet]
        [EnableQuery]
        public IActionResult GetRef([FromODataUri] Guid key, string navigationProperty)
        {
            var query = this.featuresProvider.dbContext.Tenants.AsNoTracking().Where(p => p.Id == key).Where(p => p.Address != null).Select(p => p.Address);

            return Ok(SingleResult.Create<StreetAddress>(query));
        }

        [HttpPost]
        [HttpPut]
        public IActionResult CreateRef([FromODataUri] Guid key, string navigationProperty, [FromBody] Uri link)
        {
            return Ok($"CreateRef - {key}: {navigationProperty}");
        }

        [HttpDelete]
        public IActionResult DeleteRef([FromODataUri] Guid key, string navigationProperty)
        {
            return Ok($"DeleteRef - {key}: {navigationProperty}");
        }

        [HttpDelete]
        public IActionResult DeleteRef([FromODataUri] Guid key, [FromODataUri] Guid relatedKey, string navigationProperty)
        {
            return Ok($"DeleteRef - {key} - {relatedKey}: {navigationProperty}");
        }

        [HttpGet, EnableQuery]
        public IActionResult GetImage([FromODataUri] Guid key)
        {
            var query = this.featuresProvider.dbContext.Tenants.AsNoTracking().Where(p => p.Id == key).Where(p => p.Image != null).Select(p => p.Image);

            return Ok(SingleResult.Create<GenericFile>(query));
        }

    }

o-glethorpe avatar Nov 16 '21 11:11 o-glethorpe

@fernandobracaroto Thanks for your codes. The controller looks good to me. So, what do you see at "/$odata" debug page for the "GetAddress" endpoint?

By the way, can you also share with me your "C# models" class? for example: Tenant?

xuzhg avatar Nov 16 '21 18:11 xuzhg

this is my /$odata endpoint:

Api.Controllers.TenantsController.Get (Api) | GET | Tenants
Api.Controllers.TenantsController.Get (Api) | GET | Tenants/$count
Api.Controllers.TenantsController.Get (Api) | GET | Tenants({key})
Api.Controllers.TenantsController.Get (Api) | GET | Tenants/{key}
Api.Controllers.TenantsController.Post (Api) | POST | Tenants
Api.Controllers.TenantsController.Put (Api) | PUT | Tenants({key})
Api.Controllers.TenantsController.Put (Api) | PUT | Tenants/{key}
Api.Controllers.TenantsController.Patch (Api) | PATCH | Tenants({key})
Api.Controllers.TenantsController.Patch (Api) | PATCH | Tenants/{key}
Api.Controllers.TenantsController.Delete (Api) | DELETE | Tenants({key})
Api.Controllers.TenantsController.Delete (Api) | DELETE | Tenants/{key}
Api.Controllers.TenantsController.GetRef (Api) | GET | Tenants({key})/{navigationProperty}/$ref
Api.Controllers.TenantsController.GetRef (Api) | GET | Tenants/{key}/{navigationProperty}/$ref
Api.Controllers.TenantsController.CreateRef (Api) | POST,PUT | Tenants({key})/{navigationProperty}/$ref
Api.Controllers.TenantsController.CreateRef (Api) | POST,PUT | Tenants/{key}/{navigationProperty}/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants({key})/{navigationProperty}/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants/{key}/{navigationProperty}/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants({key})/{navigationProperty}({relatedKey})/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants({key})/{navigationProperty}/{relatedKey}/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants/{key}/{navigationProperty}({relatedKey})/$ref
Api.Controllers.TenantsController.DeleteRef (Api) | DELETE | Tenants/{key}/{navigationProperty}/{relatedKey}/$ref

this is my tenant class

    public class Tenant : AuditableEntity
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public string CompanyName { get; set; }
        public string BusinessName { get; set; }
        public string DocumentNumber { get; set; }
        public string Slug { get; set; }
        public string PhoneCountryCode { get; set; }
        public string PhonePrefix { get; set; }
        public string PhoneNumber { get; set; }
        public GenericFile Image { get; set; }
        public Guid? ImageId { get; set; }
        public TenantSettings Settings { get; set; }

        public TenantAddress Address { get; set; }
        public Guid AddressId { get; set; }

        public ICollection<User> Owners { get; set; }
    }

auditable entity:

  public class AuditableEntity //: ISoftDelete
    {
        public DateTime Created { get; set; }
        public DateTime? Updated { get; set; }
        public bool Deleted { get; set; }
    }

o-glethorpe avatar Nov 16 '21 18:11 o-glethorpe

@fernandobracaroto I see. Since you builder.EnableLowerCamelCase();, all of the property names are lower camel case.

So, we can't find the property name using "Address", since "address" is the correct name in the model.

By conventional, the name at action should be the same as the name defined in the model.

Maybe, I can extend it to use "case-insensitive" for the name in action future version.

xuzhg avatar Nov 16 '21 19:11 xuzhg

Yes, that would be great, I think its common to people to like to serve camelcase json properties and a the same time use the c# method naming convention pascalcase.

o-glethorpe avatar Nov 18 '21 14:11 o-glethorpe

The problem is solved. See #391

circler3 avatar Oct 11 '22 14:10 circler3