Как мне сопоставить запрос OData с DTO другому объекту?

Мой вопрос очень похож на этот: как мне сопоставить запрос OData с DTO к объекту EF? У меня есть простая настройка для тестирования функциональности фильтра OData V4 $ в ASP.NET Web API. Я хотел бы сделать так, чтобы "псевдоним" некоторых свойств ProductDTO соответствовал свойствам сущности Product. Пользователь вызовет ProductsController, например, со следующим запросом:

ПОЛУЧИТЬ продукты?$ Filter= DisplayName eq 'test'

Класс продукта:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Level { get; set; }
    public Product()
    { }
}

Класс ProductDTO:

public class ProductDTO
{
    public int Id { get; set; }
    public string DisplayName { get; set; }
    public int DisplayLevel { get; set; }
    public ProductDTO(Product product)
    {
        this.DisplayName = product.Name;
        this.DisplayLevel = product.Level;
    }
}

Контроллер продуктов:

public class ProductsController : ApiController
{
    public IEnumerable<ProductDTO> Get(ODataQueryOptions<Product> q)
    {
        IQueryable<Product> products = this._products.AsQueryable();
        if (q.Filter != null) products = q.Filter.ApplyTo(this._products.AsQueryable(), new ODataQuerySettings()) as IQueryable<Product>;
        return products.Select(p => new ProductDTO(p));
    }
}

Конечно, я получаю следующее исключение:

Не удалось найти свойство с именем "DisplayName" для типа "TestAPI.Models.Product"

Я попытался использовать недавно введенную функцию псевдонимов, добавив следующие строки в WebApiConfig.cs

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        …
        IEdmModel model = GetModel();
        config.MapODataServiceRoute("*", "*", model);
    }

    private static IEdmModel GetModel()
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        EntitySetConfiguration<Product> products = builder.EntitySet<Product>("Product");
        products.EntityType.Property(p => p.Name).Name = "DisplayName";
        products.EntityType.Property(p => p.Level).Name = "DisplayLevel";
        return builder.GetEdmModel();
    }
}

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

ПОЛУЧИТЬ продукты?$ Filter= Имя eq 'test'

Обновить:

Я согласен с Гдорон, Get Конечная точка должна выглядеть так:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

Но это должно быть решаемо без AutoMapper?

6 ответов

Решение

Я нашел решение без использования AutoMapper.

ProductsController теперь выглядит так:

public class ProductsController : ApiController
{
    public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)
    {
        IQueryable<Product> products = this._products.AsQueryable();

        IEdmModel model = GetModel();
        IEdmType type = model.FindDeclaredType("TestAPI.Models.Product");
        IEdmNavigationSource source = model.FindDeclaredEntitySet("Products");
        ODataQueryOptionParser parser = new ODataQueryOptionParser(model, type, source, new Dictionary<string, string> { { "$filter", q.Filter.RawValue } });
        ODataQueryContext context = new ODataQueryContext(model, typeof(Product), q.Context.Path);
        FilterQueryOption filter = new FilterQueryOption(q.Filter.RawValue, context, parser);

        if (filter != null) products = filter.ApplyTo(products, new ODataQuerySettings()) as IQueryable<Product>;
        return products.Select(p => new ProductDTO(p));
    }
}

WebApiConfig:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        …
        IEdmModel model = GetModel();
        config.MapODataServiceRoute("*", "*", model);
    }

    private static IEdmModel GetModel()
    {
        ODataModelBuilder builder = new ODataConventionModelBuilder();
        EntitySetConfiguration<Product> product = builder.EntitySet<Product>("Products");
        product.EntityType.Name = "Product";
        product.EntityType.Namespace = "TestAPI.Models";
        product.EntityType.Property(p => p.Name).Name = "DisplayName";
        product.EntityType.Property(p => p.Level).Name = "DisplayLevel";
        return builder.GetEdmModel();
    }
}

Если вы решили, что хотите использовать DTO (что, на мой взгляд, хорошая идея), используйте...
$metadata должен отражать имена свойств DTO, а не сущности EF, так как это то, что получают клиенты, и это то, что должны отправлять клиенты.
Это означает, что вы должны изменить Get конечная точка примерно так:

public IEnumerable<ProductDTO> Get(ODataQueryOptions<ProductDTO> q)

Чтобы избежать связи между ProductDTO а также Product Вы можете использовать AutoMapper для отображения между классами для вас. Кроме того, если вы используете AutoMapper's Project метод, вы можете очистить ваши методы, например:

public IQueryable<ProductDTO> Get(ProductDTO dto)

Вы можете проверить официальную демоверсию Asp.net для управления версиями, она интенсивно использует DTO и AutoMapper, она даст вам хорошее направление, просто проигнорируйте управление версиями, если оно вас сейчас не интересует.

Для тех, кто использует .NET 6 сMicrosoft.AspNetCore.OData 8.0.8вы можете сделать это так:

      [ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
    private readonly MyDbContext _context;

    public ProductsController(MyDbContext context)
    {
        _context = context; 
    }

    [HttpGet]
    [EnableQuery]
    public IQueryable<ProductDto> Get()
    {
        return _context.Products
            .Select(p => new ProductDto()
            {
                DisplayName = p.DisplayName,
                DisplayLevel = p.DisplayLevel
            });
    }
}

И в вашем стартапе:

      builder.Services
    .AddControllers()
    .AddOData(opt => opt.Filter().Select())

Обратите внимание, что ключ находится в проекции. Изменение его на .Select(p => new ProductDto(p)не будет работать, так как его нельзя перевести в SQL. Больше нет необходимости в EDM-модели. А GETдля этого URL:

http://localhost:XXXX/products?$filter=DisplayName eq 'myName'&select=DisplayLevel

Сгенерирует SQL, выглядящий примерно так:

      exec sp_executesql N'SELECT [a].[DisplayLevel]
FROM [dbo].[Products] AS [a]
WHERE [a].[DisplayName] = @__TypedProperty_0',N'@__TypedProperty_0 nvarchar(4000)',@__TypedProperty_0=N'myName'

Для меня решением было просто добавить DTO в конфигурацию EDM (v4):

edmBuilder.EntitySet<Contact>("Contacts");
edmBuilder.EntityType<ContactDto>();

Попробуйте использовать AutoMapper, вам нужно будет добавить эти ссылки на ваш контроллер

using AutoMapper;
using AutoMapper.QueryableExtensions;

Ваш метод

[EnableQuery(AllowedQueryOptions = AllowedQueryOptions.All)]
public IQueryable<ObjectDTO> Get()
{
    return dbContext.Entities.ProjectTo<ObjectDTO>();
}

В вашем глобальном

protected void Application_Start()
{
        //Usually in a diff class Mapping.ConfigureDataTransferObjects();
        Mapper.CreateMap<MyEntity, ObjectDTO>();
        Mapper.CreateMap<ObjectDTO, MyEntity>();
}

Патрик, вы можете заполнить целевое значение из вычисленного sourceValue, например:

Mapper.CreateMap<Customer, CustomerDTO>()
    .ForMember(dest => dest.InvoiceCount, opt =>
        opt.MapFrom(src => src.Invoices.Count()));

Я получил этот пример от: http://codethug.com/2015/02/13/web-api-deep-dive-dto-transformations-and-automapper-part-5-of-6/

Arturo, вы можете использовать reverseMap для CreateMap, если это не сложное отображение, чтобы сделать однострочную.

Другие вопросы по тегам