Skip to content

Commit 4702d7d

Browse files
Update "Custom Model Binding" to 3.1. (dotnet#16424)
* 3.1 Sample. * Updated links to 3.1 sample. * Update custom-model-binding.md Co-authored-by: Rick Anderson <[email protected]>
1 parent 2b18a3d commit 4702d7d

File tree

14 files changed

+362
-14
lines changed

14 files changed

+362
-14
lines changed

aspnetcore/mvc/advanced/custom-model-binding.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ title: Custom Model Binding in ASP.NET Core
33
author: ardalis
44
description: Learn how model binding allows controller actions to work directly with model types in ASP.NET Core.
55
ms.author: riande
6-
ms.date: 01/01/2020
6+
ms.date: 01/06/2020
77
uid: mvc/advanced/custom-model-binding
88
---
99
# Custom Model Binding in ASP.NET Core
1010

1111
::: moniker range=">= aspnetcore-3.0"
1212

13-
By [Steve Smith](https://ardalis.com/)
13+
By [Steve Smith](https://ardalis.com/) and [Kirk Larkin](https://twitter.com/serpent5)
1414

1515
Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders. Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).
1616

17-
[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/advanced/custom-model-binding/) ([how to download](xref:index#how-to-download-a-sample))
17+
[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/advanced/custom-model-binding/samples) ([how to download](xref:index#how-to-download-a-sample))
1818

1919
## Default model binder limitations
2020

@@ -28,7 +28,7 @@ Before creating your own custom model binder, it's worth reviewing how existing
2828

2929
### Working with the ByteArrayModelBinder
3030

31-
Base64-encoded strings can be used to represent binary data. For example, an image can be encoded as a string. The sample includes an image as a base64-encoded string in [Base64String.txt](https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/advanced/custom-model-binding/samples/2.x/CustomModelBindingSample/Base64String.txt).
31+
Base64-encoded strings can be used to represent binary data. For example, an image can be encoded as a string. The sample includes an image as a base64-encoded string in [Base64String.txt](https://github.com/aspnet/AspNetCore.Docs/blob/master/aspnetcore/mvc/advanced/custom-model-binding/samples/3.x/CustomModelBindingSample/Base64String.txt).
3232

3333
ASP.NET Core MVC can take a base64-encoded string and use a `ByteArrayModelBinder` to convert it into a byte array. The <xref:Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ByteArrayModelBinderProvider> maps `byte[]` arguments to `ByteArrayModelBinder`:
3434

@@ -42,7 +42,8 @@ public IModelBinder GetBinder(ModelBinderProviderContext context)
4242

4343
if (context.Metadata.ModelType == typeof(byte[]))
4444
{
45-
return new ByteArrayModelBinder();
45+
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
46+
return new ByteArrayModelBinder(loggerFactory);
4647
}
4748

4849
return null;
@@ -53,15 +54,15 @@ When creating your own custom model binder, you can implement your own `IModelBi
5354

5455
The following example shows how to use `ByteArrayModelBinder` to convert a base64-encoded string to a `byte[]` and save the result to a file:
5556

56-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Controllers/ImageController.cs?name=post1)]
57+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Controllers/ImageController.cs?name=snippet_Post)]
5758

5859
You can POST a base64-encoded string to this api method using a tool like [Postman](https://www.getpostman.com/):
5960

6061
![postman](custom-model-binding/images/postman.png "postman")
6162

6263
As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. The following example shows how to use `ByteArrayModelBinder` with a view model:
6364

64-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Controllers/ImageController.cs?name=post2&highlight=2)]
65+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Controllers/ImageController.cs?name=snippet_SaveProfile&highlight=2)]
6566

6667
## Custom model binder sample
6768

@@ -73,24 +74,24 @@ In this section we'll implement a custom model binder that:
7374

7475
The following sample uses the `ModelBinder` attribute on the `Author` model:
7576

76-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Data/Author.cs?highlight=6)]
77+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Data/Author.cs?highlight=6)]
7778

7879
In the preceding code, the `ModelBinder` attribute specifies the type of `IModelBinder` that should be used to bind `Author` action parameters.
7980

8081
The following `AuthorEntityBinder` class binds an `Author` parameter by fetching the entity from a data source using Entity Framework Core and an `authorId`:
8182

82-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Binders/AuthorEntityBinder.cs?name=demo)]
83+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Binders/AuthorEntityBinder.cs?name=snippet_Class)]
8384

8485
> [!NOTE]
8586
> The preceding `AuthorEntityBinder` class is intended to illustrate a custom model binder. The class isn't intended to illustrate best practices for a lookup scenario. For lookup, bind the `authorId` and query the database in an action method. This approach separates model binding failures from `NotFound` cases.
8687
8788
The following code shows how to use the `AuthorEntityBinder` in an action method:
8889

89-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Controllers/BoundAuthorsController.cs?name=demo2&highlight=2)]
90+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Controllers/BoundAuthorsController.cs?name=snippet_Get&highlight=2)]
9091

9192
The `ModelBinder` attribute can be used to apply the `AuthorEntityBinder` to parameters that don't use default conventions:
9293

93-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Controllers/BoundAuthorsController.cs?name=demo1&highlight=2)]
94+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Controllers/BoundAuthorsController.cs?name=snippet_GetById&highlight=2)]
9495

9596
In this example, since the name of the argument isn't the default `authorId`, it's specified on the parameter using the `ModelBinder` attribute. Both the controller and action method are simplified compared to looking up the entity in the action method. The logic to fetch the author using Entity Framework Core is moved to the model binder. This can be a considerable simplification when you have several methods that bind to the `Author` model.
9697

@@ -100,14 +101,14 @@ You can apply the `ModelBinder` attribute to individual model properties (such a
100101

101102
Instead of applying an attribute, you can implement `IModelBinderProvider`. This is how the built-in framework binders are implemented. When you specify the type your binder operates on, you specify the type of argument it produces, **not** the input your binder accepts. The following binder provider works with the `AuthorEntityBinder`. When it's added to MVC's collection of providers, you don't need to use the `ModelBinder` attribute on `Author` or `Author`-typed parameters.
102103

103-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Binders/AuthorEntityBinderProvider.cs?highlight=17-20)]
104+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Binders/AuthorEntityBinderProvider.cs?highlight=17-20)]
104105

105106
> Note:
106107
> The preceding code returns a `BinderTypeModelBinder`. `BinderTypeModelBinder` acts as a factory for model binders and provides dependency injection (DI). The `AuthorEntityBinder` requires DI to access EF Core. Use `BinderTypeModelBinder` if your model binder requires services from DI.
107108
108109
To use a custom model binder provider, add it in `ConfigureServices`:
109110

110-
[!code-csharp[](custom-model-binding/samples/2.x/CustomModelBindingSample/Startup.cs?name=snippet_ConfigureServices&highlight=5-10)]
111+
[!code-csharp[](custom-model-binding/samples/3.x/CustomModelBindingSample/Startup.cs?name=snippet_ConfigureServices&highlight=5-8)]
111112

112113
When evaluating model binders, the collection of providers is examined in order. The first provider that returns a binder is used. Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. In this example, the custom provider is added to the beginning of the collection to ensure it's used for `Author` action arguments.
113114

@@ -137,7 +138,7 @@ By [Steve Smith](https://ardalis.com/)
137138

138139
Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. Mapping between incoming request data and application models is handled by model binders. Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).
139140

140-
[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/advanced/custom-model-binding/) ([how to download](xref:index#how-to-download-a-sample))
141+
[View or download sample code](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/advanced/custom-model-binding/samples) ([how to download](xref:index#how-to-download-a-sample))
141142

142143
## Default model binder limitations
143144

aspnetcore/mvc/advanced/custom-model-binding/samples/3.x/CustomModelBindingSample/Base64String.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using CustomModelBindingSample.Data;
4+
using Microsoft.AspNetCore.Mvc.ModelBinding;
5+
6+
namespace CustomModelBindingSample.Binders
7+
{
8+
#region snippet_Class
9+
public class AuthorEntityBinder : IModelBinder
10+
{
11+
private readonly AuthorContext _context;
12+
13+
public AuthorEntityBinder(AuthorContext context)
14+
{
15+
_context = context;
16+
}
17+
18+
public Task BindModelAsync(ModelBindingContext bindingContext)
19+
{
20+
if (bindingContext == null)
21+
{
22+
throw new ArgumentNullException(nameof(bindingContext));
23+
}
24+
25+
var modelName = bindingContext.ModelName;
26+
27+
// Try to fetch the value of the argument by name
28+
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
29+
30+
if (valueProviderResult == ValueProviderResult.None)
31+
{
32+
return Task.CompletedTask;
33+
}
34+
35+
bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
36+
37+
var value = valueProviderResult.FirstValue;
38+
39+
// Check if the argument value is null or empty
40+
if (string.IsNullOrEmpty(value))
41+
{
42+
return Task.CompletedTask;
43+
}
44+
45+
if (!int.TryParse(value, out var id))
46+
{
47+
// Non-integer arguments result in model state errors
48+
bindingContext.ModelState.TryAddModelError(
49+
modelName, "Author Id must be an integer.");
50+
51+
return Task.CompletedTask;
52+
}
53+
54+
// Model will be null if not found, including for
55+
// out of range id values (0, -3, etc.)
56+
var model = _context.Authors.Find(id);
57+
bindingContext.Result = ModelBindingResult.Success(model);
58+
return Task.CompletedTask;
59+
}
60+
}
61+
#endregion
62+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using CustomModelBindingSample.Data;
2+
using Microsoft.AspNetCore.Mvc.ModelBinding;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
4+
using System;
5+
6+
namespace CustomModelBindingSample.Binders
7+
{
8+
public class AuthorEntityBinderProvider : IModelBinderProvider
9+
{
10+
public IModelBinder GetBinder(ModelBinderProviderContext context)
11+
{
12+
if (context == null)
13+
{
14+
throw new ArgumentNullException(nameof(context));
15+
}
16+
17+
if (context.Metadata.ModelType == typeof(Author))
18+
{
19+
return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
20+
}
21+
22+
return null;
23+
}
24+
}
25+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using CustomModelBindingSample.Data;
3+
4+
namespace CustomModelBindingSample.Controllers
5+
{
6+
[ApiController]
7+
[Route("api/[controller]")]
8+
public class AuthorsController : Controller
9+
{
10+
private readonly AuthorContext _context;
11+
12+
public AuthorsController(AuthorContext context)
13+
{
14+
_context = context;
15+
}
16+
17+
[HttpGet("{id}")]
18+
public IActionResult GetById(int id)
19+
{
20+
var author = _context.Authors.Find(id);
21+
22+
if (author == null)
23+
{
24+
return NotFound();
25+
}
26+
27+
return Ok(author);
28+
}
29+
}
30+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using CustomModelBindingSample.Data;
3+
4+
namespace CustomModelBindingSample.Controllers
5+
{
6+
[ApiController]
7+
[Route("api/[controller]")]
8+
public class BoundAuthorsController : ControllerBase
9+
{
10+
#region snippet_GetById
11+
[HttpGet("{id}")]
12+
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
13+
{
14+
if (author == null)
15+
{
16+
return NotFound();
17+
}
18+
19+
return Ok(author);
20+
}
21+
#endregion
22+
23+
#region snippet_Get
24+
[HttpGet("get/{authorId}")]
25+
public IActionResult Get(Author author)
26+
{
27+
if (author == null)
28+
{
29+
return NotFound();
30+
}
31+
32+
return Ok(author);
33+
}
34+
#endregion
35+
}
36+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.IO;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.Extensions.Configuration;
4+
5+
namespace CustomModelBindingSample.Controllers
6+
{
7+
8+
[ApiController]
9+
[Route("api/[controller]")]
10+
public class ImageController : ControllerBase
11+
{
12+
private readonly string _targetFilePath;
13+
14+
public ImageController(IConfiguration config)
15+
{
16+
_targetFilePath = config["StoredFilesPath"];
17+
}
18+
19+
#region snippet_Post
20+
[HttpPost]
21+
public void Post([FromForm] byte[] file, string filename)
22+
{
23+
// Don't trust the file name sent by the client. Use
24+
// Path.GetRandomFileName to generate a safe random
25+
// file name. _targetFilePath receives a value
26+
// from configuration (the appsettings.json file in
27+
// the sample app).
28+
var trustedFileName = Path.GetRandomFileName();
29+
var filePath = Path.Combine(_targetFilePath, trustedFileName);
30+
31+
if (System.IO.File.Exists(filePath))
32+
{
33+
return;
34+
}
35+
36+
System.IO.File.WriteAllBytes(filePath, file);
37+
}
38+
#endregion
39+
40+
#region snippet_SaveProfile
41+
[HttpPost("Profile")]
42+
public void SaveProfile([FromForm] ProfileViewModel model)
43+
{
44+
// Don't trust the file name sent by the client. Use
45+
// Path.GetRandomFileName to generate a safe random
46+
// file name. _targetFilePath receives a value
47+
// from configuration (the appsettings.json file in
48+
// the sample app).
49+
var trustedFileName = Path.GetRandomFileName();
50+
var filePath = Path.Combine(_targetFilePath, trustedFileName);
51+
52+
if (System.IO.File.Exists(filePath))
53+
{
54+
return;
55+
}
56+
57+
System.IO.File.WriteAllBytes(filePath, model.File);
58+
}
59+
60+
public class ProfileViewModel
61+
{
62+
public byte[] File { get; set; }
63+
public string FileName { get; set; }
64+
}
65+
#endregion
66+
}
67+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
<PropertyGroup>
3+
<TargetFramework>netcoreapp3.1</TargetFramework>
4+
</PropertyGroup>
5+
<ItemGroup>
6+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
7+
</ItemGroup>
8+
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using CustomModelBindingSample.Binders;
2+
using Microsoft.AspNetCore.Mvc;
3+
4+
namespace CustomModelBindingSample.Data
5+
{
6+
[ModelBinder(BinderType = typeof(AuthorEntityBinder))]
7+
public class Author
8+
{
9+
public int Id { get; set; }
10+
public string Name { get; set; }
11+
public string GitHub { get; set; }
12+
public string Twitter { get; set; }
13+
public string BlogUrl { get; set; }
14+
}
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace CustomModelBindingSample.Data
4+
{
5+
public class AuthorContext : DbContext
6+
{
7+
public AuthorContext(DbContextOptions<AuthorContext> options)
8+
: base(options) { }
9+
10+
public DbSet<Author> Authors { get; set; }
11+
}
12+
}

0 commit comments

Comments
 (0)