Создание ModelBinder для MongoDB ObjectId в Asp.Net Core - PullRequest
0 голосов
/ 11 ноября 2018

Я пытаюсь создать очень простую привязку модели для типов 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;
    }
}

Вот класс, к которому я пытаюсь привязать параметр 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 и удалил атрибут [FromBody] из параметра Player.

Когда я запускаю это, я могу войти в метод BindModelAsync подшивки модели, однако я не могу получить значение параметра Id из данных поста. Я вижу, что bindingContext.FieldName является правильным; для него установлено значение Id , но result.FirstValue равно нулю.

Я некоторое время отсутствовал в Asp.Net MVC, и, похоже, многое изменилось и стало более запутанным: -)

EDIT Основываясь на комментариях, я думаю, что я должен предоставить больше контекста.

Если я поставлю [FromBody] перед параметром действия Player, player будет установлен в null. Если я удалю [FromBody], для player будет установлено значение по умолчанию, а не значения, которые я публикую. Тело сообщения показано ниже, это просто JSON:

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

Ответы [ 2 ]

0 голосов
/ 10 февраля 2019

Вы ошиблись в 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;
    }
}
0 голосов
/ 14 ноября 2018

Если я удаляю [FromBody], player устанавливается в значение по умолчанию, а не в значения, которые я публикую.

Чтение данных из тела - opt-in (если вы не используете [ApiController]). Когда вы удаляете [FromBody] из параметра Player, процесс привязки модели будет искать заполнение свойств Player, используя маршрут, строку запроса и значения формы по умолчанию. В вашем примере таких свойств нет в этих местах, и поэтому ни одно из свойств Player не установлено.

Если я поставлю [FromBody] перед параметром действия Player, player будет установлен в null.

При наличии атрибута [FromBody] процесс привязки модели пытается выполнить чтение из тела в соответствии с Content-Type, предоставленным в запросе. Если это application/json, тело будет проанализировано как JSON и сопоставлено со свойствами Player. В вашем примере процесс JSON-разбора завершается неудачей, поскольку он не знает, как преобразовать из string в ObjectId. Когда это произойдет, ModelState.IsValid в вашем контроллере вернет false, а ваш параметр Player будет null.

Чтобы этот код работал, я имею в виду запуск связывателя модели, я унаследовал контроллер от Controller и удалил атрибут [FromBody] из параметра Player.

Когда вы удаляете [FromBody], атрибут [ModelBinder(...)], который вы установили в свойстве Id, соблюдается, и ваш код запускается. Однако при наличии [FromBody] этот атрибут фактически игнорируется. Здесь много чего происходит за кулисами, но по сути это сводится к тому, что вы уже включили привязку модели из тела в виде JSON, и в этом случае привязка модели останавливается в этом сценарии.


Я упоминал выше, что это процесс синтаксического анализа JSON, который здесь не работает из-за непонимания того, как обрабатывать ObjectId. Поскольку этот разбор 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());
        );
...