OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to define `EDM model` that add subquery on control to `OData Endpoint Mapping`?

SMAH1 opened this issue · comments

I have two Data classes:

public class Book
{
    public decimal Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public int AuthorId { get; set; } = 0;
}

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

and define list of books and authors and relation with AuthorId .
I use EF with UseInMemoryDatabase for store data (Not matter):

    // ---------------------------------------- DbContext
    public class BookStoreContext : DbContext
    {
        public BookStoreContext(DbContextOptions<BookStoreContext> options)
          : base(options)
        {
        }

        public DbSet<Book> Books { get; set; }
        public DbSet<Author> Authors { get; set; }
    }

    // ---------------------------------------- Sample Data
    public static class DataSource
    {
        private static IList<Book> _books { get; set; } = new List<Book>();
        private static IList<Author> _authors { get; set; } = new List<Author>();

        public static IList<Book> GetBooks()
        {
            if (_books.Count != 0)
            {
                return _books;
            }

            InitDb();

            return _books;
        }

        public static IList<Author> GetAuthors()
        {
            if (_authors.Count != 0)
            {
                return _authors;
            }

            InitDb();

            return _authors;
        }

        private static void InitDb()
        {
            _books.Add(new Book() { Id = 1, Title = "A", AuthorId = 1, });
            _books.Add(new Book() { Id = 2, Title = "B", AuthorId = 1, });
            _books.Add(new Book() { Id = 3, Title = "C", AuthorId = 2, });
            _books.Add(new Book() { Id = 4, Title = "D", AuthorId = 3, });
            _books.Add(new Book() { Id = 5, Title = "E", AuthorId = 2, });
            _books.Add(new Book() { Id = 6, Title = "F", AuthorId = 1, });


            _authors.Add(new Author() { Id = 1, Name = "Z-1", });
            _authors.Add(new Author() { Id = 2, Name = "Z-2", });
            _authors.Add(new Author() { Id = 3, Name = "Z-3", });
            _authors.Add(new Author() { Id = 4, Name = "Z-4", });
        }
    }

I define new asp net core 8.0 with odata 8. This is Program.cs file:

using Microsoft.AspNetCore.OData;
using Microsoft.EntityFrameworkCore;
using Microsoft.OData.Edm;
using Microsoft.OData.ModelBuilder;
using TestOdata.Models;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

var services = builder.Services;

services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();

services.AddDbContext<BookStoreContext>(opt => opt.UseInMemoryDatabase("BookLists"));
services.AddControllers().AddOData(opt =>
{
    opt
    .Count().Filter().Expand().Select().OrderBy().SetMaxTop(5)
    .AddRouteComponents("odata", GetEdmOdataModel())
    ;
});

var app = builder.Build();

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

    app.UseODataRouteDebug();
}

app.UseAuthorization();

app.MapControllers();

app.Run();

IEdmModel GetEdmOdataModel()
{
    ODataConventionModelBuilder builder = new ODataConventionModelBuilder();

    builder.EntitySet<Author>("Authors");
    builder.EntitySet<Book>("Books");

    return builder.GetEdmModel();
}

NOW:

I define AuthorsController that has two method with EnableQuery:

  • GetAuthors return list of all authors
  • GetBooks return books of specify authors
{
    public class AuthorsController : ODataController
    {
        private BookStoreContext _db;

        public AuthorsController(BookStoreContext context)
        {
            _db = context;
            if (context.Books.Count() == 0)
            {
                foreach (var b in DataSource.GetBooks())
                {
                    context.Books.Add(b);
                }
                foreach (var a in DataSource.GetAuthors())
                {
                    context.Authors.Add(a);
                }
                context.SaveChanges();
            }
        }

        [EnableQuery]
        public IQueryable<Author> GetAuthors()
        {
            return _db.Authors.AsQueryable<Author>();
        }

        [HttpGet("{authorId:int}"), EnableQuery]
        public IQueryable<Book> GetBooks(int authorId)
        {
            return _db.Books.Where(x => x.AuthorId == authorId).AsQueryable<Book>();
        }
    }

in the OData Endpoint Mappings we have:

DB1

My Question:

How to change GetEdmOdataModel that GetBooks add to OData Endpoint Mappings?

There are all above codes in HERE.

Please leverage the "Discussions" area in Github for questions like this.

@SMAH1 It depends how do you want to get the books?

First, Let me describe why 'GetAuthors' works. You may notice the "IsConventional=Yes" for the endpoints on 'GetAuthors', it mean the endpoints "odata/authors" and "odata/authors/$count" are built on conventions:

  1. Controller convention: the controller name "Authors" is the entity set name defined in the model
  2. Action name convnetion: the action name in the controller is "Get" or "Get{EntitySetName}".
    So, 'GetAuthors' meets the above two conventions and we build the default endpoints for it.

Secod, your 'GetBooks' doesn't meet the above conventions, because the potential entity set "Books" in your action name 'GetBooks' is not equal to the controller name. Moreover, your "Author" type has no relationship with "Book" type, such relationship is built using navigation property. It means you should at least have a navigation property defined in the "Author" type as below:

public class Author
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public IList<Book> Books {get;set;} // new added
}

The above is based on your original goal that you want to get the "books" for a certain author using the 'authorId' in the [HttpGet].

Once you add the above navigation property, you can use the attirbute routing to define an endpoint on 'GetBooks' as:
image

You can see the following endpoints (be noted, the "IsConventional= --")
image

thanks