Skip to content

Commit ebe4315

Browse files
committed
[add] strongly typed model support
1 parent 0489ef3 commit ebe4315

8 files changed

Lines changed: 155 additions & 27 deletions

File tree

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,32 @@ public class MyController : Controller<MultipartViewModel>
5959
}
6060
}
6161
```
62+
63+
### Binding parameters to a strongly typed model
64+
65+
Instead of searching through the `Parameters` list manually, you can bind the multipart form parameters to a strongly typed model the same way as for a regular query/form/JSON request. Inherit your model from `MultipartModel` (which exposes `Files`) and add your own properties:
66+
67+
```csharp
68+
public class UploadModel : MultipartModel
69+
{
70+
public string Title { get; set; }
71+
72+
public int Count { get; set; }
73+
}
74+
```
75+
76+
The parameters are parsed into the model properties automatically (the same parser as `Simplify.Web` query/form binding is reused, so `[BindProperty]`, `[Exclude]`, `[Format]`, `IList<T>` properties and validation attributes are all supported), while the uploaded files remain accessible via `Model.Files`:
77+
78+
```csharp
79+
public class MyController : Controller2<UploadModel>
80+
{
81+
public async Task<ControllerResponse> Invoke()
82+
{
83+
Model.Title; // bound from the "Title" multipart parameter
84+
Model.Count; // bound from the "Count" multipart parameter
85+
Model.Files; // uploaded files
86+
}
87+
}
88+
```
89+
90+
The legacy `MultipartViewModel` (which exposes the raw `Parameters` list) still works as before and now also inherits from `MultipartModel`.

src/Simplify.Web.Multipart/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
### Added
66

7+
- `MultipartModel` base class which exposes the uploaded `Files` and allows binding the multipart form parameters to a strongly typed model (reusing the `Simplify.Web` model parser, so `[BindProperty]`, `[Exclude]`, `[Format]`, `IList<T>` properties and validation attributes are supported)
78
- .NET 10.0 explicit support
89

910
### Removed
1011

1112
- .NET 6.0 explicit support
1213
- .NET 4.8 explicit support
1314

15+
### Changed
16+
17+
- `MultipartViewModel` now inherits from `MultipartModel` (fully backward compatible, still exposes the raw `Parameters` list)
18+
1419
### Dependencies
1520

1621
- Simplify.Web bump to 5.3
Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using System;
1+
using System.Collections.Generic;
2+
using System.Linq;
23
using System.Threading.Tasks;
34
using HttpMultipartParser;
45
using Simplify.Web.Model.Binding;
6+
using Simplify.Web.Model.Binding.Parsers;
57

68
namespace Simplify.Web.Multipart.Model.Binding;
79

@@ -14,26 +16,34 @@ public class HttpMultipartFormModelBinder : IModelBinder
1416
/// <summary>
1517
/// Binds the model.
1618
/// </summary>
17-
/// <typeparam name="T">Model type</typeparam>
19+
/// <typeparam name="T">Model type, should inherit from <see cref="MultipartModel" />.</typeparam>
1820
/// <param name="args">The <see cref="ModelBinderEventArgs{T}" /> instance containing the event data.</param>
1921
public async Task BindAsync<T>(ModelBinderEventArgs<T> args)
2022
{
2123
if (!args.Context.Request.ContentType.Contains("multipart/form-data"))
2224
return;
2325

24-
var multipartModelType = typeof(MultipartViewModel);
25-
26-
if (typeof(T) != multipartModelType)
27-
throw new ModelBindingException("For HTTP multipart form data model type should be: " + multipartModelType.Name);
26+
if (!typeof(MultipartModel).IsAssignableFrom(typeof(T)))
27+
throw new ModelBindingException("For HTTP multipart form data the model type should inherit from: " + nameof(MultipartModel));
2828

2929
var parser = await MultipartFormDataParser.ParseAsync(args.Context.Request.Body);
30-
var obj = Activator.CreateInstance<T>();
3130

32-
var model = (MultipartViewModel)(object)obj;
31+
// Bind the parameters to the strongly typed model properties reusing Simplify.Web parser.
32+
var model = ListToModelParser.Parse<T>(ToKeyValuePairs(parser.Parameters));
33+
34+
var multipartModel = (MultipartModel)(object)model;
3335

34-
model.Files = parser.Files;
35-
model.Parameters = parser.Parameters;
36+
multipartModel.Files = parser.Files;
3637

37-
args.SetModel(obj);
38+
// Keep the raw parameters available for the backward compatible model.
39+
if (multipartModel is MultipartViewModel viewModel)
40+
viewModel.Parameters = parser.Parameters;
41+
42+
args.SetModel(model);
3843
}
39-
}
44+
45+
private static IList<KeyValuePair<string, string[]>> ToKeyValuePairs(IEnumerable<ParameterPart> parameters) =>
46+
[.. parameters
47+
.GroupBy(x => x.Name)
48+
.Select(x => new KeyValuePair<string, string[]>(x.Key, [.. x.Select(p => p.Data)]))];
49+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Collections.Generic;
2+
using HttpMultipartParser;
3+
using Simplify.Web.Model.Binding.Attributes;
4+
5+
namespace Simplify.Web.Multipart.Model;
6+
7+
/// <summary>
8+
/// Base HTTP multipart form data model providing access to the uploaded files.
9+
/// Inherit from this class and add your own properties to have the multipart form
10+
/// parameters bound to them automatically (the same way as for a regular query/form/JSON model).
11+
/// </summary>
12+
public class MultipartModel
13+
{
14+
/// <summary>
15+
/// HTTP multipart form data files
16+
/// </summary>
17+
/// <value>
18+
/// The files.
19+
/// </value>
20+
[Exclude]
21+
public IReadOnlyList<FilePart> Files { get; set; }
22+
}
Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,22 @@
1-
using System.Collections.Generic;
1+
using System.Collections.Generic;
22
using HttpMultipartParser;
3+
using Simplify.Web.Model.Binding.Attributes;
34

45
namespace Simplify.Web.Multipart.Model;
56

67
/// <summary>
7-
/// HTTP multipart form data model
8+
/// HTTP multipart form data model exposing the raw files and parameters lists.
9+
/// Use it when you want to work with the parameters list directly; to bind the
10+
/// parameters to a strongly typed model inherit from <see cref="MultipartModel" /> instead.
811
/// </summary>
9-
public class MultipartViewModel
12+
public class MultipartViewModel : MultipartModel
1013
{
11-
/// <summary>
12-
/// HTTP multipart form data files
13-
/// </summary>
14-
/// <value>
15-
/// The files.
16-
/// </value>
17-
public IReadOnlyList<FilePart> Files { get; set; }
18-
1914
/// <summary>
2015
/// HTTP multipart form data parameters
2116
/// </summary>
2217
/// <value>
2318
/// The parameters.
2419
/// </value>
20+
[Exclude]
2521
public IReadOnlyList<ParameterPart> Parameters { get; set; }
26-
}
22+
}

src/TestClient/Program.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
using System;
1+
using System;
22
using System.IO;
33
using Flurl.Http;
44

55
var result = await "http://localhost:5000/api/v1/testIn"
66
.PostMultipartAsync(mp =>
77
mp.AddFile("test file", new MemoryStream("Hello World!!!"u8.ToArray()), "MyFile.txt", "text/plain"));
88

9-
Console.WriteLine("HTTP status: " + result.StatusCode);
10-
Console.ReadLine();
9+
Console.WriteLine("Untyped model HTTP status: " + result.StatusCode);
10+
11+
var typedResult = await "http://localhost:5000/api/v1/testInTyped"
12+
.PostMultipartAsync(mp =>
13+
{
14+
mp.AddFile("test file", new MemoryStream("Hello World!!!"u8.ToArray()), "MyFile.txt", "text/plain");
15+
mp.AddString("Title", "My title");
16+
mp.AddString("Count", "42");
17+
});
18+
19+
Console.WriteLine("Typed model HTTP status: " + typedResult.StatusCode);
20+
21+
Console.ReadLine();
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using Simplify.Web;
5+
using Simplify.Web.Attributes;
6+
using Simplify.Web.Multipart.Model;
7+
using TestServer.Requests;
8+
9+
namespace TestServer.Controllers.Api.v1;
10+
11+
[Post("/api/v1/testInTyped")]
12+
public class TestInTypedController : Controller2<UploadModel>
13+
{
14+
public async Task<ControllerResponse> Invoke()
15+
{
16+
var file = Model.Files[0] ?? throw new ArgumentException("No files in model");
17+
18+
using var stream = new StreamReader(file.Data);
19+
20+
var fileData = await stream.ReadToEndAsync();
21+
22+
// Assert parameters were bound to the typed model
23+
24+
if (Model.Title != "My title")
25+
return Content($"Wrong title, actual: '{Model.Title}'", 500);
26+
27+
if (Model.Count != 42)
28+
return Content($"Wrong count, actual: '{Model.Count}'", 500);
29+
30+
// Assert file is still accessible
31+
32+
if (file.FileName != "MyFile.txt")
33+
return Content($"Wrong file name, actual: '{file.FileName}'", 500);
34+
35+
if (fileData != "Hello World!!!")
36+
return Content($"Wrong file data, actual: '{fileData}'", 500);
37+
38+
return NoContent();
39+
}
40+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Simplify.Web.Model.Validation.Attributes;
2+
using Simplify.Web.Multipart.Model;
3+
4+
namespace TestServer.Requests;
5+
6+
/// <summary>
7+
/// Strongly typed multipart model: the parameters are bound to the model properties automatically.
8+
/// </summary>
9+
public class UploadModel : MultipartModel
10+
{
11+
[Required]
12+
public string Title { get; set; }
13+
14+
public int Count { get; set; }
15+
}

0 commit comments

Comments
 (0)