Mapping (Haritalama) Rehberi
Bu doküman, workflow betikleriyle girdi/çıktı haritalaması yapmayı ve ScriptContext, ScriptResponse, ScriptBase kullanımını kavramsal olarak anlatır. Arabirim sözleşmelerinin tam listesi ve imza detayları Interfaces sayfasındadır.
İçindekiler
- Mapping Nedir?
- Mapping Türleri
- Script Engine
- ScriptContext Kullanımı
- ScriptResponse Kullanımı
- ScriptBase Kullanımı
- Implementasyon Örnekleri
- Notification Mapping
- Best Practices
- Hata Yönetimi
Mapping Nedir?
Platformda haritalama, görev veya akış bağlamına veri bağlamayı ifade eder: göreve hangi verilerin gideceğini hazırlamak, çıktının örnek verisinde nasıl tutulacağını düzenlemek ve gerektiğinde geçiş (transition) yükünü dönüştürmektir.
- Task mapping: Görev çalışmadan önce input/cihaz yapılandırması; sonra output işleme.
- Subflow / subprocess: Üst-alt akış veya bağımsız alt süreç için input (ve alt akış için output) bağlama mantığı arabirimlerle ifade edilir — detaylar için Interfaces.
- Timer / condition: Zamanlama ve otomatik geçiş kuralları ayrı mapping türleridir — yine Interfaces.
Mapping Türleri
Haritalama temelde iki alanda yapılır:
InputMapping
Göreve iletilecek verileri hazırlamak için kullanılır. Task çalıştırılmadan önce:
- Input verilerini dönüştürme
- Task yapılandırmasını ayarlama
- Başlık ve kimlik doğrulama bilgisi ekleme
- Doğrulama
OutputMapping
Görevden dönen verileri işlemek için kullanılır. Task çalıştırıldıktan sonra:
- Yanıtı dönüştürme
- Instance verisiyle birleştirme
- Hata işleme
- Denetim günlükleri ve metadata
Script Engine
Mapping betikleri Roslyn ile derlenir ve çalıştırılır.
C# sürüm desteği
Mapping motoru C# 12 özelliklerini destekler; örneğin:
- Collection expressions (
[1, 2, 3]) - Primary constructors
- Inline arrays
- Lambda ifadelerinde opsiyonel parametreler
requiredüyeler
Varsayılan using’ler
Aşağıdaki namespace’ler betiklerde otomatik kullanılabilir:
| Namespace | Açıklama |
|---|---|
System | Temel sistem tipleri |
System.Collections.Generic | Generic koleksiyonlar |
System.Linq | LINQ |
System.Threading.Tasks | async/await |
System.Text.Json | JSON serileştirme |
Örnek — JsonSerializer:
public async Task<ScriptResponse> OutputHandler(ScriptContext context)
{
var response = context.Body;
var json = JsonSerializer.Serialize(response);
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
return new ScriptResponse
{
Data = new { serialized = json }
};
}
Encoding (B64 / NAT) ve transition şemasında mapping
Betik içeriği şemada encoding ile belirlenir:
- B64: BASE64 kodlanmış kod (
locationalanı gerekebilir) - NAT: Doğrudan C# kodu (
locationolmadan da kullanılabilir)
BASE64 örneği:
{
"key": "approve-order",
"source": "pending-approval",
"target": "approved",
"triggerType": 0,
"labels": [
{ "language": "tr-TR", "label": "Siparişi Onayla" }
],
"mapping": {
"location": "./OrderApprovalTransitionMapping.csx",
"code": "dXNpbmcgU3lzdGVtLlRocmVhZGluZy5UYXNrczsKdXNpbmc...",
"encoding": "B64"
}
}
NAT örneği:
{
"key": "approve-order",
"source": "pending-approval",
"target": "approved",
"triggerType": 0,
"labels": [
{ "language": "tr-TR", "label": "Siparişi Onayla" }
],
"mapping": {
"code": "public class OrderApprovalTransitionMapping : ScriptBase, ITransitionMapping { ... }",
"encoding": "NAT"
}
}
ScriptContext Kullanımı
ScriptContext, betik çalışırken elinizdeki bağlamdır: örnek, akış tanımı, geçiş, istek gövdesi/başlıkları ve tamamlanan görev yanıtları.
Özet özellikler
public sealed class ScriptContext
{
public dynamic? Body { get; private set; }
public dynamic? Headers { get; private set; }
public dynamic? QueryParameters { get; private set; }
public dynamic? RouteValues { get; private set; }
public Instance Instance { get; private set; }
public Definitions.Workflow Workflow { get; private set; }
public Transition Transition { get; private set; }
public IRuntimeInfoProvider Runtime { get; private set; }
public Dictionary<string, dynamic> Definitions { get; private set; }
public Dictionary<string, dynamic?> TaskResponse { get; private set; }
public Dictionary<string, dynamic> MetaData { get; private set; }
public sealed class Builder { /* ... */ }
}
Body
İstek gövdesi veya göreve özgü yük dinamiktir.
var userId = context.Body?.userId;
var amount = context.Body?.amount;
Headers
Tüm başlık adları küçük harfe normalize edilir.
var authorization = context.Headers?.authorization;
var contentType = context.Headers?.["content-type"];
var userAgent = context.Headers?.["user-agent"];
QueryParameters
Yalnızca Function görevleri için anlamlıdır; dizin ile erişilir.
var userId = context.QueryParameters?.["userId"];
var cityId = context.QueryParameters?.["cityId"];
var page = context.QueryParameters?.["page"];
Function görevleri için örnek sınıf (query parametrelerini okuma):
public class FunctionTaskMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var userId = context.QueryParameters?.["userId"];
var cityId = context.QueryParameters?["cityId"];
LogInformation("Function işleniyor - userId: {0}, cityId: {1}",
args: new object?[] { userId, cityId });
return Task.FromResult(new ScriptResponse());
}
public Task<ScriptResponse> OutputHandler(ScriptContext context) =>
Task.FromResult(new ScriptResponse());
}
Instance.Data
Workflow boyunca biriken veri burada tutulur; property adları camelCase olarak tutulur.
var userInfo = context.Instance.Data.userInfo;
var paymentSchedule = context.Instance.Data.paymentSchedule;
var currentLogin = context.Instance.Data.login.currentLogin;
TaskResponse
Tamamlanan görevlerin sonuçları; output handler’larda sık kullanılır.
var httpTaskResult = context.TaskResponse["httpTask"];
var scriptTaskResult = context.TaskResponse["scriptTask"];
CurrentTransition
Input ve transition mapping’lerde orijinal geçiş isteğinin gövdesi ve başlıklarına dokunmak için kullanılır (instance’a yazmadan pipeline içinde kullanım).
- Data: Orijinal geçiş gövdesi (
dynamic). - Header: Orijinal başlıklar (anahtarlar küçük harf).
var transitionBody = context.CurrentTransition.Data;
var authHeader = context.CurrentTransition.Header?.authorization;
ScriptResponse Kullanımı
ScriptResponse, mapping betiklerinden dönen standart modeldir: örnek verisine birleştirilecek yük, audit/metadata ve etiketler.
Yapı
public sealed class ScriptResponse
{
public string? Key { get; set; }
public dynamic? Data { get; set; }
public dynamic? Headers { get; set; }
public dynamic? RouteValues { get; set; }
public string[] Tags { get; set; } = [];
}
- Key: Yanıtı tanımlayan anahtar
- Data: Birleştirilecek ana yük
- Headers, RouteValues: Bağlam göre kullanılan metadata/parametreler
- Tags: Etiketleme / filtreleme
Önemli uyarılar — ScriptResponse.Data
Kritik:
Datadynamic olduğu için derleme zamanı güvenliği yoktur; null ve serileştirme davranışına dikkat edin.
- Tip güvenliği
// ❌ Karmaşık tipler sorun çıkarabilir
response.Data = new { invalidProperty = someComplexObject };
// ✅ Basit, serileştirilebilir alanlar
response.Data = new { success = true, userId = 123 };
- Null güvenliği
// ❌
var result = context.Body.data.result;
// ✅
var result = context.Body?.data?.result;
- Serileştirme
// ❌ Serileştirilmeyebilir
response.Data = new { complexObject = new SomeComplexClass() };
// ✅
response.Data = new
{
id = obj.Id,
name = obj.Name,
timestamp = DateTime.UtcNow
};
- İsimlendirme — camelCase
// ❌ PascalCase instance birleştirmesiyle uyumsuz olabilir
response.Data = new { UserId = 123, PaymentStatus = "success" };
// ✅
response.Data = new { userId = 123, paymentStatus = "success" };
Kısa kullanım
return new ScriptResponse
{
Data = new { success = true, userId = 123 }
};
return new ScriptResponse
{
Data = requestData,
Headers = new { Authorization = "Bearer " + token },
Tags = new[] { "authentication", "success" }
};
ScriptBase Kullanımı
ScriptBase, secret store, yapılandırma, günlük ve yardımcı metodları betiklere taşır.
Secret yönetimi
Dapr secret store üzerinden tekil veya toplu secret okuma:
public abstract class ScriptBase
{
protected string GetSecret(string storeName, string secretStore, string secretKey);
protected Task<string> GetSecretAsync(string storeName, string secretStore, string secretKey);
protected Dictionary<string, string> GetSecrets(string storeName, string secretStore);
protected Task<Dictionary<string, string>> GetSecretsAsync(string storeName, string secretStore);
}
Örnek:
public class MyMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var apiKey = GetSecret("dapr_store", "secret_store", "api_key");
var httpTask = task as HttpTask;
httpTask.SetHeaders(new Dictionary<string, string>
{
["Authorization"] = $"Bearer {apiKey}"
});
return Task.FromResult(new ScriptResponse());
}
public Task<ScriptResponse> OutputHandler(ScriptContext context) =>
Task.FromResult(new ScriptResponse());
}
Property yardımcıları
public abstract class ScriptBase
{
protected bool HasProperty(object obj, string propertyName);
protected static object? GetPropertyValue(object obj, string propertyName);
protected static T? GetPropertyValue<T>(object obj, string propertyName);
protected static T GetPropertyValue<T>(object obj, string propertyName, T defaultValue);
}
Örnek:
public class SafePropertyAccessMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
if (HasProperty(context.Instance.Data, "card"))
{
var cardData = GetPropertyValue(context.Instance.Data, "card");
if (HasProperty(cardData, "products"))
{
var productsData = GetPropertyValue(cardData, "products");
// ...
}
}
var userId = GetPropertyValue<int>(context.Instance.Data, "userId");
var amount = GetPropertyValue<decimal>(context.Body, "amount");
var isActive = GetPropertyValue<bool>(context.Instance.Data, "isActive");
if (userId.HasValue && amount.HasValue)
{
LogInformation("Kullanıcı {0} için {1} tutarında işlem",
args: new object?[] { userId.Value, amount.Value });
}
return Task.FromResult(new ScriptResponse());
}
public Task<ScriptResponse> OutputHandler(ScriptContext context) =>
Task.FromResult(new ScriptResponse());
}
Koleksiyon ve dynamic nesne yardımcıları
HasProperty / GetPropertyValue ile birlikte Instance.Data altındaki listeler için kullanılır.
| Metod | Amaç |
|---|---|
CreateObject() | SetProperty ile doldurulacak yeni dynamic nesne |
CreateList() | Yeni boş liste |
SetProperty(obj, name, value) | Özellik atama |
RemoveProperty(obj, name) | Kaldırıldıysa true |
ToDictionary(obj) | Dictionary<string, object> kopyası; null → boş sözlük |
GetList(instanceData, propertyName) | İç içe veriden liste okuma |
AsList(value) | Güvenli listeye normalize |
ListAdd, ListRemove, ListFilter, ListCount, ListAny, ListFirst, ListLast, ListSelect<TResult> | Liste işlemleri |
var items = GetList(context.Instance?.Data, "lineItems");
var active = ListFilter(items, x => x.status == "active");
var codes = ListSelect<string>(active, x => (string)x.productCode);
Önemli (CS1977):
dynamickaynaktan gelen listeyi doğrudan lambda ile kullanmak derleme hatasına yol açar. ÖnceAsList(...)veyaGetList(...)ileList<object?>tipine dönüştürün, ardındanListFilter,ListAnyvb. çağırın.// ❌ Derleme hatası — dynamic argüman + lambda aynı çağrıdaListAny(context.Instance.Data.items, x => x.status == "pending");// ✅ Önce listeye dönüştürvar items = AsList(context.Instance.Data.items);var hasPending = ListAny(items, x => x.status == "pending");
XML yardımcıları
SOAP Task ve XML tabanlı entegrasyonlarda ham XML verisini parse etmek veya XmlDocument'ı stringe çevirmek için kullanılır.
| Metod | Dönüş | Açıklama |
|---|---|---|
ParseXml(string? xmlString) | XmlDocument? | XML stringini parse eder. Boş/null input veya parse hatası durumunda null döner, asla fırlatmaz |
XmlToString(XmlDocument? xmlDoc) | string? | XmlDocument'ı XML stringine çevirir. null input için null döner |
public Task<ScriptResponse> OutputHandler(ScriptContext context)
{
var rawXml = context.Body?.data?.ToString();
var doc = ParseXml(rawXml);
if (doc == null)
{
return Task.FromResult(new ScriptResponse
{
Key = "xml-parse-error",
Data = new { error = "Geçersiz XML yanıtı" }
});
}
var ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/");
var fault = doc.SelectSingleNode("//soap:Fault", ns);
if (fault != null)
{
return Task.FromResult(new ScriptResponse
{
Key = "soap-fault",
Data = new { error = fault.InnerText }
});
}
// Gerekirse belgeyi tekrar stringe çevir
var resultXml = XmlToString(doc);
return Task.FromResult(new ScriptResponse
{
Key = "soap-success",
Data = new { xml = resultXml }
});
}
Loglama
Tüm log metodları çağıran dosya/satır bilgisini otomatik ekler. Parametreli mesajlarda args kullanın.
public abstract class ScriptBase
{
protected void LogTrace(string message, ...);
protected void LogDebug(string message, ...);
protected void LogInformation(string message, ...);
protected void LogWarning(string message, ...);
protected void LogError(string message, ...);
protected void LogCritical(string message, ...);
}
public class PaymentProcessingMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
LogInformation("Kullanıcı için ödeme işlemi başlatılıyor: {0}",
args: new object?[] { context.Instance.Data.userId });
try
{
var amount = context.Body?.amount;
if (amount == null || amount <= 0)
{
LogWarning("Geçersiz ödeme tutarı alındı: {0}", args: new object?[] { amount });
return Task.FromResult(new ScriptResponse
{
Data = new { error = "Geçersiz tutar" }
});
}
LogDebug("Ödeme tutarı işleniyor: {0}", args: new object?[] { amount });
LogInformation("Ödeme başarıyla işlendi");
return Task.FromResult(new ScriptResponse { Data = new { success = true } });
}
catch (Exception ex)
{
LogError("Ödeme işlemi başarısız oldu: {0}", args: new object?[] { ex.Message });
throw;
}
}
public Task<ScriptResponse> OutputHandler(ScriptContext context)
{
LogTrace("OutputHandler çağrıldı, durum kodu: {0}",
args: new object?[] { context.Body?.statusCode });
return Task.FromResult(new ScriptResponse());
}
}
Konfigürasyon
public abstract class ScriptBase
{
protected string? GetConfigValue(string key);
protected string GetConfigValue(string key, string defaultValue);
protected T? GetConfigValue<T>(string key);
protected T GetConfigValue<T>(string key, T defaultValue);
protected string? GetConnectionString(string name);
protected bool ConfigExists(string key);
}
Örnek:
public class ConfigAwareMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var apiUrl = GetConfigValue("ExternalApi:BaseUrl");
var timeout = GetConfigValue("ExternalApi:Timeout", "30");
var maxRetries = GetConfigValue<int>("ExternalApi:MaxRetries", 3);
var enableLogging = GetConfigValue<bool>("Features:EnableDetailedLogging", false);
if (ConfigExists("ExternalApi:SecondaryUrl"))
{
var secondaryUrl = GetConfigValue("ExternalApi:SecondaryUrl");
LogInformation("Secondary URL yapılandırıldı: {0}", args: new object?[] { secondaryUrl });
}
var dbConnection = GetConnectionString("DefaultConnection");
if (enableLogging)
{
LogDebug("API URL: {0}, Timeout: {1}, Max Retries: {2}",
args: new object?[] { apiUrl, timeout, maxRetries });
}
var httpTask = task as HttpTask;
httpTask.SetUrl(apiUrl);
httpTask.SetTimeout(int.Parse(timeout));
return Task.FromResult(new ScriptResponse());
}
public Task<ScriptResponse> OutputHandler(ScriptContext context) =>
Task.FromResult(new ScriptResponse());
}
Örnek appsettings yapısı (hiyerarşik anahtarlar : ile):
{
"ExternalApi": {
"BaseUrl": "https://api.example.com",
"Timeout": "30",
"MaxRetries": 3,
"SecondaryUrl": "https://backup-api.example.com"
},
"Features": { "EnableDetailedLogging": true },
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=mydb;..."
}
}
Implementasyon Örnekleri
Aşağıdaki sınıflar yalnızca örnektir; arabirim sözleşmesi için Interfaces sayfasına bakın.
HTTP görev mapping
public class AddToCartMapping : ScriptBase, IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var httpTask = task as HttpTask;
if (httpTask == null)
throw new ArgumentException("Task must be of type HttpTask");
var headers = new Dictionary<string, string?>
{
["Authorization"] = $"Bearer {context.Instance.Data?.login?.currentLogin?.accessToken}"
};
httpTask.SetHeaders(headers);
// Örnek: ürün ekleme sonrası sepet nesnesi üretilir
object? cartData = null;
return Task.FromResult(new ScriptResponse
{
Data = new { cart = cartData }
});
}
public Task<ScriptResponse> OutputHandler(ScriptContext context)
{
var statusCode = context.Body?.statusCode ?? 500;
if (statusCode >= 200 && statusCode <= 300)
{
return Task.FromResult(new ScriptResponse
{
Data = new
{
success = true,
cart = new
{
success = true,
id = context.Body?.data?.id,
products = context.Body?.data?.products
}
}
});
}
return Task.FromResult(new ScriptResponse { Data = new { success = false } });
}
}
Ödeme (payment) mapping
public class ProcessPaymentMapping : IMapping
{
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var httpTask = task as HttpTask;
var paymentRequest = new
{
scheduleId = context.Instance?.Data?.paymentSchedule?.scheduleId,
userId = context.Instance?.Data?.userId,
amount = context.Instance?.Data?.amount,
currency = context.Instance?.Data?.currency,
processedAt = DateTime.UtcNow
};
httpTask.SetBody(paymentRequest);
httpTask.SetHeaders(new Dictionary<string, string?>
{
["Content-Type"] = "application/json",
["X-Payment-Request-Id"] = Guid.NewGuid().ToString()
});
return Task.FromResult(new ScriptResponse());
}
public async Task<ScriptResponse> OutputHandler(ScriptContext context)
{
var statusCode = context.Body?.statusCode ?? 500;
if (statusCode >= 200 && statusCode < 300)
{
return new ScriptResponse
{
Key = "payment-success",
Data = new
{
paymentResult = new
{
status = "success",
transactionId = context.Body?.data?.transactionId,
processedAt = DateTime.UtcNow
}
},
Tags = new[] { "payments", "success" }
};
}
return new ScriptResponse
{
Key = "payment-failure",
Data = new { error = "Payment failed" },
Tags = new[] { "payments", "failure" }
};
}
}
Koşul (condition) mapping
public class AuthorizationSuccessRule : IConditionMapping
{
public async Task<bool> Handler(ScriptContext context)
{
return context.Instance.Data.authentication?.success == true;
}
}
Timer mapping
public class PaymentDueTimerRule : ITimerMapping
{
public async Task<TimerSchedule> Handler(ScriptContext context)
{
try
{
var paymentSchedule = context.Instance.Data.paymentSchedule;
if (paymentSchedule == null)
return TimerSchedule.FromDuration(TimeSpan.FromDays(1));
var frequency = paymentSchedule.frequency?.ToString().ToLower() ?? "monthly";
return frequency switch
{
"daily" => TimerSchedule.FromDateTime(DateTime.UtcNow.Date.AddDays(1).AddHours(9)),
"weekly" => TimerSchedule.FromDuration(TimeSpan.FromDays(7)),
"monthly" => TimerSchedule.FromDuration(TimeSpan.FromDays(30)),
"immediate" => TimerSchedule.Immediate(),
_ => TimerSchedule.FromDuration(TimeSpan.FromDays(30))
};
}
catch (Exception)
{
return TimerSchedule.FromDuration(TimeSpan.FromDays(1));
}
}
}
public class BusinessHoursTimerRule : ITimerMapping
{
public async Task<TimerSchedule> Handler(ScriptContext context)
{
var now = DateTime.UtcNow;
var isWeekend = now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday;
var hour = now.Hour;
if (isWeekend || hour < 9 || hour >= 17)
{
var nextBusinessDay = now.Date.AddDays(1);
while (nextBusinessDay.DayOfWeek == DayOfWeek.Saturday
|| nextBusinessDay.DayOfWeek == DayOfWeek.Sunday)
{
nextBusinessDay = nextBusinessDay.AddDays(1);
}
return TimerSchedule.FromDateTime(nextBusinessDay.AddHours(9));
}
return TimerSchedule.FromDuration(TimeSpan.FromHours(1));
}
}
Timer yardımcıları — özet:
TimerSchedule.FromDateTime(DateTime.UtcNow.AddHours(2));
TimerSchedule.FromDuration(TimeSpan.FromMinutes(30));
TimerSchedule.Immediate();
Transition mapping
Mapping yoksa geçiş gövdesi doğrudan instance verisine birleştirilir. Transition mapping ile dönüşümü siz kontrol edersiniz (ITransitionMapping — detay Interfaces).
Davranış Değişikliği New: Transition mapping tanımlı olduğunda, mapping output değeri artık transition history'ye de yazılır. Önceki davranışta mapping output yalnızca instance data'ya aktarılırken, transition history ham (raw) payload'ı kaydediyordu. Yeni davranış ile her iki hedefte de mapping çıktısı kullanılır.
public class OrderApprovalTransitionMapping : ScriptBase, ITransitionMapping
{
public async Task<dynamic> Handler(ScriptContext context)
{
LogInformation("Sipariş onay transition'ı işleniyor");
return new
{
approval = new
{
approvedBy = context.Body?.userId,
approvedAt = DateTime.UtcNow,
comments = context.Body?.comments ?? "Yorum yok",
status = "approved"
}
};
}
}
public class PaymentTransitionMapping : ScriptBase, ITransitionMapping
{
public async Task<dynamic> Handler(ScriptContext context)
{
var amount = context.Body?.amount;
var currency = context.Body?.currency;
if (amount == null || amount <= 0)
{
LogWarning("Geçersiz ödeme tutarı: {0}", args: new object?[] { amount });
throw new ArgumentException("Geçerli tutar gereklidir");
}
if (string.IsNullOrEmpty(currency))
{
currency = "TRY";
LogDebug("Para birimi belirtilmedi, TRY varsayılan olarak ayarlandı");
}
return new
{
payment = new
{
amount = decimal.Parse(amount.ToString()),
currency = currency.ToString().ToUpper(),
requestedAt = DateTime.UtcNow,
status = "pending"
}
};
}
}
Koşullu geçiş yükü:
public class ConditionalTransitionMapping : ScriptBase, ITransitionMapping
{
public async Task<dynamic> Handler(ScriptContext context)
{
var actionType = context.Body?.actionType?.ToString();
LogDebug("İşlem tipi işleniyor: {0}", args: new object?[] { actionType });
return actionType switch
{
"approve" => new { status = "approved", approvedBy = context.Body?.userId, approvedAt = DateTime.UtcNow },
"reject" => new
{
status = "rejected",
rejectedBy = context.Body?.userId,
rejectedAt = DateTime.UtcNow,
reason = context.Body?.reason
},
"defer" => new
{
status = "deferred",
deferredBy = context.Body?.userId,
deferredUntil = context.Body?.deferUntil ?? DateTime.UtcNow.AddDays(1)
},
_ => new { status = "pending", message = "Bilinmeyen işlem tipi" }
};
}
}
Mevcut alanları koruyarak genişletme:
public async Task<dynamic> Handler(ScriptContext context)
{
var existingData = context.Instance.Data;
return new
{
preservedField = existingData?.preservedField,
newField = context.Body?.newField,
updatedAt = DateTime.UtcNow
};
}
Transition mapping için ek öneriler
- Girdiyi doğrulayın; null ve tipleri kontrol edin.
- Önemli adımları loglayın.
- Riskli blokları try-catch ile sarın veya anlamlı istisna fırlatın.
- Dönüşümde camelCase kullanın.
- Ağır iş yükünden kaçının; odak veri şekillendirmedir.
- Beklenen payload’ı kodda/yorumda kısaca dokümante edin.
Notification mapping
Notification Task (type: 10) çok kanallı bildirim gönderir. Kanal bazında mesaj üretimi INotificationMapping ile, state kanalı metadata zenginleştirme ise opsiyonel IStateNotificationMapping ile yapılır. Arabirim detayları için Interfaces sayfasına bakın.
Kanal bazlı mesaj üretimi
ChannelHandler her kanal için ayrı çağrılır; channel parametresine göre dallanma yapılabilir:
public class NotifyMapping : INotificationMapping
{
public Task<NotificationMessage?> ChannelHandler(string channel, ScriptContext context)
{
var instance = context.Instance;
return Task.FromResult<NotificationMessage?>(new NotificationMessage
{
Data = new
{
instanceId = instance?.Id.ToString(),
state = instance?.CurrentState,
channel
},
Metadata = new Dictionary<string, string>
{
["X-Correlation-Id"] = instance?.Id.ToString() ?? string.Empty
}
});
}
}
Koşullu kanal atlama
Belirli koşullarda bir kanalı atlamak için null döndürülür:
public class ConditionalNotifyMapping : INotificationMapping
{
public Task<NotificationMessage?> ChannelHandler(string channel, ScriptContext context)
{
var instance = context.Instance;
if (channel == "sms" && !InstanceStatus.Completed.Equals(instance?.Status))
return Task.FromResult<NotificationMessage?>(null);
return Task.FromResult<NotificationMessage?>(new NotificationMessage
{
Data = new { instanceId = instance?.Id.ToString(), channel }
});
}
}
State kanalı metadata zenginleştirme
state kanalının verisi platform tarafından üretilir; IStateNotificationMapping yalnızca ek metadata eklemek için kullanılır:
public class StateEnrichMapping : IStateNotificationMapping
{
public Task<StateNotificationMetadata> EnrichAsync(ScriptContext context)
{
var headers = (IDictionary<string, object?>)context.Headers;
var metadata = new Dictionary<string, string>();
if (headers.TryGetValue("X-Device-Id", out var deviceId) && deviceId is not null)
metadata["X-Device-Id"] = deviceId.ToString()!;
if (headers.TryGetValue("X-Token-Id", out var tokenId) && tokenId is not null)
metadata["X-Token-Id"] = tokenId.ToString()!;
return Task.FromResult(new StateNotificationMetadata
{
Metadata = metadata
});
}
}
INotificationMapping ve IStateNotificationMapping aynı .csx dosyasında tek bir sınıf içinde implement edilebilir:
public class FullNotifyMapping : INotificationMapping, IStateNotificationMapping
{
public Task<NotificationMessage?> ChannelHandler(string channel, ScriptContext context)
{
// sms, email vb. kanallar için mesaj üret
}
public Task<StateNotificationMetadata> EnrichAsync(ScriptContext context)
{
// state kanalı metadata zenginleştirme
}
}
Best Practices
Hata yakalama (örnek şablon)
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
try
{
return Task.FromResult(new ScriptResponse { Data = result });
}
catch (Exception ex)
{
return Task.FromResult(new ScriptResponse
{
Key = "error",
Data = new { error = ex.Message, timestamp = DateTime.UtcNow },
Tags = new[] { "error", "exception" }
});
}
}
Null ve tip güvenliği
var userId = context.Instance?.Data?.userId ?? 0;
var amount = context.Body?.amount != null ? Convert.ToDecimal(context.Body.amount) : 0m;
if (context.Instance?.Data?.paymentSchedule != null)
{
var schedule = context.Instance.Data.paymentSchedule;
}
var httpTask = task as HttpTask;
if (httpTask == null)
throw new ArgumentException("Task must be of type HttpTask");
if (int.TryParse(context.Body?.id?.ToString(), out int id))
{
// id kullan
}
Performans — async secret
public async Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var apiKey = await GetSecretAsync("store", "secrets", "api_key");
// ...
}
Etiketleme ve izlenebilirlik
return new ScriptResponse
{
Key = "operation-result",
Data = result,
Tags = new[] { "operation", "success", "user-" + userId }
};
Hata Yönetimi
HTTP durum kodlarına göre dallanma
public Task<ScriptResponse> OutputHandler(ScriptContext context)
{
var statusCode = context.Body?.statusCode ?? 500;
return statusCode switch
{
>= 200 and < 300 => HandleSuccess(context),
400 => HandleBadRequest(context),
401 => HandleUnauthorized(context),
404 => HandleNotFound(context),
>= 500 => HandleServerError(context),
_ => HandleUnknownError(context)
};
}
Yeniden deneme ipucu
var retryCount = context.Instance?.Data?.retryCount ?? 0;
var maxRetries = context.Instance?.Data?.maxRetries ?? 3;
if (retryCount < maxRetries)
{
return new ScriptResponse
{
Data = new
{
shouldRetry = true,
retryCount = retryCount + 1,
retryAfter = TimeSpan.FromMinutes(Math.Pow(2, retryCount))
}
};
}
Toplu doğrulama
public Task<ScriptResponse> InputHandler(WorkflowTask task, ScriptContext context)
{
var errors = new List<string>();
if (context.Body?.userId == null)
errors.Add("UserId is required");
if (context.Body?.amount == null || Convert.ToDecimal(context.Body.amount) <= 0)
errors.Add("Valid amount is required");
if (errors.Any())
{
return Task.FromResult(new ScriptResponse
{
Key = "validation-error",
Data = new { errors },
Tags = new[] { "validation", "error" }
});
}
return Task.FromResult(new ScriptResponse());
}