OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
457 stars 158 forks source link

Model aliasing doesn't work when not using DataContract and DataMember attributes #278

Open jolleekin opened 3 years ago

jolleekin commented 3 years ago

I was trying to add OData query options support with model aliasing to an ASP.NET Core API project without using OData routing.

Assemblies affected

Reproduce steps

  1. Registering the OData services

    // Api/Startup.cs
           public void ConfigureServices(IServiceCollection services)
           {
               services.AddControllers().AddOData(options =>
               {
                   options
                       .AddRouteComponents(GetEdmModel())
                       .Filter()
                       .OrderBy()
                       .SetMaxTop(100)
                       .EnableNoDollarQueryOptions = true;
               });
  2. Building the EDM model

    // Api/Startup.cs
           private static IEdmModel GetEdmModel()
           {
               var builder = new ODataConventionModelBuilder();
               var account = builder.EntityType<Core.Models.Account>();
               account.Property(e => e.Code).Name = "code";
               account.Property(e => e.Name).Name = "name";
               return builder.GetEdmModel();
           }
    
    // Core/Models/Account.cs
    namespace Core.Models
    {
       public class Account
       {
           public string Code { get; set; }
    
           public string Name { get; set; }
       }
    }
  3. Creating the controller

    // Api/Controllers/AccountsController
       public class AccountsController : ControllerBase
       {
           [HttpGet]
           public async Task<IEnumerable<Api.Models.Account>> Get(
               ODataQueryOptions<Core.Models.Account> options,
               CancellationToken cancellationToken = default)
           {
               var coreModels = await Service.ListAsync(options, cancellationToken);
               return Mapper.Map<IEnumerable<Api.Models.Account>>();
           }
       }
  4. Execute a GET request (simplified and formatted for clarity)

    GET /accounts
       ?filter=code eq 'F4'
       &orderby=name

Expected result

A list of accounts.

Actual result

Microsoft.OData.ODataException: Could not find a property named 'name' on type 'Core.Models.Account'.

Additional detail

When I switched to using the DataContext and DataMember attributes, everything worked as expected. However, this is not an option as it causes the Core layer to depend on the API layer.

// Api/Startup.cs
        private static IEdmModel GetEdmModel()
        {
            var builder = new ODataConventionModelBuilder();
            builder.EntityType<Core.Models.Account>();
            return builder.GetEdmModel();
        }

// Core/Models/Account.cs
namespace Core.Models
{
    [DataContract]
    public class Account
    {
        [DataMember(Name = "code")]
        public string Code { get; set; }

        [DataMember(Name = "name")]
        public string Name { get; set; }
    }
}
xuzhg commented 3 years ago

@jolleekin

1) You didn't create an entity set, so why do you think you can access "GET /accounts"? What are "accounts"?

2) Since you don't have the "accounts", you only create an entity type named "account". So, I don't think there's an OData route template created to handle "GET /accounts" request.

3) So, anything you made in 'GetEdmModel' has no affection.

4) In addition, you mentioned 'When I switched to using the DataContext and DataMember attributes, everything worked as expected.', I am curious about the routing, maybe you miss to list the attributes on the accountscontroller?

5) Even it's working, because it goes into non-odata-model (without Edm model) scenarios (your Edm model doesn't have affection, it's not used). In without Edm Model, OData model builder will build the model on the fly, the model builder can use [DataContract] and [DataMember] to build the model alias. So that's why it works.

Would you please share the whole repro for me?

jolleekin commented 3 years ago

I'm using endpoint routing rather than OData routing.

In short, this is what I want: endpoint routing + OData query options + different API, BLL, and DAL models.

In the current system, I use ODataQueryMapper to map the query options from API models to BLL models. However, this project is no longer developed and doesn't support ASP.NET Core. So, in the new system, I need to use something else. I tried OData model aliasing and it didn't work the way I wanted. From what you said, it seems like I can't use model aliasing without OData routing and data annotation.

P/S: I think I found a simple solution: modify the request query string before creating ODataQueryOptions<T>.