Open orty opened 1 year ago
Can you show your EDM registration code?
If EntityB
is being detected as a complex type something is wrong.
Hi @orty. Let me know if the following code points you in the right direction
// Controller
public class AController : ODataController
{
private static readonly List<EntityA> listOfAs = new List<EntityA>
{
new EntityA { Id = 1, PropertyA = "PropertyA - 1" },
new EntityA { Id = 2, PropertyA = "PropertyA - 2" },
new EntityB { Id = 3, PropertyA = "PropertyA - 3", PropertyB = "PropertyB - 3" },
new EntityB { Id = 4, PropertyA = "PropertyA - 4", PropertyB = "PropertyB - 4" }
};
public ActionResult<IEnumerable<EntityA>> Get()
{
return listOfAs.OfType<EntityA>().ToList();
}
[HttpGet("prefix/A/B")]
public ActionResult<IEnumerable<EntityB>> GetB()
{
return listOfAs.OfType<EntityB>().ToList();
}
}
// Edm model
var modelBuilder = new ODataConventionModelBuilder();
var aEntityConfiguration = modelBuilder.EntitySet<EntityA>("A").EntityType;
aEntityConfiguration.Collection.Function("B").ReturnsCollectionFromEntitySet<EntityA>("A");
builder.Services.AddControllers().AddOData(
options => options.EnableQueryFeatures().AddRouteComponents(
routePrefix: "prefix",
model: modelBuilder.GetEdmModel()));
To retrieve A collection: /prefix/A
To retrieve B collection: /prefix/A/B
NOTE: We're using attribute routing on the GetB
function. B
alone won't cut it. You need prefix/A/B
, or A/B
if you have not configured a route prefix.
I just realized OP used the [controller]
replacement in the Route
attribute. On that:
First things first, thank you for your quick answers :)
I just realized OP used the
[controller]
replacement in theRoute
attribute. On that:
Yes it is an issue we ran across, and I omitted many complex things in my current context. For instance, we have two versions of our API ("beta" and "v1") which are based on the namespace
the controllers are in. Our route template for each controller is then written as [Route("[namespace]/[controller]")]
.
This issue has been addressed by implementing the two following classes (I would be glad to reference this in the issue you linked if it can provide any help) (credits to maxc137 from this stackoverflow post)
a custom IApplicationModelProvider
internal class ApiRoutingApplicationModelProvider : IApplicationModelProvider
{
public int Order => 0;
public virtual void OnProvidersExecuted(ApplicationModelProviderContext context)
{
}
public void OnProvidersExecuting(ApplicationModelProviderContext context)
{
new CustomRouteTokenConvention(
"namespace",
ApplicationModelExtensions.GetApiVersion).Apply(context.Result);
new CustomRouteTokenConvention("controller",
ApplicationModelExtensions.GetControllerName).Apply(context.Result);
}
}
internal static class ApplicationModelExtensions { public static string GetApiVersion(this ControllerModel controllerModel) => controllerModel.ControllerType.Namespace?.Split('.') .Last() .ToLower();
public static string GetControllerName(this ControllerModel controllerModel) =>
controllerModel.ControllerName.ToLower();
}
- and a custom `IApplicationModelConvention`
```c#
internal class CustomRouteTokenConvention : IApplicationModelConvention
{
private readonly string tokenRegex;
private readonly Func<ControllerModel, string?> valueGenerator;
public CustomRouteTokenConvention(string tokenName, Func<ControllerModel, string?> valueGenerator)
{
this.tokenRegex = $@"(\[{tokenName}])(?<!\[\1(?=]))";
this.valueGenerator = valueGenerator;
}
public void Apply(ApplicationModel application)
{
foreach (var controller in application.Controllers)
{
string? tokenValue = this.valueGenerator(controller);
this.UpdateSelectors(
controller.Selectors,
tokenValue);
this.UpdateSelectors(
controller.Actions.SelectMany(a => a.Selectors),
tokenValue);
}
}
private string? InsertTokenValue(string? template, string? tokenValue)
{
if (template is null)
{
return null;
}
return Regex.Replace(
template,
this.tokenRegex,
tokenValue ?? string.Empty);
}
private void UpdateSelectors(IEnumerable<SelectorModel> selectors, string? tokenValue)
{
foreach (var attributeRouteModel in selectors.Select(s => s.AttributeRouteModel))
{
if (attributeRouteModel is null)
{
continue;
}
attributeRouteModel.Template = this.InsertTokenValue(
attributeRouteModel.Template,
tokenValue);
attributeRouteModel.Name = this.InsertTokenValue(
attributeRouteModel.Name,
tokenValue);
}
}
}
From there, the goal is to dynamically generate the EdmModel
for each prefix and register it against the proper value ("beta" or "v1"). The EdmModel
s themselves are built using information from the controllers' actions metadata, mainly [ProduceResponseType]
which we rely on to provide the EntityType
or ComplexType
returned by any Function
or Action
(outside of the basic CRUD ones, which seem to be retrieved automatically by the ODataModelConventionBuilder
once the EntitySet
related to the default [HttpGet]
route has been registered).
That being said, it mostly works, but the inital requirement is to have an always (in IT business, == with a minimum of maintenance effort) up-to-date OData $metadata contract which we can rely on, to be able to dynamically build the data display component client-side.
I tried the code you provided @gathogojr, and the result seems to be the same as the one I have, since the B
function is referenced as having an EntityA
return type. Check this out:
{
"$Version": "4.0",
"$EntityContainer": "Default.Container",
"API.Controllers": {
"EntityA": {
"$Kind": "EntityType",
"$Key": [
"Id"
],
"Id": {
"$Type": "Edm.Int32"
},
"PropertyA": {
"$Nullable": true
}
},
"EntityB": {
"$Kind": "EntityType",
"$BaseType": "API.Controllers.EntityA",
"PropertyB": {
"$Nullable": true
}
}
},
"Default": {
"B": [
{
"$Kind": "Function",
"$IsBound": true,
"$Parameter": [
{
"$Name": "bindingParameter",
"$Collection": true,
"$Type": "API.Controllers.EntityA",
"$Nullable": true
}
],
"$ReturnType": {
"$Collection": true,
"$Type": "API.Controllers.EntityA",
"$Nullable": true
}
}
],
"Container": {
"$Kind": "EntityContainer",
"A": {
"$Collection": true,
"$Type": "API.Controllers.EntityA"
}
}
}
}
Here follows the answer from the /poc/A/B
endpoint:
{
"@odata.context": "https://localhost:5001/poc/$metadata#A/API.Controllers.EntityB",
"value": [
{
"@odata.type": "#API.Controllers.EntityB",
"Id": 3,
"PropertyA": "PropertyA - 3",
"PropertyB": "PropertyB - 3"
},
{
"@odata.type": "#API.Controllers.EntityB",
"Id": 4,
"PropertyA": "PropertyA - 4",
"PropertyB": "PropertyB - 4"
}
]
}
I do not see a way to match the $metadata#A/API.Controllers.EntityB
value easily to something in the full $metadata
contract to parse the available properties. As a nice-to-have feature, I would like eventually to get rid of all fully qualified names in that contract, and show only type names we generate from our automatic model builder (lowercased, shortened, namespace-free).
I trimmed all automatic model generation and took your sample litterally to implement this POC, just within my own project.
Hi,
I can't seem to find the right way to build my Edm model with the following requirement :
a controller for the whole inheritance chain, calling the default route would return a collection of
EntityA
s, the/B
route a collection ofEntityB
s, and so onWhen I declare my Edm model using the
ODataConventionModelBuilder
, I cannot find a way to declare theGetAllB
method as aFunction
on theEntitySet<EntityA>
which returns a collection ofEntityB
. Either I tell it itReturnsCollection<EntityB>()
, and then when the convention builder map the types hierarchy internally, it cannot registerEntityB
as anEntityType
because it is already registered as aComplexType
, or I try to make a call toReturnsCollectionFromEntitySet<>()
but then I have to register a newEntitySet
with a different name (which I think I understood will not match anymore with myodata/A/B
route ?) or to bind it to theEntitySet<EntityA>
and then OData sends me back the wrong type in the response'sodata.context
property (which I need to be exact to perform some client-side operations for displaying the data).What did I miss ?