using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.Extensions.Logging; using Volo.Abp.DependencyInjection; using System.Diagnostics; using System.Collections.Concurrent; using Erp.Platform.DeveloperKit; namespace Erp.Platform.DynamicServices; /// /// Dynamic C# kod derleme servisi /// public class DynamicServiceCompiler : ITransientDependency { private readonly ILogger _logger; // Tenant bazlı yüklenmiş assembly'leri takip etmek için private static readonly ConcurrentDictionary> _tenantAssemblies = new(); // Assembly kaydı için delegate public static Action? NotifyAssemblyRegistration { get; set; } // Güvenlik için yasaklı namespace'ler private static readonly string[] ForbiddenNamespaces = { "System.IO", "System.Diagnostics", "System.Environment", "System.Net.Sockets", "System.Reflection.Emit", "System.Runtime.InteropServices", "Microsoft.Win32", "System.Security.Cryptography", "System.Net.NetworkInformation" }; // Güvenlik için yasaklı sınıflar/metotlar private static readonly string[] ForbiddenTypes = { "Process", "ProcessStartInfo", "ThreadStart", "File", "Directory", "FileStream", "StreamWriter", "StreamReader", "Socket", "TcpClient", "UdpClient", "HttpWebRequest", "WebClient", "Assembly.Load" }; public DynamicServiceCompiler(ILogger logger) { _logger = logger; } /// /// Kodu derler ve validate eder, ancak assembly yüklemez /// public async Task CompileAndValidateAsync(string code, Guid? tenantId = null) { var stopwatch = Stopwatch.StartNew(); try { // Güvenlik kontrolü var securityCheck = ValidateCodeSecurity(code); if (!securityCheck.Success) { return securityCheck; } // Roslyn ile derleme var compilation = CreateCompilation(code, $"DynamicAssembly_{Guid.NewGuid()}"); using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); stopwatch.Stop(); var result = new CompileResultDto { Success = emitResult.Success, CompilationTimeMs = stopwatch.ElapsedMilliseconds, Errors = new List(), Warnings = new List() }; // Hataları ve uyarıları topla foreach (var diagnostic in emitResult.Diagnostics) { var error = new CompilationErrorDto { Code = diagnostic.Id, Message = diagnostic.GetMessage(), Severity = diagnostic.Severity.ToString(), Line = diagnostic.Location.GetLineSpan().StartLinePosition.Line + 1, Column = diagnostic.Location.GetLineSpan().StartLinePosition.Character + 1, FileName = diagnostic.Location.SourceTree?.FilePath ?? "DynamicCode.cs" }; if (diagnostic.Severity == DiagnosticSeverity.Error) { result.Errors.Add(error); } else if (diagnostic.Severity == DiagnosticSeverity.Warning) { result.Warnings.Add(error.Message); result.HasWarnings = true; } } if (!result.Success) { result.ErrorMessage = $"Derleme {result.Errors.Count} hata ile başarısız oldu."; } _logger.LogInformation("Kod derlemesi tamamlandı. Başarılı: {Success}, Süre: {Time}ms, Hata sayısı: {ErrorCount}", result.Success, stopwatch.ElapsedMilliseconds, result.Errors?.Count ?? 0); return result; } catch (Exception ex) { stopwatch.Stop(); _logger.LogError(ex, "Kod derlemesi sırasında beklenmeyen hata"); return new CompileResultDto { Success = false, ErrorMessage = $"Derleme sırasında hata: {ex.Message}", CompilationTimeMs = stopwatch.ElapsedMilliseconds, Errors = new List() }; } } /// /// Kodu derler ve belirtilen tenant için assembly yükler /// public async Task CompileAndRegisterForTenantAsync(Guid tenantId, string code, string assemblyName) { try { // Önce validate et var validateResult = await CompileAndValidateAsync(code, tenantId); if (!validateResult.Success) { return validateResult; } // Assembly oluştur var compilation = CreateCompilation(code, assemblyName); using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); if (!emitResult.Success) { return validateResult; // Zaten hata bilgisi mevcut } ms.Seek(0, SeekOrigin.Begin); // Tenant'a özel assembly load context var contextName = $"Tenant_{tenantId}_Context"; var loadContext = new AssemblyLoadContext(contextName, isCollectible: true); var assembly = loadContext.LoadFromStream(ms); var appServiceTypes = assembly.GetTypes() .Where(t => IsApplicationServiceType(t)) .ToList(); _tenantAssemblies.AddOrUpdate(tenantId, new List { assembly }, (key, existing) => { existing.Add(assembly); return existing; }); NotifyAssemblyRegistration?.Invoke(tenantId, assembly, assemblyName); return new CompileResultDto { Success = true, CompilationTimeMs = validateResult.CompilationTimeMs, LoadedAssembly = assembly }; } catch (Exception ex) { _logger.LogError(ex, "Assembly yükleme sırasında hata. Tenant: {TenantId}", tenantId); return new CompileResultDto { Success = false, ErrorMessage = $"Assembly yükleme hatası: {ex.Message}", Errors = new List() }; } } /// /// Kod güvenlik kontrolü /// private CompileResultDto ValidateCodeSecurity(string code) { var errors = new List(); // Yasaklı namespace kontrolü foreach (var forbiddenNs in ForbiddenNamespaces) { if (code.Contains($"using {forbiddenNs}") || code.Contains($"{forbiddenNs}.")) { errors.Add(new CompilationErrorDto { Code = "SECURITY001", Message = $"Güvenlik nedeniyle '{forbiddenNs}' namespace'i kullanılamaz", Severity = "Error", Line = GetLineNumber(code, forbiddenNs), Column = 1 }); } } // Yasaklı tip kontrolü foreach (var forbiddenType in ForbiddenTypes) { if (code.Contains(forbiddenType)) { errors.Add(new CompilationErrorDto { Code = "SECURITY002", Message = $"Güvenlik nedeniyle '{forbiddenType}' tipi kullanılamaz", Severity = "Error", Line = GetLineNumber(code, forbiddenType), Column = 1 }); } } return new CompileResultDto { Success = errors.Count == 0, ErrorMessage = errors.Count > 0 ? "Güvenlik kontrolü başarısız" : null, Errors = errors }; } /// /// Roslyn compilation oluşturur /// private CSharpCompilation CreateCompilation(string code, string assemblyName) { var syntaxTree = CSharpSyntaxTree.ParseText(code); var references = GetDefaultReferences(); return CSharpCompilation.Create( assemblyName, new[] { syntaxTree }, references, new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Debug, allowUnsafe: false // Güvenlik için unsafe kod yasağı )); } /// /// Varsayılan assembly referanslarını döner /// private List GetDefaultReferences() { var references = new List(); // .NET Core temel referanslar var runtimeAssemblies = new[] { typeof(object).Assembly, // System.Private.CoreLib typeof(Console).Assembly, // System.Console typeof(System.ComponentModel.DataAnnotations.RequiredAttribute).Assembly, // System.ComponentModel.DataAnnotations typeof(System.Linq.Enumerable).Assembly, // System.Linq typeof(System.Collections.Generic.List<>).Assembly, // System.Collections Assembly.Load("System.Runtime"), Assembly.Load("System.Collections"), Assembly.Load("netstandard") }; foreach (var assembly in runtimeAssemblies) { try { references.Add(MetadataReference.CreateFromFile(assembly.Location)); } catch (Exception ex) { _logger.LogWarning(ex, "Assembly referansı eklenemedi: {Assembly}", assembly.FullName); } } // ABP Framework referansları try { var abpAssemblies = AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) .Where(a => !string.IsNullOrEmpty(a.FullName) && (a.FullName.Contains("Volo.Abp") || a.FullName.Contains("Erp.Platform") || a.FullName.Contains("Microsoft.AspNetCore"))) .ToList(); foreach (var assembly in abpAssemblies) { references.Add(MetadataReference.CreateFromFile(assembly.Location)); } } catch { } return references; } /// /// Bir tipin ApplicationService olup olmadığını kontrol eder /// private bool IsApplicationServiceType(Type type) { try { return !type.IsAbstract && !type.IsInterface && type.IsClass && (type.Name.EndsWith("AppService") || type.Name.EndsWith("ApplicationService")) && HasApplicationServiceBase(type); } catch { return false; } } /// /// Tipin ApplicationService base class'ından türediğini kontrol eder /// private bool HasApplicationServiceBase(Type type) { var currentType = type.BaseType; while (currentType != null) { if (currentType.Name.Contains("ApplicationService")) { return true; } currentType = currentType.BaseType; } return false; } /// /// Kod içinde belirli bir string'in satır numarasını bulur /// private int GetLineNumber(string code, string searchText) { var lines = code.Split('\n'); for (int i = 0; i < lines.Length; i++) { if (lines[i].Contains(searchText)) { return i + 1; } } return 1; } /// /// Assembly içindeki ApplicationService'lerden endpoint listesini çıkarır /// public List ExtractEndpointsFromAssembly(Assembly assembly, string serviceName) { var endpoints = new List(); try { // Assembly içindeki ApplicationService türlerini bul var appServiceTypes = assembly.GetTypes() .Where(t => IsApplicationServiceType(t) && t.Name == serviceName) .ToList(); foreach (var serviceType in appServiceTypes) { // Controller adını oluştur (ABP convention: CustomerAppService -> Customer) var controllerName = serviceType.Name; if (controllerName.EndsWith("AppService")) { controllerName = controllerName.Substring(0, controllerName.Length - "AppService".Length); } else if (controllerName.EndsWith("ApplicationService")) { controllerName = controllerName.Substring(0, controllerName.Length - "ApplicationService".Length); } // ABP kebab-case convention (DynamicCustomer -> dynamic-customer) var routePrefix = ToKebabCase(controllerName); // Public method'ları bul (async method'lar genelde Async suffix'i ile biter) var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Where(m => !m.IsSpecialName) // Property getter/setter'ları hariç tut .ToList(); foreach (var method in methods) { // Method adından Async suffix'ini kaldır ve kebab-case'e çevir var methodName = method.Name; if (methodName.EndsWith("Async")) { methodName = methodName.Substring(0, methodName.Length - "Async".Length); } var methodRoute = ToKebabCase(methodName); // HTTP verb'ü belirle (basit heuristic) var httpVerb = DetermineHttpVerb(method); // Endpoint'i oluştur var endpoint = $"{httpVerb} /api/app/{routePrefix}/{methodRoute}"; endpoints.Add(endpoint); } } } catch (Exception ex) { _logger.LogError(ex, "Endpoint çıkarma sırasında hata"); } return endpoints; } /// /// PascalCase'i kebab-case'e çevirir (DynamicCustomer -> dynamic-customer) /// private string ToKebabCase(string value) { if (string.IsNullOrEmpty(value)) return value; return string.Concat( value.Select((x, i) => i > 0 && char.IsUpper(x) ? "-" + x.ToString() : x.ToString()) ).ToLower(); } /// /// Method'un HTTP verb'ünü belirler (ABP convention'a göre) /// private string DetermineHttpVerb(MethodInfo method) { var methodName = method.Name.ToLowerInvariant(); // ABP naming convention if (methodName.StartsWith("get") || methodName.StartsWith("find") || methodName.StartsWith("list")) return "GET"; if (methodName.StartsWith("create") || methodName.StartsWith("insert") || methodName.StartsWith("add")) return "POST"; if (methodName.StartsWith("update") || methodName.StartsWith("edit")) return "PUT"; if (methodName.StartsWith("delete") || methodName.StartsWith("remove")) return "DELETE"; // Default olarak GET (ABP'de parametre yoksa GET, varsa POST) var parameters = method.GetParameters(); if (parameters.Length == 0 || parameters.All(p => p.ParameterType.IsValueType || p.ParameterType == typeof(string))) { return "GET"; } return "POST"; } }