Bir asp.net çekirdek webapi denetleyicisindeki istek gövdesi nasıl okunur?


115

OnActionExecutingYöntemdeki istek gövdesini okumaya çalışıyorum , ancak her zaman nullvücut için alıyorum .

var request = context.HttpContext.Request;
var stream = new StreamReader(request.Body);
var body = stream.ReadToEnd();

Akış konumunu açıkça 0 olarak ayarlamaya çalıştım, ancak bu da işe yaramadı. Bu ASP.NET Core olduğu için işler biraz farklı bence. Eski web API sürümlerine atıfta bulunan tüm örnekleri burada görebiliyorum.

Bunu yapmanın başka bir yolu var mı?


5
Dikkatli olun, istek hattı sırasında istek metni daha önce
Fabio


@Fabio Bilgi için teşekkürler, konumu belirleyip tekrar okuyabilir miyiz?
Kasun Koswattha

@KasunKoswattha - Tasarım gereği vücut içeriği, yalnızca bir kez okunabilen yalnızca ileri akış olarak değerlendirilir.
user270576

Sanırım soru, denetleyicilerden çok filtreleri veya ara yazılımları hedefliyor.
Jim Aho

Yanıtlar:


116

ASP.Net Core'da gövde talebini birkaç kez okumak karmaşık görünmektedir, ancak ilk denemeniz bunu doğru şekilde yaparsa, sonraki denemelerde sorun yaşamamalısınız.

Örneğin vücut akışını değiştirerek birkaç geri dönüş okudum, ancak aşağıdaki en temiz olanı düşünüyorum:

En önemli noktalar

  1. isteğin metnini iki veya daha fazla kez okuyacağınızı bilmesini sağlamak için,
  2. vücut akışını kapatmamak ve
  3. dahili işlemin kaybolmaması için onu başlangıç ​​konumuna geri sarmak.

[DÜZENLE]

Murad'ın belirttiği gibi, .Net Core 2.1 uzantısından da yararlanabilirsiniz: EnableBufferingBüyük istekleri bellekte tutmak yerine diskte depolar, bellekte depolanan büyük akış sorunlarını (dosyalar, görüntüler, ...) . ASPNETCORE_TEMPOrtam değişkenini ayarlayarak geçici klasörü değiştirebilirsiniz ve istek sona erdiğinde dosyalar silinir.

Bir AuthorizationFilter'da şunları yapabilirsiniz:

// Helper to enable request stream rewinds
using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableBodyRewind : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var bodyStr = "";
        var req = context.HttpContext.Request;

        // Allows using several time the stream in ASP.Net Core
        req.EnableRewind(); 

        // Arguments: Stream, Encoding, detect encoding, buffer size 
        // AND, the most important: keep stream opened
        using (StreamReader reader 
                  = new StreamReader(req.Body, Encoding.UTF8, true, 1024, true))
        {
            bodyStr = reader.ReadToEnd();
        }

        // Rewind, so the core is not lost when it looks the body for the request
        req.Body.Position = 0;

        // Do whatever work with bodyStr here

    }
}



public class SomeController : Controller
{
    [HttpPost("MyRoute")]
    [EnableBodyRewind]
    public IActionResult SomeAction([FromBody]MyPostModel model )
    {
        // play the body string again
    }
}

Ardından, istek işleyicisinde gövdeyi tekrar kullanabilirsiniz.

Sizin durumunuzda boş bir sonuç alırsanız, bu muhtemelen vücudun daha önceki bir aşamada okunduğu anlamına gelir. Bu durumda bir ara yazılım kullanmanız gerekebilir (aşağıya bakın).

Bununla birlikte, büyük akışları idare ediyorsanız dikkatli olun, bu davranış her şeyin belleğe yüklendiğini gösterir, dosya yükleme durumunda bu tetiklenmemelidir.

Bunu bir Ara Yazılım olarak kullanmak isteyebilirsiniz

Benimki şuna benziyor (yine, büyük dosyaları indirir / yüklerseniz, bellek sorunlarını önlemek için bu devre dışı bırakılmalıdır):

public sealed class BodyRewindMiddleware
{
    private readonly RequestDelegate _next;

    public BodyRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        try { context.Request.EnableRewind(); } catch { }
        await _next(context);
        // context.Request.Body.Dipose() might be added to release memory, not tested
    }
}
public static class BodyRewindExtensions
{
    public static IApplicationBuilder EnableRequestBodyRewind(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        return app.UseMiddleware<BodyRewindMiddleware>();
    }

}

Konum 0'a geri
sarılsam

2
Kullandın req.EnableRewind();mı Yukarıdaki kodu kullanıyorum ve iyi çalışıyor.
Jean

1
req.EnableRewind () kullandı; çalışmıyor. Konum = 0, vücut uzunluğu = 26 elde ediyorum, ancak 'gövde' akışını okurken boş bir dizge çıkıyor.
Adrian Nasui

Bu mükemmel cevap
Gayan

3
Ayrıca kullanmak da mümkündür request.EnableBuffering()(wrapper over EnableRewind()) ASP.NET Core 2.1 docs.microsoft.com/en-us/dotnet/api/…
Murad

30

Daha net bir çözüm, ASP.Net Core 2.1 / 3.1'de çalışır

Sınıfı filtrele

using Microsoft.AspNetCore.Authorization;
// For ASP.NET 2.1
using Microsoft.AspNetCore.Http.Internal;
// For ASP.NET 3.1
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;

public class ReadableBodyStreamAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // For ASP.NET 2.1
        // context.HttpContext.Request.EnableRewind();
        // For ASP.NET 3.1
        // context.HttpContext.Request.EnableBuffering();
    }
}

Bir Kontrolörde

[HttpPost]
[ReadableBodyStream]
public string SomePostMethod()
{
    //Note: if you're late and body has already been read, you may need this next line
    //Note2: if "Note" is true and Body was read using StreamReader too, then it may be necessary to set "leaveOpen: true" for that stream.
    HttpContext.Request.Body.Seek(0, SeekOrigin.Begin);

    using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
    {
        string body = stream.ReadToEnd();
        // body = "param=somevalue&param2=someothervalue"
    }
}

3
Netcore3.0 için,.EnableRewind()
mr5

Teşekkürler @ mr5 - Cevabım güncellendi
Andriod

Bunu EnableRewind () yolunu bozan bazı .net Core 2.2 -> Core 3.1 yükseltmelerini düzeltirken buldum. Sanırım bunun bir satır daha koda ihtiyacı var, bu olmadan Body'yi yeniden okuyamam: HttpContext.Request.Body.Seek (0, SeekOrigin.Begin);
Larry Smith

2
Bu yalnızca değiştirdikten sonra benim için çalıştı AuthorizeAttributeüzere Attribute(3.1 ASP.Net Çekirdek olarak).
Sigmund

Çocuklar lütfen belirtilen kitaplıkları eklediğinizden emin olun. Koda zaten sahiptim ama Microsoft.AspNetCore.Http referansının eksik olduğunu fark edene kadar EnableBuffering kırmızı dalgalı çizgi gösteriyordu. Android'e teşekkürler!
kaarthick raman

18

Talep gövdesini geri sarabilmek için @ Jean'ın cevabı, iyi işleyen bir çözüm bulmama yardımcı oldu. Bunu şu anda Global Exception Handler Middleware için kullanıyorum ancak prensip aynı.

Temel olarak istek gövdesinde (bir dekoratör yerine) geri sarmayı sağlayan bir ara yazılım oluşturdum.

using Microsoft.AspNetCore.Http.Internal;
[...]
public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

public static class EnableRequestRewindExtension
{
    public static IApplicationBuilder UseEnableRequestRewind(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<EnableRequestRewindMiddleware>();
    }
}

Bu daha sonra beğenilerinizde kullanılabilir Startup.cs:

[...]
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    [...]
    app.UseEnableRequestRewind();
    [...]
}

Bu yaklaşımı kullanarak, istek gövde akışını başarılı bir şekilde geri sarabildim.


1
Bu benim için harika çalıştı @SaoBiz teşekkür ederim! bir yazım hatası, değişim 2 bu kadar oluşturucu içinde UseEnableRequestRewind(this IApplicationBuilder builder).
Richard Logwood

@RichardLogwood Yardımcı olduğuna sevindim! Yazım hatasını bulduğunuz için teşekkürler! Sabit. :)
SaoBiz

8

Bu biraz eski bir konu, ama buraya geldiğimden beri, bulgularımı başkalarına yardımcı olabilmeleri için göndereceğimi düşündüm.

İlk olarak, Request.Body'yi almak ve bununla bir şeyler yapmak istediğimde aynı sorunu yaşadım (kayıt / denetleme). Ama aksi halde uç noktanın aynı görünmesini istedim.

Dolayısıyla, EnableBuffering () çağrısı hile yapabilir gibi görünüyordu. Daha sonra gövde üzerinde bir Arama (0, xxx) yapabilir ve içeriği vb. Yeniden okuyabilirsiniz.

Ancak bu, bir sonraki sorunuma yol açtı. Uç noktaya erişirken "Eşzamanlı işlemlere izin verilmiyor" istisnaları alıyorum. Dolayısıyla, çözüm, seçeneklerde AllowSynchronousIO = true özelliğini ayarlamaktır. Bunu gerçekleştirmenin bir çok yolu vardır (ancak burada detaylandırmak önemli değildir ..)

O ZAMAN, bir sonraki sorun, Request.Body'yi okumaya gittiğimde zaten elden çıkarılmış olmasıdır. Ugh. Peki ne verir?

Newtonsoft.JSON'u endpiont çağrısında [FromBody] ayrıştırıcım olarak kullanıyorum. Eşzamanlı okumalardan sorumlu olan budur ve tamamlandığında akışı da kapatır. Çözüm? JSON ayrıştırmasına geçmeden önce akışı okuyun mu? Elbette, işe yarıyor ve sonunda şunu elde ettim:

 /// <summary>
/// quick and dirty middleware that enables buffering the request body
/// </summary>
/// <remarks>
/// this allows us to re-read the request body's inputstream so that we can capture the original request as is
/// </remarks>
public class ReadRequestBodyIntoItemsAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context == null) return;

        // NEW! enable sync IO beacuse the JSON reader apparently doesn't use async and it throws an exception otherwise
        var syncIOFeature = context.HttpContext.Features.Get<IHttpBodyControlFeature>();
        if (syncIOFeature != null)
        {
            syncIOFeature.AllowSynchronousIO = true;

            var req = context.HttpContext.Request;

            req.EnableBuffering();

            // read the body here as a workarond for the JSON parser disposing the stream
            if (req.Body.CanSeek)
            {
                req.Body.Seek(0, SeekOrigin.Begin);

                // if body (stream) can seek, we can read the body to a string for logging purposes
                using (var reader = new StreamReader(
                     req.Body,
                     encoding: Encoding.UTF8,
                     detectEncodingFromByteOrderMarks: false,
                     bufferSize: 8192,
                     leaveOpen: true))
                {
                    var jsonString = reader.ReadToEnd();

                    // store into the HTTP context Items["request_body"]
                    context.HttpContext.Items.Add("request_body", jsonString);
                }

                // go back to beginning so json reader get's the whole thing
                req.Body.Seek(0, SeekOrigin.Begin);
            }
        }
    }
}

Artık gövdeye, [ReadRequestBodyIntoItems] özniteliğine sahip uç noktalarda HttpContext.Items ["request_body"] kullanarak erişebilirim.

Ama adamım, bu atlamak için çok fazla çember gibi görünüyor. İşte burada bitirdim ve bundan gerçekten memnunum.

Uç noktam şöyle başladı:

[HttpPost("")]
[ReadRequestBodyIntoItems]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData([FromBody] MyJsonObjectType value)
{
    val bodyString = HttpContext.Items["request_body"];
    // use the body, process the stuff...
}

Ancak imzayı değiştirmek çok daha kolaydır, örneğin:

[HttpPost("")]
[Consumes("application/json")]
public async Task<IActionResult> ReceiveSomeData()
{
    using (var reader = new StreamReader(
           Request.Body,
           encoding: Encoding.UTF8,
           detectEncodingFromByteOrderMarks: false
    ))
    {
        var bodyString = await reader.ReadToEndAsync();

        var value = JsonConvert.DeserializeObject<MyJsonObjectType>(bodyString);

        // use the body, process the stuff...
    }
}

Bunu gerçekten beğendim çünkü vücut akışını yalnızca bir kez okuyor ve seriyi kaldırma işleminin kontrolü bende. Elbette, ASP.NET çekirdeğinin bu sihri benim için yapması güzel, ancak burada akışı iki kez okumakla (belki her seferinde ara belleğe alarak) zaman kaybetmiyorum ve kod oldukça açık ve temiz.

Bu işlevselliğe birçok uç noktada ihtiyacınız varsa, ara yazılım yaklaşımları daha temiz olabilir veya en azından gövde ayıklamayı kodu daha kısa hale getirmek için bir uzantı işlevine kapsülleyebilirsiniz.

Her neyse, bu konunun 3 yönüne de değinen herhangi bir kaynak bulamadım, dolayısıyla bu yazı. Umarım bu birine yardımcı olur!

BTW: Bu, ASP .NET Core 3.1 kullanıyordu.


Program JSON dizesini NyObjectType olarak ayrıştıramazsa, "request_body" den değer okuyamıyorum
Ericyu67

3

ASP.NET Core 2.1 kullanırken benzer bir sorun yaşadım:

  • POST ile gönderilen verileri okumak ve bunlara karşı bazı güvenlik kontrolleri gerçekleştirmek için özel bir ara yazılım gerekiyor
  • Bir yetkilendirme filtresi kullanmak, etkilenen çok sayıda eylem nedeniyle pratik değildir
  • Eylemlerde nesnelerin bağlanmasına izin vermeliyim ([FromBody] someObject). SaoBizBu çözüme işaret ettiğiniz için teşekkürler .

Bu nedenle, bariz çözüm, isteğin geri sarılabilir olmasına izin vermektir, ancak gövdeyi okuduktan sonra bağlamanın hala çalıştığından emin olun.

EnableRequestRewindMiddleware

public class EnableRequestRewindMiddleware
{
    private readonly RequestDelegate _next;

    ///<inheritdoc/>
    public EnableRequestRewindMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public async Task Invoke(HttpContext context)
    {
        context.Request.EnableRewind();
        await _next(context);
    }
}

Startup.cs

(bunu Yapılandırma yönteminin başına yerleştirin)

app.UseMiddleware<EnableRequestRewindMiddleware>();

Başka bir ara katman yazılımı

Bu, öğeleri kontrol etmek için POST ile gönderilen bilgilerin paketinden çıkarılmasını gerektiren ara yazılımın bir parçasıdır.

using (var stream = new MemoryStream())
{
    // make sure that body is read from the beginning
    context.Request.Body.Seek(0, SeekOrigin.Begin);
    context.Request.Body.CopyTo(stream);
    string requestBody = Encoding.UTF8.GetString(stream.ToArray());

    // this is required, otherwise model binding will return null
    context.Request.Body.Seek(0, SeekOrigin.Begin);
}

3

Son zamanlarda, yapısı hakkında hiçbir fikrinizin olmadığı, rastgele JSON alan çok zarif bir çözümle karşılaştım:

    [HttpPost]
    public JsonResult Test([FromBody] JsonElement json)
    {
        return Json(json);
    }

Sadece bu kadar kolay.


Teşekkür ederim, gerçekten işe yarıyor. Ben kullanılan GetRawText()JsonElement metodunu ve benim JSON metnini aldı.
mrNone

DTO, siz onu yarattığınızda, varsayılan değerleri ayarlamak gibi bazı işlemler yaparsa, bu size gerçek istek gövdesini vermeyecektir.
Ebram Shehata

3

.NET Core 3.1'de yanıt arabelleği eklemenin hızlı bir yolu

    app.Use((context, next) =>
    {
        context.Request.EnableBuffering();
        return next();
    });

Startup.cs içinde. Bunun ayrıca akış okunmadan önce arabelleğe almanın etkinleştirileceğini garanti ettiğini buldum, ki bu da gördüğüm diğer ara yazılım / yetkilendirme filtresi yanıtlarından bazılarıyla.

Ardından, HttpContext.Request.Bodydiğerlerinin önerdiği gibi , istek metninizi işleyicinizde okuyabilirsiniz .

Ayrıca, EnableBufferinggeçici bir dosya kullanmadan önce bellekte ne kadar arabelleğe alınacağını sınırlamanıza izin veren aşırı yüklemelere ve ayrıca arabelleğiniz için genel bir sınıra sahip olması da dikkate alınmalıdır . NB, bir istek bu sınırı aşarsa bir istisna atılır ve istek hiçbir zaman işleyicinize ulaşmaz.


Bu benim için harika çalıştı (3.1). Sizden farklı bir soru
aktardı

3.1'de çalıştı. Ayrıca, bunu kullanacak kullanıcılar için bir hatırlatma: Startup.cs içinde doğru sıraya yerleştirdiğinizden emin olun.
Ebram Shehata

2

okumak için Body, zaman uyumsuz okuma olabilir.

asyncaşağıdaki gibi bir yöntem kullanın :

public async Task<IActionResult> GetBody()
{
      string body="";
      using (StreamReader stream = new StreamReader(Request.Body))
      {
           body = await stream.ReadToEndAsync();
      }
    return Json(body);
}

Postacı ile test edin:

görüntü açıklamasını buraya girin

İyi çalışıyor ve Asp.net coresürümde test edildi 2.0 , 2.1 , 2.2, 3.0.

Umarım faydalıdır.


1

IHttpContextAccessorBu yolu gitmek isterseniz yöntem çalışır.

TLDR;

  • Enjekte IHttpContextAccessor

  • Geri sar - HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);

  • Oku - System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body); JObject asObj = JObject.Parse(sr.ReadToEnd());

Daha fazla - Kullanılabilir bir duruma ulaşmak için yerinde olduğundan emin olmanız gereken öğelerin kısa, derlenmemiş bir örneğini deneme girişimi IHttpContextAccessor. Cevaplar, istek metnini okumaya çalıştığınızda başlangıca geri gitmeniz gerekeceğini doğru bir şekilde gösterdi. CanSeek, Positionİstek gövdesi üzerinde özellikleri bu doğrulanması açısından yararlıdır akışı.

.NET Core DI Belgeleri

// First -- Make the accessor DI available
//
// Add an IHttpContextAccessor to your ConfigureServices method, found by default
// in your Startup.cs file:
// Extraneous junk removed for some brevity:
public void ConfigureServices(IServiceCollection services)
{
    // Typical items found in ConfigureServices:
    services.AddMvc(config => { config.Filters.Add(typeof(ExceptionFilterAttribute)); });
    // ...

    // Add or ensure that an IHttpContextAccessor is available within your Dependency Injection container
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

// Second -- Inject the accessor
//
// Elsewhere in the constructor of a class in which you want
// to access the incoming Http request, typically 
// in a controller class of yours:
public class MyResourceController : Controller
{
    public ILogger<PricesController> Logger { get; }
    public IHttpContextAccessor HttpContextAccessor { get; }

    public CommandController(
        ILogger<CommandController> logger,
        IHttpContextAccessor httpContextAccessor)
    {
        Logger = logger;
        HttpContextAccessor = httpContextAccessor;
    }

    // ...

    // Lastly -- a typical use 
    [Route("command/resource-a/{id}")]
    [HttpPut]
    public ObjectResult PutUpdate([FromRoute] string id, [FromBody] ModelObject requestModel)
    {
        if (HttpContextAccessor.HttpContext.Request.Body.CanSeek)
        {
            HttpContextAccessor.HttpContext.Request.Body.Seek(0, System.IO.SeekOrigin.Begin);
            System.IO.StreamReader sr = new System.IO.StreamReader(HttpContextAccessor.HttpContext.Request.Body);
            JObject asObj = JObject.Parse(sr.ReadToEnd());

            var keyVal = asObj.ContainsKey("key-a");
        }
    }
}    

1

Bunun gibi bir asp.net core 3.1 uygulamasında istek gövdesini okuyabildim (arabelleğe almayı etkinleştiren basit bir ara yazılımla birlikte - etkinleştirilebilir geri sarma önceki .Net Core sürümleri için çalışıyor gibi görünüyor -):

var reader = await Request.BodyReader.ReadAsync();
Request.Body.Position = 0;
var buffer = reader.Buffer;
var body = Encoding.UTF8.GetString(buffer.FirstSpan);
Request.Body.Position = 0;

0

Ayrıca Request.Body'yi bazı eylem parametresi modellerine otomatik olarak eşlemeden okumak istedim. Bunu çözmeden önce birçok farklı yol denedim. Ve burada açıklanan çalışan bir çözüm bulamadım. Bu çözüm şu anda .NET Core 3.0 çerçevesine dayanmaktadır.

reader.readToEnd () basit bir şekilde birleştirildi, derlenmiş olmasına rağmen, eşzamansız çağrı kullanmamı gerektiren bir çalışma zamanı istisnası attı. Bunun yerine ReadToEndAsync () kullandım, ancak bazen çalıştı, bazen işe yaramadı. Akış kapatıldıktan sonra okunamıyor gibi hatalar veriyor. Sorun şu ki, sonucu aynı iş parçacığında döndüreceğini garanti edemiyoruz (await'i kullansak bile). Yani bir tür geri aranmaya ihtiyacımız var. Bu çözüm benim için çalıştı.

[Route("[controller]/[action]")]
public class MyController : ControllerBase
{

    // ...

    [HttpPost]
    public async void TheAction()
    {
        try
        {
            HttpContext.Request.EnableBuffering();
            Request.Body.Position = 0;
            using (StreamReader stream = new StreamReader(HttpContext.Request.Body))
            {
                var task = stream
                    .ReadToEndAsync()
                    .ContinueWith(t => {
                        var res = t.Result;
                        // TODO: Handle the post result!
                    });

                // await processing of the result
                task.Wait();
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to handle post!");
        }
    }

0

Bunu yapmanın en basit yolu şudur:

  1. Denetleyici yönteminde gövdeyi çıkarmanız gerekir, şu parametreyi ekleyin: [FromBody] SomeClass değeri

  2. "SomeClass" ı şu şekilde bildirin: class SomeClass {public string SomeParameter {get; Ayarlamak; }}

Ham gövde json olarak gönderildiğinde .net çekirdeği onu nasıl okuyacağını çok kolay bilir.


0

Sadece istekten içeriği (istek gövdesi) almak isteyenler için:

[FromBody]Denetleyici yöntemi parametrenizdeki özniteliği kullanın .

[Route("api/mytest")]
[ApiController]
public class MyTestController : Controller
{
    [HttpPost]
    [Route("content")]
    public async Task<string> ReceiveContent([FromBody] string content)
    {
        // Do work with content
    }
}

Dokümanın dediği gibi: bu öznitelik, bir parametrenin veya özelliğin istek gövdesi kullanılarak bağlanması gerektiğini belirtir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.