受欢迎的博客标签

Model Binding MongoDB ObjectId with ASP.NET Core MVC

Published

If you’re using the MongoDB C# driver with ASP.NET MVC and have a property of type ObjectId in your model you will get the following error when trying to bind back to the model.

The parameter conversion from type ‘System.String’ to type ‘MongoDB.Bson.ObjectId’ failed because no type converter can convert between these types.
This can easily be resolved by creating a custom Model Binder which I’ll explain how to do in this article.

Normally I try not to use third party library types in View Models, and stick to simple types, so I’d probably have the ObjectId in my Domain model which is represented as a string in my View Model, then I’d use Automapper to manage the conversion.

Anyway, that’s not what this article is about, if you do choose to use an ObjectId in your View Model, you may have a hidden field on your form for that property for updating an object. The hidden input will get rendered correctly with the ObjectId as a string, but when you come to bind back to the model you’ll get some problems.

To create a custom Model Binder, create a class that implements the IModelBinder interface. I have created a class called ObjectIdBinder which looks like this.

class ObjectIdModelBinder : IModelBinder

using System;
using System.Collections.Immutable;
using System.Threading;

using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging;

using Microsoft.AspNetCore.Http;
using MongoDB.Bson;
using Nop.Web.Framework.Mvc.Models;

namespace Nop.Web.Framework.Mvc
{


    //http://stackoverflow.com/questions/27451661/setting-the-type-of-an-object-a-custom-binder-would-apply-to-in-a-vnext-applicat
    // demo .net 5.x:https://github.com/dotnet/aspnetcore/blob/c925f99cddac0df90ed0bc4a07ecda6b054a0b02/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ByteArrayModelBinder.cs
    // http://stackoverflow.com/questions/30061543/mvc6-custom-model-binder-not-firing
    //https://github.com/aspnet/Mvc/tree/dev/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding

    /// <summary>
    /// Objectid 模型绑定,将mvc传回的字符串转换为ObjectId类型
    /// </summary>
    public partial class ObjectIdModelBinder : IModelBinder
    {

        private readonly ILogger _logger;

        /// <summary>
        /// Initializes a new instance of <see cref="ByteArrayModelBinder"/>.
        /// come from:https://github.com/FMI-RobertoDeresu/LoginApp/blob/522da2d398f6cb526165ce823ccaeb1593965e62/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/ByteArrayModelBinder.cs
        /// </summary>
        /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
        public ObjectIdModelBinder(ILoggerFactory loggerFactory)
        {
            if (loggerFactory == null)
            {
                throw new ArgumentNullException(nameof(loggerFactory));
            }

            _logger = loggerFactory.CreateLogger<ObjectIdModelBinder>();
        }


        /// <summary>
        /// Objectid 模型绑定,将mvc传回的字符串转换为ObjectId类型
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public Task  BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

           // _logger.AttemptingToBindModel(bindingContext);

            //不是ObjectId,return
            if (bindingContext.ModelType != typeof(ObjectId))
                 return Task.CompletedTask; 

            ValueProviderResult result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            //获取parentId 的的相关信息
            ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (valueProviderResult == ValueProviderResult.None)
            {
                return Task.CompletedTask;
            }

            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

            //取得parentId 的值,为字符串型
            var value = valueProviderResult.FirstValue;


            if (string.IsNullOrEmpty(value))
            {
                return Task.CompletedTask;
            }




            
          

            

            BaseNopModel model = new BaseNopModel();
            try
            {
                
                //修改objectId值
                if (value == null)
                {
                    model.Id = ObjectId.Empty;
                }
                else
                {
                    model.Id = new ObjectId(value.ToString());

                }

                bindingContext.Result = ModelBindingResult.Success(model.Id);
               
                return Task.CompletedTask;
            }
            catch (Exception exception)
            {
                bindingContext.ModelState.TryAddModelError(
                    bindingContext.ModelName,
                    exception,
                    bindingContext.ModelMetadata);
                return Task.CompletedTask;
            }


        }

      
    
    }


   
   
}

class ObjectIdModelBinderProvider

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;


using MongoDB.Bson;

using Nop.Web.Framework.Mvc;

namespace Nop.Web.Framework.Mvc
{


    /// <summary>
    ///.net5.x see:https://github.com/dotnet/aspnetcore/blob/c925f99cddac0df90ed0bc4a07ecda6b054a0b02/src/Mvc/Mvc.Core/src/ModelBinding/Binders/ByteArrayModelBinderProvider.cs
    /// </summary>
    public class ObjectIdModelBinderProvider : IModelBinderProvider
    {
        /// <inheritdoc />
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(ObjectId))
            {
                var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
                return new ObjectIdModelBinder(loggerFactory);
                
            }

            return null;
        }
    }
}

What this does is gets a ValueProviderResult for the given property represented by ModelBindingContext.ModelName. I then create a new ObjectId using the ValueProdiverResult.AttemptedValue which gets the raw value converted to a string.

This is all that is required for the Model Binder itself, all that’s left to do now is register it in the Global.asax Application_Start method.

protected void Application_Start()
{
    ModelBinders.Binders.Add(typeof(ObjectId), new ObjectIdBinder());
}

Now you can successfully bind back to a model with an ObjectId property type.

 

https://github.com/dotnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/advanced/custom-model-binding/samples/3.x/CustomModelBindingSample/Startup.cs