ODATA: несвязанная функция, возвращающая сложный тип с коллекцией

Я пытался реализовать что-то с odata и ASP.NET Core 3, что просто не хочет работать должным образом, и я не могу понять, что не так. Я создал небольшой образец приложения для демонстрации.

У меня есть служба odata, которую я могу использовать для запроса узлов. Узлы могут быть узлами type1 или type2, и это открытые типы с динамическими свойствами. Запрос их работает отлично. Что я хочу сделать, так это рассчитать пути между узлами. Пути не являются сущностями — у них нет идентичности. Поэтому я не считаю правильным создавать ресурс для этого. Это просто результаты вычислений пути, содержащие списки узлов, которые находятся вдоль пути, поэтому я думаю, что функция — лучший способ сообщить API, что я хочу.

Итак, я создал функцию odata для выполнения вычислений и возврата доступных путей, и она работает, за исключением того, что я не могу заставить ее возвращать список узлов, через которые проходит путь, а это единственная информация, которая мне действительно нужна.

Я создал пример кода, демонстрирующий проблему:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.OData;
using Microsoft.AspNet.OData.Builder;
using Microsoft.AspNet.OData.Extensions;
using Microsoft.AspNet.OData.Routing;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace OdataSample
{
    public static class Program {
        public static void Main(string[] args) {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }

    public class Startup {
        public void ConfigureServices(IServiceCollection services) {
            services.AddOData();
            services.AddSingleton<IDataProvider, DataProvider>();
            services.AddMvc(options => options.EnableEndpointRouting = false);
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
            app.UseDeveloperExceptionPage();

            var builder = new ODataConventionModelBuilder(app.ApplicationServices);
            builder.EntitySet<Node>("Nodes");
            builder.ComplexType<Path>()
                .HasMany(x => x.Nodes)
                .HasDerivedTypeConstraints(typeof(Type1Node), typeof(Type2Node));

            var calculatePath = builder.Function("CalculatePaths");
            calculatePath.Parameter<string>("source");
            calculatePath.Parameter<string>("target");
            calculatePath.ReturnsCollection<Path>();
            
            app.UseMvc(routeBuilder =>
            {
                routeBuilder.EnableDependencyInjection();
                routeBuilder.Select().Expand().Filter().OrderBy().MaxTop(10).Count();
                routeBuilder.MapODataServiceRoute("ODataRoute", "odata", builder.GetEdmModel());
            });
        }
    }

    public abstract class Node {
        public string Id { get; set; }
        public string Kind { get; set; }
        public IDictionary<string, object> CustomProperties { get; set; }
    }

    public sealed class Type1Node : Node {
    }

    public sealed class Type2Node : Node {
        public string Source { get; set; }
        public string Target { get; set; }
    }

    public sealed class Path {
        public string SourceId { get; set; }
        public string TargetId { get; set; }
        public List<Node> Nodes { get; set; }
    }

    public interface IDataProvider {
        Task<IEnumerable<Node>> GetNodes();
        Task<IEnumerable<Path>> GetPaths(string source, string target);
    }

    public sealed class DataProvider : IDataProvider {
        private static readonly IList<Node> Nodes = new List<Node> {
            new Type1Node{Id = "first", Kind="type1-kind1", CustomProperties = new Dictionary<string, object>()},
            new Type1Node{Id = "second", Kind = "type1-kind2", CustomProperties = new Dictionary<string, object>{{"foo", "bar"}}},
            new Type2Node{Id = "third", Kind="type2-kind1", Source = "first", Target = "second"},
            new Type2Node{Id = "fourth", Kind="type2-kind1", Source = "first", Target = "second", CustomProperties = new Dictionary<string, object>{{"red", "blue"}}}
        };

        public async Task<IEnumerable<Node>> GetNodes() {
            await Task.Yield();
            return Nodes.ToList();
        }

        public async Task<IEnumerable<Path>> GetPaths(string source, string target) {
            await Task.Yield();
            return new List<Path> {
                new Path { SourceId = source, TargetId = target, Nodes = new List<Node> {Nodes[0], Nodes[2], Nodes[1]}},
                new Path { SourceId = source, TargetId = target, Nodes = new List<Node> {Nodes[0], Nodes[3], Nodes[1]}}};
        }
    }

    public class NodesController : ODataController {
        private readonly IDataProvider dataProvider;
        public NodesController(IDataProvider dataProvider) => this.dataProvider = dataProvider;
        [EnableQuery]
        public async Task<List<Node>> Get() => (await dataProvider.GetNodes()).ToList();
    }

    public class PathsController : ODataController {
        private readonly IDataProvider dataProvider;
        public PathsController(IDataProvider dataProvider) => this.dataProvider = dataProvider;
        [EnableQuery]
        [HttpGet]
        [ODataRoute("CalculatePaths")]
        public async Task<List<Path>> Get(string source, string target) =>
            (await dataProvider.GetPaths(source, target)).ToList();
    }
}

Извиняюсь за некрасивость, старался как можно компактнее.

Теперь http://host:port/odata/CalculatePaths?source=A&target=B должен вернуть 2 пути, и это так. Но есть только два строковых свойства, свойства коллекции нет:

GET host:port/odata/CalculatePaths?source=A&target=B вернется: {"@odata.context":"http://host:port/odata/$metadata#Collection(OdataSample.Path)","value":[{"SourceId":"A","TargetId":"B"},{"SourceId":"A","TargetId":"B"}]}

Я пытался возиться с ним по-разному без удовольствия. Единственный раз, когда я приблизился к тому, что хочу, это когда я изменил путь, чтобы иметь только идентификаторы узлов (строку) вместо узлов. Но это не идеально, так как мне нужно будет запросить отдельные узлы, хотя у меня уже есть вся необходимая информация.

Что нужно изменить, чтобы в ответе отображались и узлы?


person tgz    schedule 01.09.2020    source источник


Ответы (1)


Я попробовал ваши коды и получил тот же результат, что и не удалось включить Nodes в результат. Я сделал небольшое изменение с EntitySet<Path>, чтобы получить Nodes хорошо, но не знаю, подходит ли это для вас или нет.

        builder.EntitySet<Path>("CalculatePaths");
        //builder.ComplexType<Path>()
        //    .HasMany(x => x.Nodes)
        //    .HasDerivedTypeConstraints(typeof(Type1Node), typeof(Type2Node));

Необходимо добавить ключ Id в сущность Path.

public sealed class Path
{
    public int Id { get; set; }
    public string SourceId { get; set; }
    public string TargetId { get; set; }
    public List<Node> Nodes { get; set; }
}

Ожидаемый результат

введите здесь описание изображения

Ссылки по теме: Свойство навигации в сложном типе

person Michael Wang    schedule 02.09.2020
comment
Благодарю за ваш ответ. Да, если я создам объекты путей, я смогу расширить узлы и получить данные. Но семантически это неверно. Пути для меня не сущности, это просто отбрасываемые результаты вычислений. Я не хочу управлять их жизненным циклом, и они сопоставимы по стоимости (A-›B-›C == A-›B-›C, независимо от того, когда были подсчитаны результаты). Таким образом, создание их сущностями и назначение GUID только для того, чтобы не заботиться о них, похоже на обходной путь. Если это ограничение технологии, я отмечу ваш ответ как решение, но я все еще надеюсь на что-то другое. - person tgz; 02.09.2020
comment
Вам удобно возвращать Path результат как Entity. После завершения расчета с ресурсами пути используйте Odata для запроса, настройки и представления данных. - person Michael Wang; 04.09.2020