490 lines
16 KiB
C#
490 lines
16 KiB
C#
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";
|
||
}
|
||
}
|
||
|