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

#c# #function #asp.net-core #odata #unbound

#c# #функция #asp.net-core #odata #несвязанная

Вопрос:

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

У меня есть служба odata, которую я могу использовать для запроса узлов. Узлы могут быть узлами типа 1 или типа 2, и это открытые типы с динамическими свойствами. Запрос к ним работает отлично. Что я хочу сделать, так это вычислить пути между узлами. Пути не являются объектами — у них нет идентификатора. Поэтому я не считаю, что было бы правильно создавать ресурс для этого. Это просто результаты вычислений пути, содержащие списки узлов, которые находятся вдоль пути, поэтому я думаю, что функция — лучший способ сообщить 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=Aamp;target=B должно вернуться 2 пути, и это происходит. Но есть только два строковых свойства, свойство collection отсутствует:

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

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

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

Ответ №1:

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

         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; }
}
  

Результат, который вы ожидаете

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

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

Комментарии:

1. спасибо за ваш ответ. Да, если я создам объекты paths, я смогу развернуть узлы и получить данные. Но семантически это неверно. Пути для меня не являются объектами, они просто отбрасываемые результаты вычислений. Я не хочу управлять их жизненным циклом, и они сопоставимы по значению (A-> B-> C == A->B-> C, независимо от того, когда были рассчитаны результаты). Поэтому создание их сущностей и назначение идентификаторов GUID только для того, чтобы не заботиться о них, похоже на обходной путь. Если это ограничение в технологии, я отмечу ваш ответ как решение, но я все еще надеюсь на что-то другое.

2. Вам удобно возвращать Path результат как объект. После завершения вычисления с ресурсами пути используйте Odata для запроса, настройки и представления данных.