daron yöndem | Microsoft Regional Director | Silverlight MVP
Microsoft Regional Director | Nokia Developer Champion | Azure MVP
Bu yazıyı yazmam 68 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 3 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Daha önceki Azure Functions ve Webhooks konusundaki yazıda Github'ı örnek olarak kullanmıştık. Fakat aslına bakarsanız Azure Functions ile beraber Github ve Slack için hazır entegrasyon yapıları geliyor. Örneğin yine Azure Functions ve Webhooks yazısından hatırlarsanız Github implementasyonunda Secret'ı kullanmamış ve o kısmı size bırakmıştım. Onun yerine biz URL üzerinden giden Function based Authentication kullanmıştık. Oysa Secret kullansaydık buna da gerek kalmayacaktı :) Peki neden o yazıyı öyle yazdın? derseniz :) ben konuyu daha genel bir yapıda anlatmak istedim. Bu yazıda ise Azure Functions için httpTrigger'lardaki webHookType özelliğine bakacağız.

httpTrigger'da webHookType

Azure Functions'da trigger olarak httpTrigger kullanırken webHookType denilen bir özelliği de opsyonel olarak tanımlayabiliyorsunuz. Bu özelliğin alabildiği şu an için üç değer var;

  • genericJson : Bu seçenekte aslında tek yapılan httpTrigger'ın sadece POST almasını sağlamak ve application/json content-type sınırlaması yapmak. Bunun dışında bir işlevsellik sağlamıyor. Biz bunu kapaca Azure Functions ve Webhooks konusundaki yazıda webHookType kullanmadan yaptık.
  • github : Bu tahmin edebileceğiniz üzere doğrudan github uyumlu olarak çalışabilen bir webhook receiver implementasyonu yapmamızı sağlayacak. Yine Azure Functions ve Webhooks konusundaki yazıda bahsettiğimiz Secret kısmı ile ilgili implementasyon burada otomatik olarak geliyor. Dikkat edilmesi gereken tek nokta trigger definition'da authLevel özelliğinin ayarlanmamış olması gerekiyor. Nedeni ise aslında anonim bir metod kullanarak validasyonu * X-Hub-Signature* header'ı üzerinden yapacak olmamız.
  • slack : Aynı bir önceki github örneğinde olduğu gibi bu seçenek de doğrudan Slack için hazır bir yapı getiriyor. Bu seçenekte de authLevel özelliğinin trigger definition'da tanımlanmaması gerekiyor.
[function.js]

{
  "bindings": [
    {
      "type": "httpTrigger",
      "direction": "in",
      "webHookType": "github",
      "name": "req"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

Yukarıdaki bindinglerden de anlayabileceğiniz üzere örneğimizde bir httpTrigger ile input binding kullanırken yine Azure Functions ve Webhooks konusundaki yazıda yaptığımız gibi kayıtları alıp Table Services'a atacağız. Normal şartlardaki bir httpTrigger'dan farklı olarak burada bir de webHookType denilen bir özelliğe github değerini veriyoruz.

Github Secret doğrudan ekranda.

webHookTypegithub olarak ayarladığınız anda Azure Web Portal'ındaki Azure Functions editöründe bir değişiklik göreceksiniz. Artık code parametresi ile URL üzerinden bir erişim kodu gitmeyecek. Onun yerine doğrudan Github Secret adında bir field ekleniyor. Bu fielddeki değeri alıp Github'da WebHook tanımlarken doğrudan Github Secret olarak vereceğiz.

Github Secret'ı Webhook tanımlarken Github'a veriyoruz.

Azure Portal'ından aldığımız key'i yukarıdaki gibi github'da WebHook'a Secret olarak verdiğimizde aslında işimiz neredeyse bitmiş oluyor.

[run.csx]

using System.Net;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, ICollector<GithubKaydi> cikanNesneler, TraceWriter log)
{
   dynamic data = await req.Content.ReadAsAsync<object>();

   GithubKaydi cikanLog = new GithubKaydi();
   cikanLog.PartitionKey = data?.action;
   cikanLog.RowKey = System.Guid.NewGuid().ToString();
   cikanLog.JSONPayload = data?.ToString();
   cikanNesneler.Add(cikanLog);

   return req.CreateResponse(HttpStatusCode.OK, new
   {
      body = $"From GitHub : {data.assignee.id}"
   });
}

public class GithubKaydi
{
   public string PartitionKey { get; set; }
   public string RowKey { get; set; }
   public string JSONPayload { get; set; }
}

Yukarıdaki örnek yine Azure Functions ve Webhooks konusundaki yazıdan çalıntı :) Değişen tek kısım benim Newtonsoft kullanmaktan vazgeçmem oldu :) Onun dışında kod bire bir aynı.

Github'dan gelen webhook verileri Table Services'da.

Verileri yine alıp Table Services'a attığımız için yukarıdaki gibi full JSON Payload'u görebiliyoruz.

İtiraf etmek gerekirse webhooktypelar zamanla artacak gibi hissediyorum :) Hatta bu konuda ipucu olarak Github'da yakaladağım fakat daha resmi dokümantasyonda olmayan şeyler de var :) Bakalım ilerleyen zamanlarda başka neler göreceğiz :)

Görüşmek üzere.

Bu yazıyı yazmam 55 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 4 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Dünkü Azure Functions ve Notification Hub Binding yazısından sonra gelen birkaç soru ile farkına vardım ki aslında Notification Hub konusunda da bir yazı iyi olurmuş :) O nedenle gelin Azure'daki Notification Hub'a bir giriş yapalım. Azure Functions yazısı için test ortamı kurulumunu yaparken attığım adımları sizlerle paylaşarak aslında Notification Hub ile WNS (Windows Push Notification Services) kullanarak basit bir UWP uygulamasına Push Notification yollayacağız.

WNS ve Live Services Kaydı

Azure tarafına geçmeden önce kendimize bir WNS hesabı ayarlayıp erişim bilgilerini almamız gerekiyor. Bunu da ancak bir uygulama ile eşleştirerek yapıyoruz çünkü WNS o uygulamanın deployment sertifikasını kullanacak arka planda. O nedenle ilk olarak Windows Dev Center'a gidip bir uygulama kaydı yaratıyoruz.

Dev Center'da yeni bir uygulama...

Dev Center'a girdikten sonra yukarıdaki gibi bir manzara ile karşılaşacaksınız. Create a new app diyerek ilerleyebilirsiniz. Uygulamanızı Store'a göndermeniz falan gerek yok :) Sadece ismini alıyoruz ve gerekli ek kayıtların yaratılmasını sağlıyoruz.

WNS ayarlarının yolunda...

Uygulamayı yarattıktan sonra hemen yukarıdaki ekran görüntüsündeki gibi Services kısmına gidip Push Notification'a tıklıyoruz. Burada New Notification vs gibi kısımlara kanmayın :) girmek istediğimiz yer WNS/MPNS sayfası.

Live Services sitesine yolculuk...

Yukarıdaki gibi yeni sayfaya yönlendirildiğiniz anda bir sonraki tıklamamız gereken link de "visit Live Services site" kısmı olacak. Buraya tıkladığımızda doğrudan WNS registration sayfasına yönlendirileceğiz.

Application Secret karşımızda

Karşımızda yeni açılacak olan sayfada ilk karşılaşacağınız yer Application Secrets kısmı. Buradaki key'e Azure'da Notification Hub'ımızı ayarlarken ihtiyacımız olacak. O nedenle bir kenara yazmanızda fayda var :)

Package SID'yi de alıyoruz.

Aynı sayfanın altına doğru scroll ederseniz bu sefer de yukarıdaki kısımla karşılaşacaksınız. Buradan da Package SID denilen kısmı alıyor olmamız gerekiyor. Özetle, secret key ile SID'yi kenara yazdınız mı işimiz tamam demektir.

Azure Notification Hub'ı alma zamanı

Artık Azure Notification Hub'ımızı ayarlayabiliriz. Azure Portal'ına gidip yeni bir Notification Hub hizmeti alabilirsiniz. Hizmeti alırken bir namespace, bir de hub ismi vermeniz gerekecek. Free Tier denilen ücretsiz seviyeden devam edebilirsiniz. Bu seviyede namespace başında 10 hub yaratabiliyor ve 500 cihaz için toplam 1 milyon push notification desteği alabiliyorsunuz. Sonraki seviyelerde tabi ki bu sayı artıyor. Test için bu seviye bize yeterli olacaktır.

WNS Erişim bilgilerini Notification Hub'a veriyoruz.

Notifcation Hub'ı aldıktan sonra hemen kullanacağınız Push Notification hizmetlerini ayarlamanız gerekecek. Azure Notification Hub apns (Apple Push Notification Service), adm (Amazon Device Messaging), gcm (Google Cloud Messaging), wns (Windows Push Notification Services), Baidu ve mpns (Microsoft Push Notification Service) desteğine sahip. Tüm bu hizmetlerle bizim adımıza konuşabilir. Yapmamız gereken şey bu yazının başında WNS için yaptığımız gibi gerekli ayarları yapıp gerekli erişim bilgilerini edinmek. Hatırlarsanız elimizde bir Package SID ve Secret vardı. İşte bu bilgileri şimdi Azure Portal'ında Azure Notification Hub'ta Push Notification Services olarak WNS seçip ekleyeceğiz. Bu bilgileri tamamladıktan sonra artık bu Notification Hub ile WNS push notificationları gönderebiliriz.

UWP Appininin hazırlanması.

Şu ana kadar UWP tarafına pek bulaşmadık. İlk başta Dev Center'da uygulama adını tanımlayıp Live Services'dan da WNS bilgilerini aldık o kadar. Sıra geldi Visual Studio açıp bir UWP uygulaması oluşturmaya.

Application Secret karşımızda

UWP uygulamasını yarattıktan sonra ilk yapmamız gereken bu uygulama ile Store'da yarattığımız uygulamanın ilişkisini kurmak. Malum şu anda bu uygulama aslında benim örneğimdeki Daron-test uygulaması olduğunu bilmiyor. Bu bilgiye sahip olması kritik aksi halde o aldığımız SID ve Secret'ın bu proje ile bir ilişkisi olmaz. Bu nedenle hemen yukarıdaki gibi Store / Associate App with Store derseniz karşınıza gelecek olan ekranda Dev Center'da kullandığınız Microsoft Account ile login olarak uygulama listesine ulaşabilirsiniz. Oradan da tek yapmanız gereken Store'da ismini verdiğiniz uygulamayı seçip onaylamak.

Application Secret karşımızda

Sıra geldi artık yavaştan birkaç satır kod yazmaya :) Ama ona da başlamadan önce WindowsAzure.Messaging nuget paketini almamız gerekiyor ki Notification Hub ile konuşabilelim. Bu nuget paketini de projeye ekledikten sonra artık kod yazmaya hazırız :)

[App.xaml.cs]

protected async override void OnLaunched(LaunchActivatedEventArgs e)
{
   var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();

   var hub = new NotificationHub("daronnotification", "Endpoint=sb://{ENDPOINT}.servicebus.windows.net/;SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey={KEY}");
   var result = await hub.RegisterNativeAsync(channel.Uri);

   // Displays the registration ID so you know it was successful
   if (result.RegistrationId != null)
   {
      var dialog = new MessageDialog("Registration tamamlandı: " + result.RegistrationId);
      dialog.Commands.Add(new UICommand("OK"));
      await dialog.ShowAsync();
   }

Yukarıdaki kod app.xaml.cs'in şu an için bizi ilgilendiren kısmı. İlk satırda mevcut app için bir PushNotificationChannel oluşturuyoruz. Sonra bu kanalı NotificationHub'a vermemiz gerekiyor ki artık bu kanaldan notification yollama için Hub yapsık. Bunun için de biraz önce eklediğimiz Nuget paketini kullanacağız.

Access Policy'ler burada.

Kod içerisindeki NotificationHub nesnesini yaratırken hub'ın adını ve portalden Access Policy'lerden alacağımız DefaultListenSharedAccessSignature endpointini veriyoruz. Son olarak da RegisterNativeAsync ile kanalın adresini Notification Hub'a gönderiyoruz. Ben ten amaçlı olarak kanalın ID'sini bir MessageDialog ile de gösterdim.

Herşey hazır.

Artık Push Notification gönderebilmemiz için herşey hazır. Test amaçlı olarak Azure Portal'ındaki "Test Send" kısmını kullanabiliriz.

Test notification gönderme zamanı.

Testimizi Windows platformu için Toast Notification olarak yapalım. Zaten sadece WNS ayarladığımız için diğer platformlara şu an için notification gönderemeyiz. Ekranın alt kısmına bakarsanız UWP appini çalıştırdığımızda (En azından bir defa çalıştırdığınızı varsayıyorum) kaydolan Push Kanalı'nın bilgilerini görebilirsiniz. Geriye Send diyip sonucu görmek kalıyor :)

İşte tüm bunları dünkü Azure Functions ve Notification Hub yazısı ile birleştirdiğinizde ortaya güzel bir manzara çıkıyor :) Tabi daha NotificationHub ile ilgii karıştırmadığımız özellikler var. Onlar da sonraki yazılara ;)

Görüşmek üzere.

Bu yazıyı yazmam 105 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 3 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Mobil uygulamaların back-end tarafı ile ilgili bana gelen en popüler sorular push notificationlar ile ilgili oluyor. Belirli aralıklarla bazı şeyleri kontrol edip notification yollamak bunların arasında en popüleri. Azure Functions ile beraber gelen Azure Notification Hub Output Binding desteği aslında bu tarz senaryolar için biçilmiş kaftan.

Hazırlıklar

Hazırlıklar kapsamında elinizde var olduğunu varsaydığımız bazı şeyler var :) Bunlardan ilki tabi ki Azure Notifican Hub hizmeti ve o hizmeti kullanan, push notification alabilen istemciler. Elinizde var olan Notification Hub'ın DefaultFullSharedAccessSignature Access Policy'sini Azure Portalı'ndan alıp appsettings.json'a koymanız gerekiyor.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName={NAME};AccountKey={KEY}",
    "AzureWebJobsDashboard": "DefaultEndpointsProtocol=https;AccountName={NAME};AccountKey={KEY}",
    "NotifHubConnectionString": "Endpoint=sb://{ENDPOINT}.servicebus.windows.net/;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey={KEY}"
  }
}

Ben appsettins.json'da key olarak NotifHubConnectionString kullandım. Bunu birazdan Azure Functions'da binding definitionda kullanacağız.

Output binding

Output binding olarak notificationHub bindingi kullanacağız. Bunun yanı sıra Azure Function'ı tetiklemek için de bir timerTrigger kullanalım. Örnekteki timerTrigger 30 saniyede bir çalışacak. Böylece amacımız şu an için 30 saniyede bir tüm clientlara notification göndermek. Her zamanki gibi siz bu yapıyı farklı tasarlayabilirsiniz.

[function.js]

{
  "bindings": [
    {
      "schedule": "0/30 * * * * *",
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in"
    },
    {
      "name": "notification",
      "type": "notificationHub",
      "tagExpression": "",
      "hubName": "daronnotification",
      "connection": "NotifHubConnectionString",
      "platform": "wns",
      "direction": "out"
    }
  ],
  "disabled": false
}

Yukarıdaki function.js dosyasında timerTrigger haricindeki kısımı incelersek notificationHub tipinde bir binding daha görüyoruz. Bu bindingin doğal olarak directionout olarak ayarlanmak zorunda. tagExpression aslında Notification Hub'daki taglerinizi kullanabileceğiniz bir alan. Ben bu örnekte kullanmadım ve boş geçtim. hubName notification hub'ınız adı olacak. connection özelliği bir önceki adımda appsettings.json'da tanımladığımız key/value çiftinin key kısmı. Böylece notificationHub bindingi hangi service account'a gideceğini bilebilecek. Son olarak bir de platform özelliği var. Burada Notification Hubs'ın desteklediği apns (Apple Push Notification Service), adm (Amazon Device Messaging), gcm (Google Cloud Messaging), wns (Windows Push Notification Services), mpns (Microsoft Push Notification Service) veya yine Notification Hubs'ın template özelliğini kullanacağınızı belirtebilirsiniz. Ben bu örnekte istemci olarak bir UWP uygulaması kullanacağım için wns seçtim. Farklı platformlara ayrı ayrı yollamak isterseniz ya ayrı ayrı bindingler ayarlamanız gerekecek ya da doğrudan Notification Hub'ın template özelliğine başvurmanız gerekecek.

Implementasyon

Artık herşey hazır ve kod yazmaya başlayabiliriz demeden önce bir de kullanacağımız Microsoft Azure Notification Hubs .NET SDK'ın nuget paketini project.json'a eklememiz gerekiyor.

[project.json]

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "Microsoft.Azure.NotificationHubs": "1.0.7"
      }
    }
  }
}

Bu da tamamlandığına göre yavaştan run.csx'e geçip kodumuzu yazabiliriz.

[run.csx]

 #r "Microsoft.Azure.NotificationHubs"
using System;
using Microsoft.Azure.NotificationHubs;

public static async Task Run(TimerInfo myTimer, IAsyncCollector<Notification> notification, TraceWriter log)
{
   log.Info($"Sending WNS toast notification of a new user");
   string wnsNotificationPayload = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                                    "<toast><visual><binding template=\"ToastText01\">" +
                                        "<text id=\"1\">" +
                                            "Push " + DateTime.Now.ToString() +
                                        "</text>" +
                                    "</binding></visual></toast>";

   log.Info($"{wnsNotificationPayload}");
   await notification.AddAsync(new WindowsNotification(wnsNotificationPayload));
}

Yukarıdaki metodun imzasına baktığınızda bir timerTrigger ve bir de notificationHub bindingden gelen parametreleri görebilirsiniz. IAsyncCollector'a istediğiniz sayıda notification verebilirsiniz. Örneğin hem Tile hem de Toast notification göndermek isterseniz bunu bir defada yapmak mümkün. Tüm bunları tabi ki notificationHub bindingde tanımladığımız üzere tabi ki wns üzerinden gideceğini unutmayın. Eğer farklı platformları aynı anda hedeflemek isterseniz Notification Hub'ın template kısmına bulaşmanız gerek.

Ben bu örnekte basit bir şekilde saat bilgisini push notification olarak gönderdim.

Azure Functions'dan Push Notification gönderirken...

İşte gördüğünüz gibi olay bu kadar basit. Artık ister timerTrigger gibi yapılarla belirli aralıklarla bir durumu kontrol edip ona göre push notification gönderme kararı alın, ister bir kuyruğu dinleyip görev geldiğinde notification gönderin. Dediğim gibi bundan sonrası sizin ihtiyaçlarınıza göre şekillendirilebilir. Önemli olan kısım Azure Functions ve NotificationHub Binding ile beraber aslında full bir back-end derdine girmeden notification ihtiyaçlarınızın neredeyse hepsini karşılayabiliyorsunuz.

Hepinize kolay gelsin.

Bu yazıyı yazmam 170 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 4 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

WebHooks bildiğiniz üzere çoğu üçüncü parti servisin entegrasyonu için kullanılıyor. Azure Functions özellikle Consumption Plan'da dışarıdan gelen WebHooks'ları beklemek ve gerektiğinde tepki verebilmek adına süper uygun bir araç. O nedenle şöyle ufak bir WebHooks implementasyon örneği yapalım istedim. Basit bir şekilde Github'dan gelen eventleri alıp Table Services'a atacağız :)

Github Webhooks ve Azure Functions

Webhooks konusunda en hızlı aklıma gelen yer Github oldu :) Github'ın Webhooks dokümantasyonuna buradan göz atabilirsiniz. Biz örneğimizde PullRequestEvent'i kullanacağız. Böylece bir PR(Pull Request)'ın "assigned", "unassigned", "labeled", "unlabeled", "opened", "edited", "closed", veya "reopened" durumlarından haberdar olabileceğiz. Bu durumların hepsini bir Table'a kaydedeceğiz. TableBinding konusuna daha önce bakmıştık. "Amacın nedir?" derseniz :) bir anlamda githubdaki PR eventlerinin bir logunu tutmuş olacağız. Örneğin, toplandığımız veriden bir PR'ın bize assign edildikten ne kadar süre sonra kapatıldığı konusunda bir istatistik çıkarmak mümkün :) Maksat fantazi ve biraz da örnek olsun :)

Adım 1 : Bindingler hazırlanır

İlk olarak bindinglerimizi hazırlayarak başlayalım. InputBinding olarak bir HttpTrigger kullanacağız. Normalden farklı olarak burada methods arrayini kullanarak bu API'a sadece POST ile ulaşılmasına izin vereceğiz. Zaten tüm WebHook'lar da bu şekilde çalışır ve Github'ınki de tabi ki böyle çalışıyor.

function.json

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in",
      "authLevel": "function",
      "methods": [ "POST" ]
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

HTTP Method ayarının dışında authLevel'a da dikkat. Onu da function level yaparak kullanacağımız erişim anahtarının bu function'a özel olmasını sağlayacağız. Genel olarak baktığınızda HttpTrigger'ımız bu kadar. İçeriye req parametresi ile alıp geri kalanları function'ın içinde halledeceğiz.

Gelelim outputbinding'e. Daha öncede söylediğim gibi biz Table Services kullanacağız. Bunun için ben Azure Functions ile beraber gelen storage hesabını kullanacağım. Her zamanki gibi connection bilgisini appsettings.json'a atmayı unutmayın. Bizim örneğimizde table adı olarak da Cikanlar adını kullanacağız. Zaten farkına vardıysanız işin bu tarafını Table Binding makalesinden çaldım :)

Adım 2 : Esas kod yazılır.

[run.csx]

 #r "Newtonsoft.json"
using Newtonsoft.Json;

using System.Net;
using System.Threading.Tasks;

public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, ICollector<GithubKaydi> cikanNesneler, TraceWriter log)
{
   string jsonContent = await req.Content.ReadAsStringAsync();
   dynamic data = JsonConvert.DeserializeObject(jsonContent);

   GithubKaydi cikanLog = new GithubKaydi();
   cikanLog.PartitionKey = data.action;
   cikanLog.RowKey = System.Guid.NewGuid().ToString();
   cikanLog.JSONPayload = data.ToString();
   cikanNesneler.Add(cikanLog);

   return req.CreateResponse(HttpStatusCode.OK, new
   {
       body = $"From GitHub : {data.assignee.id}"
   });
}

public class GithubKaydi
{
   public string PartitionKey { get; set; }
   public string RowKey { get; set; }
   public string JSONPayload { get; set; }
}

Yukarıdan aşağıya göz atacak olursak, ilk olarak tabi ki Newtonsoft'un JSON kütüphanesini import ediyorum. Bunu aslında project.json içerisinde de nuget paketi ile de yapabilirsiniz, fakat itiraf etmek gerekirse bende bu sefer ısrarla nuget restore'u çalıştırmadı Azure Functions :) Aşağıdaki gibi dev console'da elle giriştim, yine yediremedim.

Portal'daki Dev Console'daki çırpınışlar

[project.json]

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "Newtonsoft.Json": "9.0.1"
      }
    }
  }
}

Ama siz garantiye almak için yukarıdaki şekilde project.json'ı da editlerseniz iyi olur.

string jsonContent = await req.Content.ReadAsStringAsync();
dynamic data = JsonConvert.DeserializeObject(jsonContent);

Bizim en üstteki kodu parça parça inceleyecek olursak, ilk başta requesti alıp doğrudan bir JSON olarak deserialize edip dynamic bir objeye attığımı görebilirsiniz.

GithubKaydi cikanLog = new GithubKaydi();
cikanLog.PartitionKey = data.action;
cikanLog.RowKey = System.Guid.NewGuid().ToString();
cikanLog.JSONPayload = data.ToString();
cikanNesneler.Add(cikanLog);

Sonrasında aslında artık yukarıdaki gibi Github'dan gelen data ile uğraşıyoruz. Table Services'a atarken ParitionKey'e Github'dan gelen aksyonu ve RowKey olarak da random bir GUID verdim.

return req.CreateResponse(HttpStatusCode.OK, new
{
    body = $"From GitHub :{data.assignee.id}"
});

En sonda verdiğimiz bu response'un Github için hiçbir değeri yok. Yani HTTP 200 tabi ki önemli :) bahsettiğim body kısmıydı.

Adım 3 : Github'da WebHook tanımlanır.

Github arayüzüne gidip istediğiniz repo'nun Settings kısmına geçerseniz WebHooks ayarlarını bulabilirsiniz. Buradan yeni bir webhook eklememiz gerekiyor.

github'ta yeni webhook tanımlarken

Function App ile beraber gelen default access key'i kullanırsanız sondaki "==" kısımları canınızı sıkabilir. Github aldığı değeri URL encode ediyor :) O nedenle o "=="ler %3D'lere dönüşüyor. O nedenle benim tavsiyem Azure Functions portaline gidip özel key tanımlamanız.

Kendi Function Access Key'lerinizi portalde ekleyebilirsiniz.

Portalde keyinizi tanımladıktan sonra bunu her zamanki gibi URL'e code parametresi ile ekleyip son URL'i github'a verebilirsiniz.

Github'taki WebHook eventlerinin listesi.

Biz sadece Pull Request'leri ile ilgili eventleri dinleyeceğimiz için webhook yaratırken de sadece bunu seçmenizde fayda var.

Github'daki Secret kısmını merak edenler varsa, o kısımda verdiğiniz key ile payload HMAC hexdigest ile hashlenir. İsterseniz tabi ki bunu da Azure Function içerisinde validate edebilirsiniz fakat ben yazıyı daha uzatmamak için orayı atlıyorum. Siz isterseniz signature'a X-Hub-Signature header'ından ulaşabilirsiniz.

Sonuç

Github eventleri Table Services'da loglandı

Sonuç olarak githubdaki eventler WebHook üzerinden bize geliyor ve biz de Table Services'a atıyoruz. Bundan sonrası artık size kalmış :)

Bu yazıyı yazmam 110 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 4 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Eğer IoT taraflarıyla uğraştıysanız Azure Event Hub ile de karşılaşmış olmanız olası. Ben bu yazıda Event Hub'ın detaylarını ileriki bir yazıya bırarak Event Hub deneyimizin olduğunu varsayarak Azure Functions entegrasyonundan bahsedeceğim. Azure Functions ile beraber Event Hub ile entegre olabilmek adına hem eventHubTrigger geliyor hem de input ve output binding özellikleri geliyor.

Test ortamımız

Test ortamımız epey basit. eventHubTrigger'ı test edebilmek adına içine eventData gelen bir Hub'a ihtiyacımız var. Bunun için Azure tarafında bir darontest adında Event Hub yaratıp içinde de darontesthub adında bir hub yarattım ben. Sonrasında basit bir uygulama ile hub'a eventData gönderdim.

[uygulama.cs]

string eventHubName = "darontesthub";
string connectionString = "{BURAYA SİZİN CONNECTION STRING GELECEK}";

var eventHubClient = EventHubClient.CreateFromConnectionString(connectionString, eventHubName);

var message = Guid.NewGuid().ToString();
eventHubClient.Send(new EventData(Encoding.UTF8.GetBytes(message)) { PartitionKey = "Test" });

Yukarıdaki kodu çalıştırdığımızda basit bir şekilde için GUID olan bir mesajı Test adında bir Partition'a atıyoruz.

Event Hub Trigger Binding

Event Hub Trigger'ımızı tanımlarken Azure'daki ortamın connection stringini kullanmamız gerekecek. Bunun için appsettings.json'ı kullanabiliriz.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true",
    "AzureEventHubConnectionString": "{BURAYA SİZİN CONNECTION STRING GELECEK}"
  }
}

Yukarıdaki şekilde Event Hub connection stringini Azure portalından alıp appsettings.json'a yazdıktan sonra artık klasik trigger tanımımızı yapabiliriz. Bunun için de functions.json'a geçiyoruz.

[function.json]

{
  "bindings": [
    {
      "type": "eventHubTrigger",
      "name": "telemetriGelen",
      "direction": "in",
      "path": "darontesthub",
      "consumerGroup": "$Default",
      "connection": "AzureEventHubConnectionString"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

Ben daha önceki bir yazıda yaptığımız Table Trigger'dan yola çıkarak devam edelim dedim :) O nedenle hızlıca yukarıya bir Table Output Binding koydum. Amacımız Event Hub'a eventData geldiğinde bunu yakalayıp Table'a kaydetmek. Tabi ki arada siz farklı işlemler de yapabilirsiniz. Ben örnek için "al gülüm, ver gülüm" yapıyorum :)

Yukarıdaki function.json'a bakarsanız üst kısımda eventHubTrigger'ı bulabilirsiniz. Binding'imize type olarak eventHubTrigger dedikten sonra Azure Function'da metod imzasında bind edilecek parametrenin adını da name değerine veriyoruz. Dışarıdan içeriye data geleceği için direction in olarak kalıyor. path dediğimiz ise kullanacağımız hub'ın adı. consumerGroup kısmını aslında boş bıraksam da Event Hub'daki $Default'a yönlenirdi. Ben yine de boş geçmemek adına yazdım, fakat siz tabi ki istediğiniz Consumer Group adını yazabilirsiniz. Azure Event Hub'a özel bir tavsiye olarak Event Hub'a bağlı functionlarınızı çok şişirmemenizi tavsiye ederim. Onun yerine birden çok Function yazıp farklı Consumer Group'lar kullanmanız daha performanslı olacaktır. Azure Functions SDK arka planda EventProcessorHost kullanıyor ve EventProcessorHost Consumer Group'larda Parition başına tek reader kullanır. EventProcessorHost kendi içerisinde ölçeklenebilse de işinizi bölebiliyorsanız ayrı götürmen daha da hızlandıracaktır. Son olarak bir önceki adımda appsettings.json'a eklediğimiz connection stringi burada da connection olarak veriyoruz.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string telemetriGelen, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
    OrnekObje gidenObje = new OrnekObje();
    gidenObje.PartitionKey = System.Guid.NewGuid().ToString();
    gidenObje.RowKey = System.Guid.NewGuid().ToString();
    gidenObje.Metin = $"{telemetriGelen} alındı";
    cikanNesneler.Add(gidenObje);
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yukarıdaki function'da telemetriGelen string tipinden bir değişken ve bize doğrudan Event Hub'a gönderilen veriyi getiriyor. Biz bu örnekte Random birer ParitionKey ve RowKey vererek doğrudan Table Services'a atıyoruz. Eğer eventData ile ilgili daha çok veriye ulaşmak isterseniz **Service Bus SDK'ine başvurmanız gerekecek.

Table Services'a attığımız Event Hub verisi.

[project.json]

{
  "frameworks": {
    "net46":{
      "dependencies": {
        "WindowsAzure.ServiceBus" : "3.4.3"
      }
    }
  }
}

İlk aşamada yukarıdaki gibi ServiceBus nuget paketini projeye ekleyin. Bunu yaptıktan sonra projeyi çalıştırdığınızda Azure Functions Runtime function'ı çalıştırmadan önce nuget restore da yapacak. Sonrasında doğrudan using ile kütüphaneyi kullanabiliriz. Gelin onu da output binding'e bakarken yapalım.

Output Binding

Output binding için function.json dosyasındaki Table Binding'i kaldırarak yerine tahmin edebileceğiniz üzere bir Event Hub Binding koyuyoruz. **Event Hub Trigger'dan farklı olarak direction out olacak ve Consumer Group olmayacak.

[function.json]

{
  "bindings": [
    {
      "type": "eventHubTrigger",
      "name": "telemetriGelen",
      "direction": "in",
      "path": "darontesthub",
      "consumerGroup": "$Default",
      "connection": "AzureEventHubConnectionString"
    },
    {
      "type": "eventHub",
      "name": "telemetriGiden",
      "path": "darontesthub",
      "connection": "AzureEventHubConnectionString2",
      "direction": "out"
    }
  ],
  "disabled": false
}

Arada ben ikinci bir Hub daha yaratıp onun da connection stringi ekledim appsettings.json'a. Böylece bir hub'dan alıp diğerine aktarıyor olacağız.

[run.csx]

using Microsoft.ServiceBus.Messaging;

public static void Run(EventData telemetriGelen, ICollector<string> telemetriGiden, TraceWriter log)
{
    telemetriGiden.Add("Mesaj 1 " + telemetriGelen);
    telemetriGiden.Add("Mesaj 2 " + telemetriGelen);
}

Gördüğünüz üzere Event Hub Trigger'ın bindinginde nesne tipi olarak EventData kullandık. Böylece artık eventData'nın sadece body değil tüm bilgilerine ulaşabiliriz. Ayrıca output binding'de de ICollector kullanarak geriye birden çok event gönderiyoruz. Burada ICollector yerine isterseniz ICollector de kullanabilirsiniz.

Ek ayarlar

[Host.json]

{
    "eventHub": {
      "maxBatchSize": 64,
      "prefetchCount": 256
    },
}

Yukarıdaki ayarları varsayılan değerleri ile yazdım. maxBatchSize bir defada event hub'dan alınacak mesaj sayısını belirliyor. PrefetchCount da tahmin edebileceğiniz üzere aslında Azure Functions SDK'in arkaplanda kullandığı EventProcessorHost ile ilgili. Toplamda ne kadar mesajın işlenme öncesinde belleğe alınması gerektiğini belirtiyor. Azure Functions SDK mesaj göndermek için EventHubClient ve dinlemek için EventProcessorHost kullanıyor.

Bu yazıyı yazmam 87 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 6 dakikada, göz gezdirerek ortalama 2 dakikada okuyabilirsiniz ;)

Azure Functions ile Azure Storage'daki Table Services arasında bir TableTrigger yok. Fakat QueueTrigger veya TimerTrigger gibi tetkikleyicilerle beraber kullanarak TableBinding ile Table Services'dan veri alıp gönderebilirsiniz. Tüm bunlardan önce eğer "Table Services da neyin nesi?" diyorsanız :) Giriş, gelişme ve sonuç diyerek tüm bunların öncesinde Table Services yazılarımı okumanızı tavsiye ederim.

Input Binding

Table Binding Test Ortamımız

İlk olarak yukarıdaki şekilde local storage ortamını hazırlayalım. Örneğimizde ornekkuyruk adında bir kuyruk kullanacağız. Bu kuyruğa atacağımız ilçe ismine göre Nesneler adındaki Table'dan PartitionKey şehir ve RowKey de ilçe olacak şekilde doğru ilçeyi bulacağız. Sonrasında eldeki veriden birkaç absürd değişiklik yapıp :) Cikanlar tablosuna da ayrı bir binding ile veri atmayı deneriz.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "partitionKey": "Istanbul",
      "rowKey": "{queueTrigger}",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    }
  ],
  "disabled": false
}

İlk aşamada function.json dosyamız yukarıdaki gibi olacak. Üst kısımda basit bir QueueTrigger tanımı yapıyoruz. QueueTrigger detaylarını burada bulabilirsiniz. Böylece birazdan göreceğimiz Azure Function'a bu kuyruğa gelen görevlerin kuyruk adlı bir nesne ile gönderileceğini biliyoruz.

Sonraki adımda bir Table Binding oluşturuyoruz. Bunun için typetable olan bir nesne koymamız gerekiyor. Bu JSON nesnesinin name özelliği yine bizim Azure Function içerisine parametre olarak gönderilecek verinin adını tanımlıyor. Tabi Table Storage'dan bahsettiğimiz için bir Table'ı hedeflememiz gerek. Onun için de tableName'e ilk adımda yarattığımız test table'larından Nesneler adındaki table'ı veriyoruz. connection yine appsettings.jsondaki storage account connection stringi gösteriyor, direction ise içeriye data aktaracağımız için in şeklinde tanımlanmış.

Farkındaysanız paritionKey ve rowKey özelliklerini atladım. Onlara ayrıca odaklanmak istedim. Table Services'da bildiğiniz üzere her nesne bir ParitionKey ve RowKey sahibi olmak zorunda. Detaylarını yazının başında linkini verdiğim yazılardan inceleyebilirsiniz. Bu noktada Table Services'ı bildiğinizi varsayıyorum. TableBinding yaparken Table'dan bir data bulmaya çalışacağız. Bu datanın bir şekilde ParitionKey ve RowKey ile aranması gerekiyor. Bizim örneğimizde ParitionKey olarak Istanbul değerini vererek aslında bu Function'da kullanılacak binding'in her zaman Istanbul Parition'ına gideceğini söylemiş oluyoruz. Siz tabi ki ihtiyacınıza göre farklı bir tasarım yapabilirsiniz. rowKey özelliğine baktığınızda ise {queueTrigger} diye bir placeholder göreceksiniz. Aslında bu da bir binding. Bu keyword'ü kullanarak QueueTrigger'dan gelen değeri TableBinding'in içindeki RowKey'e bind etmiş oluyoruz. Yani eğer kuyruktan için "Sisli" yazan bir mesaj gelirse bizim Table Binding ile Istanbul PartitionKey'li ve "Sisli" yazan RowKey'li obje dönecek. Eğer kuyruktan gelen veriyi bu kadar rahat bir şekilde ParitionKey veya RowKey'e bind edemiyorsanız yazının sonunda bu filtrelemeyi Azure Function'ın kendi içinde nasıl yapabileceğinizden de bahsedeceğim.

[run.cxs]

public static void Run(string kuyruk, OrnekObje gelenNesne, TraceWriter log)
{
    log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
    log.Info($"Table Services'dan gelen metin: {gelenNesne.Metin}");
    gelenNesne.Metin += "OK";
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yukarıdaki koda baktığınızda birincisi alt kısımda binding için kullanacağımız OrnekObje adındaki POCO'yu görüyorsunuz. Nesne Table Services'dan geleceği için ParitionKey ve RowKey özellikleri mecburi.

Esas Azure Function'a baktığımızda ise hem kuyruktan gelen verinin kuyruk değişkeni ile geldiğini hem de Table Services'dan Istanbul ParitionKey'li ve kuyruktan gelen değere uyan RowKeyli nesnenin gelenNesne adı ve OrnekObje tipi ile geldiğini görebilirsiniz.

Daha da fazlası, gelenNesnenin Metin adındaki Property'sinde ufak bir değişiklik de yapıyoruz. Bu değişikliği ayrıca commit etmeniz vs gerekmiyor. Function bittiği anda bu değişiklik doğrudan Table Services'daki Entity'ye yansıyacak.

Output Binding

Şimdi sıra geldi bir de Output Binding denemeye. Nesneler adındaki table'dan veri aldık. Aldığımız veriyi biraz değiştirip bu sefer Cikanlar adındaki table'a atacağız.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "partitionKey": "Istanbul",
      "rowKey": "{queueTrigger}",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

Bir önceki örneğin üzerine adı cikanNesneler olan, tipi table olan ve direction'ı da out olan bir binding daha ekledik. Tablo adı olarak da Cikanlar'ı verdik. Azure Functions tarafına bu tablo ile ilgili bir ICollector gelecek.

[run.csx]

public static void Run(string kuyruk, OrnekObje gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
    log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
    log.Info($"Table Services'dan gelen metin: {gelenNesne.Metin}");
    gelenNesne.Metin += "OK";
    OrnekObje gidenObje = new OrnekObje();
    gidenObje.PartitionKey = gelenNesne.PartitionKey;
    gidenObje.RowKey = gelenNesne.RowKey;
    gidenObje.Metin = $"{gelenNesne.Metin} alındı";
    cikanNesneler.Add(gidenObje);
}

public class OrnekObje
{
    public string PartitionKey { get; set; }
    public string RowKey { get; set; }
    public string Metin { get; set; }
}

Yine önceki örnek üzerinden ilerliyorum. gidenObje adında ve OrnekObje tipinde bir değişken daha tanımladım ve Metin kısmına da bir önceki nesnenin metninin üzerine bir şeyler ekleyerek yeni bir değer verdim. Unutmayın ki bu nesne Cikanlar adındaki yeni tablomuza gidecek. O nedenle ParitionKey ve RowKey değerlerini de aynı bıraktım. Bu şekilde ICollector'ı kullanarak birden çok obje gönderebilirsiniz.

Bu örneği çalıştırıp kuyruğa "Sisli" adında bir mesaj eklediğimde ilk olarak Nesneler tablosundaki nesnenin Metin değerinin sonunda "OK" ekleniyor. Sonrasında da yeni Cikanlar tablosuna yeni bir satır ekleniyor.

İkinci table'a çıktıyı aldık

Bu noktada ufak bir sorun var :) Farkındaysanız iki ayrı tabloda işlem yapıyoruz ve daha önce de bahsettiğim gibi tüm bunlar Function bittiğinde commit oluyor. Peki ya birinde hata alırsak? Çok basit :) hata aldığınız gerçekleşmez, diğeri gerçekleşir. Yani herşey en sonda commit olsa da bütün function bir transaction'dadır gibi hayallere kapılmayalım :) (Bu arada unutmadan özellikle ICollector'ın Add vs özellikleri sona kalmaz, anında çalışır) Özellikle varsayılan ayarlarda 5 defa Function'ın tekrar deneneceğini de düşünürseniz içeride tek bir operasyondan aldığınız tek bir exception yüzünden diğer bütün operasyonların ışık hızında 5 defa yapılabilir :) O nedenle Idempotent tasarım bu noktada çok kritik.

İstediğin gibi filtrele

Bu bölüme başka başlık bulamadım :) Hatırlarsanız başlarda bir yerlerde table services'dan alacağınız veriye karar vermek için kullanacağınız filtrelemenin ille binding içerisinde olmasının sınırlayıcı olabileceğinden bahsetmiştim. Alternatif olarak ParitionKey ve RowKey'i bindinge karıştırmadan filtreleme işini tamamen Azure Function içerisinde de yapabilirsiniz.

[function.json]

{
  "bindings": [
    {
      "queueName": "ornekkuyruk",
      "connection": "AzureWebJobsStorage",
      "name": "kuyruk",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "gelenNesne",
      "type": "table",
      "tableName": "Nesneler",
      "connection": "AzureWebJobsStorage",
      "direction": "in"
    },
    {
      "name": "cikanNesneler",
      "type": "table",
      "tableName": "Cikanlar",
      "connection": "AzureWebJobsStorage",
      "direction": "out"
    }
  ],
  "disabled": false
}

İlk olarak function.json dosyasında table inputbinding'den PartitionKey ve RowKey'e dair herşeyi kaldırıyoruz. Bu noktada artık sadece bir table'ın bir referansını almış olacağız. Ek bir binding yapılmayacak.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string kuyruk, IQueryable<OrnekObje> gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
   OrnekObje tekNesne = gelenNesne.Where(p => p.PartitionKey == "Istanbul" && p.RowKey == kuyruk).SingleOrDefault();

   log.Info($"C# Queue trigger'dan gelen: {kuyruk}");
   log.Info($"Table Services'dan gelen metin: {tekNesne.Metin}");
   tekNesne.Metin += "OK";

   OrnekObje gidenObje = new OrnekObje();
   gidenObje.PartitionKey = tekNesne.PartitionKey;
   gidenObje.RowKey = tekNesne.RowKey;
   gidenObje.Metin = $"{tekNesne.Metin} alındı";
   cikanNesneler.Add(gidenObje);
}

public class OrnekObje : TableEntity
{
   public string Metin { get; set; }
}

Bir sonraki adımda ise yukarıdaki gibi birkaç değişiklik yapmamız gerekiyor. Bize Table'ın kendisi geleceği için kuyruktan gelen mesaj ile Table'ı nasıl sorgulayacağımıza biz karar vereceğiz. Bunun için de LINQ kullanmak istersek tabi ki Azure Storage SDK'i kullanmamız gerek. Üst kısımda Azure Functions'da bir harici bir assembly nasıl reference alır onu görüyorsunuz #r bu işi görüyor. Sonraki değişiklik OrnekObje ile ilgili. Nesnemizi artık TableEntityden türetmemiz gerekiyor ki rahatlıkla SDK'yi kullanabilelim. IQueryAble interface'i bunu şart koşuyor.

En sonunda artık kodun içindeki LINQ sorgusunu görebilirsiniz. Ben yine ParitionKey olarak Istanbul ve RowKey olarak de kuyruktan gelen mesajı kullandım. Siz tabi ki bu sorguyu istediğiniz gibi değiştirebilir, isterseniz birden çok row da alabilirsiniz Table Services'dan. Fakat artık sorgulamayı biz yaptığımız ve Azure Functions'ın bindingini doğrudan nesne seviyesinde kullanmadığımız için nesnelerde yaptığımız değişiklikler doğrudan table'a gitmeyecek. Yani yukarıdaki örneği çalıştırırsanız tekNesnenin Metin özelliği storage'da hiç değişmeyecek, çünkü artık onu Commit eden yok. Eğer istiyorsanız bunu elle yapmanız gerekecek.

[run.csx]

 #r "Microsoft.WindowsAzure.Storage"
using Microsoft.WindowsAzure.Storage.Table;
public static void Run(string kuyruk, CloudTable gelenNesne, ICollector<OrnekObje> cikanNesneler, TraceWriter log)
{
   TableOperation operation = TableOperation.Retrieve<OrnekObje>("Istanbul", kuyruk);
   TableResult result = gelenNesne.Execute(operation);
   OrnekObje tekNesne = (OrnekObje)result.Result;
   tekNesne.Metin += "OK";

   operation = TableOperation.Replace(tekNesne);
   gelenNesne.Execute(operation);
}

public class OrnekObje : TableEntity
{
   public string Metin { get; set; }
}

Hem inputBinding'de hem de outputBinding'de eğer metod imzasında yukarıdaki gibi obje tipini CloudTable olarak tanımlarsanız azami esnekliğe sahip olur ve Azure SDK ile beraber gelen tüm TableOperation'ları kullanabilirsiniz. Yukarıdaki örnekte hem sorgulama işinin, hem de nesne güncelleme işinin tamamen Azure Storage SDK ile yapılmış halini görebilirsiniz.

Kolay gelsin ;)

Bu yazıyı yazmam 65 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 3 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Belirli zamanlarda veya aralıkla iş yapmak hep problem olmuştur :) Hem istediğim zamanda ve aralıkta çalışmasından emin olmak isteriz hem de altyapıyı ölçeklendirdiğimizde söz konusu işin yine bir defa çalışmış olmasını isteriz :) Hayat zor :) Şaka bir yana, tüm bunlar için Azure Functions içerisinde kullanımı süper basit bir yapı var, adı da timerTrigger. Gelin hızlıca detaylarına göz atalım.

[function.js]

{
  "bindings": [
    {
      "schedule": "0 */2 * * * *",
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in"
    }
  ],
  "disabled": false
}

İlk olarak yukarıdaki gibi Azure Functions bindingimizi ayarlıyoruz. schedule kısmında bir CRON Expression kullanıyoruz. Basit bir şekilde {saniye} {dakika} {saat} {gün} {ay} {haftanın günü} formatında bir tanımdan bahsediyoruz. Bizim yukarıdaki örneğimizde timer beş saniyede bir çalışacak. name parametresinde verdiğimiz değer birazdan yazacağımız Azure Functions'a geçecek olan parametrenin de adı olacak. Timer'ımıza function içerisinden de ulaşabileceğiz. type parametresi bindingin tipini belirliyor malum :) timerTrigger kullanarak geçiyoruz. Son olarak direction için de tabi ki in kullanmamız gerek, zaten farklı bir seçenek pek de olası değil.

[run.csx]

public static void Run(TimerInfo myTimer, TraceWriter log)
{
    if (myTimer.IsPastDue)
    {
        log.Info("Geç kalmışız!");
    }
    log.Info($"Timer {DateTime.Now} zamanında çalıştı.");
    log.Info($"5 dakika sonra çalışma zamanı {myTimer.Schedule.GetNextOccurrence(System.DateTime.Now.AddMinutes(5)).ToString()}");
    log.Info($"Son çalışan zaman {myTimer.ScheduleStatus.Last.ToString()}");
}

Yukarıda da Function kodumuzun kendisini görebilirsiniz. myTimer olarak gelen parametre Timer'ın kendisi. IsPastDue daha önce kaçırılmış bir zamanlamanın çalıştırılıp çalıştırılmadığı dönüyor. Eğer fonksyon çalışma gerektiği zamanda çalıştırılmadı ve sonradan çalıştırılıyorsa burada "True" dönecektir. Tabi buradan çalıştırma isteklerinin kuyruklandığı fikrine falan kapılmayın :) Buradaki durum daha fazla Azure Function'ın çalışması için gerekli ortamın karşılaşabileceği sıkıntılarla alakalı. Örneğin birazdan bahsedeceğimiz blob lease için Storage Account'a ulaşılamamış olabilir. Bu gibi durumlarda Function Timeout'a uğramadan önce gecikirse IsPastDue true gelecektir.

Daha sonraki adımlarda TimerInfo üzerinden alabileceğimiz bazı ek bilgileri de göstermek istedim. Örneğin GetNextOccurrence ile bir zaman verip o zamandan sonra ne zaman Timer'ın çalışacağını alabiliyorsunuz. Bizim timer iki dakikada bir çalışacak ve ben beş dakika sonra hangi zamanda tekrar çalışacağını soruyorum TimerInfo'ya. Son satırda da ScheduleStatus.Last diyerek en son çalıştığı zamanı alıyorum. Eğer bir dakikadan kısa süreli timerlar kullanıyorsanız ScheduleStatus null gelecektir. Bu da hardcoded bir kural.

iki dakikada bir Timer çalışıyor.

Yukarıda da gördüğünüz gibi Timer ile ilgili bir sıkıntımız yok. Normal şartlarda Azure Functions'da Consumption Plan'daysanız birazdan soracağım soruyu sormanız anlamlı olmaz fakat özellikle App Service Plan'daysanız App Service Plan'ı scale ettiğinizde Timer'larınızın her sunucuda çalışıp çalışmayacağından, yani daha basit bir tabirle tek instance olup olmadıklarından emin olamayabilirsiniz. Cevabı; hiç fark etmez. timerTrigger'lar her zaman Singleton ;) Bunu başarabilmek için de host.json'daki storage account ayarlarından gidip bir blob lease alıyor runtime. Eğer function çalıştığında lease alamazsa zaten çalıştığını varsayacak. Eğer bir hata nedeniyle lease bırakılmamışsa bu sefer de tabi ki timeoutu beklemeniz gerekecek.

[host.json]

{
  "singleton": {
    "listenerLockPeriod": "00:01:00"
  }
}

Varsayılan ayarlarda blob lease'ler bir dakikalığına alınır. Fakat isterseniz bunu host.json içerisinde listenerLockPeriod değeri vererek değiştirebilirsiniz. Lock'ı kısaltmak hata durumlarında toparlama hızını da arttıracaktır çünkü sonuç itibari ile timeout beklemek zorunda kalacak bir sonraki instance ama tabi bir de lease yenileme maliyeti var. Eğer Azure Function'ınız 60 saniyeden uzunsa lease'in yenilenmesi gerekir, yoksa iki instance sahibi olabilirsiniz. Bunun için varsayılan ayar da listenerLockPeriod süresinin yarısında lease'in yenilenmesi şeklinde. Özetle, 30 saniyelin bir lock 15 saniyede bir lease yenileme anlamına gelir ki bu da toplam Storage Account transaction sayınızı ikiye katlar. Söylemedi demeyin :)

Bundan sonraki artık size kalmış. İsterseniz timerTrigger ile beraber Queue Binding kullanarak belirli sürelerde kuyruğa iş atarsanız, isterseniz Blob Binding maceralara atılırsınız :) tercih size kalmış. Tabi bunların hiçbirini kullanmayıp istediğiniz kodu da Azure Functions'da çalıştırabilirsin.

Görüşmek üzere.

Bu yazıyı yazmam 88 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 6 dakikada, göz gezdirerek ortalama 2 dakikada okuyabilirsiniz ;)

Azure Functions'da kullanabileceğimiz Trigger yapılarından biri de QueueTrigger. Azure Storage hizmetinin bir parçası olan Queue Storage genelde Web ve Worker Role'lerin birbirinden bağımsız olarak ölçeklendirildiğinde birbirleri ile konuşabilmesi için kullanılıyor. Bu yazıdaki amacım tabi ki Queue Storage'ı anlatmak değil. O konuyu merak edenler için daha tavsiyem daha önce yazdığım bu yazıyı okumaları ;)

Queue Trigger Tanımlamak

Queue Trigger tanımlama işimize yeni yaratacağımız bir Function'ın function.js dosyasından başlayacağız.

[function.js]

{
    "disabled": false,
    "bindings": [
      {
        "name": "queueJob",
        "queueName": "samplequeue",
        "connection": "AzureWebJobsStorage",
        "type": "queueTrigger",
        "direction": "in"
      }
    ]
}

Parametrelere bakacak olursak, name bizim functiona Queue mesajını taşıyacak olan parametrenin adı olacak, queueName dinleyeceğimiz kuyruğun adı olacak (kullanacağımız storage hesabında bu isimde bir kuyruk yaratmamız gerek), connection kısmında appsettings.json'a koyacağımız storage connection stringini key adını yazıyoruz ki functions hangi storage account'a gideceğini bilsin ve son olarak type için doğal olarak queueTrigger diyerek direction olarak da in diyoruz. Böylece queueTrigger tipinde bir inputTrigger yaratmış olduk ve hangi storage account'taki hangi kuyruğu dinlemek istediğimizi ve hangi parametre ismi ile bizim function'a gönderilmesi gerektiğini de belirtmiş olduk.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true"
  }
}

AppSettings.json dosyamız yukarıdaki şekilde local storage emülatörünü gösteriyor. Buradaki AzureWebJobsStorage connection stringini zaten function'ı tanımlarken de kullanmıştık. samplequeue adındaki kuyruğu Azure Functions Runtime bu storage account içerisinde arayacak. Localde test edebilmek için Azure Storage Explorer kullanarak local emülatörü bağlanıp elle gerekli kuyruğu yaratabilirsiniz.

[run.csx]

using System;

public static void Run(string queueJob,
    DateTimeOffset expirationTime,
    DateTimeOffset insertionTime,
    DateTimeOffset nextVisibleTime,
    string queueTrigger,
    string id,
    string popReceipt,
    int dequeueCount,
    TraceWriter log)
{
    log.Info($"C# Queue trigger function çalıştı: {queueJob}\n" +
        $"queueTrigger={queueTrigger}\n" +
        $"expirationTime={expirationTime}\n" +
        $"insertionTime={insertionTime}\n" +
        $"nextVisibleTime={nextVisibleTime}\n" +
        $"id={id}\n" +
        $"popReceipt={popReceipt}\n" +
        $"dequeueCount={dequeueCount}");
}

Yukarıda en basit hali ile bir log atan Azure Function var. Gelin şimdi bu function'ın parametrelerini inceleyelim.

  • queueJob zaten bizim function.js içerisinde name olarak verdiğimiz parametre. Bu parametreyi biz şimdilik string olarak tanımladık. Azure Functions Runtime'ı kuyruktan gelen mesaj string olarak deserialize edip bize verecek. Ben bu örnekte kuyruktaki mesaja "ornekmesaj" yazmıştım ve bu string parametrede de geriye o metin geldi.
  • queueTrigger parametresi ilk bakışta kafa karıştırabilir çünkü bu parametrenin yaptığı iş aynı bizim queueJob gibi string olarak kuyruktaki mesajın içeriği dönmek. Peki neden böyle iki parametre var? Çünkü eğer isterseniz queueJob parametresini string olarak değil de CloudQueueMessage tipinde de alabiliyorsunuz. Bu durumda ayrıca bir de string olarak aliyim derseniz queueTrigger'ı kullanabilirsiniz.
  • expirationTime kuyruktaki mesajı işlemezseniz ne kadar sürede expire edeceğini verir.
  • insertionTime kuyruktaki mesajın kuyruğa eklediği zamanı verir.
  • nextVisibleTime kuyruktan alınan bir mesajın alındıktan sonra başarılı bir şekilde işlenmezse ne zaman tekrar kuyrukta görünür olacağını verir. Eğer input parametre tipini CloudQueueMessage olarak alırsanız bu süreyi uzatma şansınız olabilir.
  • Id bu queueItem'ın unique ID'sini verir.
  • popReceipt ise eğer CloudQueueMessage mesaj ile manual işlemler yapmak isterseniz işinize yarayabilir. O anki Azure Functions instance'ının kuyruktan mesajı alırken elde ettiği popReceipt'ı size verir. Böylece eğer elinizdeki işlem uzun sürer ve başka bir instance da kuyruktan aynı işi alırsa popReceipt ile storage API'larına gittiğinizde geriye hata alma şansınız olur. Genelde popReceipt kullanımını Azure Stroge SDK zaten kendi içerisinde hallediyor. Ama Azure Storage'ın REST API'larına kendiniz gitmek isterseniz bir queue mesajına bir delete veya update yapmanız için elinizdeki kesinlikle popReceipt'ınızın olması gerekir. Bir anlamda sizin kuyruktan mesajı alma biletiniz diyebiliriz. İtiraf etmek gerekirse bir Azure Functions içerisinde bu parametreyi kullanma ihtimaliniz epey düşük.
  • dequeueCount parametresi ile bir mesajın kuyruktan kaç defa alındığını verir. Sürekli kuyruktan aldığınız fakat bir türlü başarılı bir şekilde işleyemediğiniz mesajları işlemeyi kaç defa deneyeceğinizi belirlemeniz gerekiyor. Zehirli mesajlar olarak da adlandırılan bu mesajlar bir süre sonra sayı olarak artarsa sürekli kısır döngüde aynı mesajları işlemeye çalışıp hep başarısız olan bir ortama sebep verebilir. Buna engel olmak için dequeueCount kullanarak bir mesajın kaç defa denendiğini görebilirsiniz. Varsayılan ayarlarda Azure Functions bir mesajı 5 defa işlemeyi deneyip sonra doğrudan samplequeue-poison kuyruğuna taşıyacaktır. Bu kuyruğun adı tahmin edeceğiniz üzere sürekli olarak sizin orijinal kuyruk adınızın sonuna -poison eklenerek oluşturulur. Poison (zehirli) mesaj kuyruğunu dinleyerek gerekli işlemleri yapmak da artık size kalıyor.
Queue Trigger Log çıktısı

Bir mesajın başarılı bir şekilde işlenip işlenmediğine Azure Functions nasıl karar verir? diye sorarsanız, cevabı basit. Eğer function geriye bir exception dönmüyorsa alınan mesaj başarılı bir şekilde işlendi demektir.

Runtime Konfigürasyonu

Azure Functions'daki qeueTriggerlar ile ilgili yapabileceğimiz bazı özelleştirmeler var. Bunları host.json dosyası içerisinde yapabiliyoruz ve bir Function App genelinde geçerli oluyor.

[host.json]

{
  "queues": {
    "maxPollingInterval": 2000,
    "batchSize": 16,
    "maxDequeueCount": 2,
    "newBatchThreshold": 8
  }
}

  • maxPollingInterval Azure Functions Runtime'ının ne kadar sürede bir kuyruğu kontrol edeceğini belirler. Varsayılan ayarlarda iki saniyede bir kuyruğa gidip yeni bir job olup olmadığı kontrol edilir. Bunu isterseniz burada değiştirebilirsiniz.
  • batchSize Aynı anda paralel olarak alınabilecek görev sayısını belirler. Varsayılan ayar 16, azami verebileceğiniz değer ise 32. Bu değer size az gelirse maalesef daha fazla arttırmak için birden fazla Function veya Thread ile uğraşmak zorundasınız. Bu konuda UserVoice üzerinde bir istek var, oy vermek isterseniz buyurun :)
  • maxDequeueCount dan daha önce bahsetmiştik. Varsayılan ayar beş defa bir mesajı denemek şeklinde. Bu sayıyı istediğiniz gibi değiştirebilirsiniz.
  • newBatchThreshold yeni bir Batch alınması için ulaşılması gereken asgari batch batch sayısını tanımlar. Varsayılan ayarlarda bu batchSize'ın yarısı olarak tanımlanır. Örneğin batchSize için aynı anda 16 mesaj alınabileceğini ayarladıysanız newBatchThreshold 8 olacaktır ve eldeki paralel olarak işlenen mesaj sayısı 8'e düşmedikçe yeni bir 16'lık Patch alınmayacaktır. newBatchThreshold değerini değiştirerek kuyruktan aynı anda daha fazla mesaj alınmasını sağlayabilirsiniz fakat özellikle Consumption Plan yerine klasik App Service Plan kullanıyorsanız RAM/CPU tüketimi adına aşırı paralelleşmekten uzak durmak da isteyebilirsiniz. Dikkatli olmakta fayda var :)

Queue Binding

Bu noktaya kadar bir queue trigger tanımlayıp kuyruğa mesaj atıldığında onu işlemeyi gördük. Gelin bir de bir kuyruktan diğerine mesaj atma konusuna bakalım. Özetle, queue bindingleri kullanarak birden çok kuyruk arasında iletişim sağlayacağız.

[function.js]

{
  "disabled": false,
  "bindings": [
    {
      "name": "queueJob",
      "queueName": "samplequeue",
      "connection": "AzureWebJobsStorage",
      "type": "queueTrigger",
      "direction": "in"
    },
    {
      "name": "queueJobOutput",
      "queueName": "samplequeueout",
      "connection": "AzureWebJobsStorage",
      "type": "queue",
      "direction": "out"
    }
  ]
}

Yukarıdaki örnekde ikinci bir binding daha görüyorsunuz. Yine name parametresindeki değer bizim function'ın imzasında yer alacak. queueName bu sefer yeni bir output kuyruğunun adı. connectionımız aynı, böylece aynı storage hesabını kullanmış olacağız. Son olarak binding tipimiz queue ve direction da out şeklinde ayarlanmış durumda.

[run.csx]

using System;

public static void Run(string queueJob, out string queueJobOutput, TraceWriter log)
{
    queueJobOutput = queueJob + " devam....";
}

Bu sefer function kodunu biraz daha temiz tutmak istedim. Basit bir şekilde input ve output parametrelerimiz var. queueJobOutput parametresini zaten output bindingimizi tanımlarken kullandığımız name değeri oldu. Bu functionın yapacağı şey bir kuyruğa mesaj eklendiğinde tetiklenip gelen mesajın sonunda " devam..." metnini ekleyip yeni kuyruğa yeni bir mesaj olarak eklemek olacak. Eminim siz daha anlamlı senaryolar düşünebilirsiniz :)

[run.csx]

using System;

public static void Run(string queueJob, ICollector<string> queueJobOutput, TraceWriter log)
{
    queueJobOutput.Add(queueJob + " devam...)");
    queueJobOutput.Add(queueJob + " daha da devam...");
}

Eğer aynı kuyruğa birden çok mesaj / görev atmanız gerekirse bu sefer ICollector'ı kullanabilirsiniz. Yukarıdaki örnekte bizim kaynak kuyruktan gelen görevi alıp iki farklı görev (queue job) yaratıp output bindingde tanımlı kuyruğa gönderiyoruz.

POCO Kullanımı

İsterseniz bindinglerde kendi özel objelerinizi de kullanabilirsiniz.

[run.csx]

using System;

public static void Run(string queueJob, out OrnekMesaj queueJobOutput, TraceWriter log)
{
    queueJobOutput = new OrnekMesaj() { Metin = $"{queueJob} devam..." };
}

public class OrnekMesaj
{
    public string Metin { get; set; }
}

Yukarıdaki örnekte kaynak kuyruktan gelen metnin üzerine " devam..." metnini eklerken artık geriye basit bir String olarak değil de custom OrnekMesaj nesnesi ile gönderiyoruz. Buradan yeni kuyruk objesine, göreve deserialize işlemini JSON deserializer kullaran Azure Functions Runtime kendisi halledecek. Aynı işlemi input binding'lerde de kullanabilirsiniz.

Output Binding'de JSON Deserialization

Yukarıdaki ekran görüntüsünde yaptığımız örneklerin output binding sonuçlarını görebilirsiniz.

Kolay gelsin ;)

Bu yazıyı yazmam 22 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 2 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Son zamanlarda Azure Functions yazıları yazmaya başlayınca özellikle WebJobs ile arasındaki benzerlikleri dikkat çekmiyor değil. Durum böyle olunca detaylı bir karşılaştırma yapalım istedim.

Comsumption Plan : App Service Plan

Azure Functions'ın en büyük özelliği "Serverless" olması. Bir anlamda Microsoft'un FaaS (Function-As-A-Service) hizmeti olan Azure Functions'ta fiyatlandırma ve kullanım ölçümü GB-s üzerinden yapılıyor. Yani burada saniye başına kullanılan memory ve işlemci'den bahsediyoruz. Maksimum 1.5GB memory'ye kadar yükselebiliyor bir function'ın bellek alanı. WebJobs'a baktığımızda ise ölçeklendirilmesinin tamamen App Service Plan'a dayalı olduğunu görüyoruz. Bu durumda ufak bir WebJob için bile (Hele bir de Continious Run derseniz "Always On" da gerekeceği için Basic Tier şart olacak) bir VM almak zorunda kalabiliyorsunuz. WebJob'ınızı ne kadar çalıştığını vs kimse umursamıyor ve kullansanız da kullanmasanız da VM'in parasını ödemek zorunda kalıyorsunuz.

Bazılarınız zaten hali hazırda App Service Plan'larımız var diyebilirler. O durumda hem Azure Functions'ı hem de WebJob'larınızı isterseniz klasik App Service Plan'lara da atabilirsiniz. Fakat tabi ki bu durumda Azure Functions'ın Serverless özelliğinden kopmuş oluyorsunuz.

HTTP API Sunarken

Webjobs ile HTTP API host etmek ayrı bir dertti. WebJobs kendisi Kudu SCM üzerine kurulu olduğu için HTTP endpoint verebilse de Kudu'nun authentication yapısından geçmek zorunda kalıyorduk. Bu da tabi ki API açmak isteyen bir developer için pek de anlamlı değil. Azure Functions'a baktığımızda ise çok daha geniş authentication seçenekleri görüyoruz. Azure Active Directory, Facebook, Google, Twitter, LiveID vs diyerek liste uzuyor. Ayrıca Azure Functions API Metadata'sını da OpenAPI specificationu ile sunabiliyor.

Visual Studio ve Araçlar

Visual Studio tarafında Webjobs biraz daha avantajlı. 2014 yılından beridir ortalıkla olduğu için WebJobs'ın Visual Studio entegrasyonu çok daha iyi. Azure Functions'ın ise Visual Studio araçları çıkalı daha bir hafta olmadı ve şu an için Preview'da. Fakat, Azure Portal'ına giderseniz tam tersi bir durum göreceksiniz. WebJobs için bir kod yazma ortamı yokken Azure Functions için full bir kod yazma ortamı söz konusu. Rahatlıkla Azure Portal'ına gidip bir function'ı sıfırdan yazıp, çalıştırabilir ve hatta debug bile edebilirsiniz.

Geri kalanlar

Bunların dışında her iki platform arasında bir fark yok. Trigger'lar aynı, binding özellikleri aynı. Birinin diğerinden üstün bir özelliği en azından ben bu yazıyı yazarken yok.

Sorularınız olursa aşağıda yorum olarak alabilirim ;)

Görüşmek üzere.

Bu yazıyı yazmam 112 dakikamı aldı. Siz normal bir okuma hızı ile ortalama 5 dakikada, göz gezdirerek ortalama 1 dakikada okuyabilirsiniz ;)

Azure Functions aslında bakarsanız eski WebJobs'ın üzerinden ilerlenilerek geniştirilmiş bir hizmet. O nedenle ikisi arasında süper benzerlikler var. Birazdan anlatacaklarım WebJobs deneyimi olanlar için epey tanıdık gelecektir. Nitekim 2014'te tam da bu konuda WebJobs yazısı yazmıştım :)

Gelelim konumuza, olayımız Azure Functions için farklı triggerlar kullanmak. Bunlardan httpTrigger'ı geçen bir yazıda görmüştük. Bu yazıda ise blobTrigger'a bakarak Azure Functions için Input Binding konusunu da göreceğiz.

Blob Trigger Ayarlamak

İlk olarak yeni bir Azure Function ekliyoruz. İçindeki functions.json'ı değiştirerek bir blobTrigger tanımı yapacağız. Aşağıdaki şekilde solution'ı şekillendirmek için ister Visual Studio için yeni gelen araçları kullanın, ister elle dosyaları oluşturun, sonuç fark etmeyecek.

Blob Trigger Örnek Proje Yapısı

[function.json]

{
  "disabled": false,
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "blobcontainer",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Gelin yukarıdaki parametreleri tek tek inceleyelim. Bindings collection'ı içerisinde şu an için bir tane binding var. Bu binding'in adının ne olduğu şu an için önemli değil, fakat bu adı "myBlob" olduğunu unutmayın. Bir sonraki adımda bunun nereye denk geleceğini göreceğiz. Binding'in type bilgisi blobTrigger olarak tanımlanmış. Bunu da anlamak pek zor değil :) Runtime'a bir blobTrigger kullanacağımızı burada belirtmiş oluyoruz. Sonraki direction parametresi önemli. Şu an için bir InputBinding yaratıyoruz. Böylece dışarıdan içeriyi tetikleme işlemi yapacağımızı da belirtmiş oluyoruz. OutputBinding olayı da söz konusu, ona da birazdan bakacağız. Gelelim son iki parametreye; birincisi path. Path parametresi dinleyeceğimiz Storage hesabının neresini dinleyeceğimizi belirliyor. Ben buradan blobContainer yazdım. Storage Account içerisinde de blobcontainer diye bir container olduğunu varsaydım. Örneği çalıştırırken bu containerı elle yaratmak gerekecek. İkinci parametre olan connection ise Storage Account'a ulaşmak için gerekli olan connection stringi alacağımız appsettings.json da key/value çiftinin key name'i.

[appsettings.json]

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "AzureWebJobsDashboard": "UseDevelopmentStorage=true"
  }
}

Ben örnekte local emülatörü kullandığım için appsettings.json'daki tüm değerleri de bu şekilde ayarladım.

Input Binding

[run.csx]

using System;

public static void Run(string myBlob, TraceWriter log)
{
    log.Info($"Blobun içinde ne var?: {myBlob}");
}

Sıra geldi Azure Function'ın kendisini yazmaya. Yukarıdaki gibi functionı yazarken dikkat etmemiz gereken tek nokta ilk parametre olan string parametrenin adının blobTrigger'ı tanımlarken kullandığımız name ile aynı olması. Buradaki bu parametreye doğrudan blob'un içeriği gelecek. Ben string kullandım çünkü containera bir TXT dosyası atacağım ama siz isterseniz bu parametreyi TextReader, Stream, ICloudBlob, CloudBlockBlob veya CloudPageBlob tipinde de tanımlayabilirsiniz. Functions runtime'ı deserialization işlemini halledecektir. Aslına bakarsanız eğer blob içerisinde schemasından emin olduğunuz bir JSON varsa doğrudan o tipi de verip geçebilirsiniz. Azure Functions bunu otomatik olarak deserialize edecektir. (Not: Şu an bu konuda bir bug var ve github'da da bu konuda Issue var :))

Blob Trigger çalışıyor

Yukarıdaki ekran görüntüsünden de anlayabileceğiniz üzere yeni bir blob geldiği anda içindeki metne ulaşabiliyoruz. Benim örneğimdeki metin "hop hop" şeklindeydi :)

Detaylar

Bu noktada özellikle blobTriggerlar ile ilgili bahsetmemiz gereken birkaç şey var. Azure Functions blobları takip edebilmek ve yaratılan bir blob için sadece bir defa çalışabilmek adına bazı kayıtlar tutuyor. Bu kayıtları tutmak için de yine storage account içerisinde azure-webjobs-hosts adında bir container yaratıyor. İsterseniz bu containerın farklı bir storage account'da olmasını da sağlayabilirsiniz. Runtime'ın baktığı config değeri appsettings.json'daki AzureWebJobsStorage altında saklı olmalı. Ben yaptığım örnekte bu değeri blobtrigger yaratırken de connection string olarak kullandım, ama siz bunları ayırabilirsiniz.

Diğer bir nokta ise bir türlü işlenemeyen bloblar. Bizim örnekte tabi bir olay yok, sadece okuma ve loglama yaptık ama sizin fonksiyonunuz tabi ki yeri geldiğinde hata da alabilecek işlemler yapıyor olabilir. Vazsayılan ayarlarında Azure Functions bir blobu işlemeyi en fazla 5 defa dener. Eğer beş defada bir blob başarılı bir şekilde fonksiyonunuz tarafından işlenemezse AzureWebJobsStorage ayarında belirlediğiniz storage account içerisinde webjobs-blobtrigger-poison adındaki kuyruğa bir mesaj atılır. Bu mesaj içerisinde FunctionId, blob tipi, container adı, blob adı ve etag bulunur. Bundan sonrasında konuyu çözmek size kalıyor.

BlobTrigger ile ilgili hoşunuza gitmeyecek olan bir diğer haber ise özellikle blob sayısı çok arttığında bir yavaşlama olması. Biraz önce de bahsettiğimiz gibi aslında yeni gelen blobları anlamanın yöntemi alınan kayıtlar ile blobları karşılaştırmak. Bu durumda özellikle binlerce blob söz konusu olduğunda ciddi bir yavaşlığa neden olabiliyor. Eğer anında tepki verebilmek istiyorsanız ileriki yazılarda bahsedeceğim QueueTrigger'ı kullanmak daha doğru olacaktır. Yok illa BlobTrigger kullanacağım diyorsanız bir tavsiyem trigger source olarak ayarladığınız containerı temiz tutarak tetikleyen blobları silmeniz. Unutmadan, azure-webjobs-hosts altındaki kayıtları da silinmiş bloblar için silmenizde fayda var.

Output Binding

Yazıyı bitirmeden output binding'e de bir bakalım. İlk önce function.json da bir output binding tanımlamamız gerekecek.

[function.json]

{
  "disabled": false,
  "bindings": [
    {
      "name": "myBlob",
      "type": "blobTrigger",
      "direction": "in",
      "path": "blobcontainer/{blobname}",
      "connection": "AzureWebJobsStorage"
    },
    {
      "name": "yourBlob",
      "type": "blob",
      "direction": "out",
      "path": "blobcontainer2/{blobname}",
      "connection": "AzureWebJobsStorage"
    }
  ]
}

Tanımladığımız ikinci Binding'in adı yourBlob :) Bu aynı anda bizim Azure Function'ın da output parametresi olacak. Binding tipimiz blob ve direction out. Buraya kadar herşey eskisiyle aynı. path kısmında olay biraz ilginçleşiyor. Bizim örnekte yapacağımız şey elimize gelen blobu aynı alıp başka bir container'a kopyalamak. Bu durumda tabi ki bir de blobun adını bilmek gerek. Aksi halde kopyalayacağımız yerde bloba ne isim vereceğimizi bilemeyiz. Bunun için path bilgisinin içine {blobname} değişkenini koydum. Aynı değişkeni çaktırmadan input bindingin içindeki pathe de ekledim. Böylece input binding ile gelirken blobun adı gelecek ve doğrudan output bindingdeki yerini alacak. Böylece binding ayarlarımızı tamamlamış olduk.

[run.csx]

using System;

public static void Run(string myBlob, TraceWriter log, out string yourBlob)
{
    yourBlob = myBlob;
}

Yukarıdaki kod ise final Azure Functions kodumuz. Kodun imzasında bakarsanız yeni bir output parametre göreceksiniz. İşte bu output parametre ile yeni blob içeriğini yeni container'a göndereceğiz. Function'ın içinde pek bir şey yaptığımız yok :) Siz daha anlamlı şeyler bulursunuz diye tahmin ediyorum.

İşte hepsi bu kadar, böylece bir input trigger olarak blob alıp sonra da output binding ile blob çıkarmış olduk. Bunu da bir Azure Function olarak deploy edebiliyoruz :) Eski WebJobs bilenler için tekrar uyarımı geçiyim, Azure Functions zaten WebJobs üzerinde çalışıyor, ana farklılıklardan biri "Consumption Plan", yani deployment modeli. Yakında her iki altyapıyı karşılaştıran bir yazı yazmayı planlıyorum ;)

Görüşürüz.

Twitter
RSS
Youtube
RSS Blog Search
Arşiv'de tüm yazıların listesi var. Yine de blog'da arama yapmak istersen tıkla!
Instagram Instagram