Skip to content

Commit 227633c

Browse files
Rick-AndersonmkArtakMSFTserpent5
authored
Add DTO to prevent over-posting on API (#17074)
* Add DTO to prevent over-posting on API * Add DTO to prevent over-posting on API * Add DTO to prevent over-posting on API * Add DTO to prevent over-posting on API * Add DTO to prevent over-posting on API * Apply suggestions from code review Co-Authored-By: Artak <[email protected]> * Update aspnetcore/tutorials/first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs Co-Authored-By: Artak <[email protected]> * Add DTO to prevent over-posting on API * Apply suggestions from code review Co-Authored-By: Kirk Larkin <[email protected]> * Update aspnetcore/tutorials/first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs Co-Authored-By: Artak <[email protected]> Co-authored-by: Artak <[email protected]> Co-authored-by: Kirk Larkin <[email protected]>
1 parent ee40605 commit 227633c

File tree

11 files changed

+347
-4
lines changed

11 files changed

+347
-4
lines changed

aspnetcore/tutorials/first-web-api.md

+34-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ author: rick-anderson
44
description: Learn how to build a web API with ASP.NET Core.
55
ms.author: riande
66
ms.custom: mvc
7-
ms.date: 12/05/2019
7+
ms.date: 2/25/2020
88
uid: tutorials/first-web-api
99
---
1010

1111
# Tutorial: Create a web API with ASP.NET Core
1212

13-
By [Rick Anderson](https://twitter.com/RickAndMSFT) and [Mike Wasson](https://github.com/mikewasson)
13+
By [Rick Anderson](https://twitter.com/RickAndMSFT), [Kirk Larkin](https://twitter.com/serpent5), and [Mike Wasson](https://github.com/mikewasson)
1414

1515
This tutorial teaches the basics of building a web API with ASP.NET Core.
1616

@@ -207,7 +207,7 @@ A *model* is a set of classes that represent the data that the app manages. The
207207

208208
---
209209

210-
[!code-csharp[](first-web-api/samples/3.0/TodoApi/Models/TodoItem.cs)]
210+
[!code-csharp[](first-web-api/samples/3.0/TodoApi/Models/TodoItem.cs?name=snippet)]
211211

212212
The `Id` property functions as the unique key in a relational database.
213213

@@ -453,6 +453,37 @@ Use Postman to delete a to-do item:
453453
* Set the URI of the object to delete (for example `https://localhost:5001/api/TodoItems/1`).
454454
* Select **Send**.
455455

456+
<a name="over-post"></a>
457+
458+
## Prevent over-posting
459+
460+
Currently the sample app exposes the entire `TodoItem` object. Productions apps typically limit the data that's input and returned using a subset of the model. There are multiple reasons behind this and security is a major one. The subset of a model is usually referred to as a Data Transfer Object (DTO), input model, or view model. **DTO** is used in this article.
461+
462+
A DTO may be used to:
463+
464+
* Prevent over-posting.
465+
* Hide properties that clients are not supposed to view.
466+
* Omit some properties in order to reduce payload size.
467+
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.
468+
469+
To demonstrate the DTO approach, update the `TodoItem` class to include a secret field:
470+
471+
[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Models/TodoItem.cs?name=snippet&highlight=6)]
472+
473+
The secret field needs to be hidden from this app, but an administrative app could choose to expose it.
474+
475+
Verify you can post and get the secret field.
476+
477+
Create a DTO model:
478+
479+
[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Models/TodoItemDTO.cs?name=snippet)]
480+
481+
Update the `TodoItemsController` to use `TodoItemDTO`:
482+
483+
[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs?name=snippet)]
484+
485+
Verify you can't post or get the secret field.
486+
456487
## Call the web API with JavaScript
457488

458489
See [Tutorial: Call an ASP.NET Core web API with JavaScript](xref:tutorials/web-api-javascript).
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,24 @@
1-
namespace TodoApi.Models
1+
#define First
2+
3+
namespace TodoApi.Models
24
{
5+
#if First
6+
#region snippet
37
public class TodoItem
48
{
59
public long Id { get; set; }
610
public string Name { get; set; }
711
public bool IsComplete { get; set; }
812
}
13+
#endregion
14+
#else
15+
// Use this to test you can over-post
16+
public class TodoItem
17+
{
18+
public long Id { get; set; }
19+
public string Name { get; set; }
20+
public bool IsComplete { get; set; }
21+
public string Secret { get; set; }
22+
}
23+
#endif
924
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using Microsoft.AspNetCore.Mvc;
2+
using Microsoft.EntityFrameworkCore;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using TodoApi.Models;
7+
8+
namespace TodoApi.Controllers
9+
{
10+
[Route("api/[controller]")]
11+
[ApiController]
12+
public class TodoItemsController : ControllerBase
13+
{
14+
private readonly TodoContext _context;
15+
16+
public TodoItemsController(TodoContext context)
17+
{
18+
_context = context;
19+
}
20+
21+
// GET: api/TodoItems
22+
#region snippet
23+
[HttpGet]
24+
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
25+
{
26+
return await _context.TodoItems
27+
.Select(x => ItemToDTO(x))
28+
.ToListAsync();
29+
}
30+
31+
[HttpGet("{id}")]
32+
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
33+
{
34+
var todoItemDTO = await _context.TodoItems
35+
.Where(x => x.Id == id)
36+
.Select(x => ItemToDTO(x))
37+
.SingleAsync();
38+
39+
if (todoItemDTO == null)
40+
{
41+
return NotFound();
42+
}
43+
44+
return todoItemDTO;
45+
}
46+
47+
[HttpPut("{id}")]
48+
public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
49+
{
50+
if (id != todoItemDTO.Id)
51+
{
52+
return BadRequest();
53+
}
54+
55+
var todoItem = await _context.TodoItems.FindAsync(id);
56+
if (todoItem == null)
57+
{
58+
return NotFound();
59+
}
60+
61+
todoItem.Name = todoItemDTO.Name;
62+
todoItem.IsComplete = todoItemDTO.IsComplete;
63+
64+
try
65+
{
66+
await _context.SaveChangesAsync();
67+
}
68+
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
69+
{
70+
return NotFound();
71+
}
72+
73+
return NoContent();
74+
}
75+
76+
[HttpPost]
77+
public async Task<ActionResult<TodoItem>> CreateTodoItem(TodoItemDTO todoItemDTO)
78+
{
79+
var todoItem = new TodoItem
80+
{
81+
IsComplete = todoItemDTO.IsComplete,
82+
Name = todoItemDTO.Name
83+
};
84+
85+
_context.TodoItems.Add(todoItem);
86+
await _context.SaveChangesAsync();
87+
88+
return CreatedAtAction(
89+
nameof(GetTodoItem),
90+
new { id = todoItem.Id },
91+
ItemToDTO(todoItem));
92+
}
93+
94+
[HttpDelete("{id}")]
95+
public async Task<IActionResult> DeleteTodoItem(long id)
96+
{
97+
var todoItem = await _context.TodoItems.FindAsync(id);
98+
99+
if (todoItem == null)
100+
{
101+
return NotFound();
102+
}
103+
104+
_context.TodoItems.Remove(todoItem);
105+
await _context.SaveChangesAsync();
106+
107+
return NoContent();
108+
}
109+
110+
private bool TodoItemExists(long id) =>
111+
_context.TodoItems.Any(e => e.Id == id);
112+
113+
private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
114+
new TodoItemDTO
115+
{
116+
Id = todoItem.Id,
117+
Name = todoItem.Name,
118+
IsComplete = todoItem.IsComplete
119+
};
120+
}
121+
#endregion
122+
}
123+
124+
/* // This method is just for testing populating the secret field
125+
// POST: api/TodoItems/test
126+
[HttpPost("test")]
127+
public async Task<ActionResult<TodoItem>> PostTestTodoItem(TodoItem todoItem)
128+
{
129+
_context.TodoItems.Add(todoItem);
130+
await _context.SaveChangesAsync();
131+
132+
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
133+
}
134+
135+
// This method is just for testing
136+
// GET: api/TodoItems/test
137+
[HttpGet("test")]
138+
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTestTodoItems()
139+
{
140+
return await _context.TodoItems.ToListAsync();
141+
}
142+
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.EntityFrameworkCore;
2+
3+
namespace TodoApi.Models
4+
{
5+
public class TodoContext : DbContext
6+
{
7+
public TodoContext(DbContextOptions<TodoContext> options)
8+
: base(options)
9+
{
10+
}
11+
12+
public DbSet<TodoItem> TodoItems { get; set; }
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace TodoApi.Models
2+
{
3+
#region snippet
4+
public class TodoItem
5+
{
6+
public long Id { get; set; }
7+
public string Name { get; set; }
8+
public bool IsComplete { get; set; }
9+
public string Secret { get; set; }
10+
}
11+
#endregion
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace TodoApi.Models
2+
{
3+
#region snippet
4+
public class TodoItemDTO
5+
{
6+
public long Id { get; set; }
7+
public string Name { get; set; }
8+
public bool IsComplete { get; set; }
9+
}
10+
#endregion
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Hosting;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Hosting;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace TodoApi
11+
{
12+
public class Program
13+
{
14+
public static void Main(string[] args)
15+
{
16+
CreateHostBuilder(args).Build().Run();
17+
}
18+
19+
public static IHostBuilder CreateHostBuilder(string[] args) =>
20+
Host.CreateDefaultBuilder(args)
21+
.ConfigureWebHostDefaults(webBuilder =>
22+
{
23+
webBuilder.UseStartup<Startup>();
24+
});
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Builder;
6+
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.AspNetCore.HttpsPolicy;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.EntityFrameworkCore;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Hosting;
13+
using Microsoft.Extensions.Logging;
14+
using TodoApi.Models;
15+
16+
namespace TodoApi
17+
{
18+
public class Startup
19+
{
20+
public Startup(IConfiguration configuration)
21+
{
22+
Configuration = configuration;
23+
}
24+
25+
public IConfiguration Configuration { get; }
26+
27+
// This method gets called by the runtime. Use this method to add services to the container.
28+
public void ConfigureServices(IServiceCollection services)
29+
{
30+
services.AddDbContext<TodoContext>(opt =>
31+
opt.UseInMemoryDatabase("TodoList"));
32+
services.AddControllers();
33+
}
34+
35+
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
36+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
37+
{
38+
if (env.IsDevelopment())
39+
{
40+
app.UseDeveloperExceptionPage();
41+
}
42+
43+
app.UseHttpsRedirection();
44+
45+
app.UseRouting();
46+
47+
app.UseAuthorization();
48+
49+
app.UseEndpoints(endpoints =>
50+
{
51+
endpoints.MapControllers();
52+
});
53+
}
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netcoreapp3.1</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
9+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
10+
<PrivateAssets>all</PrivateAssets>
11+
</PackageReference>
12+
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
13+
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
14+
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
15+
</ItemGroup>
16+
17+
18+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Warning",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft": "Warning",
6+
"Microsoft.Hosting.Lifetime": "Information"
7+
}
8+
},
9+
"AllowedHosts": "*"
10+
}

0 commit comments

Comments
 (0)