erp-platform/api/src/Erp.Platform.Application/DeveloperKit/DynamicServiceCompiler.cs

491 lines
16 KiB
C#
Raw Normal View History

2025-11-11 19:49:52 +00:00
using System;
2025-11-04 22:08:36 +00:00
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;
2025-11-11 19:49:52 +00:00
using Erp.Platform.DeveloperKit;
2025-11-04 22:08:36 +00:00
2025-11-11 19:49:52 +00:00
namespace Erp.Platform.DynamicServices;
2025-11-04 22:08:36 +00:00
/// <summary>
/// Dynamic C# kod derleme servisi
/// </summary>
public class DynamicServiceCompiler : ITransientDependency
{
private readonly ILogger<DynamicServiceCompiler> _logger;
2025-11-12 12:59:31 +00:00
// Tenant bazlı yüklenmiş assembly'leri takip etmek için
2025-11-04 22:08:36 +00:00
private static readonly ConcurrentDictionary<Guid, List<Assembly>> _tenantAssemblies = new();
2025-11-12 12:59:31 +00:00
// Assembly kaydı için delegate
2025-11-04 22:08:36 +00:00
public static Action<Guid, Assembly, string>? NotifyAssemblyRegistration { get; set; }
2025-11-12 12:59:31 +00:00
// Güvenlik için yasaklı namespace'ler
2025-11-04 22:08:36 +00:00
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"
};
2025-11-12 12:59:31 +00:00
// Güvenlik için yasaklı sınıflar/metotlar
2025-11-04 22:08:36 +00:00
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>
2025-11-12 12:59:31 +00:00
/// Kodu derler ve validate eder, ancak assembly yüklemez
2025-11-04 22:08:36 +00:00
/// </summary>
public async Task<CompileResultDto> CompileAndValidateAsync(string code, Guid? tenantId = null)
{
var stopwatch = Stopwatch.StartNew();
try
{
2025-11-12 12:59:31 +00:00
// Güvenlik kontrolü
2025-11-04 22:08:36 +00:00
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>()
};
2025-11-12 12:59:31 +00:00
// Hataları ve uyarıları topla
2025-11-04 22:08:36 +00:00
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)
{
2025-11-12 12:59:31 +00:00
result.ErrorMessage = $"Derleme {result.Errors.Count} hata ile başarısız oldu.";
2025-11-04 22:08:36 +00:00
}
2025-11-12 12:59:31 +00:00
_logger.LogInformation("Kod derlemesi tamamlandı. Başarılı: {Success}, Süre: {Time}ms, Hata sayısı: {ErrorCount}",
2025-11-04 22:08:36 +00:00
result.Success, stopwatch.ElapsedMilliseconds, result.Errors?.Count ?? 0);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
2025-11-12 12:59:31 +00:00
_logger.LogError(ex, "Kod derlemesi sırasında beklenmeyen hata");
2025-11-04 22:08:36 +00:00
return new CompileResultDto
{
Success = false,
2025-11-12 12:59:31 +00:00
ErrorMessage = $"Derleme sırasında hata: {ex.Message}",
2025-11-04 22:08:36 +00:00
CompilationTimeMs = stopwatch.ElapsedMilliseconds,
Errors = new List<CompilationErrorDto>()
};
}
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Kodu derler ve belirtilen tenant için assembly yükler
2025-11-04 22:08:36 +00:00
/// </summary>
public async Task<CompileResultDto> CompileAndRegisterForTenantAsync(Guid tenantId, string code, string assemblyName)
{
try
{
2025-11-12 12:59:31 +00:00
// Önce validate et
2025-11-04 22:08:36 +00:00
var validateResult = await CompileAndValidateAsync(code, tenantId);
if (!validateResult.Success)
{
return validateResult;
}
2025-11-12 12:59:31 +00:00
// Assembly oluştur
2025-11-04 22:08:36 +00:00
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);
2025-11-12 12:59:31 +00:00
// Tenant'a özel assembly load context
2025-11-04 22:08:36 +00:00
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,
2025-11-05 13:17:10 +00:00
CompilationTimeMs = validateResult.CompilationTimeMs,
LoadedAssembly = assembly
2025-11-04 22:08:36 +00:00
};
}
catch (Exception ex)
{
2025-11-12 12:59:31 +00:00
_logger.LogError(ex, "Assembly yükleme sırasında hata. Tenant: {TenantId}", tenantId);
2025-11-04 22:08:36 +00:00
return new CompileResultDto
{
Success = false,
2025-11-12 12:59:31 +00:00
ErrorMessage = $"Assembly yükleme hatası: {ex.Message}",
2025-11-04 22:08:36 +00:00
Errors = new List<CompilationErrorDto>()
};
}
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Kod güvenlik kontrolü
2025-11-04 22:08:36 +00:00
/// </summary>
private CompileResultDto ValidateCodeSecurity(string code)
{
var errors = new List<CompilationErrorDto>();
2025-11-12 12:59:31 +00:00
// Yasaklı namespace kontrolü
2025-11-04 22:08:36 +00:00
foreach (var forbiddenNs in ForbiddenNamespaces)
{
if (code.Contains($"using {forbiddenNs}") || code.Contains($"{forbiddenNs}."))
{
errors.Add(new CompilationErrorDto
{
Code = "SECURITY001",
2025-11-12 12:59:31 +00:00
Message = $"Güvenlik nedeniyle '{forbiddenNs}' namespace'i kullanılamaz",
2025-11-04 22:08:36 +00:00
Severity = "Error",
Line = GetLineNumber(code, forbiddenNs),
Column = 1
});
}
}
2025-11-12 12:59:31 +00:00
// Yasaklı tip kontrolü
2025-11-04 22:08:36 +00:00
foreach (var forbiddenType in ForbiddenTypes)
{
if (code.Contains(forbiddenType))
{
errors.Add(new CompilationErrorDto
{
Code = "SECURITY002",
2025-11-12 12:59:31 +00:00
Message = $"Güvenlik nedeniyle '{forbiddenType}' tipi kullanılamaz",
2025-11-04 22:08:36 +00:00
Severity = "Error",
Line = GetLineNumber(code, forbiddenType),
Column = 1
});
}
}
return new CompileResultDto
{
Success = errors.Count == 0,
2025-11-12 12:59:31 +00:00
ErrorMessage = errors.Count > 0 ? "Güvenlik kontrolü başarısız" : null,
2025-11-04 22:08:36 +00:00
Errors = errors
};
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Roslyn compilation oluşturur
2025-11-04 22:08:36 +00:00
/// </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,
2025-11-12 12:59:31 +00:00
allowUnsafe: false // Güvenlik için unsafe kod yasağı
2025-11-04 22:08:36 +00:00
));
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Varsayılan assembly referanslarını döner
2025-11-04 22:08:36 +00:00
/// </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)
{
2025-11-12 12:59:31 +00:00
_logger.LogWarning(ex, "Assembly referansı eklenemedi: {Assembly}", assembly.FullName);
2025-11-04 22:08:36 +00:00
}
}
2025-11-12 12:59:31 +00:00
// ABP Framework referansları
2025-11-04 22:08:36 +00:00
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") ||
2025-11-11 19:49:52 +00:00
a.FullName.Contains("Erp.Platform") ||
2025-11-04 22:08:36 +00:00
a.FullName.Contains("Microsoft.AspNetCore")))
.ToList();
foreach (var assembly in abpAssemblies)
{
references.Add(MetadataReference.CreateFromFile(assembly.Location));
}
}
2025-11-05 13:17:10 +00:00
catch
2025-11-04 22:08:36 +00:00
{
}
return references;
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Bir tipin ApplicationService olup olmadığını kontrol eder
2025-11-04 22:08:36 +00:00
/// </summary>
private bool IsApplicationServiceType(Type type)
{
try
{
return !type.IsAbstract &&
!type.IsInterface &&
type.IsClass &&
(type.Name.EndsWith("AppService") || type.Name.EndsWith("ApplicationService")) &&
HasApplicationServiceBase(type);
}
2025-11-05 13:17:10 +00:00
catch
2025-11-04 22:08:36 +00:00
{
return false;
}
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// Tipin ApplicationService base class'ından türediğini kontrol eder
2025-11-04 22:08:36 +00:00
/// </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>
2025-11-12 12:59:31 +00:00
/// Kod içinde belirli bir string'in satır numarasını bulur
2025-11-04 22:08:36 +00:00
/// </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;
}
2025-11-05 13:17:10 +00:00
/// <summary>
2025-11-12 12:59:31 +00:00
/// Assembly içindeki ApplicationService'lerden endpoint listesini çıkarır
2025-11-05 13:17:10 +00:00
/// </summary>
public List<string> ExtractEndpointsFromAssembly(Assembly assembly, string serviceName)
{
var endpoints = new List<string>();
try
{
2025-11-12 12:59:31 +00:00
// Assembly içindeki ApplicationService türlerini bul
2025-11-05 13:17:10 +00:00
var appServiceTypes = assembly.GetTypes()
.Where(t => IsApplicationServiceType(t) && t.Name == serviceName)
.ToList();
foreach (var serviceType in appServiceTypes)
{
2025-11-12 12:59:31 +00:00
// Controller adını oluştur (ABP convention: CustomerAppService -> Customer)
2025-11-05 13:17:10 +00:00
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);
2025-11-12 12:59:31 +00:00
// Public method'ları bul (async method'lar genelde Async suffix'i ile biter)
2025-11-05 13:17:10 +00:00
var methods = serviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
2025-11-12 12:59:31 +00:00
.Where(m => !m.IsSpecialName) // Property getter/setter'ları hariç tut
2025-11-05 13:17:10 +00:00
.ToList();
foreach (var method in methods)
{
2025-11-12 12:59:31 +00:00
// Method adından Async suffix'ini kaldır ve kebab-case'e çevir
2025-11-05 13:17:10 +00:00
var methodName = method.Name;
if (methodName.EndsWith("Async"))
{
methodName = methodName.Substring(0, methodName.Length - "Async".Length);
}
var methodRoute = ToKebabCase(methodName);
2025-11-12 12:59:31 +00:00
// HTTP verb'ü belirle (basit heuristic)
2025-11-05 13:17:10 +00:00
var httpVerb = DetermineHttpVerb(method);
2025-11-12 12:59:31 +00:00
// Endpoint'i oluştur
2025-11-05 13:17:10 +00:00
var endpoint = $"{httpVerb} /api/app/{routePrefix}/{methodRoute}";
endpoints.Add(endpoint);
}
}
}
catch (Exception ex)
{
2025-11-12 12:59:31 +00:00
_logger.LogError(ex, "Endpoint çıkarma sırasında hata");
2025-11-05 13:17:10 +00:00
}
return endpoints;
}
/// <summary>
2025-11-12 12:59:31 +00:00
/// PascalCase'i kebab-case'e çevirir (DynamicCustomer -> dynamic-customer)
2025-11-05 13:17:10 +00:00
/// </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>
2025-11-12 12:59:31 +00:00
/// Method'un HTTP verb'ünü belirler (ABP convention'a göre)
2025-11-05 13:17:10 +00:00
/// </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";
}
2025-11-04 22:08:36 +00:00
}
2025-11-11 19:49:52 +00:00