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";
}
}