Skip to content

Commit

Permalink
Add DTO to prevent over-posting on API (dotnet#17074)
Browse files Browse the repository at this point in the history
* 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 <34246760+mkArtakMSFT@users.noreply.github.com>

* Update aspnetcore/tutorials/first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs

Co-Authored-By: Artak <34246760+mkArtakMSFT@users.noreply.github.com>

* Add DTO to prevent over-posting on API

* Apply suggestions from code review

Co-Authored-By: Kirk Larkin <6025110+serpent5@users.noreply.github.com>

* Update aspnetcore/tutorials/first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs

Co-Authored-By: Artak <34246760+mkArtakMSFT@users.noreply.github.com>

Co-authored-by: Artak <34246760+mkArtakMSFT@users.noreply.github.com>
Co-authored-by: Kirk Larkin <6025110+serpent5@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 20, 2020
1 parent ee40605 commit 227633c
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 4 deletions.
37 changes: 34 additions & 3 deletions aspnetcore/tutorials/first-web-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ author: rick-anderson
description: Learn how to build a web API with ASP.NET Core.
ms.author: riande
ms.custom: mvc
ms.date: 12/05/2019
ms.date: 2/25/2020
uid: tutorials/first-web-api
---

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

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

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

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

---

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

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

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

<a name="over-post"></a>

## Prevent over-posting

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.

A DTO may be used to:

* Prevent over-posting.
* Hide properties that clients are not supposed to view.
* Omit some properties in order to reduce payload size.
* Flatten object graphs that contain nested objects. Flattened object graphs can be more convenient for clients.

To demonstrate the DTO approach, update the `TodoItem` class to include a secret field:

[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Models/TodoItem.cs?name=snippet&highlight=6)]

The secret field needs to be hidden from this app, but an administrative app could choose to expose it.

Verify you can post and get the secret field.

Create a DTO model:

[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Models/TodoItemDTO.cs?name=snippet)]

Update the `TodoItemsController` to use `TodoItemDTO`:

[!code-csharp[](first-web-api/samples/3.0/TodoApiDTO/Controllers/TodoItemsController.cs?name=snippet)]

Verify you can't post or get the secret field.

## Call the web API with JavaScript

See [Tutorial: Call an ASP.NET Core web API with JavaScript](xref:tutorials/web-api-javascript).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
namespace TodoApi.Models
#define First

namespace TodoApi.Models
{
#if First
#region snippet
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
#endregion
#else
// Use this to test you can over-post
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
public string Secret { get; set; }
}
#endif
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using TodoApi.Models;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{
private readonly TodoContext _context;

public TodoItemsController(TodoContext context)
{
_context = context;
}

// GET: api/TodoItems
#region snippet
[HttpGet]
public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
{
return await _context.TodoItems
.Select(x => ItemToDTO(x))
.ToListAsync();
}

[HttpGet("{id}")]
public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
{
var todoItemDTO = await _context.TodoItems
.Where(x => x.Id == id)
.Select(x => ItemToDTO(x))
.SingleAsync();

if (todoItemDTO == null)
{
return NotFound();
}

return todoItemDTO;
}

[HttpPut("{id}")]
public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
{
if (id != todoItemDTO.Id)
{
return BadRequest();
}

var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
return NotFound();
}

todoItem.Name = todoItemDTO.Name;
todoItem.IsComplete = todoItemDTO.IsComplete;

try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
{
return NotFound();
}

return NoContent();
}

[HttpPost]
public async Task<ActionResult<TodoItem>> CreateTodoItem(TodoItemDTO todoItemDTO)
{
var todoItem = new TodoItem
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};

_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();

return CreatedAtAction(
nameof(GetTodoItem),
new { id = todoItem.Id },
ItemToDTO(todoItem));
}

[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTodoItem(long id)
{
var todoItem = await _context.TodoItems.FindAsync(id);

if (todoItem == null)
{
return NotFound();
}

_context.TodoItems.Remove(todoItem);
await _context.SaveChangesAsync();

return NoContent();
}

private bool TodoItemExists(long id) =>
_context.TodoItems.Any(e => e.Id == id);

private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
new TodoItemDTO
{
Id = todoItem.Id,
Name = todoItem.Name,
IsComplete = todoItem.IsComplete
};
}
#endregion
}

/* // This method is just for testing populating the secret field
// POST: api/TodoItems/test
[HttpPost("test")]
public async Task<ActionResult<TodoItem>> PostTestTodoItem(TodoItem todoItem)
{
_context.TodoItems.Add(todoItem);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
}
// This method is just for testing
// GET: api/TodoItems/test
[HttpGet("test")]
public async Task<ActionResult<IEnumerable<TodoItem>>> GetTestTodoItems()
{
return await _context.TodoItems.ToListAsync();
}
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
public class TodoContext : DbContext
{
public TodoContext(DbContextOptions<TodoContext> options)
: base(options)
{
}

public DbSet<TodoItem> TodoItems { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace TodoApi.Models
{
#region snippet
public class TodoItem
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
public string Secret { get; set; }
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace TodoApi.Models
{
#region snippet
public class TodoItemDTO
{
public long Id { get; set; }
public string Name { get; set; }
public bool IsComplete { get; set; }
}
#endregion
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace TodoApi
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using TodoApi.Models;

namespace TodoApi
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<TodoContext>(opt =>
opt.UseInMemoryDatabase("TodoList"));
services.AddControllers();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();

app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
</ItemGroup>


</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}

0 comments on commit 227633c

Please sign in to comment.