Building RESTful Web APIs in C# with [HttpGet]
– The Ultimate Guide
Building RESTful Web APIs in C# with [HttpGet]
– The Ultimate Guide
1. Introduction to RESTful APIs
1.1 What Are RESTful APIs?
In the world of web development, RESTful APIs are the backbone of modern systems. They power everything from mobile apps and front-end interfaces to backend microservices. REST stands for Representational State Transfer, and it’s a design pattern used to structure communication between systems over HTTP.
RESTful APIs follow a simple principle—each HTTP method (GET, POST, PUT, DELETE) represents a specific type of action. GET requests retrieve data, POST creates, PUT updates, and DELETE removes resources. This clear mapping between HTTP methods and CRUD operations makes REST easy to implement and even easier to consume.
This guide dives deep into building RESTful APIs using [HttpGet]
in C# and ASP.NET Core. From basic setup to advanced techniques like pagination, filtering, authentication, and performance optimizations—you’ll find everything you need here.
1.2 Benefits of REST and HTTP Verbs
- Scalability: RESTful services can be cached, load-balanced, and scaled horizontally.
- Simplicity: It uses standard HTTP methods, making it familiar and easy to test.
- Statelessness: Each call from the client contains all the information needed.
- Cacheability: GET methods, in particular, benefit from native browser or CDN caching.
1.3 Role of [HttpGet]
in REST
The [HttpGet]
attribute in ASP.NET Core Web APIs is used to mark a controller method that should respond to HTTP GET requests. These methods are designed to fetch data without modifying it. They are idempotent, meaning that multiple identical requests have the same effect as a single one.
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase {
[HttpGet]
public IActionResult GetAllUsers() {
return Ok(UserRepository.GetAll());
}
}
This method maps directly to GET /api/users
and returns all users. It’s clean, efficient, and highly readable for developers consuming your API.
2. Setting Up ASP.NET Core Web API Project
2.1 Installing .NET SDK
To start building Web APIs in C#, you need the .NET SDK installed. Head to the official .NET download page and install the latest LTS version. Once installed, open a terminal or command prompt to verify:
dotnet --version
This command outputs the installed SDK version. You’re ready to scaffold your API project.
2.2 Creating Your First API Project
Use the .NET CLI to create a new Web API project:
dotnet new webapi -n MyRestApi
cd MyRestApi
This command sets up a complete ASP.NET Core Web API project with WeatherForecast sample controller. Run it using:
dotnet run
Navigate to https://localhost:5001/swagger
in your browser to see Swagger UI in action. Congratulations! Your API project is live.
2.3 Project Structure Overview
Your Web API project includes several files and folders:
Controllers
: Contains controller classes likeWeatherForecastController.cs
Program.cs
: Main entry point and DI registrationappsettings.json
: App configuration (DB, logging, etc.)Properties/launchSettings.json
: Environment settings for dev mode
Keeping this structure clean is essential for scaling your API. You’ll likely add Models, Services, Repositories, and DTOs as the project grows.
3. Routing & Controllers
3.1 What Is Routing?
Routing is how ASP.NET Core maps incoming HTTP requests to specific controller actions. It uses templates like [Route("api/[controller]")]
to define these patterns. By default, the route uses the controller name minus the "Controller" suffix.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase {
[HttpGet]
public IActionResult GetProducts() {
return Ok(productService.GetAll());
}
}
3.2 Attribute Routing with [HttpGet]
Attribute routing allows you to fine-tune the endpoint URL. Instead of relying on conventions, you define the full route for a method.
[HttpGet("latest")]
public IActionResult GetLatestProducts() {
return Ok(productService.GetLatest());
}
This will respond to requests like GET /api/products/latest
. It’s simple, clean, and flexible.
3.3 Default Routing Conventions
If no custom route is defined, ASP.NET Core uses the method name and HTTP verb. So [HttpGet]
without parameters will map to GET /api/controllername
. Adding a route like [HttpGet("{id}")]
maps to GET /api/controllername/5
.
[HttpGet("{id}")]
public IActionResult GetById(int id) {
var item = productService.GetById(id);
if (item == null) return NotFound();
return Ok(item);
}
4. Building GET Endpoints
4.1 Simple GET Returning Strings
Here’s your "Hello World" of GET endpoints. It returns plain text when you hit the URL.
[HttpGet("ping")]
public string Ping() {
return "API is alive!";
}
Open your browser and visit /api/yourcontroller/ping
. This verifies your API is working and responding to GET requests.
4.2 GET Returning JSON Models
Let’s return structured data. Create a model:
public class Product {
public int Id { get; set; }
public string Name { get; set; }
}
Now update your controller to return a list of products:
[HttpGet("list")]
public IActionResult GetAll() {
var products = new List<Product> {
new Product { Id = 1, Name = "Book" },
new Product { Id = 2, Name = "Pen" }
};
return Ok(products);
}
Accessing /api/products/list
now returns JSON formatted data—perfect for frontend clients.
4.3 GET with URL Parameters (id)
[HttpGet("{id}")]
public IActionResult GetProduct(int id) {
var product = productService.GetById(id);
if (product == null) return NotFound();
return Ok(product);
}
This matches routes like /api/products/5
and returns the corresponding product or a 404 if not found.
4.4 Query String Parameters
Query strings allow for filtering, pagination, and more:
[HttpGet("search")]
public IActionResult Search([FromQuery] string keyword) {
var results = productService.Search(keyword);
return Ok(results);
}
Now you can call /api/products/search?keyword=book
and get results based on the user’s input.
5. CRUD Operations Overview
5.1 HTTP Verbs and REST Design
In RESTful APIs, each HTTP verb has a distinct purpose. Understanding their role helps you follow best practices and deliver a predictable, scalable API experience:
- GET – Retrieve a resource or collection of resources (read-only)
- POST – Create a new resource
- PUT – Update an existing resource
- DELETE – Remove a resource
Using these correctly ensures idempotency, improves client compatibility, and simplifies API documentation. [HttpGet]
specifically handles safe, read-only operations that don’t modify server state.
5.2 Setting Up POST, PUT, DELETE
Here’s how you might define the other CRUD methods alongside your GET endpoint:
[HttpPost]
public IActionResult CreateProduct([FromBody] Product product) {
var created = productService.Create(product);
return CreatedAtAction(nameof(GetProduct), new { id = created.Id }, created);
}
[HttpPut("{id}")]
public IActionResult UpdateProduct(int id, [FromBody] Product product) {
var updated = productService.Update(id, product);
if (updated == null) return NotFound();
return Ok(updated);
}
[HttpDelete("{id}")]
public IActionResult DeleteProduct(int id) {
bool success = productService.Delete(id);
return success ? NoContent() : NotFound();
}
Note how each method returns a proper HTTP status code—this is important for client clarity and API consistency.
5.3 Combining GET with Other Operations
It’s common to combine multiple GET endpoints alongside POST/PUT/DELETE. For instance, you may have:
GET /api/products
– List all productsGET /api/products/5
– Get a single product by IDGET /api/products/search?query=pen
– Filter products
Each endpoint should be clear in purpose and return predictable results. Using descriptive routes and query parameters helps frontend and third-party developers consume your API with confidence.
---6. Data Persistence & EF Core
6.1 Setting Up DbContext
Entity Framework Core (EF Core) is Microsoft’s ORM that helps you interact with databases using C#. First, install the EF Core packages in your API project:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Create your model and DbContext:
public class Product {
public int Id { get; set; }
public string Name { get; set; }
}
public class AppDbContext : DbContext {
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
}
6.2 Migrations and Database Setup
Now that your context is ready, register it in Program.cs
:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
Then create your migration and apply it:
dotnet ef migrations add InitialCreate
dotnet ef database update
This creates the SQL schema based on your models—zero manual scripting needed.
6.3 Using DbContext in GET Methods
Inject your DbContext in a controller to fetch records with [HttpGet]
:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase {
private readonly AppDbContext _context;
public ProductsController(AppDbContext context) {
_context = context;
}
[HttpGet]
public async Task<IActionResult> GetProducts() {
var products = await _context.Products.ToListAsync();
return Ok(products);
}
}
This is a fully connected Web API backed by a real SQL database. You can now retrieve live data directly from the database using GET requests.
---7. Querying and Filtering Data
7.1 Pagination
Pagination allows you to split results into pages to reduce payload size and improve client-side performance. Here’s a basic paginated GET endpoint:
[HttpGet("paged")]
public async Task<IActionResult> GetPaged([FromQuery] int page = 1, [FromQuery] int size = 10) {
var products = await _context.Products
.Skip((page - 1) * size)
.Take(size)
.ToListAsync();
return Ok(products);
}
Call it using: /api/products/paged?page=2&size=5
7.2 Filtering and Search Parameters
GET endpoints can support multiple filters using query strings:
[HttpGet("filter")]
public async Task<IActionResult> FilterProducts([FromQuery] string name) {
var filtered = await _context.Products
.Where(p => p.Name.Contains(name))
.ToListAsync();
return Ok(filtered);
}
This approach supports frontend autocomplete, dynamic search, and lightweight filtering.
7.3 Sorting Results
Support sorting by name or ID using a query parameter:
[HttpGet("sort")]
public async Task<IActionResult> SortProducts([FromQuery] string sortBy = "name") {
var products = sortBy == "id"
? await _context.Products.OrderBy(p => p.Id).ToListAsync()
: await _context.Products.OrderBy(p => p.Name).ToListAsync();
return Ok(products);
}
These small enhancements greatly improve the developer experience for consumers of your API.
8. Handling HTTP Responses
8.1 Returning IActionResult
In ASP.NET Core Web APIs, returning IActionResult
gives you flexibility to return different HTTP status codes depending on your business logic. This approach is preferred over returning raw data types like string
or List<T>
for public-facing APIs.
[HttpGet("{id}")]
public IActionResult GetProductById(int id) {
var product = _context.Products.Find(id);
if (product == null) return NotFound();
return Ok(product);
}
This code returns a 200 OK
if the product is found and a 404 Not Found
otherwise. Clients and search engines understand and respect these standardized responses, which improves UX and SEO crawlability.
8.2 Returning Ok, NotFound, BadRequest
Use these built-in methods for clear communication with clients:
Ok()
: Return data successfullyNotFound()
: Indicate missing dataBadRequest()
: Client error, usually due to invalid inputCreatedAtAction()
: Return new resource URI after creation
[HttpGet("{name}")]
public IActionResult FindProductByName(string name) {
if (string.IsNullOrWhiteSpace(name))
return BadRequest("Name cannot be empty.");
var product = _context.Products.FirstOrDefault(p => p.Name == name);
return product == null ? NotFound() : Ok(product);
}
8.3 Exception Filters and Error Handling
To handle errors globally, create a custom exception filter:
public class GlobalExceptionFilter : IExceptionFilter {
private readonly ILogger<GlobalExceptionFilter> _logger;
public GlobalExceptionFilter(ILogger<GlobalExceptionFilter> logger) {
_logger = logger;
}
public void OnException(ExceptionContext context) {
_logger.LogError(context.Exception, "Unhandled exception occurred");
context.Result = new ObjectResult("Something went wrong")
{
StatusCode = 500
};
}
}
Register it in Program.cs
:
builder.Services.AddControllers(options =>
options.Filters.Add<GlobalExceptionFilter>());
This ensures consistent error handling across all GET and other endpoints—great for AdSense-safe sites where errors must be gracefully handled.
---9. Versioning Web APIs
9.1 Why Version Your API?
Over time, your API evolves. To maintain backward compatibility, it's best to version your APIs. This helps consumers migrate slowly without breaking existing apps. GET endpoints often change as new query parameters or models are introduced.
9.2 URL vs Header Versioning
There are two popular approaches:
- URL Versioning:
/api/v1/products
,/api/v2/products
- Header Versioning: Pass version info via custom HTTP headers
URL versioning is more SEO-friendly and easier to document in Swagger.
9.3 Decorating Endpoints with Version Metadata
Install the versioning NuGet package:
dotnet add package Microsoft.AspNetCore.Mvc.Versioning
In Program.cs
:
builder.Services.AddApiVersioning(options => {
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
});
Then decorate your controller:
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/products")]
[ApiController]
public class ProductsV1Controller : ControllerBase { ... }
---
10. Security & CORS
10.1 Enabling CORS for GET Requests
Cross-Origin Resource Sharing (CORS) is vital for allowing web apps from other domains to access your API. This is particularly relevant for frontend SPAs hosted on different domains.
builder.Services.AddCors(options => {
options.AddPolicy("AllowAll", builder =>
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
});
app.UseCors("AllowAll");
You can also restrict CORS to only GET:
.AllowAnyOrigin().WithMethods("GET").AllowAnyHeader();
10.2 API Key Authentication
Secure your public-facing GET endpoints using simple API key headers:
public class ApiKeyMiddleware {
private readonly RequestDelegate _next;
private const string APIKEY = "X-API-KEY";
public ApiKeyMiddleware(RequestDelegate next) {
_next = next;
}
public async Task Invoke(HttpContext context) {
if (!context.Request.Headers.TryGetValue(APIKEY, out var key) ||
key != "secret-api-key") {
context.Response.StatusCode = 401;
await context.Response.WriteAsync("API Key is missing or invalid");
return;
}
await _next(context);
}
}
10.3 JWT Tokens and Claim-Based Auth
For full user-based security, implement JWT authentication. Install the necessary packages and configure JWT in Program.cs
. Then decorate your controller with:
[Authorize]
[HttpGet]
public IActionResult GetSecureData() {
return Ok("This data requires valid token.");
}
Google AdSense prefers well-secured APIs with proper access control, so adding auth is a plus.
---11. Documentation with Swagger
11.1 Adding Swashbuckle
Swagger auto-generates API documentation, making your endpoints discoverable by other devs and SEO crawlers. Install the package:
dotnet add package Swashbuckle.AspNetCore
11.2 Customizing GET Endpoint Docs
Enable Swagger in Program.cs
:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
In app.UseSwagger()
and app.UseSwaggerUI()
, you expose your API via the browser at /swagger
.
Add XML comments above each method:
/// <summary>Returns a list of all products</summary>
[HttpGet]
public IActionResult GetAllProducts() {
return Ok(productService.GetAll());
}
11.3 Testing APIs via Swagger UI
Swagger UI lets you test GET, POST, PUT, DELETE endpoints directly from your browser. This improves the discoverability of your API and builds trust with AdSense reviewers and users alike.
12. Testing GET Endpoints
12.1 Unit Testing Controllers
Unit testing ensures your GET endpoints behave correctly under different conditions. Use xUnit or NUnit to test controller logic in isolation:
public class ProductsControllerTests {
[Fact]
public void GetAll_ReturnsOk_WithListOfProducts() {
var fakeProducts = new List<Product> {
new() { Id = 1, Name = "Keyboard" },
new() { Id = 2, Name = "Mouse" }
};
var mockContext = new Mock<AppDbContext>();
// Setup mocking logic here...
var controller = new ProductsController(mockContext.Object);
var result = controller.GetAll();
Assert.IsType<OkObjectResult>(result);
}
}
Unit tests run fast and give you confidence your controller logic is solid.
12.2 Integration Testing with WebApplicationFactory
For real HTTP simulation, use integration tests to validate entire request/response flows:
public class ApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>> {
private readonly HttpClient _client;
public ApiIntegrationTests(WebApplicationFactory<Program> factory) {
_client = factory.CreateClient();
}
[Fact]
public async Task Get_Endpoint_Returns_200() {
var response = await _client.GetAsync("/api/products");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
Use this to test security, headers, routing, and real database scenarios.
12.3 Mocking DbContext
To isolate tests from the database, mock your DbContext
with InMemory
or a mocking framework:
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));
This avoids SQL Server dependencies and allows full test coverage for GET logic.
---13. Performance Optimizations
13.1 Response Caching GET Results
GET endpoints can be cached since they are read-only. Use ResponseCache
to avoid unnecessary processing:
[HttpGet]
[ResponseCache(Duration = 60)]
public IActionResult GetCachedData() {
return Ok(dataService.GetTopItems());
}
Cache headers help CDNs and browsers serve repeated requests instantly.
13.2 Efficient Serialization Options
Use System.Text.Json
with optimized settings in Program.cs
:
builder.Services.AddControllers().AddJsonOptions(opts => {
opts.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
This reduces payload size and accelerates GET responses.
13.3 Using IHttpClientFactory for Outbound GET Calls
If your API calls external services, avoid new HttpClient()
. Register and inject IHttpClientFactory
:
builder.Services.AddHttpClient();
Then use it in services:
public class ExternalApiService {
private readonly HttpClient _http;
public ExternalApiService(HttpClient http) {
_http = http;
}
public async Task<string> FetchData() {
var response = await _http.GetAsync("https://api.example.com/data");
return await response.Content.ReadAsStringAsync();
}
}
---
14. Monitoring & Logging
14.1 Built‑in Logging in .NET
ASP.NET Core includes logging via ILogger<T>
. Inject it into controllers and services:
private readonly ILogger<ProductsController> _logger;
public ProductsController(ILogger<ProductsController> logger) {
_logger = logger;
}
[HttpGet]
public IActionResult GetAll() {
_logger.LogInformation("Fetching all products");
return Ok(_context.Products.ToList());
}
14.2 Logging GET Requests and Responses
Use middleware to log each request and response body:
app.Use(async (context, next) => {
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Request: {Method} {Path}", context.Request.Method, context.Request.Path);
await next();
});
14.3 Health Checks and Telemetry
Add HealthChecks
to monitor API uptime for bots and load balancers:
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");
---
15. Best Practices & Anti-Patterns
15.1 Keeping GET Endpoints Idempotent
Never perform writes, updates, or deletions inside [HttpGet]
endpoints. GETs should never change data. This helps with SEO, AdSense trust, and browser cache reliability.
15.2 Avoiding “Fat Controllers”
Move business logic into services. Controllers should only handle routing and minimal orchestration.
15.3 Proper Use of DTOs and View Models
Don’t expose your database models directly. Create DTOs to shape responses and hide sensitive fields:
public class ProductDto {
public int Id { get; set; }
public string Name { get; set; }
}
---
16. Frequently Asked Questions (FAQs)
1. What is the difference between GET and POST in C# APIs?
GET is used to retrieve data without changing server state, while POST is used to create or modify data. GET can be cached and bookmarked—POST cannot.
2. Can I cache GET responses?
Yes. Use ResponseCache
or external caching layers like Redis or CDN edge caching for performance and SEO boosts.
3. How do I handle 404 errors in GET endpoints?
Return NotFound()
when data isn’t available. Avoid throwing exceptions or returning null
directly.
4. How to secure GET endpoints?
Use API keys or JWT token authorization. CORS policies also help limit access from unauthorized domains.
5. What tools help test GET APIs?
Use Swagger UI, Postman, curl, or integration test frameworks like xUnit + TestServer. Always test real-world scenarios.
---17. Conclusion
RESTful APIs are the foundation of modern app development, and [HttpGet]
endpoints are the most commonly used. Whether you're building a public API, an internal service, or a backend for a frontend app, the principles and techniques in this guide will help you design fast, scalable, and secure GET operations in ASP.NET Core.
By following best practices like versioning, filtering, caching, and DTO separation, your API will not only be developer-friendly—it will also meet the standards Google AdSense and search engines look for when evaluating quality content and crawlable web structure.
---Please don’t forget to leave a review.
Post a Comment