Rest Api in Csharp: Complete Solution & Deep Dive Guide


Build a Powerful REST API in C#: The Complete Guide from Zero to Hero

Learn to build a fully functional REST API in C# using the modern ASP.NET Core framework. This comprehensive guide walks you through creating, testing, and understanding the core concepts of API development for a practical IOU (I Owe You) tracking application from scratch.


The Annoying Problem of Keeping Track

Imagine living with roommates. One day you lend Alex $10 for pizza, the next day Brenda covers your $5 coffee, and a week later, you borrow $20 from Chris for groceries. Trying to remember who owes whom becomes a tangled mess of mental notes and forgotten favors. This small, everyday problem is a perfect analogy for the data management challenges that businesses and applications face on a much larger scale.

You've probably felt this frustration in a technical context, too. How do you get a mobile app to talk to a server? How does a web frontend fetch user data? The answer often lies in a well-structured bridge between the client and the server. This bridge is a REST API.

This guide promises to demystify the process. We will take that chaotic roommate IOU scenario and build a clean, robust, and elegant solution: a RESTful API in C#. By the end, you won't just have a working application; you'll have a deep understanding of the principles that power modern web communication.


What is a REST API? The Digital Bridge Explained

At its core, a REST (Representational State Transfer) API is an architectural style for designing networked applications. Think of it as a set of rules and conventions that allow different software systems to communicate with each other over the internet. It's not a specific technology but a blueprint for building web services that are simple, scalable, and reliable.

The central idea of REST is the concept of a resource. A resource can be anything: a user, a product, an order, or in our case, an IOU record. Each resource is identified by a unique URL (Uniform Resource Locator), which we call an endpoint. For example, /users could be the endpoint to access a list of all users.

To interact with these resources, clients (like a web browser or mobile app) send HTTP requests to the server. These requests use standard HTTP methods (also known as verbs) to indicate the desired action:

  • GET: Retrieve a resource.
  • POST: Create a new resource.
  • PUT: Update an existing resource completely.
  • PATCH: Partially update an existing resource.
  • DELETE: Remove a resource.

The server then processes the request and sends back an HTTP response, which typically includes a status code (like 200 OK or 404 Not Found) and the requested data, usually formatted in JSON (JavaScript Object Notation).


Why Build Your API with C# and ASP.NET Core?

While you can build a REST API in almost any programming language, C# combined with the ASP.NET Core framework offers a compelling package. It's not just a choice; it's a strategic advantage for developers aiming for high performance and scalability.

ASP.NET Core, the modern, open-source successor to the original ASP.NET, is engineered from the ground up for performance. It's cross-platform, meaning your C# API can run seamlessly on Windows, macOS, and Linux. This flexibility is crucial in today's diverse cloud-hosting environment.

Furthermore, C#'s strong typing system catches errors at compile-time, not runtime, leading to more robust and maintainable code. The extensive tooling provided by Visual Studio and Visual Studio Code, including powerful debuggers and integrated project management, drastically speeds up the development lifecycle. This ecosystem makes C# an exceptional choice for building everything from simple microservices to complex, enterprise-level APIs.


How to Build the IOU Tracking REST API in C#

Let's dive into the practical part of this guide. We'll build our IOU tracking API step-by-step, starting from project setup to implementing the core logic. This project is based on a learning module from the exclusive kodikra.com C# curriculum.

Step 1: Project Setup with the .NET CLI

First, ensure you have the .NET SDK installed. You can check by opening your terminal or command prompt and running dotnet --version. We'll use the .NET CLI to create a new Web API project.

Execute the following command:

dotnet new webapi -n IouApi --no-openapi

This command creates a new project named IouApi. The --no-openapi flag keeps the project minimal by excluding the default Swagger/OpenAPI setup, allowing us to focus on the core API logic.

Navigate into your new project directory:

cd IouApi

Step 2: Understanding the API Request Lifecycle

Before we write code, let's visualize how a request flows through our ASP.NET Core application. When a client sends an HTTP request, it's handled by a series of middleware components before reaching our specific logic.

● Client sends HTTP Request (e.g., POST /iou)
│
▼
┌──────────────────────────┐
│  ASP.NET Core Kestrel Web Server │
└────────────┬─────────────┘
             │
             ▼
      [ Middleware Pipeline ]
      (HTTPS, Authentication, etc.)
             │
             ▼
   ┌───────────────────┐
   │      Routing      │ Finds the matching controller & action
   └─────────┬─────────┘
             │
             ▼
      ╔════════════════════╗
      ║   IouController    ║ Our C# code
      ╚════════════════════╝
             │
             ▼
   ┌───────────────────┐
   │   Business Logic  │ Updates user data
   └─────────┬─────────┘
             │
             ▼
      ╔════════════════════╗
      ║  In-Memory Database  ║ (A static Dictionary)
      ╚════════════════════╝
             │
             ▼
   ┌───────────────────┐
   │  Serialize to JSON  │ Prepares the response
   └─────────┬─────────┘
             │
             ▼
● Server sends HTTP Response (e.g., 200 OK)

Step 3: Defining the Data Models

A well-structured API starts with well-defined models. We need to represent our data in C# classes. Create a new file named Models.cs and add the following code. These classes will be used for both internal data storage and for deserializing incoming JSON requests.


using System.Text.Json.Serialization;
using System.Collections.Generic;

// Represents the structure of an incoming POST request to /iou
public class IouRequest
{
    [JsonPropertyName("lender")]
    public string Lender { get; set; }

    [JsonPropertyName("borrower")]
    public string Borrower { get; set; }

    [JsonPropertyName("amount")]
    public decimal Amount { get; set; }
}

// Represents a user's financial state internally
public class User
{
    public string Name { get; set; }
    
    // Key: Person this user owes money to. Value: Amount.
    public Dictionary<string, decimal> Owes { get; set; } = new Dictionary<string, decimal>();
    
    // Key: Person who owes this user money. Value: Amount.
    public Dictionary<string, decimal> OwedBy { get; set; } = new Dictionary<string, decimal>();

    // Calculated property: Total money owed by others minus total money this user owes.
    public decimal Balance => GetTotal(OwedBy) - GetTotal(Owes);

    private decimal GetTotal(Dictionary<string, decimal> dictionary)
    {
        decimal total = 0;
        foreach (var amount in dictionary.Values)
        {
            total += amount;
        }
        return total;
    }
}

// Represents the structure of the JSON response for the GET /users endpoint
public class UserResponse
{
    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("owes")]
    public Dictionary<string, decimal> Owes { get; set; }

    [JsonPropertyName("owed_by")]
    public Dictionary<string, decimal> OwedBy { get; set; }

    [JsonPropertyName("balance")]
    public decimal Balance { get; set; }
}

We've created three classes: IouRequest to map the incoming JSON, User for our internal business logic and data storage, and UserResponse as a Data Transfer Object (DTO) to shape the final JSON output exactly as required.

Step 4: Implementing the Controller Logic

The controller is the heart of our API. It receives requests, processes them, and returns responses. Delete the default WeatherForecastController.cs file and create a new file named IouController.cs inside the Controllers folder.

Here is the complete implementation:


using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;

[ApiController]
public class IouController : ControllerBase
{
    // Using a static dictionary to act as an in-memory database.
    // This data will persist as long as the application is running.
    private static readonly Dictionary<string, User> _users = new Dictionary<string, User>();

    // Endpoint to get all users and their financial summary
    // HTTP GET /users
    [HttpGet("users")]
    public IActionResult GetUsers([FromQuery] string[] users)
    {
        // Filter the database to only include users specified in the query, if any.
        var usersToReturn = (users != null && users.Length > 0)
            ? _users.Values.Where(u => users.Contains(u.Name))
            : _users.Values;

        // Transform the internal User objects into UserResponse DTOs.
        // This ensures the output JSON matches the required format.
        var response = usersToReturn
            .Select(u => new UserResponse
            {
                Name = u.Name,
                Owes = u.Owes,
                OwedBy = u.OwedBy,
                Balance = u.Balance
            })
            .OrderBy(u => u.Name) // Sort the final list alphabetically by name.
            .ToList();

        return Ok(new { users = response });
    }

    // Endpoint to add a new IOU record
    // HTTP POST /iou
    [HttpPost("iou")]
    public IActionResult AddIou([FromBody] IouRequest iou)
    {
        // Get or create the lender user object.
        var lender = GetOrCreateUser(iou.Lender);
        
        // Get or create the borrower user object.
        var borrower = GetOrCreateUser(iou.Borrower);

        // --- Core Transaction Logic ---
        // Settle any existing debt between the two parties first.
        decimal amountToSettle = iou.Amount;

        // Case 1: Borrower already owes the lender. Add to the existing debt.
        if (borrower.Owes.ContainsKey(lender.Name))
        {
             borrower.Owes[lender.Name] += amountToSettle;
             lender.OwedBy[borrower.Name] += amountToSettle;
        }
        // Case 2: Lender owes the borrower. The new IOU settles this debt first.
        else if (lender.Owes.ContainsKey(borrower.Name))
        {
            decimal existingDebt = lender.Owes[borrower.Name];
            if (amountToSettle >= existingDebt)
            {
                // The new IOU is enough to clear the old debt.
                lender.Owes.Remove(borrower.Name);
                borrower.OwedBy.Remove(lender.Name);
                
                // If there's a remaining amount, it becomes a new debt in the other direction.
                decimal remainingAmount = amountToSettle - existingDebt;
                if (remainingAmount > 0)
                {
                    borrower.Owes[lender.Name] = remainingAmount;
                    lender.OwedBy[borrower.Name] = remainingAmount;
                }
            }
            else
            {
                // The new IOU only partially pays off the old debt.
                lender.Owes[borrower.Name] -= amountToSettle;
                borrower.OwedBy[lender.Name] -= amountToSettle;
            }
        }
        // Case 3: No prior debt exists between them. Create a new record.
        else
        {
            borrower.Owes[lender.Name] = amountToSettle;
            lender.OwedBy[borrower.Name] = amountToSettle;
        }

        // After processing, return the updated state of both users involved.
        var updatedUsers = new[] { lender, borrower }
            .Select(u => new UserResponse
            {
                Name = u.Name,
                Owes = u.Owes,
                OwedBy = u.OwedBy,
                Balance = u.Balance
            })
            .OrderBy(u => u.Name)
            .ToList();

        return Ok(new { users = updatedUsers });
    }

    // Helper method to retrieve a user from our in-memory store or create them if they don't exist.
    private User GetOrCreateUser(string name)
    {
        if (!_users.ContainsKey(name))
        {
            _users[name] = new User { Name = name };
        }
        return _users[name];
    }
}

Step 5: Code Walkthrough and Logic Explanation

Let's break down the key parts of the IouController.

In-Memory Storage: The line private static readonly Dictionary<string, User> _users = new Dictionary<string, User>(); creates a static dictionary. The static keyword is crucial here; it ensures that this dictionary is created only once and shared across all HTTP requests. This acts as our simple, in-memory database. For a production application, you would replace this with a real database like PostgreSQL or SQL Server.

The GET /users Endpoint: This method, decorated with [HttpGet("users")], handles GET requests to the /users URL. It retrieves all users from the _users dictionary, transforms them into the UserResponse DTO format, sorts them alphabetically, and returns them wrapped in a parent JSON object {"users": [...]}. It also supports optional filtering via a query string like /users?users=Adam&users=Bob.

The POST /iou Endpoint: This is where the main logic resides. It's decorated with [HttpPost("iou")].

  1. Deserialization: The [FromBody] IouRequest iou parameter tells ASP.NET Core to automatically parse the JSON body of the POST request and create an IouRequest object from it.
  2. User Management: The GetOrCreateUser helper method simplifies logic by ensuring we always have a User object to work with, whether they are new or existing.
  3. Transaction Logic: The core of the method handles the financial transaction. It correctly settles debts. For instance, if Bob owes Adam $5 and Adam then lends Bob $10, the API doesn't just create a new record. It should update the state to show Bob now owes Adam $15. Our logic also handles cases where a new loan clears an existing debt in the opposite direction.

Here is a visualization of the complex debt-settling logic within the POST endpoint:

● Start POST /iou with (Lender, Borrower, Amount)
│
▼
┌─────────────────────────────────┐
│ Get or Create Lender & Borrower │
└───────────────┬─────────────────┘
                │
                ▼
  ◆ Borrower already owes Lender?
  ╱               ╲
Yes                No
 │                  │
 ▼                  ▼
┌───────────────┐  ◆ Lender owes Borrower?
│ Add to Debt   │ ╱           ╲
│ (Simple Sum)  │Yes           No
└───────────────┘ │             │
                │             ▼
                │ ┌──────────────────────────┐
                │ │ Create new debt record:  │
                │ │ Borrower owes Lender     │
                │ └──────────────────────────┘
                ▼
  ┌─────────────────────────────────┐
  │ Settle existing debt first...   │
  └───────────────┬─────────────────┘
                  │
                  ▼
       ◆ New Amount >= Old Debt?
      ╱               ╲
    Yes                No
     │                  │
     ▼                  ▼
┌────────────┐   ┌─────────────────────────┐
│ Clear Old Debt │   │ Reduce Old Debt by Amount │
└────────────┘   └─────────────────────────┘
     │
     ▼
◆ Any remainder?
╱               ╲
Yes                No
 │                  │
 ▼                  ▼
┌────────────────┐ [End Transaction]
│ Create New Debt│
│ with Remainder │
└────────────────┘

Step 6: Running and Testing the API

Now, let's run our application. In your terminal, at the root of the IouApi project, execute:

dotnet run

Your API is now running, typically on https://localhost:7001 or http://localhost:5001. You can use a tool like curl, Postman, or Insomnia to test the endpoints.

Test Case 1: Add a new IOU

Send a POST request to add a transaction where Adam lends Bob $12.0.


curl -X POST -H "Content-Type: application/json" -d '{"lender":"Adam","borrower":"Bob","amount":12.0}' http://localhost:5001/iou

The API should respond with the updated status of Adam and Bob.

Test Case 2: Get all users

Send a GET request to see the summary of all users.


curl -X GET http://localhost:5001/users

The response will be a JSON object containing a list of all users and their financial states, sorted alphabetically.

Test Case 3: Settle a debt

Now, let's say Bob lends Adam $3. This should reduce Bob's debt to Adam.


curl -X POST -H "Content-Type: application/json" -d '{"lender":"Bob","borrower":"Adam","amount":3.0}' http://localhost:5001/iou

If you now call GET /users again, you will see that Bob's debt to Adam is reduced to $9.0, and all balances are updated accordingly. Our logic works!


REST APIs: Strengths and Alternatives

REST has been the dominant architectural style for APIs for years due to its simplicity and scalability. However, it's essential to understand its trade-offs and be aware of alternatives like GraphQL.

Aspect REST API (Our Implementation) GraphQL
Data Fetching Client gets a fixed data structure from each endpoint. This can lead to over-fetching (getting more data than needed) or under-fetching (needing to call multiple endpoints). Client specifies exactly what data it needs in a single request, preventing over/under-fetching.
Endpoints Multiple endpoints for different resources (e.g., /users, /products). Typically a single endpoint (e.g., /graphql) that handles all queries.
Learning Curve Easier to get started. Based on familiar HTTP concepts. Steeper learning curve. Requires understanding of schemas, types, and a new query language.
Caching Simple to cache at the HTTP level due to standard GET requests and URLs. More complex to cache, as most requests are POSTs to a single endpoint.
Best For Standard CRUD operations, public APIs, microservices where resource boundaries are clear. Complex data graphs, mobile applications where bandwidth is a concern, frontends with rapidly changing data requirements.

Alternative Implementation: Using a Service Layer

In our example, all the business logic is inside the controller. For larger applications, this can become messy. A common best practice is to separate concerns by introducing a service layer.

You would create an IouService class that contains the core logic for managing users and transactions. The controller would then simply call methods on this service. This makes the code more organized, easier to test (you can mock the service), and more maintainable. The controller's job becomes validating input and handling HTTP-related tasks, while the service handles the "how".


Frequently Asked Questions (FAQ)

What is the difference between HTTP POST and PUT?

POST is used to create a new resource. Sending the same POST request multiple times will typically create multiple new resources. PUT is used to update an existing resource (or create it if it doesn't exist). Sending the same PUT request multiple times should always result in the same state on the server, a concept known as idempotency.

What does "stateless" mean in the context of REST?

Statelessness means that the server does not store any information about the client's state between requests. Each request from a client must contain all the information needed for the server to understand and process it. This design improves scalability because any server instance can handle any client request.

How do I handle errors in a C# REST API?

ASP.NET Core has built-in mechanisms for error handling. You can use try-catch blocks and return specific HTTP status codes like BadRequest() (400) for invalid input or NotFound() (404) for missing resources. For global error handling, you can implement custom middleware to catch unhandled exceptions and format a consistent error response.

What is a DTO (Data Transfer Object)?

A DTO is an object that carries data between processes. In our case, UserResponse is a DTO. We use it to decouple our internal data model (User) from the external representation exposed by the API. This allows us to change the internal model without breaking API clients, and to shape the JSON output precisely.

Should I use `async` and `await` in my controller actions?

Absolutely. While our example uses an in-memory dictionary and doesn't perform I/O operations, any real-world API will interact with a database, file system, or another network service. These operations should be asynchronous to avoid blocking threads. You would change your method signatures to public async Task<IActionResult> GetUsers() and use await for any I/O calls.

How can I secure my C# REST API?

Securing an API is critical. ASP.NET Core provides robust support for various security schemes. Common methods include implementing HTTPS to encrypt traffic, using authentication mechanisms like JWT (JSON Web Tokens) or OAuth 2.0 to verify user identity, and applying authorization policies (e.g., using the [Authorize] attribute) to control access to specific endpoints.


Conclusion: Your Journey into API Development

You have successfully built a complete, functional REST API in C# from the ground up. We transformed a common, real-world problem into a structured, digital solution using the power and elegance of ASP.NET Core. You've learned not just the "how" of writing the code, but the "why" behind REST principles, data modeling with DTOs, and the request-response lifecycle.

This project is more than just an exercise; it's a foundational building block. The skills you've developed here—defining endpoints, handling HTTP verbs, managing data state, and structuring application logic—are directly applicable to building complex, production-ready web services. The journey of a software developer is one of continuous learning, and mastering API development is a crucial milestone.

To continue your journey, explore adding a persistent database with Entity Framework Core, implementing authentication, or deploying your API to a cloud service like Azure or AWS. The possibilities are vast.

Explore more advanced topics in our complete C# learning path at kodikra.com or see where this module fits in the overall kodikra.com developer roadmap.


Technology Disclaimer: The code and concepts in this article are based on .NET 8 and C# 12, the latest stable versions at the time of writing. While the core principles are timeless, always consult the official documentation for the most current syntax and best practices.


Published by Kodikra — Your trusted Csharp learning resource.