Kodunuz ağ veri isteklerini, veritabanı erişimini veya dosya sistemi okuma/yazmalarını desteklemek için G/Ç'ye bağlı senaryolar uyguluyorsa, zaman uyumsuz programlama en iyi yaklaşımdır. Ayrıca, pahalı hesaplamalar gibi CPU'ya bağlı senaryolar için zaman uyumsuz kod da yazabilirsiniz.
C#, geri çağrılarla uğraşmak veya asenkronizmi destekleyen bir kütüphaneye uymak zorunda kalmadan kolayca asenkron kod yazmanızı sağlayan dil düzeyinde bir asenkron programlama modeline sahiptir. Model, Görev tabanlı zaman uyumsuz desen (TAP)olarak bilinen modeli izler.
Zaman uyumsuz programlama modelini keşfetme
Task ve Task<T> nesneleri zaman uyumsuz programlamanın çekirdeğini temsil eder. Bu nesneler, async ve await anahtar sözcükleri destekleyerek zaman uyumsuz işlemleri modellemek için kullanılır. Çoğu durumda model hem G/Ç hem de CPU'ya bağlı senaryolar için oldukça basittir. Bir async yönteminin içinde:
G/Ç ile ilişkili kod, async yöntemindeki bir Task veya Task<T> nesnesiyle temsil edilen bir işlemi başlatır.
CPU'ya bağlı kodTask.Run yöntemiyle bir arka plan iş parçacığı üzerinde bir işlem başlatır.
Her iki durumda da etkin bir Task, tamamlanmamış olabilir bir zaman uyumsuz işlemi temsil eder.
Anahtar await sözcük, sihrin gerçekleştiği yerdir.
await ifadesini içeren yöntemi çağırana denetim verir ve sonuçta kullanıcı arabiriminin yanıt vermesine veya bir hizmetin esnek olmasına izin verir.
async ve await ifadelerini kullanmak dışında zaman uyumsuz koda yaklaşmanın yolları olsa da, bu makale dil düzeyindeki yapılara odaklanır.
Not
Bu makalede sunulan bazı örnekler, web hizmetinden veri indirmek için System.Net.Http.HttpClient sınıfını kullanır. Örnek kodda, s_httpClient nesnesi Program sınıfı türünde statik bir alandır:
Daha fazla bilgi için bu makalenin sonundaki tam örnek kod bakın.
Temel kavramları gözden geçirme
C# kodunuzda zaman uyumsuz programlama uyguladığınızda, derleyici programınızı bir durum makinesine dönüştürür. Bu yapı, kod bir await ifadeye ulaştığında yürütmeyi duraklatma ve arka plan işi tamamlandığında yürütmeye devam etme gibi kodunuzda çeşitli işlemleri ve durumu takip eder.
Bilgisayar bilimi teorisi açısından, zaman uyumsuz programlama, Promise modelininzaman uyumsuzluk içerisindeki bir uygulamasıdır.
Zaman uyumsuz programlama modelinde, anlaşılması gereken birkaç temel kavram vardır:
Hem G/Ç hem de CPU'ya bağlı kod için zaman uyumsuz kod kullanabilirsiniz, ancak uygulama farklıdır.
Zaman uyumsuz kod, arka planda çalışmayı modellemek için Task<T> ve Task nesnelerini yapılar olarak kullanır.
async anahtar sözcüğü, yöntem gövdesinde await anahtar sözcüğünü kullanmanıza olanak tanıyan bir yöntemi zaman uyumsuz bir yöntem olarak bildirir.
await anahtar sözcüğünü uyguladığınızda kod çağırma yöntemini askıya alır ve görev tamamlanana kadar denetimi çağırana geri verir.
await ifadesini yalnızca zaman uyumsuz bir yöntemde kullanabilirsiniz.
G/Ç ile sınırlı örnek: Web hizmetinden veri indirme
Bu örnekte, kullanıcı bir düğme seçtiğinde uygulama bir web hizmetinden veri indirir. İndirme işlemi sırasında uygulamanın kullanıcı arabirimi iş parçacığını engellemek istemezsiniz. Aşağıdaki kod bu görevi gerçekleştirir:
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
Kod, Task nesneleriyle etkileşimde karmaşıklaşmadan amacı (verileri zaman uyumsuz olarak indirme) ifade eder.
CPU'ya bağlı örnek: Oyun hesaplaması çalıştırma
Sonraki örnekte, bir mobil oyun, bir düğme olayına yanıt olarak ekrandaki birkaç karaktere hasar verir. Hasar hesaplaması yapmak pahalı olabilir. Kullanıcı arabirimi iş parçacığında hesaplamanın çalıştırılması, hesaplama sırasında görüntüleme ve kullanıcı arabirimi etkileşimi sorunlarına neden olabilir.
Görevi işlemenin en iyi yolu, çalışmayı Task.Run yöntemiyle tamamlamak için bir arka plan işlem başlatmaktır. İşlem, bir await ifadesi kullanılarak sonuç verir. Görev tamamlandığında işlem sürdürülür. Bu yaklaşım, iş arka planda tamamlarken kullanıcı arabiriminin sorunsuz çalışmasını sağlar.
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
Kod, düğme Clicked olayının amacını açıkça ifade eder. Arka plan iş parçacığını manuel olarak yönetmek gerekmez ve görevi engellemeyen bir biçimde tamamlar.
CPU bağlı ve Girdi/Çıktı bağlı senaryoları tanıma
Önceki örneklerde G/Ç ve CPU'ya bağlı işler için async değiştirici ve await ifadesinin nasıl kullanılacağı gösterilmektedir. Her senaryo için bir örnek, işlemin bağlı olduğu yere göre kodun nasıl farklı olduğunu gösterir. Uygulamanıza hazırlanmak için bir işlemin G/Ç veya CPU'ya bağlı olduğunu belirlemeyi anlamanız gerekir. Uygulama seçiminiz kodunuzun performansını büyük ölçüde etkileyebilir ve olası olarak yapıların yanlış kullanılıp kullanılmamasına neden olabilir.
Kod yazmadan önce ele alınması gereken iki birincil soru vardır:
Soru
Senaryo
Uygulama
Kod, veritabanındaki veriler gibi bir sonucu veya eylemi beklemeli mi?
G/Ç bağlı
async değiştiriciyi ve await ifade 'yi Task.Run yöntemini olmadan kullanın.
Görev Paralel Kitaplığı'nı kullanmaktan kaçının.
Kod pahalı bir hesaplama çalıştırmalıdır?
CPU'ya bağlı
async değiştiricisini ve await ifadesini kullanın, ancak işi Task.Run yöntemiyle başka bir iş parçacığına aktarın. Bu yaklaşım, CPU yanıt hızıyla ilgili endişeleri ele alır.
Çalışma eşzamanlılık ve paralellik için uygunsa, Görev Paralel Kitaplığı'nı da kullanmayı göz önünde bulundurun.
Kodunuzun yürütülmesini her zaman ölçün. Çok iş parçacıklı çalışma sırasında, bağlam değişimlerinin maliyetine kıyasla CPU'ya bağlı çalışmanızın yeterince maliyetli olmadığını fark edebilirsiniz. Her seçimin dezavantajları vardır. Durumunuz için doğru dengeyi seçin.
Diğer örnekleri keşfedin
Bu bölümdeki örneklerde, C# dilinde zaman uyumsuz kod yazmanın çeşitli yolları gösterilmektedir. Karşılaşabileceğiniz birkaç senaryo ele alınıyor.
Ağdan veri ayıklama
Aşağıdaki kod, belirli bir URL'den HTML'yi indirir ve ".NET" dizesinin HTML'de kaç kez gerçekleştiğini sayar. Kod, görevi gerçekleştiren ve sayıyı döndüren bir Web API denetleyicisi yöntemi tanımlamak için ASP.NET kullanır.
Not
Üretim kodunda HTML ayrıştırma yapmayı planlıyorsanız normal ifadeleri kullanmayın. Bunun yerine ayrıştırma kütüphanesi kullanın.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
Evrensel Windows Uygulaması için benzer kod yazabilir ve bir düğmeye bastıktan sonra sayma görevini gerçekleştirebilirsiniz:
private readonly HttpClient _httpClient = new HttpClient();
private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
// Capture the task handle here so we can await the background task later.
var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");
// Any other work on the UI thread can be done here, such as enabling a Progress Bar.
// It's important to do the extra work here before the "await" call,
// so the user sees the progress bar before execution of this method is yielded.
NetworkProgressBar.IsEnabled = true;
NetworkProgressBar.Visibility = Visibility.Visible;
// The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
// This action is what allows the app to be responsive and not block the UI thread.
var html = await getDotNetFoundationHtmlTask;
int count = Regex.Matches(html, @"\.NET").Count;
DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";
NetworkProgressBar.IsEnabled = false;
NetworkProgressBar.Visibility = Visibility.Collapsed;
}
Birden çok görev tamamlanmasını bekleyin
Bazı senaryolarda kodun birden çok veri parçasını eşzamanlı olarak alması gerekir.
Task API'leri, birden çok arka plan işi üzerinde engellemesiz bekleme gerçekleştiren zaman uyumsuz kod yazmanızı sağlayan yöntemler sağlar:
Aşağıdaki örnekte, bir dizi userId nesnesi için User nesne verilerini nasıl alabileceğiniz gösterilmektedir.
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
LINQ kullanarak daha az kod yazmanıza rağmen LINQ'yi zaman uyumsuz kodla karıştırırken dikkatli olun. LINQ ertelenen (veya gecikmeli) yürütmeyi kullanır. Zaman uyumsuz çağrılar, .ToList() veya .ToArray() yöntemine yapılan bir çağrıyla oluşturulan sırayı yinelemeye zorlamadığınız takdirde, foreach döngüsünde olduğu gibi hemen gerçekleşmez. Bu örnekte sorguyu hevesle gerçekleştirmek ve sonuçları bir dizide depolamak için Enumerable.ToArray yöntemi kullanılır. Bu yaklaşım, id => GetUserAsync(id) deyimini çalıştırıp görevi başlatmaya zorlar.
Zaman uyumsuz programlama için dikkat edilmesi gereken noktaları gözden geçirin
Zaman uyumsuz programlamada, beklenmeyen davranışları önleyebilecek birkaç ayrıntı göz önünde bulundurulması gerekir.
async() yöntemi gövdesi içinde await komutunu kullanın
async değiştiricisini kullandığınızda, yöntem gövdesine bir veya daha fazla await ifadesi eklemeniz gerekir. Derleyici bir await ifadesiyle karşılaşmazsa, yöntem sonuç vermez. Derleyici bir uyarı oluştursa da, kod yine de derlenir ve derleyici yöntemini çalıştırır. C# derleyicisi tarafından zaman uyumsuz yöntem için oluşturulan durum makinesi hiçbir şey başarmadığından tüm işlem son derece verimsizdir.
Asenkron yöntem adlarına "Async" son ekini ekleyin.
.NET stil kuralı, tüm zaman uyumsuz yöntem adlarına "Async" sonekini eklemektir. Bu yaklaşım, zaman uyumlu ve zaman uyumsuz yöntemler arasında daha kolay ayrım yapmaya yardımcı olur. Kodunuz tarafından açıkça çağrılmamış bazı yöntemler (olay işleyicileri veya web denetleyicisi yöntemleri gibi) bu senaryoda geçerli olmayabilir. Bu öğeler kodunuz tarafından açıkça çağrılmadığından, açık adlandırma kullanmak o kadar önemli değildir.
Yalnızca olay işleyicilerinden 'async void' döndür
Olay işleyicilerinin void dönüş türleri bildirmesi gerekir ve diğer yöntemler gibi Task ve Task<T> nesneleri kullanamaz veya döndüremez. Zaman uyumsuz olay işleyicileri yazarken, işleyiciler için void dönüş yönteminde async değiştiricisini kullanmanız gerekir.
async void dönüş yöntemlerinin diğer uygulamaları TAP modelini izlemez ve zorluklar sunabilir:
async void yöntemi içinde oluşan istisnalar bu yöntemin dışında yakalanamaz
async void yöntemleri test etmek zordur
async void yöntemleri, çağıran bunların eşzamansız olmasını beklemiyorsa negatif yan etkilere neden olabilir.
LINQ'te zaman uyumsuz lambdalarla dikkatli olun
LINQ ifadelerinde zaman uyumsuz lambdalar uygularken dikkatli olmanız önemlidir. LINQ'teki Lambda ifadeleri ertelenen yürütmeyi kullanır, bu da kodun beklenmeyen bir zamanda yürütülebileceği anlamına gelir. Bu senaryoya engelleyici görevlerin eklenmesi, kod doğru yazılmıyorsa kolayca kilitlenmeye neden olabilir. Ayrıca, zaman uyumsuz kodun iç içe yerleştirilmiş olması da kodun yürütülmesiyle ilgili mantık yürütmeyi zorlaştırabilir. Asenkron ve LINQ güçlüdür, ancak bu teknikler mümkün olduğunca dikkatli ve açık şekilde birlikte kullanılmalıdır.
Bloklamadan görevler için kontrolü devretmek
Programınız bir görevin sonucuna ihtiyaç duyuyorsa, await ifadesini engelleyici olmayan bir şekilde uygulayan kod yazın. Geçerli iş parçacığını bir Task öğesinin tamamlanmasını senkron olarak beklemek için engellemek, kilitlenmelere ve engellenen bağlam iş parçacıklarına neden olabilir. Bu programlama yaklaşımı daha karmaşık hata işleme gerektirebilir. Aşağıdaki tabloda, erişimin engelleyici olmayan bir yolla görevlerden nasıl sonuç alınıyor olduğu konusunda rehberlik sağlanmaktadır:
Görev senaryosu
Geçerli kod
'await' ile değiştirin
Arka plan görev sonucunu alma
Task.Wait veya Task.Result
await
Herhangi bir görev tamamlandığında devam et
Task.WaitAny
await Task.WhenAny
Tüm görevler tamamlandığında devam
Task.WaitAll
await Task.WhenAll
Bir süre sonra devam
Thread.Sleep
await Task.Delay
ValueTask türünü kullanmayı göz önünde bulundurun
Zaman uyumsuz bir yöntem Task nesnesi döndürdüğünde, belirli yollarda performans sorunları ortaya çıkabilir.
Task bir başvuru türü olduğundan, yığından bir Task nesnesi ayrılır.
async değiştiricisi ile bildirilen bir yöntem önbelleğe alınmış bir sonuç döndürürse veya zaman uyumlu olarak tamamlanırsa, ek ayırmalar kodun performans açısından kritik bölümlerinde önemli zaman maliyetleri tahakkuk edebilir. Ayırmalar sıkı döngülerde gerçekleştiğinde bu senaryo maliyetli hale gelebilir. Daha fazla bilgi için genelleştirilmiş zaman uyumsuz dönüş türleri bölümüne bakın.
ConfigureAwait(false) özelliğinin ne zaman ayarlandığını anlama
Geliştiriciler genellikle Task.ConfigureAwait(Boolean) boole değerinin ne zaman kullanılacağını sorgular. Bu API, Task örneğinin herhangi bir await ifadesini uygulayan durum makinesine bağlamı yapılandırmasına olanak tanır. Boole değeri doğru ayarlanmadığında performans düşebilir veya kilitlenmeler oluşabilir. Daha fazla bilgi için bkz. ConfigureAwait SSS.
Daha az durum odaklı kod yaz
Genel nesnelerin durumuna veya belirli yöntemlerin yürütülmesine bağlı kod yazmaktan kaçının. Bunun yerine, yalnızca yöntemlerin dönüş değerlerine bağlıdır. Daha az durumsal olan kod yazmanın birçok avantajı vardır.
Kod hakkında daha kolay mantık yürütme
Kodu test etmek daha kolay
Eşzamansız ve eşzamanlı kodu karıştırmak daha kolay
Kodda yarış durumlarından kaçınmak mümkün
Dönüş değerlerine bağlı zaman uyumsuz kodu koordine etmek basit
(Bonus) Koda bağımlılık ekleme ile iyi çalışır
Önerilen bir hedef, kodunuzda eksiksiz veya neredeyse eksiksiz Referans Şeffaflığı sağlamaktır. Bu yaklaşım tahmin edilebilir, test edilebilir ve sürdürülebilir bir kod tabanına neden olur.
Tüm örneği gözden geçirin
Aşağıdaki kod, Program.cs örnek dosyasında bulunan tam örneği temsil eder.
using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;
class Button
{
public Func<object, object, Task>? Clicked
{
get;
internal set;
}
}
class DamageResult
{
public int Damage
{
get { return 0; }
}
}
class User
{
public bool isEnabled
{
get;
set;
}
public int id
{
get;
set;
}
}
public class Program
{
private static readonly Button s_downloadButton = new();
private static readonly Button s_calculateButton = new();
private static readonly HttpClient s_httpClient = new();
private static readonly IEnumerable<string> s_urlList = new string[]
{
"https://learn.microsoft.com",
"https://learn.microsoft.com/aspnet/core",
"https://learn.microsoft.com/azure",
"https://learn.microsoft.com/azure/devops",
"https://learn.microsoft.com/dotnet",
"https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
"https://learn.microsoft.com/education",
"https://learn.microsoft.com/shows/net-core-101/what-is-net",
"https://learn.microsoft.com/enterprise-mobility-security",
"https://learn.microsoft.com/gaming",
"https://learn.microsoft.com/graph",
"https://learn.microsoft.com/microsoft-365",
"https://learn.microsoft.com/office",
"https://learn.microsoft.com/powershell",
"https://learn.microsoft.com/sql",
"https://learn.microsoft.com/surface",
"https://dotnetfoundation.org",
"https://learn.microsoft.com/visualstudio",
"https://learn.microsoft.com/windows",
"https://learn.microsoft.com/maui"
};
private static void Calculate()
{
// <PerformGameCalculation>
static DamageResult CalculateDamageDone()
{
return new DamageResult()
{
// Code omitted:
//
// Does an expensive calculation and returns
// the result of that calculation.
};
}
s_calculateButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI while CalculateDamageDone()
// performs its work. The UI thread is free to perform other work.
var damageResult = await Task.Run(() => CalculateDamageDone());
DisplayDamage(damageResult);
};
// </PerformGameCalculation>
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
// <UnblockingDownload>
s_downloadButton.Clicked += async (o, e) =>
{
// This line will yield control to the UI as the request
// from the web service is happening.
//
// The UI thread is now free to perform other work.
var stringData = await s_httpClient.GetStringAsync(URL);
DoSomethingWithData(stringData);
};
// </UnblockingDownload>
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
// <GetUsersForDataset>
private static async Task<User> GetUserAsync(int userId)
{
// Code omitted:
//
// Given a user Id {userId}, retrieves a User object corresponding
// to the entry in the database with {userId} as its Id.
return await Task.FromResult(new User() { id = userId });
}
private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
var getUserTasks = new List<Task<User>>();
foreach (int userId in userIds)
{
getUserTasks.Add(GetUserAsync(userId));
}
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDataset>
// <GetUsersForDatasetByLINQ>
private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
// </GetUsersForDatasetByLINQ>
// <ExtractDataFromNetwork>
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
// Suspends GetDotNetCount() to allow the caller (the web server)
// to accept another request, rather than blocking on this one.
var html = await s_httpClient.GetStringAsync(URL);
return Regex.Matches(html, @"\.NET").Count;
}
// </ExtractDataFromNetwork>
static async Task Main()
{
Console.WriteLine("Application started.");
Console.WriteLine("Counting '.NET' phrase in websites...");
int total = 0;
foreach (string url in s_urlList)
{
var result = await GetDotNetCount(url);
Console.WriteLine($"{url}: {result}");
total += result;
}
Console.WriteLine("Total: " + total);
Console.WriteLine("Retrieving User objects with list of IDs...");
IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
var users = await GetUsersAsync(ids);
foreach (User? user in users)
{
Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
}
Console.WriteLine("Application ending.");
}
}
// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.
İlgili bağlantılar
Görev zaman uyumsuz programlama modeli (C#)
GitHub'da bizimle işbirliği yapın
Bu içeriğin kaynağı GitHub'da bulunabilir; burada ayrıca sorunları ve çekme isteklerini oluşturup gözden geçirebilirsiniz. Daha fazla bilgi için katkıda bulunan kılavuzumuzu inceleyin.
.NET geri bildirimi
.NET, açık kaynak bir projedir. Geri bildirim sağlamak için bir bağlantı seçin:
Azure İşlevleri oluşturmak, web uygulamalarını uygulamak ve yönetmek, Azure depolamayı kullanan çözümler geliştirmek ve daha fazlasını yapmak için Microsoft Azure'da uçtan uca çözümler oluşturun.