3 minute read

4 min read 806 words

1. Introduction

In modern web development, users expect fast, responsive interfaces that don’t require a full page reload for every action. While my previous post covered the basics of JavaScript in MVC, this “Deep Dive” focuses specifically on how to wire up AJAX using the Fetch API to talk to your ASP.NET Core MVC controllers.

We will build a “Product Like” feature to demonstrate the full end-to-end flow.


2. The Data Transfer Object (DTO)

First, we need a simple class to represent the data being sent from the client to the server.

namespace MyApp.Models.Dtos
{
    public class LikeProductDto
    {
        public int ProductId { get; set; }
        public bool IsLiked { get; set; }
    }
}

3. The Controller Action

Your controller needs an action method decorated with [HttpPost] and [ValidateAntiForgeryToken]. It uses [FromBody] to tell MVC to deserialize the JSON request body into our DTO.

using Microsoft.AspNetCore.Mvc;
using MyApp.Models.Dtos;

public class ProductsController : Controller
{
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Like([FromBody] LikeProductDto data)
    {
        if (data == null || data.ProductId <= 0)
        {
            return BadRequest(new { message = "Invalid product data." });
        }

        // Logic to update the database would go here...
        // e.g., _service.ToggleLike(data.ProductId, data.IsLiked);

        return Json(new { 
            success = true, 
            message = $"Product {data.ProductId} was {(data.IsLiked ? "liked" : "unliked")}!",
            newLikeCount = 125 // Example updated state
        });
    }
}

4. The Razor View (Index.cshtml)

The view needs three things:

  1. The HTML element to trigger the action.
  2. The Antiforgery token (usually generated by a hidden form).
  3. A script section to handle the click event.
@model ProductViewModel

<div class="product-card">
    <h3>@Model.Name</h3>
    <p>@Model.Description</p>
    
    <!-- We use data-attributes to store the ID for JS to read -->
    <button id="like-button" 
            class="btn btn-outline-primary" 
            data-product-id="@Model.Id"
            data-is-liked="false">
        Like (<span id="like-count">@Model.LikeCount</span>)
    </button>
</div>

<!-- This helper generates the hidden input for the Antiforgery Token -->
@Html.AntiForgeryToken()

@section Scripts {
    <script>
        document.getElementById('like-button').addEventListener('click', async function() {
            const btn = this;
            const productId = btn.getAttribute('data-product-id');
            const isLiked = btn.getAttribute('data-is-liked') === 'true';
            
            // 1. Get the token value from the hidden input generated by @Html.AntiForgeryToken()
            const token = document.querySelector('input[name="__RequestVerificationToken"]').value;

            try {
                // 2. Perform the Fetch call
                const response = await fetch('/Products/Like', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'RequestVerificationToken': token // Crucial for security!
                    },
                    body: JSON.stringify({
                        productId: parseInt(productId),
                        isLiked: !isLiked // Toggle the state
                    })
                });

                // 3. Handle the response
                if (response.ok) {
                    const result = await response.json();
                    
                    // 4. Update the UI dynamically
                    btn.setAttribute('data-is-liked', !isLiked);
                    btn.classList.toggle('btn-primary');
                    btn.classList.toggle('btn-outline-primary');
                    document.getElementById('like-count').innerText = result.newLikeCount;
                    
                    console.log(result.message);
                } else {
                    alert('Something went wrong on the server.');
                }
            } catch (error) {
                console.error('Error during Fetch:', error);
            }
        });
    </script>
}

5. Key Takeaways

  1. Antiforgery Token: MVC’s security middleware expects the RequestVerificationToken header for POST/PUT/DELETE requests. Without it, you’ll get a 400 Bad Request or 403 Forbidden.
  2. JSON Serialization: Use JSON.stringify() in JavaScript and [FromBody] in C# to ensure the data is mapped correctly.
  3. Data Attributes: Use data-* attributes to bridge the gap between your C# Model and your JavaScript logic without hardcoding values in scripts.
  4. Error Handling: Always use try/catch and check response.ok to handle network issues or server errors gracefully.

6. Summary of the Interaction

  1. Browser: User clicks the button.
  2. JavaScript: Collects the Product ID from the button’s data- attribute and the security token from the hidden input.
  3. JavaScript: Sends an HTTP POST request to /Products/Like with a JSON body.
  4. Server: MVC validates the Antiforgery token.
  5. Server: MVC deserializes the JSON into a LikeProductDto object.
  6. Server: The Like action runs, processes the logic, and returns a JsonResult.
  7. JavaScript: Receives the JSON response and updates the DOM (button text and count) without reloading the page.

Leave a comment