erp-platform/api/src/Erp.Platform.Application/DeveloperKit/DynamicServiceCompiler.cs
2025-11-12 15:59:31 +03:00

490 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// Dynamic C# kod derleme servisi
/// </summary>
public class DynamicServiceCompiler : ITransientDependency
{
private readonly ILogger<DynamicServiceCompiler> _logger;
// Tenant bazlı yüklenmiş assembly'leri takip etmek için
private static readonly ConcurrentDictionary<Guid, List<Assembly>> _tenantAssemblies = new();
// Assembly kaydı için delegate
public static Action<Guid, Assembly, string>? 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<DynamicServiceCompiler> logger)
{
_logger = logger;
}
/// <summary>
/// Kodu derler ve validate eder, ancak assembly yüklemez
/// </summary>
public async Task<CompileResultDto> 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<CompilationErrorDto>(),
Warnings = new List<string>()
};
// 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<CompilationErrorDto>()
};
}
}
/// <summary>
/// Kodu derler ve belirtilen tenant için assembly yükler
/// </summary>
public async Task<CompileResultDto> 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> { 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<CompilationErrorDto>()
};
}
}
/// <summary>
/// Kod güvenlik kontrolü
/// </summary>
private CompileResultDto ValidateCodeSecurity(string code)
{
var errors = new List<CompilationErrorDto>();
// 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
};
}
/// <summary>
/// Roslyn compilation oluşturur
/// </summary>
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ğı
));
}
/// <summary>
/// Varsayılan assembly referanslarını döner
/// </summary>
private List<MetadataReference> GetDefaultReferences()
{
var references = new List<MetadataReference>();
// .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;
}
/// <summary>
/// Bir tipin ApplicationService olup olmadığını kontrol eder
/// </summary>
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;
}
}
/// <summary>
/// Tipin ApplicationService base class'ından türediğini kontrol eder
/// </summary>
private bool HasApplicationServiceBase(Type type)
{
var currentType = type.BaseType;
while (currentType != null)
{
if (currentType.Name.Contains("ApplicationService"))
{
return true;
}
currentType = currentType.BaseType;
}
return false;
}
/// <summary>
/// Kod içinde belirli bir string'in satır numarasını bulur
/// </summary>
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;
}
/// <summary>
/// Assembly içindeki ApplicationService'lerden endpoint listesini çıkarır
/// </summary>
public List<string> ExtractEndpointsFromAssembly(Assembly assembly, string serviceName)
{
var endpoints = new List<string>();
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;
}
/// <summary>
/// PascalCase'i kebab-case'e çevirir (DynamicCustomer -> dynamic-customer)
/// </summary>
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();
}
/// <summary>
/// Method'un HTTP verb'ünü belirler (ABP convention'a göre)
/// </summary>
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";
}
}