{"id":4661,"date":"2019-12-31T18:28:44","date_gmt":"2020-01-01T00:28:44","guid":{"rendered":"http:\/\/neosmart.net\/blog\/?p=4661"},"modified":"2019-12-31T18:55:26","modified_gmt":"2020-01-01T00:55:26","slug":"adding-a-modelbindingprovider-for-asp-net-core","status":"publish","type":"post","link":"https:\/\/neosmart.net\/blog\/adding-a-modelbindingprovider-for-asp-net-core\/","title":{"rendered":"Adding a Razor Pages <code>ModelBindingProvider<\/code> in ASP.NET Core"},"content":{"rendered":"<p><a href=\"https:\/\/docs.microsoft.com\/en-us\/aspnet\/core\/mvc\/advanced\/custom-model-binding?view=aspnetcore-3.1\" rel=\"follow\">Microsoft&#8217;s official documentation<\/a> on adding custom model binding providers to convert between (typically) a string and a custom type for complex model binding in ASP.NET Core as of .NET Core 3.1 goes something like this:<\/p>\n<ul>\n<li>Create an <code>IModelBinder<\/code> for your class and use <code>[ModelBinder(BinderType = typeof(MyModelEntityBinder)]<\/code> to decorate each and every binding site, e.g.<br \/>\n<code>public async Task&lt;IActionResult&gt; OnPost([ModelBinder(BinderType = typeof(MyModelEntityBinder)]) MyModel model)<\/code>, which provides the runtime with the type information it needs to instantiate the model binding provider and convert the input to a model.<\/li>\n<li>Optionally create an <code>IModelBinderProvider<\/code> class and register it with the ASP.NET Core host to provide the type information ahead-of-time (once and for all), so that you can instead use the barebones and much shorter decoration at each model binding site instead:<br \/>\n<code>public async Task&lt;IActionResult&gt; OnPost([ModelBinder] MyModel model)<\/code><\/li>\n<\/ul>\n<p>The latter is significantly easier on the eyes and far less error prone&#8230; but where does the type registration take place? Per the linked documentation, the recommendation is the following in <code>Startup.cs<\/code>:<\/p>\n<p><!--more--><\/p>\n<pre><code class=\"language-csharp\">services.AddMvc(options =&gt;\r\n{\r\n    \/\/ add custom binder to beginning of collection\r\n    options.ModelBinderProviders.Insert(0, new MyModelBindingProvider());\r\n});\r\n<\/code><\/pre>\n<p>All of which is fairly straight and to the point, except it requires pulling in the MVC dependency and components, which may not be desirable for overhead if you&#8217;re working on a greenfield project with Razor Pages only.<\/p>\n<p>Unfortunately, the default Razor Pages options configurator does not have a similar <code>ModelBinderProviders<\/code> property that we can set instead, as <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.aspnetcore.mvc.razorpages.razorpagesoptions?view=aspnetcore-3.1\" rel=\"follow\">the <code>RazorPagesOptions<\/code> object<\/a> exposed by the <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/microsoft.extensions.dependencyinjection.mvcservicecollectionextensions.addrazorpages?view=aspnetcore-3.1#Microsoft_Extensions_DependencyInjection_MvcServiceCollectionExtensions_AddRazorPages_Microsoft_Extensions_DependencyInjection_IServiceCollection_System_Action_Microsoft_AspNetCore_Mvc_RazorPages_RazorPagesOptions__\" rel=\"follow\"><code>AddRazorPages()<\/code> extension method<\/a> does not have any such property.<\/p>\n<p>However, we can actually call the <code><a href=\"https:\/\/www.google.com\/search?q=addmvcoptions&amp;oq=addmvcoptions&amp;aqs=chrome..69i57j0l2.7365j0j7&amp;sourceid=chrome&amp;ie=UTF-8\">AddMvcOptions<\/a>(options =&gt; { ... })<\/code> extension method on the <code>IServiceCollection<\/code> directly, allowing us to configure the <code>ModelBinderProviders<\/code> collection without explicitly calling <code>AddMvc()<\/code> in our project:<\/p>\n<pre><code class=\"language-csharp\">services.AddRazorPages()\r\n    .AddMvcOptions(options =&gt;\r\n    {\r\n        options.ModelBinderProviders.Add(new MyModelBindingProvider());\r\n    }\r\n<\/code><\/pre>\n<p>In this case, <code>AddMvcOptions<\/code> is actually just syntactic sugar hiding the dependency injection and explicitly providing the types. When the (Razor Pages) runtime needs to get the model binding configuration, it just obtains a reference to the globally-injected <code>MvcOptions<\/code> returned by the setup action, from where it has access to the <code>ModelBinderProviders<\/code> that&#8217;s now been configured with our model&#8217;s custom binding provider&#8230; except it&#8217;s actually even more dogfooded and with Razor Pages you can actually just inject the <code>IModelBinderProvider<\/code>\u00a0<em>directly<\/em> into the <code>IServiceCollection<\/code> and have it resolved at runtime all the same:<\/p>\n<pre><code class=\"language-csharp\">services.AddRazorPages();\r\nservices.AddSingleton&lt;IModelBinderProvider&gt;(new MyModelBindingProvider());\r\n<\/code><\/pre>\n<p>I don&#8217;t know if that works for actual MVC projects, and I imagine that there are scenarios there that would directly access the <code>ModelBinderProviders<\/code> instance rather than directly use dependency injection to query the binding provider instance.<\/p>\n<p>More importantly, <code>IModelProvider<\/code> isn&#8217;t a generic interface and its type info does not contain any hint about the type of models it can resolve: this is one of the reasons the official documentation says to use the first code sample with <code>ModelBinderProviders.Insert(0, ...)<\/code> rather than the second with <code>ModelBinderProviders.Add(...)<\/code> because it explicitly adds your custom model binding provider to the top of the providers list, whereas neither of <code>.Add(...)<\/code> and <code>AddSingleton&lt;IModelBindingProvider&gt;<\/code> provide any explicit precedence or type specificity, meaning it is possible that a different model binding provider could be picked over your own.<\/p>\n<p>But that also gives us a hint about something else: performance. An <code>IModelBinderProvider<\/code> declaration, like we said, doesn&#8217;t contain any hints about the supported types. Instead, each registered provider must be invoked (in some order) to see whether or not it&#8217;ll match the binding site, which is why a typical <code>IModelBinderProvider<\/code> implementation will look something like this:<\/p>\n<pre><code class=\"language-csharp\">public IModelBinder? GetBinder(ModelBinderProviderContext context)\r\n{\r\n    if (context?.Metadata.ModelType == typeof(Ulid))\r\n    {\r\n        return new BinderTypeModelBinder(typeof(UlidEntityBinder));\r\n    }\r\n\r\n    return null;\r\n}\r\n\r\n<\/code><\/pre>\n<p>This may test for supported types and error out quickly, but it&#8217;s not hard to imagine a poor implementation doing more work (and even some heap allocations!) before failing to providing a converter and returning <code>null<\/code>. Even if not, you never know how many model binders will run before yours and how long it&#8217;ll take to invoke them all. That&#8217;s why if you&#8217;re just doing a simple text-to-object conversion and don&#8217;t have any complex binding requirements, it is recommended to use <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/api\/system.componentmodel.typeconverter?view=netframework-4.8\" rel=\"follow\">a <code>TypeConverter<\/code> class<\/a> instead.<\/p>\n<div class=\"sendy_widget\" style='margin-bottom: 0.5em;'>\n<p><em>If you would like to receive a notification the next time we release a nuget package for .NET or release resources for .NET Core and ASP.NET Core, you can subscribe below. Note that you'll only get notifications relevant to .NET programming and development by NeoSmart Technologies. If you want to receive email updates for all NeoSmart Technologies posts and releases, please sign up in the sidebar to the right instead.<\/em><\/p>\n<iframe tabIndex=-1 onfocus=\"sendy_no_focus\" src=\"https:\/\/neosmart.net\/sendy\/subscription?f=BUopX8f2VyLSOb892VIx6W4BB8V5K2ReYGLVwsfKUZLXCc892Ffz8rIgRyIGoE22cZVr&title=Join+the+dotnet+mailing+list\" style=\"height: 300px; width: 100%;\"><\/iframe>\n<\/div>\n<script type=\"text\/javascript\">function sendy_no_focus(e) { e.preventDefault(); }<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Microsoft&#8217;s official documentation on adding custom model binding providers to convert between (typically) a string and a custom type for complex model binding in ASP.NET Core as of .NET Core 3.1 goes something like this: Create an IModelBinder for your &hellip; <a href=\"https:\/\/neosmart.net\/blog\/adding-a-modelbindingprovider-for-asp-net-core\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":505,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[999,1],"tags":[997,1000,1001,1002],"class_list":["post-4661","post","type-post","status-publish","format-standard","hentry","category-programming","category-software","tag-asp-net-core","tag-imodelbinder","tag-imodelbinderprovider","tag-model-binding"],"aioseo_notices":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p4xDa-1db","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/4661","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/users\/505"}],"replies":[{"embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/comments?post=4661"}],"version-history":[{"count":9,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/4661\/revisions"}],"predecessor-version":[{"id":4671,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/posts\/4661\/revisions\/4671"}],"wp:attachment":[{"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/media?parent=4661"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/categories?post=4661"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/neosmart.net\/blog\/wp-json\/wp\/v2\/tags?post=4661"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}