ASP.NET Core ile RESTful API Geliştirme
# ASP.NET Core ile RESTful API Gelistirme
ASP.NET Core, modern ve performansli API'ler olusturmak icin en guclu seceneklerden biri. Hizli HTTP pipeline'i, olgun middleware altyapisi ve surdurulebilir servisler icin saglam konvansiyonlari ile kurumsal projelerde kendini defalarca kanitlamis bir framework. Uretim ortaminda gelistirdigim API'lerde, framework'un yogun yuk altinda bile guvenilir sekilde calistigini defalarca gordum; ancak en iyi sonucu almak icin ilk gunden itibaren bilineli mimari kararlari almak sart.
API Temelleri
Proje Yapisi ve Controller Tasarimi
Iyi organize edilmis bir API, temiz bir controller katmaniyla baslar. Controller'lar ince birer orkestrator olmali ve is mantigi servis katmanina devredilmeli:
[ApiController]
[Route(class="code-string">"api/v{version:apiVersion}/[controller]")]
[Produces(class="code-string">"application/json")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IProductService productService, ILogger<ProductsController> logger)
{
_productService = productService;
_logger = logger;
}
[HttpGet]
[ProducesResponseType(typeof(PagedResult<ProductDto>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetAll([FromQuery] ProductQueryParameters query)
{
var result = await _productService.GetAllAsync(query);
return Ok(result);
}
[HttpGet(class="code-string">"{id:guid}")]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetById(Guid id)
{
var product = await _productService.GetByIdAsync(id);
if (product is null)
return NotFound();
return Ok(product);
}
[HttpPost]
[ProducesResponseType(typeof(ProductDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
{
var product = await _productService.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
[HttpPut(class="code-string">"{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> Update(Guid id, [FromBody] UpdateProductRequest request)
{
await _productService.UpdateAsync(id, request);
return NoContent();
}
[HttpDelete(class="code-string">"{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Delete(Guid id)
{
await _productService.DeleteAsync(id);
return NoContent();
}
}Model Binding ve DTO'lar
Domain entity'lerinizi asla dogrudan disariya acmayin. API kontratinizi kararli tutmak icin ozel request ve response modelleri kullanin:
public record CreateProductRequest(
string Name,
string Description,
decimal Price,
string Category
);
public record ProductDto(
Guid Id,
string Name,
string Description,
decimal Price,
string Category,
DateTime CreatedAt
);
public record ProductQueryParameters
{
public int Page { get; init; } = class="code-number">1;
public int PageSize { get; init; } = class="code-number">20;
public string? SortBy { get; init; }
public string? Search { get; init; }
}FluentValidation ile Dogrulama
Basit durumlar icin Data Annotations yeterli olabilir, ancak kurallar karmasiklastiginda FluentValidation gercek gucunu gosterir. Uretim ortaminda gelistirdigim API'lerde FluentValidation, hatali isteklerin debuglanmasinda sayisiz saat kazandirdi:
public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
public CreateProductRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage(class="code-string">"Urun adi zorunludur.")
.MaximumLength(class="code-number">200).WithMessage(class="code-string">"Urun adi class="code-number">200 karakteri asamaz.");
RuleFor(x => x.Description)
.MaximumLength(class="code-number">2000);
RuleFor(x => x.Price)
.GreaterThan(class="code-number">0).WithMessage(class="code-string">"Fiyat pozitif bir deger olmalidir.")
.PrecisionScale(class="code-number">10, class="code-number">2, ignoreTrailingZeros: true)
.WithMessage(class="code-string">"Fiyat en fazla class="code-number">2 ondalik basamak icermelidir.");
RuleFor(x => x.Category)
.NotEmpty()
.Must(BeAValidCategory).WithMessage(class="code-string">"Gecersiz kategori.");
}
private bool BeAValidCategory(string category)
{
var validCategories = new[] { class="code-string">"Electronics", class="code-string">"Clothing", class="code-string">"Books", class="code-string">"Food" };
return validCategories.Contains(category);
}
}FluentValidation'i `Program.cs` dosyanizda kaydedin:
builder.Services.AddValidatorsFromAssemblyContaining<CreateProductRequestValidator>();
builder.Services.AddFluentValidationAutoValidation();ProblemDetails ile Global Hata Yonetimi
Tutarli hata yanitlari tartismaya acik degildir. Yerlesik `ProblemDetails` altyapisini global bir exception handler ile birlikte kullanin:
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions[class="code-string">"traceId"] = context.HttpContext.TraceIdentifier;
};
});
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.ContentType = class="code-string">"application/problem+json";
var exceptionFeature = context.Features.Get<IExceptionHandlerFeature>();
var exception = exceptionFeature?.Error;
var problemDetails = exception switch
{
NotFoundException ex => new ProblemDetails
{
Status = StatusCodes.Status404NotFound,
Title = class="code-string">"Kaynak Bulunamadi",
Detail = ex.Message,
Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.5.class="code-number">5"
},
ConflictException ex => new ProblemDetails
{
Status = StatusCodes.Status409Conflict,
Title = class="code-string">"Catisma",
Detail = ex.Message,
Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.5.class="code-number">10"
},
_ => new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = class="code-string">"Sunucu Hatasi",
Detail = class="code-string">"Beklenmeyen bir hata olustu.",
Type = class="code-string">"https:class="code-commentclass="code-string">">//tools.ietf.org/html/rfc9110#section-class="code-number">15.6.class="code-number">1"
}
};
context.Response.StatusCode = problemDetails.Status ?? class="code-number">500;
await context.Response.WriteAsJsonAsync(problemDetails);
});
});API Tasarim En Iyi Pratikleri
Kaynak Adlandirma Kurallari
Kaynaklar icin cogul isimler kullanin ve URL'leri tahmin edilebilir tutin:
URL'lerde fiil kullanmaktan kacinin. `/api/products/getAll` yerine sadece `GET /api/products` kullanin. HTTP metodu zaten islemi ifade eder.
HTTP Metodlari ve Durum Kodlari
Durum kodlarini bilineli kullanin. Her sey icin 200 dondurmek, iletisim firsatini kacirmak demektir:
| Islem | Basari Kodu | Anlami |
|-------|-------------|--------|
| GET | 200 OK | Kaynak bulundu ve donduruldu |
| POST | 201 Created | Kaynak olusturuldu, Location header ekle |
| PUT | 204 No Content | Kaynak basariyla guncellendi |
| PATCH | 200 OK | Kismi guncelleme yapildi, guncel hali don |
| DELETE | 204 No Content | Kaynak silindi |
Hatalar icin bu kodlara bagli kalin:
Sayfalama, Filtreleme ve Siralama
Her liste endpoint'i basindan itibaren sayfalama desteklemeli. Sonradan eklemek her zaman daha zahmetli olur:
public class PagedResult<T>
{
public IReadOnlyList<T> Items { get; init; } = [];
public int TotalCount { get; init; }
public int Page { get; init; }
public int PageSize { get; init; }
public bool HasNextPage => Page * PageSize < TotalCount;
public bool HasPreviousPage => Page > class="code-number">1;
}Versiyonlama Stratejisi
Ilk surumden itibaren versiyonlama planlayin. Uretim ortaminda gelistirdigim API'lerde versiyonlamayi atlam, her zaman agri dolu breaking-change goculeriyle sonuclandi. URL tabanli versiyonlama en acik ve istemci dostu yaklasimdir:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(class="code-number">1, class="code-number">0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader(class="code-string">"X-API-Version")
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = class="code-string">"class="code-string">'v'VVV";
options.SubstituteApiVersionInUrl = true;
});Breaking change gerektiginde, eski controller'i yasatirken yeni bir versiyon olusturun:
[ApiController]
[Route(class="code-string">"api/v{version:apiVersion}/products")]
[ApiVersion(class="code-string">"class="code-number">2.0")]
public class ProductsV2Controller : ControllerBase
{
[HttpGet(class="code-string">"{id:guid}")]
public async Task<IActionResult> GetById(Guid id)
{
class=class="code-string">"code-comment">// V2 daha zengin bir yanit yapisi doner
var product = await _productService.GetByIdDetailedAsync(id);
return Ok(product);
}
}Kullanicilar goc edebilsin diye eski versiyonlari acikca deprecated olarak isaretleyin:
[ApiVersion(class="code-string">"class="code-number">1.0", Deprecated = true)]OpenAPI/Swagger Dokumantasyonu
Iyi API dokumantasyonu, destek taleplerinin cogunlugunu ortadan kaldirir. Swagger'i zengin metadata ile yapilandirin:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc(class="code-string">"v1", new OpenApiInfo
{
Title = class="code-string">"Urun Katalogu API",
Version = class="code-string">"v1",
Description = class="code-string">"Urun katalogu yonetimi icin API.",
Contact = new OpenApiContact
{
Name = class="code-string">"API Destek",
Email = class="code-string">"api-destek@ornek.com"
}
});
options.SwaggerDoc(class="code-string">"v2", new OpenApiInfo
{
Title = class="code-string">"Urun Katalogu API",
Version = class="code-string">"v2"
});
class=class="code-string">"code-comment">// Projenizdeki XML yorumlarini dahil edin
var xmlFile = $class="code-string">"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
class=class="code-string">"code-comment">// Swagger UIclass="code-string">'a JWT kimlik dogrulamasi ekleyin
options.AddSecurityDefinition(class="code-string">"Bearer", new OpenApiSecurityScheme
{
Description = class="code-string">"Bearer semasi ile JWT yetkilendirme header'i.",
Name = class="code-string">"Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = class="code-string">"bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = class="code-string">"Bearer"
}
},
Array.Empty<string>()
}
});
});Swagger middleware'ini etkinlestirin:
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint(class="code-string">"/swagger/v1/swagger.json", class="code-string">"Urun API V1");
options.SwaggerEndpoint(class="code-string">"/swagger/v2/swagger.json", class="code-string">"Urun API V2");
});
}Uretim Ortami En Iyi Pratikleri
Performans Ipuclari
Guvenlik Temelleri
Yaygin API Tasarim Hatalari
Her sey icin 200 dondurmek. API'niz `200 OK` ile `{ "success": false, "error": "Bulunamadi" }` donduruyorsa, HTTP'yi kullanmak yerine onunla savasiyorsunuz demektir. Istemciler durum kodlarina guvenemez, cache ve load balancer gibi middleware'ler karisir, tekrar deneme mantigi bozulur.
Domain entity'lerini yanit olarak acmak. Veritabani semaniz API kontratiniza sizdiginda, her sema degisikligi bir breaking change haline gelir. Her zaman ozel DTO'lar kullanin.
Liste endpoint'lerinde sayfalama ihmal etmek. Gelistirme ortaminda 50 kayitla sorunsuz calisir. Sonra uretim ortaminda 500.000 kayit olur ve endpoint zaman asimina ugramaya baslar. Ilk gunden itibaren her zaman sayfalama yapin.
Basindan itibaren versiyonlama yapmamak. "Ihtiyac duyunca versiyonlama ekleriz" demek, kacinilmaz bir breaking change geldiginde acil goc yapacaksiniz anlamina gelir. URL tabanli versiyonlama kurulumu hicbir sey maliyeti yoktur.
Tutarsiz hata formatlari. Bazi endpoint'ler `{ "error": "mesaj" }` dondururken digerleri `ProblemDetails` donduruyorsa, her istemcinin birden fazla formati ele almasi gerekir. Tek bir standart secin ve global olarak uygulayin.
Istek dogrulamasini atlamak. Istemcilerin dogru formatta veri gonderecegine guvenmenk, bozuk durum ve anlasilmaz 500 hatalarina davetiye cikarmaktir. Her zaman sinirlarda dogrulayin.
Sonuc
ASP.NET Core, guvenilir kurumsal API'ler icin gereken yapi ve performansi saglar; ancak kalite, net kontratlardan, guclu gozlemlenebilirlikten ve disiplinli surum pratiklerinden gelir. Uretim ortaminda gelistirdigim API'lerde, iyi bir API ile harika bir API arasindaki fark her zaman detaylarda gizlidir: tutarli hata yonetimi, dusunulmus versiyonlama ve gelistiricilerin gercekten okumak istedigi dokumantasyon.
API mimarinizi birlikte tasarlayip uretim ortamina hazir hale getirebiliriz.
İlgili Makaleler
.NET Nedir? Modern Backend Geliştirme Rehberi
.NET platformunun ne olduğunu, nasıl çalıştığını ve neden kurumsal projelerde tercih edildiğini öğrenin.
Entity Framework Core: Veritabanı İşlemlerinin Modern Yolu
Entity Framework Core ile veritabanı işlemlerini yönetin. Code-first, migrations ve performans optimizasyonu.
.NET Minimal APIs: Hafif ve Hızlı Endpoint'ler
.NET Minimal API ile hızlı ve hafif endpoint'ler oluşturun. Controller-free yaklaşım ve kullanım senaryoları.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç