在 Asp.Net Core 上为 MongoDB ObjectId 创建 ModelBinder

Creating a ModelBinder for MongoDB ObjectId on Asp.Net Core

我正在尝试为我的模型中的 ObjectId 类型创建一个非常简单的模型联编程序,但到目前为止似乎无法让它工作。

这是模型活页夹:

public class ObjectIdModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);
        return Task.FromResult(new ObjectId(result.FirstValue));
    }
}

这是我编写的 ModelBinderProvider:

public class ObjectIdModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null) throw new ArgumentNullException(nameof(context));

        if (context.Metadata.ModelType == typeof(ObjectId))
        {
            return new BinderTypeModelBinder(typeof(ObjectIdModelBinder));
        }

        return null;
    }
}

这是 class 我正在尝试将 body 参数绑定到:

public class Player
{
    [BsonId]
    [ModelBinder(BinderType = typeof(ObjectIdModelBinder))]
    public ObjectId Id { get; set; }
    public Guid PlatformId { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
    public int Level { get; set; }
}

这是动作方法:

[HttpPost("join")]
public async Task<SomeThing> Join(Player player)
{
    return await _someService.DoSomethingOnthePlayer(player);
}

为了让这段代码起作用,我的意思是模型绑定器 运行,我从 Controller 继承了控制器并从 Player 参数中删除了 [FromBody] 属性。

当我 运行 这样做时,我可以进入模型联编程序的 BindModelAsync 方法,但是我似乎无法从 post 数据中获取 Id 参数值。我可以看到 bindingContext.FieldName 是正确的;它设置为 Idresult.FirstValue 为空。

我已经离开 Asp.Net MVC 一段时间了,似乎很多东西都发生了变化,变得更加混乱:-)

编辑 根据评论,我认为我应该提供更多背景信息。

如果我将 [FromBody] 放在 Player 操作参数之前,player 将设置为 null。如果我删除 [FromBody],player 将设置为默认值,而不是我 post 的值。 post body如下所示,只是一个简单的JSON:

{
    "Id": "507f1f77bcf86cd799439011"
    "PlatformId": "9c8aae0f-6aad-45df-a5cf-4ca8f729b70f"
}

If I remove [FromBody], player is set to a default value, not to the values I post.

从正文中读取数据是选择加入(除非您使用[ApiController])。当您从 Player 参数中删除 [FromBody] 时,模型绑定过程将默认使用路由、查询字符串和表单值来填充 Player 的属性。在您的示例中,这些位置没有此类属性,因此设置了 Player 属性中的 none。

If I put [FromBody] before the Player action parameter, player is set to null.

由于存在 [FromBody] 属性,模型绑定进程会尝试根据随请求提供的 Content-Type 从正文中读取。如果这是 application/json,正文将被解析为 JSON 并映射到您的 Player 的属性。在您的示例中,JSON-解析过程失败,因为它不知道如何从 string 转换为 ObjectId。发生这种情况时,您控制器中的 ModelState.IsValid 将 return false 并且您的 Player 参数将是 null.

For this code to work, I mean for the model binder to run, I inherited the controller from Controller and removed the [FromBody] attribute from the Player parameter.

当您删除 [FromBody] 时,您在 Id 属性 上设置的 [ModelBinder(...)] 属性会得到遵守,因此您的代码会运行。但是,由于 [FromBody] 的存在,该属性实际上被忽略了。这里的幕后发生了很多事情,但本质上归结为这样一个事实,即您已经选择以 JSON 的形式从正文中加入模型绑定,这就是模型绑定在此停止的地方情景.


我在上面提到,由于不了解如何处理 ObjectId,这里失败的是 JSON-解析过程。由于此 JSON-解析由 Newtonsoft.Json(又名 JSON.NET)处理,一个可能的解决方案是创建自定义 JsonConverter。这在 Stack Overflow 上有很好的介绍,所以我不会详细讨论 如何 它的工作原理。这是一个完整的示例(为简洁和懒惰而省略了错误处理):

public class ObjectIdJsonConverter : JsonConverter
{
    public override bool CanConvert(Type objectType) =>
        objectType == typeof(ObjectId);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) =>
        ObjectId.Parse(reader.Value as string);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) =>
        writer.WriteValue(((ObjectId)value).ToString());
}

要使用它,只需将现有的 [ModelBinder(...)] 属性替换为 [JsonConverter(...)] 属性,如下所示:

[BsonId]
[JsonConverter(typeof(ObjectIdJsonConverter))]    
public ObjectId Id { get; set; }

或者,您可以全局注册 ObjectIdJsonConverter,以便它适用于所有 ObjectId 属性,在 Startup.ConfigureServices:

中使用类似的内容
services.AddMvc()
        .AddJsonOptions(options =>
            options.SerializerSettings.Converters.Add(new ObjectIdJsonConverter());
        );

你在 ModelBinder 上弄错了。正确代码:

public class ObjectIdModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var result = bindingContext.ValueProvider.GetValue(bindingContext.FieldName);

        bindingContext.Result = ModelBindingResult.Success(new ObjectId(result.FirstValue));

        return Task.CompletedTask;
    }
}