Model Binding in ASP.NET Core Web API

Model Binding in ASP.NET Core Web API

In this article, I will discuss Model Binding in ASP.NET Core Web API with one Real-time Example. Please read our previous article discussing Return Types and Status Codes in ASP.NET Core Web API with Examples. Model binding is a fundamental concept in ASP.NET Core Web API that simplifies how data from HTTP requests is mapped to controller action method parameters or models. 

What is Model Binding in ASP.NET Core Web API?

Model binding in ASP.NET Core Web API is the process of automatically binding incoming HTTP request data (such as query strings, request body, route data, headers, and form data) to action method parameters or objects. This feature abstracts the complexities of extracting and converting raw request data into strongly typed .NET objects, enabling developers to write cleaner and more maintainable code.

For example, when a client sends data in a request (such as JSON in the body or query parameters), the model binding mechanism automatically translates this data into strongly typed objects. ASP.NET Core maps the request data to the corresponding model properties. For a better understanding, please have a look at the below image. In this case, ASP.NET Core automatically maps the request data to the User object.

What is Model Binding in ASP.NET Core Web API?

ASP.NET Core uses model binding to map data from multiple sources:

  • Query Strings: Parameters appended to the URL.
  • Route Data: Parameters defined in the URL path.
  • Form Data: Data submitted via HTML forms (typically for POST requests).
  • Request Body: Payload data, often in JSON or XML format (commonly for POST, PUT, or PATCH requests).
  • Headers: Custom data sent within HTTP headers.

Why Is Model Binding Important in ASP.NET Core Web API?

Model Binding is crucial because it abstracts the complexity of parsing HTTP request data into .NET objects, allowing developers to focus on business logic. The following are the key reasons why Model Binding is Important in ASP.NET Core Web API:

  • Simplifies Data Handling: It eliminates the need to extract data from the HTTP request manually. ASP.NET Core can automatically map incoming data to method parameters or object properties, saving our time and effort.
  • Maintainability: Because the binding logic is centralized and consistent, we avoid writing code to parse and convert input data throughout our controllers. This leads to more readable, cleaner, and maintainable code.
  • Supports Multiple Data Sources: Model binding in ASP.NET Core Web API supports a variety of data sources such as query string, route values, request body (JSON, XML, form data), headers, and more, making it easy to build robust APIs that handle different request formats.
  • Validation Integration: ASP.NET Core allows us to easily integrate validation logic with model binding. We can use data annotations or custom validation, and the framework will let us know if binding fails or data constraints are violated.

What are the Model Binding Techniques used in ASP.NET Core Web API?

ASP.NET Core Web API provides several attributes to specify which part of the request the parameter or object should be bound from. Each of these techniques can be applied by decorating the action method parameters with the corresponding attributes, informing the framework about the source of data. They are as follows:

FromQuery Model Binding Attribute in ASP.NET Core Web API:

The FromQuery attribute in ASP.NET Core Web API is used to bind a parameter in an action method to a value provided in the query string of the HTTP request. This attribute is useful when explicitly specifying that specific parameters should be populated from the query string.

FromQuery Model Binding Attribute in ASP.NET Core Web API

FromRoute Model Binding Attribute in ASP.NET Core Web API:

The FromRoute attribute is used to specify that a parameter in an action method should be bound from the route data in the URL. It tells the ASP.NET Core model binder to get the value for the parameter from the route parameters rather than the query string or request body. When we define routes using placeholders (like {id} in the URL path), the FromRoute attribute allows us to capture those placeholder values and pass them as parameters to controller actions.

FromRoute Model Binding Attribute in ASP.NET Core Web API

FromBody Model Binding Attribute in ASP.NET Core Web API:

The FromBody attribute in ASP.NET Core Web API indicates that an action method parameter should be bound from the body of the incoming HTTP request. When a parameter is decorated with the FromBody attribute, ASP.NET Core will attempt to deserialize the request’s body (typically in JSON format) into the type specified by the parameter. This is normally used for parameters that are complex types or data objects sent as JSON or XML formats in the body of the HTTP request.

FromBody Model Binding Attribute in ASP.NET Core Web API

FromForm Model Binding Attribute in ASP.NET Core Web API:

The FromForm attribute is used to bind data from incoming HTTP POST requests to action method parameters when data is submitted as form data (content-type: application/x-www-form-urlencoded or multipart/form-data). This attribute is typically used in POST requests where the data is sent in the request’s body as form data.

FromForm Model Binding Attribute in ASP.NET Core Web API

FromHeader Model Binding Attribute in ASP.NET Core Web API:

The FromHeader attribute in ASP.NET Core Web API binds a parameter in an action method to the value of a specific HTTP request header. It tells the framework that the value of the method parameter should be obtained from the specified HTTP header in the incoming request. This is useful for reading metadata, version information, authentication tokens, or other information transmitted in HTTP headers.

FromHeader Model Binding Attribute in ASP.NET Core Web API

Default Binding (No Attribute)

If no attribute is specified, ASP.NET Core uses its default binding conventions:

  • For simple types (int, string, bool, etc.), it tries to bind from route, query string, or form values.
  • For complex types, it typically attempts to bind from the request body.

How Do We Handle Model Binding Errors in ASP.NET Core Web API?

Model binding isn’t always perfect. Sometimes, the incoming data might be missing, malformed, or fail validation. Fortunately, ASP.NET Core provides mechanisms to detect and handle these errors: Model binding errors occur when the incoming request data cannot be bound to the model due to type mismatches, missing required fields, or invalid data formats. Let us see how we can handle them effectively:

Validation Attributes:

Use data annotations to define validation rules.

How to Handle Model Binding Errors in ASP.NET Core Web API?

Check ModelState:

After model binding occurs, you can check ModelState.IsValid to see if there were any issues. If validation attributes fail or required values are missing, ModelState will contain the errors:

How to Handle Model Binding Errors in ASP.NET Core Web API?

Note: If your controller is annotated with [ApiController], the framework automatically checks ModelState.IsValid. By default, it returns a 400 Bad Request with validation details if binding fails.

Real-time Example to Understand Model Binding in ASP.NET Core Web API:

Let us create a comprehensive real-time ECommerce application using ASP.NET Core Web API and Entity Framework (EF) Core with a Code First approach to demonstrate the power and flexibility of Model Binding in ASP.NET Core Web API. In this application, I will show the use of various Model Binding Attributes ([FromQuery], [FromRoute], [FromBody], [FromForm], [FromHeader]), default binding behavior, and the combination of multiple binding attributes.

Create the ASP.NET Core Web API Project

First, create a new ASP.NET Core Web API project named EcommerceAPI. We will use EF Core with SQL Server. Please install the following packages.

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Configure appsettings.json:

Add the connection string for the SQL Server database in the appsettings.json file. So, please modify the appsettings.json file as follows:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "EFCoreDBConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}
Defining Data Models

Models represent the core entities of the application, encapsulating the data and business logic. They are essential for defining the data structure that the application manages. We need to define the core entities of the e-commerce application and apply data annotations for validation.

First, create a folder named Models at the project root directory where we will create all our Models. In our e-commerce scenario, we will define the following entities, and each entity will have essential properties and some data annotations for validation.

  • Customer
  • Product
  • Order
  • OrderItem
Customer

Create a class file named Customer.cs within the Models folder, and copy and paste the following code. The Customer Entity represents a user in the e-commerce system. It contains information about the user’s name, email, and password, with validation attributes to ensure data integrity and consistency.

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace EcommerceAPI.Models
{
    public class Customer
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Customer name is required")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid Email")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        [StringLength(100)]
        public string Password { get; set; }

        [JsonIgnore]
        public List<Order> Orders { get; set; }
    }
}
Product

Create a class file named Product.cs within the Models folder and then copy and paste the following code. The Product Entity represents items available for purchase, including Name, Description, Category, Price, and Stock, with validation annotations for required fields, numeric ranges, and database column types.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EcommerceAPI.Models
{
    public class Product
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Product name is required")]
        public string Name { get; set; }
        public string? Description { get; set; }

        [Required(ErrorMessage = "Product Category is required")]
        public string Category { get; set; }

        [Range(0.01, 100000, ErrorMessage = "Price must be between 0.01 and 100000")]
        [Column(TypeName ="decimal(18,2)")]
        public decimal Price { get; set; }

        [Range(0, int.MaxValue, ErrorMessage = "Stock cannot be a Negative value")]
        public int Stock { get; set; }
    }
}
Order

Create a class file named Order.cs within the Models folder and then copy and paste the following code. The Order Entity represents a transaction between a customer and the store. Tracks the customer who placed the order, order status, total amount, and the list of items. The relationships to Customer and OrderItem models facilitate navigation and data integrity.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace EcommerceAPI.Models
{
    public class Order
    {
        public int Id { get; set; }

        [Required]
        public DateTime OrderDate { get; set; } = DateTime.UtcNow;

        // Relationship: One Customer can have many Orders
        [Required]
        public int CustomerId { get; set; }

        [JsonIgnore]
        public Customer Customer { get; set; }

        [Required]
        public string OrderStatus { get; set; }

        [Required]
        [Column(TypeName = "decimal(18,2)")]
        public decimal OrderAmount { get; set; }

        // One Order can have multiple OrderItems
        public ICollection<OrderItem> OrderItems { get; set; }
    }
}
OrderItem

Create a class file named OrderItem.cs within the Models folder, and copy and paste the following code. The OrderItem Entity represents individual products within a single order. Links to both the Order and Product it belongs to and stores the quantity and price for each line item. Validation ensures appropriate ranges for quantity and price.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;

namespace EcommerceAPI.Models
{
    public class OrderItem
    {
        public int Id { get; set; }

        [Required]
        public int OrderId { get; set; }

        [JsonIgnore]
        public Order Order { get; set; }

        [Required]
        public int ProductId { get; set; }

        [JsonIgnore]
        public Product Product { get; set; }

        [Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1000")]
        public int Quantity { get; set; }

        [Range(0.01, double.MaxValue, ErrorMessage = "Price cannot be a Negative Number")]
        [Column(TypeName = "decimal(18,2)")]
        public decimal UnitPrice { get; set; }
    }
}
Configuring EF Core with Seed Data

Create a folder named Data in the project root directory. Then, add a class file named ECommerceDbContext.cs within the Data folder and copy and paste the following code. It Acts as the bridge between the database and the application. Manages the EF Core configurations, DbSet properties for each entity, and seed data that pre-populates the database on creation.

using EcommerceAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace EcommerceAPI.Data
{
    public class ECommerceDbContext : DbContext
    {
        public ECommerceDbContext(DbContextOptions<ECommerceDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Seed data for Customers
            modelBuilder.Entity<Customer>().HasData(
                new Customer
                {
                    Id = 1,
                    Name = "Alice Johnson",
                    Email = "alice.johnson@example.com",
                    Password = "Password123" // Note: In real applications, passwords should be hashed
                },
                new Customer
                {
                    Id = 2,
                    Name = "Bob Smith",
                    Email = "bob.smith@example.com",
                    Password = "Password456"
                }
            );

            // Seed data for Products
            modelBuilder.Entity<Product>().HasData(
                new Product
                {
                    Id = 1,
                    Name = "Laptop",
                    Description = "A high-performance laptop.",
                    Category = "Electronics",
                    Price = 1200.00m,
                    Stock = 10
                },
                new Product
                {
                    Id = 2,
                    Name = "Smartphone",
                    Description = "Latest model smartphone.",
                    Category = "Electronics",
                    Price = 800.00m,
                    Stock = 25
                },
                new Product
                {
                    Id = 3,
                    Name = "Headphones",
                    Description = "Noise-cancelling headphones.",
                    Category = "Accessories",
                    Price = 150.00m,
                    Stock = 50
                }
            );

            // Seed data for Orders
            modelBuilder.Entity<Order>().HasData(
                new Order
                {
                    Id = 1,
                    OrderDate = new DateTime(2024, 12, 1),
                    CustomerId = 1,
                    OrderStatus = "Shipped",
                    OrderAmount = 2000.00m
                },
                new Order
                {
                    Id = 2,
                    OrderDate = new DateTime(2025, 01, 25),
                    CustomerId = 2,
                    OrderStatus = "Processing",
                    OrderAmount = 950.00m
                }
            );

            // Seed data for OrderItems
            modelBuilder.Entity<OrderItem>().HasData(
                new OrderItem
                {
                    Id = 1,
                    OrderId = 1,
                    ProductId = 1,
                    Quantity = 1,
                    UnitPrice = 1200.00m
                },
                new OrderItem
                {
                    Id = 2,
                    OrderId = 1,
                    ProductId = 2,
                    Quantity = 1,
                    UnitPrice = 800.00m
                },
                new OrderItem
                {
                    Id = 3,
                    OrderId = 2,
                    ProductId = 3,
                    Quantity = 2,
                    UnitPrice = 150.00m
                },
                new OrderItem
                {
                    Id = 4,
                    OrderId = 2,
                    ProductId = 2,
                    Quantity = 1,
                    UnitPrice = 800.00m
                }
            );
        }

        public DbSet<Customer> Customers { get; set; }
        public DbSet<Product> Products { get; set; }
        public DbSet<Order> Orders { get; set; }
        public DbSet<OrderItem> OrderItems { get; set; }
    }
}
Register DbContext in the Program.cs

Next, we need to Register the DbContext in the Program.cs class file. So, please modify the Program class as follows:

using EcommerceAPI.Data;
using Microsoft.EntityFrameworkCore;

namespace EcommerceAPI
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.

            builder.Services.AddControllers()
            .AddJsonOptions(options =>
            {
                // This will use the property names as defined in the C# model
                options.JsonSerializerOptions.PropertyNamingPolicy = null;
            });

            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            // Configure EF Core with SQL Server
            builder.Services.AddDbContext<ECommerceDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("EFCoreDBConnection")));

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.UseHttpsRedirection();

            app.UseAuthorization();

            app.MapControllers();

            app.Run();
        }
    }
}
Apply Migrations and Create the Database

Open the Package Manager Console and then execute the following Add-Migration and Update-Database commands:

Model Binding in ASP.NET Core Web API

This will generate a Migrations folder and create the ECommerceDB in SQL Server with the required tables as shown in the image below.

Different Model Binding Techniques in ASP.NET Core Web API

Creating DTOs:

DTOs are used to transfer data between the client and server, ensuring that only necessary data is exposed and received. They help in maintaining the separation of concerns and enhancing security. First, create a folder named DTOs in the project root directory, where we will create all our DTOs.

CustomerRegistrationDTO

Create a class file named CustomerRegistrationDTO.cs within the DTOs folder and then copy and paste the following code. This DTO defines the structure and validation rules for customer registration data sent from the client.

using System.ComponentModel.DataAnnotations;
namespace EcommerceAPI.DTOs
{
    // Data Transfer Object for customer registration.
    public class CustomerRegistrationDTO
    {
        [Required(ErrorMessage = "Customer name is required")]
        public string Name { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid Email")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        [StringLength(100)]
        public string Password { get; set; }
    }
}
CustomerLoginDTO

Create a class file named CustomerLoginDTO.cs within the DTOs folder and then copy and paste the following code. This DTO defines the structure and validation rules for customer login data.

using System.ComponentModel.DataAnnotations;
namespace EcommerceAPI.DTOs
{
    // Data Transfer Object for customer login.
    public class CustomerLoginDTO
    {
        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid Email")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Password is required")]
        [StringLength(100)]
        public string Password { get; set; }
    }
}
OrderDTO

Create a class file named OrderDTO.cs within the DTOs folder, and copy and paste the following code. This DTO defines the structure and validation rules for creating a new order, including customer identification and order items.

using System.ComponentModel.DataAnnotations;
namespace EcommerceAPI.DTOs
{
    // Data Transfer Object for creating an order.
    public class OrderDTO
    {
        [Required]
        public int CustomerId { get; set; }

        [Required]
        public List<OrderItemDTO> Items { get; set; }
    }
}
OrderItemDTO

Create a class file named OrderItemDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO defines the structure and validation rules for individual items within an order.

using System.ComponentModel.DataAnnotations;
namespace EcommerceAPI.DTOs
{
    // Data Transfer Object for order items.
    public class OrderItemDTO
    {
        [Required]
        public int ProductId { get; set; }

        [Range(1, 1000, ErrorMessage = "Quantity must be between 1 and 1000")]
        public int Quantity { get; set; }
    }
}
ProductCreateDTO

Create a class file named ProductCreateDTO.cs within the DTOs folder, and then copy and paste the following code. This DTO defines the structure and validation rules for creating a new product.

using System.ComponentModel.DataAnnotations;
namespace EcommerceAPI.DTOs
{
    // Data Transfer Object for creating a Product.
    public class ProductCreateDTO
    {
        [Required(ErrorMessage = "Product name is required")]
        public string Name { get; set; }
        public string? Description { get; set; }

        [Required(ErrorMessage = "Product Category is required")]
        public string Category { get; set; }

        [Range(0.01, 100000, ErrorMessage = "Price must be between 0.01 and 100000")]
        public decimal Price { get; set; }

        [Range(0, int.MaxValue, ErrorMessage = "Stock cannot be a Negative value")]
        public int Stock { get; set; }
    }
}

Creating Controllers:

Controllers handle incoming HTTP requests, perform necessary operations (like interacting with the database), and return appropriate HTTP responses. They are essential in defining the API’s behavior and endpoints.

CustomersController

Create a new API Empty Controller named CustomersController within the Controllers folder and then copy and paste the following code. The following API Controller manages customer-related operations such as registration, login, and retrieval of customer details, demonstrating various model binding attributes.

using EcommerceAPI.Data;
using EcommerceAPI.DTOs;
using EcommerceAPI.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EcommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CustomersController : ControllerBase
    {
        private readonly ECommerceDbContext _context;

        public CustomersController(ECommerceDbContext context)
        {
            _context = context;
        }

        // Register a new customer.
        // Demonstrates [FromForm].
        // Endpoint: POST /api/customers/register
        [HttpPost("register")]
        public async Task<ActionResult<Customer>> RegisterCustomer([FromForm] CustomerRegistrationDTO registrationDto) // Binding from form data
        {
            // Check if email already exists
            if (await _context.Customers.AnyAsync(c => c.Email == registrationDto.Email))
            {
                return BadRequest("Email already exists.");
            }

            var customer = new Customer
            {
                Name = registrationDto.Name,
                Email = registrationDto.Email,
                Password = registrationDto.Password 
            };

            _context.Customers.Add(customer);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetCustomer), new { id = customer.Id }, customer);
        }

        // Login a customer.
        // Demonstrates [FromHeader] and [FromBody].
        // Endpoint: POST /api/customers/login
        [HttpPost("login")]
        public async Task<IActionResult> Login(
            [FromHeader(Name = "X-Client-ID")] string clientId, // Binding from header
            [FromBody] CustomerLoginDTO loginDto) // Binding from body
        {
            // Check the custom header
            if (string.IsNullOrWhiteSpace(clientId))
                return BadRequest("Missing X-Client-ID header");

            var customer = await _context.Customers
                .FirstOrDefaultAsync(c => c.Email == loginDto.Email && c.Password == loginDto.Password);

            if (customer == null)
            {
                return Unauthorized("Invalid email or password.");
            }

            // Generate JWT or other token in real applications
            return Ok(new { Message = "Authentication successful." });
        }

        // Get customer details.
        // Demonstrates default binding (from route or query).
        // Endpoint: GET /api/customers/{id}
        [HttpGet("{id}")]
        public async Task<ActionResult<Customer>> GetCustomer(int id) // Default binding from route
        {
            var customer = await _context.Customers.FindAsync(id);

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

            return Ok(customer);
        }
    }
}
ProductsController

Create a new API Empty Controller named ProductsController within the Controllers folder and then copy and paste the following code. The following API Controller manages product-related operations such as retrieval, creation, updating, pagination, and image uploads, demonstrating various model binding attributes. 

using EcommerceAPI.Data;
using EcommerceAPI.DTOs;
using EcommerceAPI.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EcommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController] 
    public class ProductsController : ControllerBase
    {
        private readonly ECommerceDbContext _context;

        public ProductsController(ECommerceDbContext context)
        {
            _context = context;
        }

        // Get all products with optional filtering by name, category, and price range.
        // Demonstrates [FromQuery] and default binding.
        // Endpoint: GET /api/products/GetProducts?name={name}&category={category}&minPrice={minPrice}&maxPrice={maxPrice}
        [HttpGet("GetProducts")]
        public async Task<ActionResult<IEnumerable<Product>>> GetProducts(
            [FromQuery] string? name, // Explicitly binding from query string
            [FromQuery] string? category, // Explicitly binding from query string
            [FromQuery] decimal? minPrice, // Explicitly binding from query string
            decimal? maxPrice) // Default binding; since it's a simple type, binds from query string
        {
            var query = _context.Products.AsQueryable();

            if (!string.IsNullOrEmpty(name))
            {
                query = query.Where(p => p.Name.Contains(name));
            }

            if (!string.IsNullOrEmpty(category))
            {
                query = query.Where(p => p.Category.Contains(category));
            }

            if (minPrice.HasValue)
            {
                query = query.Where(p => p.Price >= minPrice.Value);
            }

            if (maxPrice.HasValue)
            {
                query = query.Where(p => p.Price <= maxPrice.Value);
            }

            var products = await query.ToListAsync();
            return Ok(products);
        }

        // Get a specific product by ID.
        // Demonstrates [FromRoute] and default binding.
        // Endpoint: GET /api/products/GetProductById/{id}
        [HttpGet("GetProductById/{id}")]
        public async Task<ActionResult<Product>> GetProductById([FromRoute] int id) // Explicitly binding from route
        {
            var product = await _context.Products.FindAsync(id);

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

            return Ok(product);
        }

        // Create a new product.
        // Demonstrates [FromBody] binding.
        // Endpoint: POST /api/products/CreateProduct
        [HttpPost("CreateProduct")]
        public async Task<ActionResult<Product>> CreateProduct([FromBody] ProductCreateDTO productCreateDto) 
        {
            // Mapping from ProductCreateDto to Product entity
            var product = new Product
            {
                Name = productCreateDto.Name,
                Description = productCreateDto.Description,
                Category = productCreateDto.Category,
                Price = productCreateDto.Price,
                Stock = productCreateDto.Stock
            };

            _context.Products.Add(product);
            await _context.SaveChangesAsync();

            // Returns 201 Created with the location header
            return CreatedAtAction(nameof(GetProductById), new { id = product.Id }, product);
        }

        // Update an existing product's price.
        // Demonstrates multiple binding attributes.
        // Endpoint: PUT /api/products/UpdateProductPrice/{id}?price={newPrice}
        [HttpPut("UpdateProductPrice/{id}")]
        public async Task<IActionResult> UpdateProductPrice(
            [FromRoute] int id, // Binding from route
            [FromQuery] decimal price) // Binding from query string
        {
            var product = await _context.Products.FindAsync(id);
            if (product == null)
            {
                return NotFound();
            }

            product.Price = price;
            await _context.SaveChangesAsync();

            return NoContent();
        }

        // Demonstrates [FromQuery] for pagination
        // Endpoint: GET: /api/products/paged?pageNumber={pageNumber}&pageSize={pageSize}
        [HttpGet("paged")]
        public async Task<ActionResult<List<Product>>> GetProductsPaged([FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 5)
        {
            var products = await _context.Products
                                   .Skip((pageNumber - 1) * pageSize)
                                   .Take(pageSize)
                                   .AsNoTracking()
                                   .ToListAsync();

            return Ok(products);
        }

        // Upload product image.
        // Demonstrates [FromForm]
        // Endpoint: POST /api/products/{id}/upload
        [HttpPost("{id}/upload")]
        public async Task<IActionResult> UploadProductImage(
            [FromRoute] int id, // Binding from route
             IFormFile file) // Binding from form data
        {
            if (file == null || file.Length == 0)
                return BadRequest("No file uploaded.");

            var product = await _context.Products.FindAsync(id);
            if (product == null)
                return NotFound();

            // For demonstration, we'll just read the file name.
            // In a real application, you'd save the file to storage and update the product's image URL.
            
            var fileName = Path.GetFileName(file.FileName);
            // TODO: Save the file and update product.ImageUrl

            return Ok(new { Message = "Image uploaded successfully.", FileName = fileName });
        }
    }
}
OrdersController

Create a new API Empty Controller named OrdersController within the Controllers folder and then copy and paste the following code. The following API Controller manages order-related operations such as creation and retrieval, demonstrating model binding from the request body and route data.

using EcommerceAPI.Data;
using EcommerceAPI.DTOs;
using EcommerceAPI.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace EcommerceAPI.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly ECommerceDbContext _context;

        public OrdersController(ECommerceDbContext context)
        {
            _context = context;
        }

        // Create a new order.
        // Demonstrates [FromBody]
        // Endpoint: POST /api/orders/CreateOrder
        [HttpPost("CreateOrder")]
        public async Task<ActionResult<Order>> CreateOrder([FromBody] OrderDTO orderDto) // Binding from body
        {
            // Validate Customer existence
            var customer = await _context.Customers.FindAsync(orderDto.CustomerId);
            if (customer == null)
            {
                return BadRequest("Customer does not exist.");
            }

            // Initialize the order
            var order = new Order
            {
                CustomerId = orderDto.CustomerId,
                OrderDate = DateTime.UtcNow,
                OrderStatus = "Processing",
                OrderAmount = 0, // Will calculate based on OrderItems
                OrderItems = new List<OrderItem>()
            };

            decimal totalAmount = 0;

            // Iterate through order items and add to order
            foreach (var item in orderDto.Items)
            {
                var product = await _context.Products.FindAsync(item.ProductId);
                if (product == null)
                {
                    return BadRequest($"Product with ID {item.ProductId} does not exist.");
                }

                if (product.Stock < item.Quantity)
                {
                    return BadRequest($"Insufficient stock for product {product.Name}.");
                }

                // Deduct stock
                product.Stock -= item.Quantity;

                // Calculate total amount
                totalAmount += item.Quantity * product.Price;

                // Create order item
                var orderItem = new OrderItem
                {
                    ProductId = item.ProductId,
                    Quantity = item.Quantity,
                    UnitPrice = product.Price
                };

                order.OrderItems.Add(orderItem);
            }

            order.OrderAmount = totalAmount;

            _context.Orders.Add(order);
            await _context.SaveChangesAsync();

            return CreatedAtAction(nameof(GetOrderById), new { id = order.Id }, order);
        }

        // Get an order by ID.
        // Demonstrates [FromRoute] 
        // Endpoint: GET /api/orders/GetOrderById/{id}
        [HttpGet("GetOrderById/{id}")]
        public async Task<ActionResult<Order>> GetOrderById([FromRoute] int id) 
        {
            var order = await _context.Orders
                .Include(o => o.OrderItems)
                .ThenInclude(oi => oi.Product)
                .Include(o => o.Customer)
                .FirstOrDefaultAsync(o => o.Id == id);

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

            return Ok(order);
        }
    }
}

Now, run the application and test the functionality; it should work as expected.

How Does Model Binding Work in ASP.NET Core Web API?

Understanding the inner workings of Model Binding can help developers troubleshoot issues and optimize their applications. The following is a step-by-step overview of how Model Binding operates in ASP.NET Core Web API:

Request Reception

When the ASP.NET Core application receives an HTTP request, the routing system determines the appropriate controller and action method to handle the request based on routing configurations.

Parameter Discovery

The framework examines the action method’s parameters to determine what data needs to be bound. Each parameter may have binding attributes like [FromBody], [FromQuery], etc., influencing where the data should come from.

Model Binder Providers Invocation

ASP.NET Core consults the registered Model Binder Providers for each parameter to find a suitable Model Binder. The first Model Binder Provider that can handle the parameter type provides the Model Binder. Examples of Model Binders Include:

  • SimpleTypeModelBinder: Handles primitive types like int and string.
  • BodyModelBinder: Handles complex types from the request body.
  • FormModelBinder: Handles form data.
  • QueryStringModelBinder: Handles data from query strings.
Value Retrieval via Value Providers

The selected Model Binder uses Value Providers to fetch raw data from the specified sources (route data, query string, form data, headers, body). The Value Providers retrieve raw data from specific parts of the request (e.g., route, query string, form, headers, body). Examples of Value Providers Include:

  • RouteValueProvider
  • QueryStringValueProvider
  • FormValueProvider
  • HeaderValueProvider
Data Conversion and Assignment

The Model Binder attempts to convert the raw data into the target .NET type using type converters and formatters.

  • If the conversion is successful, the value is assigned to the corresponding parameter or property in the model.
  • If the conversion fails (e.g., type mismatch), a model binding error is recorded.
Model Validation

After binding, ASP.NET Core performs validation based on data annotations and custom validation logic applied to the model. Any validation errors are recorded in the ModelState.

Action Method Execution

If model binding and validation succeed, the action method executes with the bound parameters. If the controller is decorated with [ApiController] and model binding fails, ASP.NET Core automatically returns a 400 Bad Request response with validation details.

Conclusion:

Model Binding is a fundamental feature of ASP.NET Core Web API that simplifies data handling by automating the process of mapping HTTP request data to action method parameters. Understanding how it works, its importance, and how to use its various techniques and error-handling mechanisms can significantly enhance the development of robust and maintainable web APIs. Whether you are dealing with simple types, complex models, or custom binding scenarios, mastering Model Binding is essential for any ASP.NET Core developer.

In the next article, I will discuss Model Binding Using FromForm in ASP.NET Core Web API with Examples. In this article, I will try to explain Model Binding Techniques in ASP.NET Core Web API with examples. I hope you enjoy this article, “Model Binding Techniques in ASP.NET Core Web API.”

1 thought on “Model Binding in ASP.NET Core Web API”

Leave a Reply

Your email address will not be published. Required fields are marked *