WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

How do I define custom/dynamic OData columns/fields/properties?

Open qwertie opened this issue 3 years ago • 5 comments

I'm not sure if this is the right place to ask, because I'm using Microsoft.AspNetCore.OData 8.0.3 and this appears to be the repository for 7.x, so let me know if I should be asking somewhere else.

I wish to expose an OData model with entities that have some fixed columns/properties and some dynamically-generated columns/properties that come from the database, e.g.

public class ODataEntity
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; } = "";

    // From the perspective of Power BI, this should produce a series of additional columns 
    // (the columns are the same on all instances, but the schema can change at any time)
    public Dictionary<string, object> CustomFields;
}

How can I do that?

qwertie avatar Feb 02 '22 21:02 qwertie

Hi @qwertie. Thank you for the question. This looks correct. Is there a problem you're experiencing when you do this? Please note that the container property will not appear in the service metadata but the entity/complex type will be marked as open.

gathogojr avatar Feb 08 '22 17:02 gathogojr

Huh? No, it doesn't work. I get output like this:

  {
    "Id": 1,
    "Name": "The Project",
    "CustomFields": {
      "Example": 123
    }
  },

What we need is output like this:

  {
    "Id": 1,
    "Name": "The Project",
    "Example": 123
  },

qwertie avatar Feb 08 '22 21:02 qwertie

@qwertie The second form is what you should get if your OData service is setup as expected. Please share a repro that we can look at. Alternatively share a set of steps that we can follow to repro the issue

gathogojr avatar Feb 09 '22 04:02 gathogojr

Wow. Okay. I am completely shocked. When I created the following example app, it worked exactly like you said:

using Microsoft.AspNetCore.OData;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Routing.Controllers;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

IMvcBuilder mvc = builder.Services.AddControllers();

mvc.AddOData(opt => opt.AddRouteComponents("odata", GetEdmModel())
   .Select().Filter().OrderBy().Count().Expand().SkipToken());

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    // I don't know the purpose of this, but without it, all controllers report 404
    endpoints.MapControllerRoute("default", "WTF is this");
});

app.Run();

static IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder() {
        Namespace = "Namespace",
        ContainerName = "Container", // no idea what this is for
    };
    builder.EntitySet<ODataEntity>("objects");

    return builder.GetEdmModel();
}

public class ODataEntity
{
    [Key]
    public int Id { get; set; } = 123;
    public string Name { get; set; } = "The Crushinator";

    public Dictionary<string, object> CustomFields { get; set; } = new() { ["Example"] = 3210 };
}

[Route("odata/objects")]
[ApiController]
public class ODataEntityController : ODataController
{
    [HttpGet]
    public IActionResult Get() => Ok(new List<ODataEntity> { new ODataEntity() });
}

// Normal controllers do not serialize things the same way
[Route("api/v1/entities")]
[ApiController]
public class NonODataController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(new List<ODataEntity> { new ODataEntity() });

    [HttpGet("{id}")]
    public IActionResult Get(int id) => Ok(new ODataEntity());
}

Sorry I doubted you, but I have never seen documentation of this effect, and I have not seen a sample app that uses it. Where is the documentation for it?

However, although the CustomFields collection is "flattened" into JSON....

{"@odata.context":"https://localhost:7218/odata/$metadata#objects","value":[{"Id":123,"Name":"The Crushinator","Example":3210}]}

The Example column doesn't show up in Power BI:

image

I assume this is because the schema at /odata/$metadata does not include it.

So, how can I dynamically change the schema at runtime to include custom columns?

qwertie avatar Feb 10 '22 22:02 qwertie

@qwertie I'll check and revert on on support for dynamic properties in Power BI With respect to documentation on dynamic properties and how they appear on the response, you can look at this link https://www.c-sharpcorner.com/article/open-types-in-odata-with-web-api

gathogojr avatar Feb 13 '22 09:02 gathogojr