AspNetCoreOData
AspNetCoreOData copied to clipboard
Value cannot be null or empty. (Parameter 'entitySetName')
ASP.NET Core OData 8.2.3
I am trying to do a post to http://localhost:5107/v1/users('michel')/calendarGroups which works without Accept: application/json;odata.metadata=full , but with I get the error
Value cannot be null or empty. (Parameter 'entitySetName')
see screenshot. It seems Identifier in this case is empty, I am not sure why. It does seems to work even with full when I rewrite the url to /v1/ instead of v1/.
What am I doing wrong ? I think I have followed most standard documentation. ( If you see errors in naming convention, I have temporarily disabled my camelcase converter to make sure that wasn't the problem, haven't updated all urls in code).
I am trying to use attribute routing and have this as my program.cs
using My.Booking.API;
using My.Booking.Domain.Models;
using My.Booking.Persistence;
using Microsoft.AspNetCore.OData.Batch;
using Microsoft.OData.ModelBuilder;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.OData;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var edmBuilder = new ODataConventionModelBuilder
{
Namespace = "graph"
};
edmBuilder.EntitySet<Entity>("entities");
edmBuilder.EntitySet<OutlookItem>("outlookItem");
edmBuilder.EntitySet<Calendar>("calendars");
edmBuilder.EntitySet<CalendarGroup>("calendarGroups");
edmBuilder.EntitySet<Event>("events");
var users = edmBuilder.EntitySet<User>("users");
users.HasRequiredBinding(p => p.Calendar,"Calendar");
edmBuilder.Singleton<Me>("me");
edmBuilder.EnableMyLowerCamelCase();
var edmModel = edmBuilder.GetEdmModel();
builder.Services.AddSingleton<IConfiguration>(builder.Configuration);
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
//policy.WithHeaders("X-API-KEY", "Origin", "X-Requested-With", "Content-Type", "Accept", "Access-Control-Request-Method","prefer","Access-Control-Allow-Origin");
//policy.WithMethods("GET", "POST", "OPTIONS", "PUT", "DELETE");
});
});
builder.Services.AddDbContext<BookingDbContext>(opt => opt.UseSqlServer());
builder.Services.AddControllers().AddOData(options =>
{
options.RouteOptions.EnablePropertyNameCaseInsensitive = true;
options.RouteOptions.EnableControllerNameCaseInsensitive = true;
options.RouteOptions.EnableKeyAsSegment = true;
options.RouteOptions.EnableKeyInParenthesis = true;
options.Select().Filter().Count().OrderBy().Expand()
.AddRouteComponents("v1", edmModel, opts =>
{
opts.AddSingleton<ODataBatchHandler,DefaultODataBatchHandler>();
});
options.TimeZone = TimeZoneInfo.Utc;
});
var app = builder.Build();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseCors();
app.UseODataRouteDebug();
app.UseODataBatching();
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
app.UseHttpsRedirection();
await app.ResetDatabaseAsync();
app.Run();
This is my UsersController.cs
using My.Booking.Domain.Models;
using My.Booking.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Deltas;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Microsoft.EntityFrameworkCore;
namespace My.Booking.API.Controllers;
public class UsersController(BookingDbContext dbContext) : ODataController
{
[EnableQuery]
public ActionResult<IQueryable<User>> Get()
{
return Ok(dbContext.Users);
}
public ActionResult<User> Get([FromRoute] string key)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user == null)
{
return NotFound();
}
return Ok(user);
}
public ActionResult Post([FromBody] User user)
{
dbContext.Users.Add(user);
dbContext.SaveChanges();
return Created(user);
}
public ActionResult Put([FromRoute] string key, [FromBody] User updatedUser)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user == null)
{
return NotFound();
}
// customer.Name = updatedCustomer.Name;
// customer.CustomerType = updatedCustomer.CustomerType;
// customer.CreditLimit = updatedCustomer.CreditLimit;
// customer.CustomerSince = updatedCustomer.CustomerSince;
dbContext.SaveChanges();
return Updated(user);
}
public ActionResult Patch([FromRoute] string key, [FromBody] Delta<User> delta)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user == null)
{
return NotFound();
}
delta.Patch(user);
dbContext.SaveChanges();
return Updated(user);
}
public ActionResult Delete([FromRoute] string key)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user != null)
{
dbContext.Users.Remove(user);
}
dbContext.SaveChanges();
return NoContent();
}
[HttpGet("v1/Users({key})/CalendarGroups")]
[HttpGet("v1/Users/{key}/CalendarGroups")]
[EnableQuery]
public IQueryable<CalendarGroup> GetCalendarGroups([FromRoute] string key)
{
Microsoft.OData.Evaluation.ODataConventionalUriBuilder.BuildEntitySetUri(
return (from cgu in dbContext.CalendarGroupUsers
join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
where cgu.UserId == key
select cg).AsNoTracking();
}
[HttpGet("v1/users({key})/calendarGroups({relatedKey})")]
[HttpGet("v1/users/{key}/calendarGroups/{relatedKey}")]
[EnableQuery]
public IQueryable<CalendarGroup> GetCalendarGroups([FromRoute] string key,[FromRoute] string relatedKey)
{
return (from cgu in dbContext.CalendarGroupUsers
join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
where cgu.UserId == key && cg.Id == relatedKey
select cg).AsNoTracking();
}
[HttpPost("v1/Users({key})/CalendarGroups")]
[HttpPost("v1/Users/{key}/CalendarGroups")]
public IActionResult PostToCalendarGroups([FromRoute] string key, [FromBody] CalendarGroup calendarGroup)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user == null)
{
return NotFound();
}
dbContext.CalendarGroups.Add(calendarGroup);
user.CalendarGroups.Add(calendarGroup);
dbContext.SaveChanges();
return Created(calendarGroup);
}
[HttpGet("v1/users({key})/calendar")]
[HttpGet("v1/users/{key}/calendar")]
public IQueryable<Calendar> GetCalendar([FromRoute] string key)
{
return (from cgu in dbContext.CalendarGroupUsers
join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
join c in dbContext.Calendars on cg.Id equals EF.Property<string>(c, "CalendarGroupId")
where c.IsDefaultCalendar == true && cgu.Default == true && cgu.UserId == key
select c)
.AsNoTracking();
}
[HttpGet("v1/users({key})/calendarGroups({relatedKey})/calendars")]
[HttpGet("v1/users/{key}/calendarGroups/{relatedKey}/calendars")]
public IQueryable<Calendar> GetCalendars([FromRoute] string key,[FromRoute] string relatedKey)
{
return (from cgu in dbContext.CalendarGroupUsers
join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
join c in dbContext.Calendars on cg.Id equals EF.Property<string>(c, "CalendarGroupId")
where cgu.UserId == key && cg.Id == relatedKey
select c)
.AsNoTracking();
}
[HttpPost("v1/users({key})/calendarGroups({relatedKey})/calendars")]
[HttpPost("v1/users/{key}/calendarGroups/{relatedKey}/calendars")]
public IActionResult PostToCalendarGroups([FromRoute] string key, [FromRoute] string relatedKey, [FromBody] Calendar calendar)
{
var user = dbContext.Users.SingleOrDefault(d => d.Id == key);
if (user == null)
{
return NotFound();
}
var t = (from cgu in dbContext.CalendarGroupUsers
join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
where cgu.UserId == key && cg.Id == relatedKey
select cg).SingleOrDefault();
if (t == null)
{
return NotFound();
}
calendar.Owner = new EmailAddress(user.Mail,user.DisplayName);
dbContext.Calendars.Add(calendar);
t.Calendars.Add(calendar);
dbContext.SaveChanges();
return Created(calendar);
}
}
If I forget about conventional routing which I can't get to work, but fix the path like
[HttpGet("/Users({key})/CalendarGroups")]
[HttpGet("/Users/{key}/CalendarGroups")]
[EnableQuery]
public IQueryable<CalendarGroup> GetCalendarGroupsFromUser(string key)
{
//Microsoft.AspNetCore.OData.Routing.Conventions.NavigationRoutingConvention
//Microsoft.AspNetCore.OData.Routing.ODataRoutingMatcherPolicy
//Microsoft.AspNetCore.OData.Routing.ODataPathSegmentHandler
//Microsoft.OData.Evaluation.ODataConventionalUriBuilder.BuildEntitySetUri(
// return (from cgu in dbContext.CalendarGroupUsers
// join cg in dbContext.CalendarGroups on cgu.CalendarGroupsId equals cg.Id
// where cgu.UserId == key
// select cg).AsNoTracking();
var u = new User()
{
Id = "123",
CalendarGroups = new List<CalendarGroup>()
{
new CalendarGroup()
{
Id = "1",
Name = "a"
}
}
};
return u.CalendarGroups.AsQueryable();
}
I get the following result in postman
{
"@odata.context": "http://localhost:5107/$metadata#Users('michel')/CalendarGroups",
"value": [
{
"@odata.type": "#graph.CalendarGroup",
"@odata.id": "Users('michel')/CalendarGroups('1')",
"@odata.editLink": "Users('michel')/CalendarGroups('1')",
"Id": "1",
"Name": "a",
"ChangeKey": "",
"[email protected]": "http://localhost:5107/Users('michel')/CalendarGroups('1')/Calendars/$ref",
"[email protected]": "http://localhost:5107/Users('michel')/CalendarGroups('1')/Calendars"
}
]
}
According to
4.5.7 Annotation odata.id The odata.id annotation contains the entity-id; see [OData-Protocol]. By convention the entity-id is identical to the canonical URL of the entity, as defined in [OData-URL]. The odata.id annotation MUST appear if odata.metadata=full is requested.
4.5.8 Annotation odata.editLink and odata.readLink The odata.editLink annotation contains the edit URL of the entity; see [OData-Protocol]. The odata.readLink annotation contains the read URL of the entity or collection; see [OData-Protocol]. The default value of both the edit URL and read URL is the entity's entity-id
Seems that both @odata.id and @odata.editLink also should include the serviceRoot. Maybe missing because of the problem above.
@michelbieleveld It's fixed at ODL side at: https://github.com/OData/odata.net/pull/2775 But it hasn't released yet.