ASP.NET Core OData 9 正式发布

ASP.NET Core OData 9 正式发布 OData 正式发布 ASP. Core NET ASP

我们很高兴地宣布,ASP.NET Core OData 9 已正式发布,并在 NuGet 上提供:

此版本的主要亮点是将 OData .NET 依赖项更新到 8.x 主版本。 通过更新依赖项,我们能够利用 Microsoft.OData.Core 8.x 和 Microsoft.OData.Edm 8.x 版本中引入的改进和新功能。

ASP.NET Core OData 9 版本将仅支持 .NET 8 或更高版本。

OData .NET 8 官方发布公告解决了该版本中引入的主要更改。建议阅读这篇文章以熟悉这些变化。

在本文中,我们将探讨其中一些更改如何影响 ASP.NET Core OData 库,以及如何在可能的情况下切换旧行为。

请求和响应负载中的字符编码

在 OData .NET 8 中,我们引入了一个新的 JSON 编写器,它在后台使用 .NET Utf8JsonWriter 来编写请求和响应有效负载。新的 JSON 编写器速度明显更快,并且是 ASP.NET Core OData 9 中的默认编写器。

您可能会观察到输出负载上的字符编码存在差异。我们将在以下各节中介绍这些差异。

编码的字符子集

在其默认配置中,新的 JSON 编写器不会像旧版 (OData .NET 7) 那样对大量字符进行编码。

默认情况下,旧版 JSON 编写器对所有整数值小于 32 且大于 127 的字符进行编码,基本上都是非 ASCII 字符。这计算出大约 65440 个字符!可以通过将 option 传递给默认的 JSON 编写器工厂构造函数来覆盖旧版 JSON 编写器的默认配置。使用这种替代配置,将对大大减少的字符子集进行编码。ODataStringEscapeOption.EscapeOnlyControls

新的 JSON 编写器使用默认情况下配置了 encoder 选项的基础。编码的结果字符集要小得多。Utf8JsonWriterJavaScriptEncoder.UnsafeRelaxedJsonEscaping

下面是一个插图 – 以 OData 服务的代码示例形式,演示了使用新的 JSON 编写器时输出有效负载的外观:

// Model
namespace Ver900Sample.Models
{
    public class Order
    {
        public int Id { get; set; }
        public decimal Amount { get; set; }
        public string Note { get; set; }
    }
}

// Controller
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Results;
using Microsoft.AspNetCore.OData.Routing.Controllers;
using Ver900Sample.Models;

namespace Ver900Sample.Controllers
{
    public class OrdersController : ODataController
    {
        private static readonly List<Order> orders = new List<Order>
        {
            new Order { Id = 1, Amount = 130m, Note = "a - z, α - Ω" },
            new Order { Id = 2, Amount = 170.50m, Note = "😀 🐂 🐕" }
        };

        [EnableQuery]
        public SingleResult<Order> Get(int key)
        {
            return SingleResult.Create(orders.AsQueryable().Where(d => d.Id == key));
        }
    }
}

// Service configuration
using Ver900Sample.Models;
using Microsoft.AspNetCore.OData;
using Microsoft.OData.ModelBuilder;

var builder = WebApplication.CreateBuilder(args);

var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.EntitySet<Order>("Orders");

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        model: modelBuilder.GetEdmModel()));

var app = builder.Build();

app.UseRouting();
app.MapControllers();

app.Run();

您可以使用该工具查询此服务的订单 1 – 浏览器可能不是此图的好选择,因为它会智能解码编码的字符。curl

curl http://localhost:5090/Orders(1)

注意:5090 是计算机分配的随机端口,因计算机而异。

以下是您将观察到的结果:

{
  "@odata.context": "http://localhost:5090/$metadata#Orders/$entity",
  "Id": 1,
  "Amount": 130,
  "Note": "a - z, α - Ω"
}

在上述情况下,新的 JSON 编写器不会对 和非 ASCII 字符进行编码。αΩ

如果在您的方案中保留旧版 JSON 编写器的行为很重要,则可以通过使用依赖项注入将新的 JSON 编写器替换为旧版 JSON 编写器来实现此目的。您可以通过修改上述代码片段中的方法调用来执行此操作,如下所示:AddOData

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddScoped<Microsoft.OData.Json.IJsonWriterFactory>(
                sp => new Microsoft.OData.Json.ODataJsonWriterFactory());
        }));

注意:该类是以前在 OData .NET 7 中命名的类。我们认为名称中包含“Default”会使歧义永久存在,因为它不是 OData .NET 8 中的默认 JSON 编写器工厂。ODataJsonWriterFactoryDefaultJsonWriterFactory

如果您在上述更改后查询订单 1,响应有效负载将如下所示:

{
  "@odata.context": "http://localhost:5090/$metadata#Orders/$entity",
  "Id": 1,
  "Amount": 130,
  "Note": "a - z, \u03b1 - \u03a9"
}

在上述情况下,将对非 ASCII 字符进行编码。

还支持更严格的 JavaScript 编码器。您可以配置您的服务以使用该编码器,如下所示:Utf8JsonWriter

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddScoped<Microsoft.OData.Json.IJsonWriterFactory>(
                sp => new Microsoft.OData.Json.ODataUtf8JsonWriterFactory(
                    System.Text.Encodings.Web.JavaScriptEncoder.Default));
        }));

如果您在上述更改后查询订单 1,响应有效负载将如下所示:

{
  "@odata.context": "http://localhost:5090/$metadata#Orders/$entity",
  "Id": 1,
  "Amount": 130,
  "Note": "a - z, \u03B1 - \u03A9"
}

上述响应有效负载看起来类似于旧版 JSON 编写器的输出。

但是,请务必注意,使用更严格的 JavaScript 编码器配置的编码器并不是旧版 JSON 编写器在编码字符方面的镜像。Utf8JsonWriter

要关闭循环,下面介绍如何将服务配置为使用具有不太严格编码选项的旧版 JSON 编写器:

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddScoped<Microsoft.OData.Json.IJsonWriterFactory>(
                sp => new Microsoft.OData.Json.ODataJsonWriterFactory(
                    Microsoft.OData.Json.ODataStringEscapeOption.EscapeOnlyControls));
        }));

选择配置选项是因为它是 ASP.NET Core 8.0 中配置的默认选项。JavaScriptEncoder.UnsafeRelaxedJsonEscaping

Unicode 码位的大写字母

新的 JSON 编写器在编码输出中使用大写字母,而旧版 JSON 编写器使用小写字母。这两个输出都是合法的,客户端反序列化任何一种格式都应该没有问题。

使用上一节中的示例服务并配置了新的 JSON 编写器,您可以按如下方式查询订单 2:

curl http://localhost:5090/Orders(2)

以下是您将观察到的结果:

{
  "@odata.context": "http://localhost:5090/$metadata#Orders/$entity",
  "Id": 2,
  "Amount": 170.50,
  "Note": "\uD83D\uDE00 \uD83D\uDC02 \uD83D\uDC15"
}

请注意,Unicode 字符的编码值为大写。

如果您在配置了旧版 JSON 编写器的情况下查询相同的顺序 2,则响应有效负载如下所示:

{
  "@odata.context": "http://localhost:5090/$metadata#Orders/$entity",
  "Id": 2,
  "Amount": 170.50,
  "Note": "\ud83d\ude00 \ud83d\udc02 \ud83d\udc15"
}

如果所描述的字符编码更改在您的方案中是一个交易破坏者,请将新的 JSON 编写器替换为旧版 JSON 编写器,如图所示。

对依赖项注入的更改

我们进行了重大更改,以适应 OData .NET 8 中的依赖关系注入重构。我们摆脱了非标准的依赖注入工件(例如),转而使用 .NET 框架依赖注入抽象。IContainerBuilder

具体而言,对 OData 核心库中方法的更改使配置 、 和如下所示成为可能:AddODataDefaultServicesODataReaderSettingsODataMessageWriterSettingsODataUriParserSettings

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddDefaultODataServices(
                odataVersion: Microsoft.OData.ODataVersion.V4,
                configureReaderAction: (messageReaderSettings) =>
                {
                    // Relevant changes to the ODataMessageReaderSettings instance here
                },
                configureWriterAction: (messageWriterSettings) =>
                {
                    // Relevant changes to the ODataMessageWriterSettings instance here
                },
                configureUriParserAction: (uriParserSettings) =>
                {
                    // // Relevant changes to the ODataUriParserSettings instance here
                });
        }));

这为开发人员在操作这些特定设置时提供了更大的灵活性。

该参数还使注入任何所需的依赖项变得简单明了:IServiceCollection

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddSingleton<Microsoft.OData.ODataPayloadValueConverter>(
                sp => new CustomODataPayloadValueConverter());
        }));

// ...

public class CustomODataPayloadValueConverter : Microsoft.OData.ODataPayloadValueConverter
{
    public override object ConvertToPayloadValue(object value, IEdmTypeReference edmTypeReference)
    {
        if (edmTypeReference.PrimitiveKind() == EdmPrimitiveTypeKind.DateTimeOffset)
        {
            var dateTimeOffset = (DateTimeOffset)value;
            return dateTimeOffset.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffffzzz");
        }

        return base.ConvertToPayloadValue(value, edmTypeReference);
    }
}

向后兼容性标志

OData .NET 8 的一个主要目标是确保迁移到新版本的客户能够输出与在 OData .NET 7 中相同的响应负载。在进行更改以更好地与 OData 标准保持一致的地方,添加了兼容性标志,以便能够切换旧行为。

我们为与 OData 标准保持一致而进行的更改的一个示例是,在服务元数据负载中编写十进制属性的属性,以及为空间属性编写属性。ScaleSRID

如果您查询上一节中示例服务的服务元数据终端节点 (http://localhost:5090/$metadata),您将获得以下有效负载:

<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
    <edmx:DataServices>
        <Schema Namespace="Ver900Sample.Models" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityType Name="Order">
                <Key>
                    <PropertyRef Name="Id" />
                </Key>
                <Property Name="Id" Type="Edm.Int32" Nullable="false" />
                <Property Name="Amount" Type="Edm.Decimal" Nullable="false" Scale="variable" />
                <Property Name="Note" Type="Edm.String" Nullable="false" />
            </EntityType>
        </Schema>
        <Schema Namespace="Default" xmlns="http://docs.oasis-open.org/odata/ns/edm">
            <EntityContainer Name="Container">
                <EntitySet Name="Orders" EntityType="Ver900Sample.Models.Order" />
            </EntityContainer>
        </Schema>
    </edmx:DataServices>
</edmx:Edmx>

在 OData .NET 7 中,该属性将写为 – 带有大写的“V”。与 OData 标准的这种偏差在 OData .NET 8 中已修复。该属性现在写入 ,如上面的有效负载所示。ScaleScale="Variable"Scale Scale="variable"

如果在您的方案中维护 OData .NET 7 行为很重要,您可以通过切换功能标志来实现这一点,如下所示:

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddDefaultODataServices(
                odataVersion: Microsoft.OData.ODataVersion.V4,
                configureReaderAction: null,
                configureWriterAction: (messageWriterSettings) =>
                {
                    messageWriterSettings.LibraryCompatibility |= Microsoft.OData.ODataLibraryCompatibility.UseLegacyVariableCasing;
                },
                configureUriParserAction: null);
        }));

切换标志后,将写入该属性,以便与 OData .NET 7 兼容。ODataLibraryCompability.UseLegacyVariableCasingScaleScale="Variable"

您可以根据需要组合任意数量的兼容性标志来实现所需的行为。

若要切换所有兼容性标志以实现 OData .NET 7 兼容性,可以使用方便的标志,如下所示:ODataLibraryCompatibility.Version7

builder.Services.AddControllers().AddOData(
    options => options.EnableQueryFeatures().AddRouteComponents(
        routePrefix: string.Empty,
        model: modelBuilder.GetEdmModel(),
        configureServices: (services) =>
        {
            services.AddDefaultODataServices(
                odataVersion: Microsoft.OData.ODataVersion.V4,
                configureReaderAction: null,
                configureWriterAction: (messageWriterSettings) =>
                {
                    messageWriterSettings.LibraryCompatibility |= Microsoft.OData.ODataLibraryCompatibility.Version7;
                },
                configureUriParserAction: null);
        }));

 

结论

我们邀请您试用 ASP.NET Core OData 9 并与我们分享您的反馈。感谢您对 OData 生态系统的持续支持和贡献。

感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是yswenli 。

评论