.NET 8: ASP.NET Core ile API Gelistirme

.NET 8 ve ASP.NET Core ile profesyonel bir REST API olusturmaya yonelik kapsamli rehber. Controller yapisi, Entity Framework Core, dogrulama ve en iyi uygulamalar.

.NET 8 ve ASP.NET Core ile REST API gelistirme rehberi

.NET 8, Microsoft'un framework ekosistemine onemli performans ve verimlilik iyilestirmeleri getiren buyuk bir surum olarak one cikiyor. ASP.NET Core, C# dilinin gucunu kurumsal uygulamalar icin ideal olan modern ve moduler bir mimariyle birlestiriyor. Bu rehber, ilk proje kurulumundan uretime hazir koda kadar profesyonel bir REST API olusturmanin tum adimlarini kapsiyor.

.NET 8 LTS

.NET 8, 3 yillik destege sahip bir Long-Term Support (LTS) surumudur. Minimal API ve Native AOT iyilestirmeleri, mikro hizmetler ve cloud-native uygulamalar icin optimal bir secim haline getirmektedir.

.NET 8 ile Proje Kurulumu

ASP.NET Core API projesi olusturmak icin .NET CLI kullanilarak optimize edilmis bir proje yapisi uretilir. Temel NuGet paketlerinin yapilandirilmasi, gelistirme icin saglam bir temel olusturur.

bash
# terminal
# Check installed .NET version
dotnet --version
# Expected: 8.0.x

# Create the API project
dotnet new webapi -n ProductApi -o ProductApi
cd ProductApi

# Add essential packages
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package FluentValidation.AspNetCore
dotnet add package Swashbuckle.AspNetCore

Bu komutlar, Entity Framework Core, dogrulama ve Swagger dokumantasyonu icin gerekli bagimliliklara sahip bir API projesi olusturur.

Program.cscsharp
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.Services;
using FluentValidation;
using FluentValidation.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Configure Entity Framework Core with SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register business services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();

// Configure controllers with validation
builder.Services.AddControllers();
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();

// Configure Swagger for documentation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "Product API", Version = "v1" });
});

var app = builder.Build();

// Middleware pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Bu yapilandirma, .NET 8'in Minimal API kalibini kullanirken, acik ve surdurulebilir bir yapi icin controller yapisini korumaktadir.

Veri Modelleri ve Entity Framework Core

Modeller, uygulamanin is varliklarini temsil eder. Entity Framework Core, Fluent API yapilandirmasi ve akilli kurallarla nesne-iliskisel eslemeleri yonetir.

Models/Product.cscsharp
namespace ProductApi.Models;

public class Product
{
    // Primary key with auto-increment
    public int Id { get; set; }

    // Required properties (non-nullable in C# 8+)
    public required string Name { get; set; }
    public required string Description { get; set; }

    // Price with decimal precision
    public decimal Price { get; set; }

    // Stock with default value
    public int StockQuantity { get; set; } = 0;

    // Product status
    public bool IsActive { get; set; } = true;

    // Relationship with Category (foreign key)
    public int CategoryId { get; set; }
    public Category? Category { get; set; }

    // Automatic tracking dates
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; set; }
}

C# 11'den gelen required anahtar kelimesi, onemli ozelliklerin nesne olusturma sirasinda her zaman baslatilmasini garanti eder.

Models/Category.cscsharp
namespace ProductApi.Models;

public class Category
{
    public int Id { get; set; }

    public required string Name { get; set; }

    // Slug for friendly URLs
    public required string Slug { get; set; }

    public string? Description { get; set; }

    // Inverse navigation: list of products in this category
    public ICollection<Product> Products { get; set; } = new List<Product>();

    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
Data/AppDbContext.cscsharp
using Microsoft.EntityFrameworkCore;
using ProductApi.Models;

namespace ProductApi.Data;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    // DbSets for each entity
    public DbSet<Product> Products => Set<Product>();
    public DbSet<Category> Categories => Set<Category>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Product entity configuration
        modelBuilder.Entity<Product>(entity =>
        {
            // Index on name for fast search
            entity.HasIndex(p => p.Name);

            // Price precision: 18 digits, 2 decimals
            entity.Property(p => p.Price)
                .HasPrecision(18, 2);

            // Relationship with Category
            entity.HasOne(p => p.Category)
                .WithMany(c => c.Products)
                .HasForeignKey(p => p.CategoryId)
                .OnDelete(DeleteBehavior.Restrict);
        });

        // Category entity configuration
        modelBuilder.Entity<Category>(entity =>
        {
            // Unique slug
            entity.HasIndex(c => c.Slug).IsUnique();

            // Maximum name length
            entity.Property(c => c.Name).HasMaxLength(100);
        });
    }
}

Fluent API yapilandirmasi, EF Core migrasyonlari tarafindan olusturulan veritabani semasi uzerinde hassas kontrol saglar.

Entity Framework Core Migrasyonlari

Migrasyonlar veritabani semasini versiyonlar. Degisiklikleri uygulamak icin dotnet ef migrations add InitialCreate ve ardindan dotnet ef database update komutlarini calistirmak gerekir.

DTO ve FluentValidation ile Dogrulama

DTO'lar (Data Transfer Objects), alan modellerini API uzerinden sunulan verilerden ayirir. FluentValidation, bildirimsel ve surdurulebilir bir dogrulama mekanizmasi sunar.

DTOs/ProductDtos.cscsharp
namespace ProductApi.DTOs;

// DTO for product creation
public record CreateProductDto(
    string Name,
    string Description,
    decimal Price,
    int StockQuantity,
    int CategoryId
);

// DTO for product update
public record UpdateProductDto(
    string? Name,
    string? Description,
    decimal? Price,
    int? StockQuantity,
    bool? IsActive
);

// DTO for response (read)
public record ProductDto(
    int Id,
    string Name,
    string Description,
    decimal Price,
    int StockQuantity,
    bool IsActive,
    string CategoryName,
    DateTime CreatedAt
);

// DTO for list with pagination
public record ProductListDto(
    int Id,
    string Name,
    decimal Price,
    int StockQuantity,
    bool IsActive,
    string CategoryName
);

C# 9+ record yapisinin kullanimi, DTO'lari degistirilemez ve ozlu kilarken otomatik deger esitligi saglar.

Validators/ProductValidators.cscsharp
using FluentValidation;
using ProductApi.DTOs;

namespace ProductApi.Validators;

public class CreateProductValidator : AbstractValidator<CreateProductDto>
{
    public CreateProductValidator()
    {
        // Name is required and limited to 200 characters
        RuleFor(x => x.Name)
            .NotEmpty().WithMessage("Product name is required.")
            .MaximumLength(200).WithMessage("Name cannot exceed 200 characters.");

        // Description required with minimum length
        RuleFor(x => x.Description)
            .NotEmpty().WithMessage("Description is required.")
            .MinimumLength(10).WithMessage("Description must contain at least 10 characters.");

        // Positive price required
        RuleFor(x => x.Price)
            .GreaterThan(0).WithMessage("Price must be greater than 0.")
            .LessThanOrEqualTo(999999.99m).WithMessage("Maximum price is 999,999.99.");

        // Non-negative stock
        RuleFor(x => x.StockQuantity)
            .GreaterThanOrEqualTo(0).WithMessage("Stock cannot be negative.");

        // Valid category
        RuleFor(x => x.CategoryId)
            .GreaterThan(0).WithMessage("A valid category is required.");
    }
}

public class UpdateProductValidator : AbstractValidator<UpdateProductDto>
{
    public UpdateProductValidator()
    {
        // Conditional validation: only if value is provided
        RuleFor(x => x.Name)
            .MaximumLength(200)
            .When(x => !string.IsNullOrEmpty(x.Name));

        RuleFor(x => x.Price)
            .GreaterThan(0)
            .When(x => x.Price.HasValue);

        RuleFor(x => x.StockQuantity)
            .GreaterThanOrEqualTo(0)
            .When(x => x.StockQuantity.HasValue);
    }
}

FluentValidation, ASP.NET Core dogrulama pipeline'i ile otomatik olarak entegre olur ve yapilandirilmis 400 hatalari dondurur.

.NET mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Is Servisleri ve Soyutlama Katmani

Servis katmani, is mantigi ve veritabani islemlerini kapsulleyerek test edilebilirlik ve bakim kolayligi saglar.

Services/IProductService.cscsharp
using ProductApi.DTOs;

namespace ProductApi.Services;

public interface IProductService
{
    // Retrieval with pagination
    Task<(IEnumerable<ProductListDto> Items, int TotalCount)> GetAllAsync(
        int page = 1,
        int pageSize = 10,
        string? search = null,
        int? categoryId = null);

    // Retrieval by ID
    Task<ProductDto?> GetByIdAsync(int id);

    // Creation
    Task<ProductDto> CreateAsync(CreateProductDto dto);

    // Update
    Task<ProductDto?> UpdateAsync(int id, UpdateProductDto dto);

    // Deletion
    Task<bool> DeleteAsync(int id);
}
Services/ProductService.cscsharp
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.DTOs;
using ProductApi.Models;

namespace ProductApi.Services;

public class ProductService : IProductService
{
    private readonly AppDbContext _context;

    public ProductService(AppDbContext context)
    {
        _context = context;
    }

    public async Task<(IEnumerable<ProductListDto> Items, int TotalCount)> GetAllAsync(
        int page = 1,
        int pageSize = 10,
        string? search = null,
        int? categoryId = null)
    {
        // Build base query
        var query = _context.Products
            .Include(p => p.Category)
            .AsQueryable();

        // Filter by text search
        if (!string.IsNullOrWhiteSpace(search))
        {
            query = query.Where(p =>
                p.Name.Contains(search) ||
                p.Description.Contains(search));
        }

        // Filter by category
        if (categoryId.HasValue)
        {
            query = query.Where(p => p.CategoryId == categoryId.Value);
        }

        // Total count before pagination
        var totalCount = await query.CountAsync();

        // Apply pagination
        var items = await query
            .OrderByDescending(p => p.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(p => new ProductListDto(
                p.Id,
                p.Name,
                p.Price,
                p.StockQuantity,
                p.IsActive,
                p.Category!.Name))
            .ToListAsync();

        return (items, totalCount);
    }

    public async Task<ProductDto?> GetByIdAsync(int id)
    {
        // Retrieve with category inclusion
        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        if (product == null) return null;

        // Map to DTO
        return new ProductDto(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.StockQuantity,
            product.IsActive,
            product.Category?.Name ?? "Uncategorized",
            product.CreatedAt);
    }

    public async Task<ProductDto> CreateAsync(CreateProductDto dto)
    {
        // Create entity
        var product = new Product
        {
            Name = dto.Name,
            Description = dto.Description,
            Price = dto.Price,
            StockQuantity = dto.StockQuantity,
            CategoryId = dto.CategoryId
        };

        // Add and save
        _context.Products.Add(product);
        await _context.SaveChangesAsync();

        // Load category for response
        await _context.Entry(product)
            .Reference(p => p.Category)
            .LoadAsync();

        return new ProductDto(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.StockQuantity,
            product.IsActive,
            product.Category?.Name ?? "Uncategorized",
            product.CreatedAt);
    }

    public async Task<ProductDto?> UpdateAsync(int id, UpdateProductDto dto)
    {
        // Retrieve existing entity
        var product = await _context.Products
            .Include(p => p.Category)
            .FirstOrDefaultAsync(p => p.Id == id);

        if (product == null) return null;

        // Conditional field updates
        if (!string.IsNullOrEmpty(dto.Name))
            product.Name = dto.Name;

        if (!string.IsNullOrEmpty(dto.Description))
            product.Description = dto.Description;

        if (dto.Price.HasValue)
            product.Price = dto.Price.Value;

        if (dto.StockQuantity.HasValue)
            product.StockQuantity = dto.StockQuantity.Value;

        if (dto.IsActive.HasValue)
            product.IsActive = dto.IsActive.Value;

        // Update modification date
        product.UpdatedAt = DateTime.UtcNow;

        await _context.SaveChangesAsync();

        return new ProductDto(
            product.Id,
            product.Name,
            product.Description,
            product.Price,
            product.StockQuantity,
            product.IsActive,
            product.Category?.Name ?? "Uncategorized",
            product.CreatedAt);
    }

    public async Task<bool> DeleteAsync(int id)
    {
        // Direct deletion without prior loading
        var result = await _context.Products
            .Where(p => p.Id == id)
            .ExecuteDeleteAsync();

        return result > 0;
    }
}

ExecuteDeleteAsync kullanimi (EF Core 7+ ile gelen yenilik), silme isleminden once varlik yuklemesini ortadan kaldirarak performansi arttirir.

API Controller ve REST Endpoint'leri

Controller'lar REST endpoint'lerini aciga cikarir ve uygun HTTP durum kodu yonetimiyle is servislerine yapilan cagrilari yonetir.

Controllers/ProductsController.cscsharp
using Microsoft.AspNetCore.Mvc;
using ProductApi.DTOs;
using ProductApi.Services;

namespace ProductApi.Controllers;

[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    /// <summary>
    /// Retrieves the list of products with pagination and filters.
    /// </summary>
    [HttpGet]
    [ProducesResponseType(typeof(PaginatedResponse<ProductListDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 10,
        [FromQuery] string? search = null,
        [FromQuery] int? categoryId = null)
    {
        // Validate pagination parameters
        if (page < 1) page = 1;
        if (pageSize < 1 || pageSize > 100) pageSize = 10;

        var (items, totalCount) = await _productService.GetAllAsync(
            page, pageSize, search, categoryId);

        // Standardized paginated response
        var response = new PaginatedResponse<ProductListDto>
        {
            Items = items,
            Page = page,
            PageSize = pageSize,
            TotalCount = totalCount,
            TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
        };

        return Ok(response);
    }

    /// <summary>
    /// Retrieves a product by its identifier.
    /// </summary>
    [HttpGet("{id:int}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);

        if (product == null)
        {
            return NotFound(new { message = $"Product with ID {id} not found." });
        }

        return Ok(product);
    }

    /// <summary>
    /// Creates a new product.
    /// </summary>
    [HttpPost]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create([FromBody] CreateProductDto dto)
    {
        // Validation is automatic via FluentValidation
        var product = await _productService.CreateAsync(dto);

        // Returns 201 with the created resource URL
        return CreatedAtAction(
            nameof(GetById),
            new { id = product.Id },
            product);
    }

    /// <summary>
    /// Updates an existing product.
    /// </summary>
    [HttpPut("{id:int}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Update(int id, [FromBody] UpdateProductDto dto)
    {
        var product = await _productService.UpdateAsync(id, dto);

        if (product == null)
        {
            return NotFound(new { message = $"Product with ID {id} not found." });
        }

        return Ok(product);
    }

    /// <summary>
    /// Deletes a product.
    /// </summary>
    [HttpDelete("{id:int}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(int id)
    {
        var deleted = await _productService.DeleteAsync(id);

        if (!deleted)
        {
            return NotFound(new { message = $"Product with ID {id} not found." });
        }

        // 204 No Content for successful deletion
        return NoContent();
    }
}

ProducesResponseType nitelikleri, otomatik Swagger dokumantasyonu olusturma icin olasi yanit kodlarini belgelemektedir.

DTOs/PaginatedResponse.cscsharp
namespace ProductApi.DTOs;

public class PaginatedResponse<T>
{
    public IEnumerable<T> Items { get; set; } = Enumerable.Empty<T>();
    public int Page { get; set; }
    public int PageSize { get; set; }
    public int TotalCount { get; set; }
    public int TotalPages { get; set; }
    public bool HasPreviousPage => Page > 1;
    public bool HasNextPage => Page < TotalPages;
}
Route Kisitlamalari

{id:int} gibi kisitlamalarin kullanimi, yonlendirme catismalarini onler ve format hatali oldugunda otomatik olarak 404 dondurur.

Global Hata Yonetimi

Hata yonetim middleware'i, tutarli ve guvenli yanitlar saglamak icin istisna islemesini merkezilestirmektedir.

Middleware/ExceptionMiddleware.cscsharp
using System.Net;
using System.Text.Json;

namespace ProductApi.Middleware;

public class ExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionMiddleware> _logger;
    private readonly IHostEnvironment _env;

    public ExceptionMiddleware(
        RequestDelegate next,
        ILogger<ExceptionMiddleware> logger,
        IHostEnvironment env)
    {
        _next = next;
        _logger = logger;
        _env = env;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            // Continue pipeline
            await _next(context);
        }
        catch (Exception ex)
        {
            // Log the error
            _logger.LogError(ex, "An unhandled exception occurred");

            // Prepare response
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;

            // Different response based on environment
            var response = _env.IsDevelopment()
                ? new ErrorResponse(
                    StatusCode: context.Response.StatusCode,
                    Message: ex.Message,
                    Details: ex.StackTrace)
                : new ErrorResponse(
                    StatusCode: context.Response.StatusCode,
                    Message: "An internal error occurred.",
                    Details: null);

            // Serialize with camelCase options
            var options = new JsonSerializerOptions
            {
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase
            };

            var json = JsonSerializer.Serialize(response, options);
            await context.Response.WriteAsync(json);
        }
    }
}

// DTO for error responses
public record ErrorResponse(int StatusCode, string Message, string? Details);

// Extension to register middleware
public static class ExceptionMiddlewareExtensions
{
    public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder app)
    {
        return app.UseMiddleware<ExceptionMiddleware>();
    }
}
Program.cs (adding middleware)csharp
var app = builder.Build();

// Exception middleware must be first
app.UseExceptionMiddleware();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
// ... rest of configuration

Yapilandirma ve Ortam Degiskenleri

Dissallastirilmis yapilandirma, kod degisikligi yapmadan uygulamayi farkli ortamlara uyarlamayi mumkun kilar.

appsettings.jsonjson
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=ProductDb;User Id=sa;Password=YourPassword;TrustServerCertificate=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning"
    }
  },
  "ApiSettings": {
    "DefaultPageSize": 10,
    "MaxPageSize": 100,
    "ApiVersion": "1.0"
  }
}
Configuration/ApiSettings.cscsharp
namespace ProductApi.Configuration;

public class ApiSettings
{
    public int DefaultPageSize { get; set; } = 10;
    public int MaxPageSize { get; set; } = 100;
    public string ApiVersion { get; set; } = "1.0";
}
Program.cs (injecting configuration)csharp
builder.Services.Configure<ApiSettings>(
    builder.Configuration.GetSection("ApiSettings"));

// Usage in a service
public class ProductService : IProductService
{
    private readonly ApiSettings _settings;

    public ProductService(IOptions<ApiSettings> settings)
    {
        _settings = settings.Value;
    }
}

xUnit ile Birim Testleri

Birim testleri, servislerin ve controller'larin davranislarini izole bir sekilde dogrular.

Tests/ProductServiceTests.cscsharp
using Microsoft.EntityFrameworkCore;
using ProductApi.Data;
using ProductApi.DTOs;
using ProductApi.Models;
using ProductApi.Services;
using Xunit;

namespace ProductApi.Tests;

public class ProductServiceTests
{
    private AppDbContext CreateInMemoryContext()
    {
        // Configure in-memory database
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        return new AppDbContext(options);
    }

    [Fact]
    public async Task CreateAsync_ValidDto_ReturnsProductDto()
    {
        // Arrange
        using var context = CreateInMemoryContext();

        // Add test category
        var category = new Category { Id = 1, Name = "Electronics", Slug = "electronics" };
        context.Categories.Add(category);
        await context.SaveChangesAsync();

        var service = new ProductService(context);
        var dto = new CreateProductDto(
            Name: "Test Product",
            Description: "Test Description",
            Price: 99.99m,
            StockQuantity: 10,
            CategoryId: 1);

        // Act
        var result = await service.CreateAsync(dto);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Test Product", result.Name);
        Assert.Equal(99.99m, result.Price);
        Assert.Equal("Electronics", result.CategoryName);
    }

    [Fact]
    public async Task GetByIdAsync_NonExistent_ReturnsNull()
    {
        // Arrange
        using var context = CreateInMemoryContext();
        var service = new ProductService(context);

        // Act
        var result = await service.GetByIdAsync(999);

        // Assert
        Assert.Null(result);
    }

    [Fact]
    public async Task UpdateAsync_ExistingProduct_UpdatesFields()
    {
        // Arrange
        using var context = CreateInMemoryContext();

        var category = new Category { Id = 1, Name = "Tech", Slug = "tech" };
        var product = new Product
        {
            Id = 1,
            Name = "Original Name",
            Description = "Original Description",
            Price = 50.00m,
            StockQuantity = 5,
            CategoryId = 1
        };

        context.Categories.Add(category);
        context.Products.Add(product);
        await context.SaveChangesAsync();

        var service = new ProductService(context);
        var updateDto = new UpdateProductDto(
            Name: "Updated Name",
            Description: null,
            Price: 75.00m,
            StockQuantity: null,
            IsActive: null);

        // Act
        var result = await service.UpdateAsync(1, updateDto);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Updated Name", result.Name);
        Assert.Equal(75.00m, result.Price);
        // Fields not provided remain unchanged
        Assert.Equal(5, result.StockQuantity);
    }

    [Fact]
    public async Task DeleteAsync_ExistingProduct_ReturnsTrue()
    {
        // Arrange
        using var context = CreateInMemoryContext();

        var category = new Category { Id = 1, Name = "Test", Slug = "test" };
        var product = new Product
        {
            Id = 1,
            Name = "To Delete",
            Description = "Will be deleted",
            Price = 10.00m,
            CategoryId = 1
        };

        context.Categories.Add(category);
        context.Products.Add(product);
        await context.SaveChangesAsync();

        var service = new ProductService(context);

        // Act
        var result = await service.DeleteAsync(1);

        // Assert
        Assert.True(result);
        Assert.Null(await context.Products.FindAsync(1));
    }

    [Fact]
    public async Task GetAllAsync_WithSearch_FiltersResults()
    {
        // Arrange
        using var context = CreateInMemoryContext();

        var category = new Category { Id = 1, Name = "Category", Slug = "category" };
        context.Categories.Add(category);

        context.Products.AddRange(
            new Product { Id = 1, Name = "Apple iPhone", Description = "Phone", Price = 999, CategoryId = 1 },
            new Product { Id = 2, Name = "Samsung Galaxy", Description = "Phone", Price = 899, CategoryId = 1 },
            new Product { Id = 3, Name = "Apple MacBook", Description = "Laptop", Price = 1999, CategoryId = 1 }
        );
        await context.SaveChangesAsync();

        var service = new ProductService(context);

        // Act
        var (items, totalCount) = await service.GetAllAsync(search: "Apple");

        // Assert
        Assert.Equal(2, totalCount);
        Assert.All(items, p => Assert.Contains("Apple", p.Name));
    }
}

Testler, proje kok dizininden dotnet test komutuyla calistirilir.

Sonuc

.NET 8 ve ASP.NET Core, profesyonel REST API'ler olusturmak icin eksiksiz ve performansli bir ekosistem sunmaktadir. Veri erisimi icin Entity Framework Core, dogrulama icin FluentValidation ve yerel bagimlilik enjeksiyonunun birlesimi, surdurulebilir ve test edilebilir uygulamalar gelistirmeyi mumkun kilar.

Kaliteli bir .NET API icin Kontrol Listesi

  • DTO'lari alan modellerinden ayirma
  • Is mantigi icin servis katmani uygulama
  • Bildirimsel dogrulama icin FluentValidation kullanma
  • Global hata yonetimi middleware'i yapilandirma
  • IOptions ile yapilandirmayi dissallastirma
  • Servisler icin birim testleri yazma
  • API'yi Swagger/OpenAPI ile belgeleme

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Katmanli mimari (Controllers -> Services -> Repository/DbContext), sorumluluk ayrimini tesvik eder ve uygulamanin gelisimini kolaylastirir. Record'lar, required ozellikleri ve ExecuteDeleteAsync gibi .NET 8 ozellikleri, performansi artirirken kodu modernize etmektedir.

Etiketler

#dotnet
#aspnet core
#csharp
#rest api
#entity framework

Paylaş

İlgili makaleler