我在OData的经历

目录

最简单的用法

OData的基本支持

分页

JSON序列化

数据转换

额外的数据

结论


最简单的用法

首先,我们需要一个Web服务。我将使用ASP.NET Core创建它。要使用OData,我们需要安装Microsoft.AspNetCore.OData NuGet包。现在我们必须配置它。以下是Program.cs文件的内容:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services
    .AddControllers()
    .AddOData(opts =>
    {
        opts
            .Select()
            .Expand()
            .Filter()
            .Count()
            .OrderBy()
            .SetMaxTop(1000);
    });

var app = builder.Build();

// Configure the HTTP request pipeline.

app.UseAuthorization();

app.MapControllers();

app.Run();

AddOData方法中,我们在OData中指定了我们允许的所有可能操作。

当然,OData旨在处理数据。让我们向应用程序添加一些数据。数据定义非常简单:

public class Author
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    public string? ImageUrl { get; set; }

    public string? HomePageUrl { get; set; }

    public ICollection
Articles { get; set; } } public class Article { [Key] public int Id { get; set; } public int AuthorId { get; set; } [Required] public string Title { get; set; } }

我将使用实体框架来使用它。测试数据是使用Bogus的:

public class AuthorsContext : DbContext
{
    public DbSet Authors { get; set; } = null!;

    public AuthorsContext(DbContextOptions options)
        : base(options)
    { }

    public async Task Initialize()
    {
        await Database.EnsureDeletedAsync();
        await Database.EnsureCreatedAsync();

        var rnd = Random.Shared;

        Authors.AddRange(
            Enumerable
                .Range(0, 10)
                .Select(_ =>
                {
                    var faker = new Faker();

                    var person = faker.Person;

                    return new Author
                    {
                        FirstName = person.FirstName,
                        LastName = person.LastName,
                        ImageUrl = person.Avatar,
                        HomePageUrl = person.Website,
                        Articles = new List
( Enumerable .Range(0, rnd.Next(1, 5)) .Select(_ => new Article { Title = faker.Lorem.Slug(rnd.Next(3, 5)) }) ) }; }) ); await SaveChangesAsync(); } }

作为数据存储,我将使用内存中的 Sqlite。以下是Program.cs中的配置:

...

var inMemoryDatabaseConnection = new SqliteConnection("DataSource=:memory:");
inMemoryDatabaseConnection.Open();

builder.Services.AddDbContext(optionsBuilder =>
    {
        optionsBuilder.UseSqlite(inMemoryDatabaseConnection);
    }
);

...

using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider.GetRequiredService().Initialize();
}

...

现在存储已准备就绪。让我们创建一个将数据返回到客户端的简单控制器:

[ApiController]
[Route("/api/v1/authors")]
public class AuthorsController : ControllerBase
{
    private readonly AuthorsContext _db;

    public AuthorsController(
        AuthorsContext db
        )
    {
        _db = db ?? throw new ArgumentNullException(nameof(db));
    }

    [HttpGet("no-odata")]
    public ActionResult GetWithoutOData()
    {
        return Ok(_db.Authors);
    }
}

现在在/api/v1/authors/no-odata,我们可以得到以下结果:

[
  {
    "id": 1,
    "firstName": "Fred",
    "lastName": "Kuhlman",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/54.jpg",
    "homePageUrl": "donald.com"
  },
  {
    "id": 2,
    "firstName": "Darrel",
    "lastName": "Armstrong",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/796.jpg",
    "homePageUrl": "angus.org"
  },
  ...
]

当然,目前还没有OData支持。但是添加它有多难?

OData的基本支持

这很容易。让我们再创建一个端点:

[HttpGet("odata")]
[EnableQuery]
public IQueryable GetWithOData()
{
    return _db.Authors;
}

如您所见,差异很小。但现在,您可以在查询中使用OData。例如,查询/api/v1/authors/odata?$filter=id lt 3&$orderby=firstName给出以下结果:

[
  {
    "id": 2,
    "firstName": "Darrel",
    "lastName": "Armstrong",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/796.jpg",
    "homePageUrl": "angus.org"
  },
  {
    "id": 1,
    "firstName": "Fred",
    "lastName": "Kuhlman",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/54.jpg",
    "homePageUrl": "donald.com"
  }
]

伟大!但是有一个小缺点。我们的方法或控制器返回IQueryable<>对象。在实践中,通常我们希望返回响应的几种变体(例如,NotFoundBadRequest...)我们能做什么?

事实证明,OData实现可以很好地将IQueryable<>对象包装为Ok

[HttpGet("odata")]
[EnableQuery]
public IActionResult GetWithOData()
{
    return Ok(_db.Authors);
}

这意味着您可以将任何验证逻辑添加到控制器操作中。

分页

您可能知道,OData只允许您获取完整结果的某些特定页面。可以使用skiptop运算符(例如,/api/v1/authors/odata?$skip=3&$top=2)。在Program.cs中配置OData时,一定不要忘记调用该SetMaxTop方法。否则,使用该top运算符可能会导致以下错误:

The query specified in the URI is not valid. 
The limit of '0' for Top query has been exceeded.

但是为了充分利用分页机制,知道您总共有多少页非常有用。我们需要端点额外返回与给定过滤器对应的项目总数。OData为此目的支持count运算符:(/api/v1/authors/odata?$skip=3&$top=2&$count=true)。但是,如果我们只是简单地添加$count=true到我们的查询中,那将没有任何作用。为了获得所需的结果,我们需要配置EDM(实体数据模型)。但首先,我们必须知道端点的地址。

假设我们希望我们的数据可以在/api/v1/authors/edm上访问。此端点将返回 类型Author的对象。在这种情况下,Program.cs文件中的OData配置将如下所示:

builder.Services
    .AddControllers()
    .AddOData(opts =>
    {
        opts.AddRouteComponents("api/v1/authors", GetAuthorsEdm());

        IEdmModel GetAuthorsEdm()
        {
            ODataConventionModelBuilder edmBuilder = new();

            edmBuilder.EntitySet("edm");

            return edmBuilder.GetEdmModel();
        }

        opts
            .Select()
            .Expand()
            .Filter()
            .Count()
            .OrderBy()
            .SetMaxTop(1000);
    });

请注意,我们的组件(api/v1/authors)的路由等于我们端点地址的前缀,实体集的名称等于此地址的其余部分(edm)。

最后一点是将ODataAttribute Routing属性添加到控制器的相应方法中:

[HttpGet("edm")]
[ODataAttributeRouting]
[EnableQuery]
public IQueryable GetWithEdm()
{
    return _db.Authors;
}

现在,请求的此/api/v1/authors/edm?$top=2&$count=true终结点将返回以下数据:

{
  "@odata.context": "http://localhost:5293/api/v1/authors/$metadata#edm",
  "@odata.count": 10,
  "value": [
    {
      "Id": 1,
      "FirstName": "Steve",
      "LastName": "Schaefer",
      "ImageUrl": "https://cloudflare-ipfs.com/ipfs/
                   Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/670.jpg",
      "HomePageUrl": "kylie.info"
    },
    {
      "Id": 2,
      "FirstName": "Stella",
      "LastName": "Ankunding",
      "ImageUrl": "https://cloudflare-ipfs.com/ipfs/
                   Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/884.jpg",
      "HomePageUrl": "allen.name"
    }
  ]
}

如您所见,该@odata.count字段包含与查询筛选器对应的数据项数。这就是我们想要的。

总的来说,EDM和特定端点之间的对应问题对我来说似乎相当复杂。如果您愿意,您可以尝试通过文档示例自行调查。

您可能会从调试页面获得一些帮助,可以按如下方式启用该页面:

if (app.Environment.IsDevelopment())
{
    app.UseODataRouteDebug();
}

现在,在/$odata,可以看到你拥有哪些终结点以及哪些模型与它们相关联。

JSON序列化

您是否注意到添加EDM后返回的数据发生了什么样的变化?所有属性名称现在都以大写字母开头(以前是firstName,现在是FirstName)。对于大写和小写字母之间存在差异的JavaScript客户端来说,这可能是一个大问题。我们必须以某种方式控制我们属性的名称。OData使用System.Text.Json命名空间的类进行数据序列化。不幸的是,使用此命名空间的属性不会提供任何内容:

[JsonPropertyName("firstName")]
public string FirstName { get; set; }

看起来ODataEDM而不是类定义中获取属性的名称。

OData实现提出了两种在使用EDM的情况下解决此问题的方法。第一个允许使用EnableLowerCamelCase方法调用为整个模型打开小写驼峰式大小写

IEdmModel GetAuthorsEdm()
{
    ODataConventionModelBuilder edmBuilder = new();

    edmBuilder.EnableLowerCamelCase();

    edmBuilder.EntitySet("edm");

    return edmBuilder.GetEdmModel();
}

现在我们有以下数据:

{
  "@odata.context": "http://localhost:5293/api/v1/authors/$metadata#edm",
  "@odata.count": 10,
  "value": [
    {
      "id": 1,
      "firstName": "Troy",
      "lastName": "Gottlieb",
      "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                   Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/228.jpg",
      "homePageUrl": "avery.net"
    },
    {
      "id": 2,
      "firstName": "Mathew",
      "lastName": "Schiller",
      "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                   Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/401.jpg",
      "homePageUrl": "marion.biz"
    }
  ]
}

很好。但是,如果我们需要对JSON属性名称进行更精细的控制,该怎么办?如果我们需要JSON中的某些属性具有C#中属性名称不允许的名称(例如@odata.count)怎么办?

它可以通过EDM完成。让我们重命名homePageUrl@url.home

IEdmModel GetAuthorsEdm()
{
    ODataConventionModelBuilder edmBuilder = new();

    edmBuilder.EnableLowerCamelCase();

    edmBuilder.EntitySet("edm");

    edmBuilder.EntityType()
        .Property(a => a.HomePageUrl).Name = "@url.home";

    return edmBuilder.GetEdmModel();
}

在这里,我们将面临一个令人不快的惊喜:

Microsoft.OData.ODataException: The property name '@url.home' is invalid; 
property names must not contain any of the reserved characters ':', '.', '@'.

让我们尝试更简单的方法:

edmBuilder.EntityType()
        .Property(a => a.HomePageUrl).Name = "url_home";

现在它可以工作:

{
    "url_home": "danielle.info",
    "id": 1,
    "firstName": "Armando",
    "lastName": "Hammes",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/956.jpg"
},

当然不愉快,但你能做什么。

数据转换

到目前为止,我们直接从数据库中为用户提供数据。但通常在大型应用程序中,习惯上划分负责存储信息的类和负责向用户提供数据的类。至少,它允许相对独立地更改这些类。让我们看看此机制如何与OData配合使用。

我将为我们的类创建简单的包装器:

public class AuthorDto
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string? ImageUrl { get; set; }

    public string? HomePageUrl { get; set; }

    public ICollection Articles { get; set; }
}

public class ArticleDto
{
    public string Title { get; set; }
}

我将使用AutoMapper进行转换。我对Mapster不是很熟悉,但我知道它也可以与实体框架一起使用。

对于AutoMapper,我们必须配置相应的转换:

public class DefaultProfile : Profile
{
    public DefaultProfile()
    {
        CreateMap();
        CreateMap();
    }
}

并在应用程序开始时注册它(我在这里使用AutoMapper.Extensions.Microsoft.DependencyInjection NuGet包):

builder.Services.AddAutoMapper(typeof(Program).Assembly);

现在,我可以向控制器再添加一个终结点:

...

private readonly IMapper _mapper;
private readonly AuthorsContext _db;

public AuthorsController(
    IMapper mapper,
    AuthorsContext db
    )
{
    _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    _db = db ?? throw new ArgumentNullException(nameof(db));
}

...

[HttpGet("mapping")]
[EnableQuery]
public IQueryable GetWithMapping()
{
    return _db.Authors.ProjectTo(_mapper.ConfigurationProvider);
}

如您所见,应用转换很容易。不幸的是,结果包含扩展的文章列表:

[
  {
    "id": 1,
    "firstName": "Edward",
    "lastName": "O'Kon",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1162.jpg",
    "homePageUrl": "zachariah.info",
    "articles": [
      {
        "title": "animi-sint-atque"
      },
      {
        "title": "aut-eum-iure"
      }
    ]
  },
  ...
]

这意味着我们无法应用扩展 OData操作。但这很容易修复。让我们为AuthorDto更改AutoMapper配置:

CreateMap()
    .ForMember(a => a.Articles, o => o.ExplicitExpansion());

现在对于/api/v1/authors/mapping,我们得到正确的结果:

[
  {
    "id": 1,
    "firstName": "Spencer",
    "lastName": "Cummerata",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/286.jpg",
    "homePageUrl": "woodrow.info"
  },
  ...
]

对于/api/v1/authors/mapping$expand=articles

InvalidOperationException: The LINQ expression '$it => new SelectAll{
Model = __TypedProperty_1,
Instance = $it,
UseInstanceForProperties = True
}
' could not be translated.

是的,一个问题。但是AutoMapper为我们提供了另一种使用OData的方式。有AutoMapper.AspNetCore.OData.EFCore NuGet包。有了它,我可以像这样实现我的端点:

[HttpGet("automapper")]
public IQueryable GetWithAutoMapper(ODataQueryOptions query)
{
    return _db.Authors.GetQuery(_mapper, query);
}

请注意,我们不会用EnableQuery属性来扩充我们的方法。相反,我们收集ODataQueryOptions对象中的所有OData查询参数,并手动应用所有必需的转换。

这次一切正常:没有扩展的请求:

[
  {
    "id": 1,
    "firstName": "Nathan",
    "lastName": "Heller",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/764.jpg",
    "homePageUrl": "jamarcus.biz",
    "articles": null
  },
  ...
]

和扩展请求:

[
  {
    "id": 1,
    "firstName": "Nathan",
    "lastName": "Heller",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/764.jpg",
    "homePageUrl": "jamarcus.biz",
    "articles": [
      {
        "title": "quidem-nulla-et"
      }
    ]
  },
  ...
]

此外,这种方法还有一个优点。它允许使用标准的JSON工具来控制对象的序列化。例如,我们可以像这样从结果中删除null值:

builder.Services
    .AddJsonOptions(configure =>
    {
        configure.JsonSerializerOptions.DefaultIgnoreCondition = 
                                        JsonIgnoreCondition.WhenWritingNull;
        configure.JsonSerializerOptions.PropertyNamingPolicy = 
                                        JsonNamingPolicy.CamelCase;
    });

此外,我们可以通过通常的属性设置JSON属性名称:

[JsonPropertyName("@url.home")]
public string? HomePageUrl { get; set; }

现在我们可以使用这样的名称:

[
  {
    "id": 1,
    "firstName": "Edward",
    "lastName": "Schmidt",
    "imageUrl": "https://cloudflare-ipfs.com/ipfs/
                 Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1046.jpg",
    "@url.home": "justen.com"
  },
  ...
]

额外的数据

如果我们的数据库字段与结果数据的字段完全匹配,我们就没有问题。但情况并非总是如此。通常,我们希望从存储中返回转换和处理的数据。在这种情况下,我们可能会面临几种情况。

首先,转型可能很简单。例如,我想返回的不是名字和姓氏分别返回,而是作者的全名:

public class ComplexAuthor
{
    [Key]
    public int Id { get; set; }

    public string FullName { get; set; }
}

我们可以像这样为这个类配置AutoMapper

CreateMap()
    .ForMember(d => d.FullName,
        opt => opt.MapFrom(s => s.FirstName + " " + s.LastName));

在这种情况下,我们得到想要的结果:

[
  {
    "id": 1,
    "fullName": "Lance Rice"
  },
  ...
]

此外,我们仍然可以按新字段(/api/v1/authors/nonsql?$filter=startswith(fullName,'A')):

[
  {
    "id": 4,
    "fullName": "Andre Medhurst"
  },
  {
    "id": 6,
    "fullName": "Amber Terry"
  }
]

我们仍然可以这样做的原因是我们的简单表达式(s.FirstName + " " + s.LastName)可以很容易地转换为SQL。下面是在这种情况下实体框架为我生成的查询:

SELECT "a"."Id", ("a"."FirstName" || ' ') || "a"."LastName"
      FROM "Authors" AS "a"
      WHERE (@__TypedProperty_0 = '') OR (((("a"."FirstName" || ' ') || 
      "a"."LastName" LIKE @__TypedProperty_0 || '%') AND 
      (substr(("a"."FirstName" || ' ') || "a"."LastName", 1, 
      length(@__TypedProperty_0)) = @__TypedProperty_0)) OR (@__TypedProperty_0 = ''))

这就是为什么过滤和排序仍然有效的原因。

但显然,并非每个转换都可以转换为SQL。假设出于某种原因,我们想要计算全名的哈希值:

public class ComplexAuthor
{
    [Key]
    public int Id { get; set; }

    public string FullName { get; set; }

    public string NameHash { get; set; }
}

现在我们的AutoMapper配置如下所示:

CreateMap()
    .ForMember(d => d.FullName,
        opt => opt.MapFrom(s => s.FirstName + " " + s.LastName))
    .ForMember(
        d => d.NameHash,
        opt => opt.MapFrom(a => string.Join(",", SHA256.HashData
               (Encoding.UTF32.GetBytes(a.FirstName + " " + a.LastName))))
    );

让我们尝试获取数据:

[
  {
    "id": 1,
    "fullName": "Julius Haag",
    "nameHash": "66,19,82,19,233,224,181,226,111,125,241,228,81,6,
                 200,47,5,112,248,30,186,26,173,91,83,73,9,137,6,158,138,115"
  },
  {
    "id": 2,
    "fullName": "Anita Wilderman",
    "nameHash": "196,131,191,35,182,3,174,193,196,91,70,199,22,173,72,54,
                 123,73,110,83,254,178,19,129,219,24,137,197,83,158,76,209"
  },
  ...
]

有趣。尽管生成的表达式不能用SQL术语表示,但系统仍在继续工作。看起来实体框架知道可以在服务器端评估什么。

现在让我们尝试按这个新字段(nameHash)过滤数据:/api/v1/authors/nonsql?$filter=nameHash eq '1'

InvalidOperationException: The LINQ expression 'DbSet()
.Where(a => (string)string.Join(
separator: ",",
values: SHA256.HashData(__UTF32_0.GetBytes(a.FirstName + " " + 
a.LastName))) == __TypedProperty_1)' could not be translated.

在这里,我们无法再避免将表达式转换为SQL。而且,由于无法完成,我们会收到错误消息。

在这个例子中,我们不能以可以将其转换为SQL的方式重写表达式。但是我们可以禁止按此字段进行过滤和排序。有几个属性可以执行此操作:NonFilterableNotFilterableNotSortableUnsortable。您可以使用其中任何一个:

public class ComplexAuthor
{
    [Key]
    public int Id { get; set; }

    public string FullName { get; set; }

    [NonFilterable]
    [Unsortable]
    public string NameHash { get; set; }
}

如果用户尝试按此字段进行过滤,我更愿意返回Bad Request。但是,仅仅添加这些属性没有任何作用。通过nameHash筛选会导致相同的错误。我们必须手动验证我们的请求:

[HttpGet("nonsql")]
public IActionResult GetNonSqlConvertible(ODataQueryOptions options)
{
    try
    {
        options.Validator.Validate(options, new ODataValidationSettings());
    }
    catch (ODataException e)
    {
        return BadRequest(e.Message);
    }

    return Ok(_db.Authors.GetQuery(_mapper, options));
}

现在,当我们尝试过滤时,我们收到以下消息:

The property 'NameHash' cannot be used in the $filter query option.

它更好。尽管返回给用户的属性名称以小写字母(nameHash)开头,而不是以大写字母(NameHash)开头。

我想知道使用该JsonPropertyName属性更改属性名称的情况如何?例如,我希望我的属性具有名称name

[JsonPropertyName("name")]
public string FullName { get; set; }

我现在可以按name/api/v1/authors/nonsql?$filter=startswith(name,'A'))过滤吗?事实证明,我不能:

Could not find a property named 'name' on type 'ODataJourney.Models.ComplexAuthor'.

如果我们回到EDM怎么办?为此,将ODataAttributeRouting属性添加到控制器方法就足够了:

[HttpGet("nonsql")]
[ODataAttributeRouting]
public IActionResult GetNonSqlConvertible(ODataQueryOptions options)

并更新我们的模型:

...

edmBuilder.EntitySet("nonsql");

edmBuilder.EntityType()
    .Property(a => a.FullName).Name = "name";

...

现在我们可以按name条件过滤:

{
  "@odata.context": "http://localhost:5293/api/v1/authors/$metadata#nonsql",
  "value": [
    {
      "name": "Leona Bauch",
      "id": 3,
      "nameHash": "56,114,131,251,22,63,188,105,37,55,74,232,36,181,152,
                   24,9,111,131,55,229,89,164,181,230,158,109,163,206,137,147,173"
    },
    {
      "name": "Leo Schimmel",
      "id": 7,
      "nameHash": "78,48,88,216,170,3,241,99,96,251,10,176,45,187,250,58,
                   240,215,104,159,26,158,217,244,93,219,183,119,206,40,130,102"
    }
  ]
}

但如您所见,数据结构已更改。我们得到OData包装器。此外,我们又回到了上述对属性名称的限制。

最后,让我们看一下另一种类型的数据转换。到目前为止,我们使用AutoMapper转换了数据。但在这种情况下,我们不能使用请求上下文。AutoMapper转换在一个单独的文件中描述,其中无法访问请求中的信息。但有时,这可能非常重要。例如,我们可能希望根据请求中接收的数据发出另一个Web请求,并使用响应更改结果数据。在下面的示例中,我使用一个简单的foreach循环来表示一些服务器端数据处理:

[HttpGet("add")]
public IActionResult ApplyAdditionalData(ODataQueryOptions options)
{
    try
    {
        options.Validator.Validate(options, new ODataValidationSettings());
    }
    catch (ODataException e)
    {
        return BadRequest(e.Message);
    }

    var query = _db.Authors.ProjectTo(_mapper.ConfigurationProvider);

    var authors = query.ToArray();

    foreach (var author in authors)
    {
        author.FullName += " (Mr)";
    }

    return Ok(authors);
}

当然,这里没有OData支持。但是我们如何添加它呢?我们不想失去过滤、排序和分页的能力。

这是一种可能的方法。我们可以应用除select之外的所有OData操作。在这个例子中,我们仍然使用完整的ComplexAuthor对象。之后,我们转换这些对象,然后应用select操作(如果请求)。这将允许我们从数据库中仅获取与我们的过滤器和页面相对应的少量记录:

[HttpGet("add")]
public IActionResult ApplyAdditionalData(ODataQueryOptions options)
{
    try
    {
        options.Validator.Validate(options, new ODataValidationSettings());
    }
    catch (ODataException e)
    {
        return BadRequest(e.Message);
    }

    var query = _db.Authors.ProjectTo(
        _mapper.ConfigurationProvider);

    var authors = options
        .ApplyTo(query, AllowedQueryOptions.Select)
        .Cast()
        .ToArray();

    foreach (var author in authors)
    {
        author.FullName += " (Mr)";
    }

    var result = options.ApplyTo(
        authors.AsQueryable(),
        AllowedQueryOptions.All & ~AllowedQueryOptions.Select
    );

    return Ok(result);
}

ODataQueryOptions对象允许我们指定应应用哪些OData操作。利用这个机会,我们将OData操作的应用分为两个阶段,在这些阶段之间我们插入我们的处理。

这种方法有其缺点。首先,我们失去了使用JSON属性更改属性名称的能力。可以使用EDM修复它,但在这种情况下,我们将更改数据形状并获取OData包装器。

此外,expand操作问题再次出现。我们的ComplexAuthor类非常简单,但是我们可以向其添加一个返回文章的属性:

public ICollection Articles { get; set; }

我们之前从AutoMapper.AspNetCore.OData.EFCore NuGet包中使用的GetQuery方法不允许部分应用OData操作。没有它,我就无法使系统正确扩展Articles属性。最后,我得到了这个难以理解的错误:

ODataException: Property 'articles' on type 'ODataJourney.Models.ComplexAuthor' 
is not a navigation property or complex property. Only navigation properties 
can be expanded.

也许有人能够克服它。

结论

尽管OData提供了一种相当简单的方法来向Web API添加强大的数据筛选操作,但事实证明,从当前的Microsoft实现中获得所需的所有内容非常困难。看起来当你实现一件事时,其他东西就会掉下来。

让我们希望我只是不明白这里的东西,并且有一种可靠的方法来克服所有这些困难。祝你好运!

PS:你可以在GitHub上找到这篇文章的源代码。

你可以在我的博客上阅读更多我的文章。

https://www.codeproject.com/Articles/5347564/My-Experience-with-OData

你可能感兴趣的:(ASP.NET,CORE,CSharp.NET,c#,OData)