DynamicServiceManager

This commit is contained in:
Sedat Öztürk 2025-11-05 01:08:36 +03:00
parent f91955a42e
commit ff088fd9d7
25 changed files with 16424 additions and 1 deletions

View file

@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Volo.Abp.Application.Dtos;
namespace Kurs.Platform.DeveloperKit;
/// <summary>
/// Dynamic AppService tanımı DTO'su
/// </summary>
public class DynamicServiceDto : FullAuditedEntityDto<Guid>
{
[StringLength(256)]
[JsonPropertyName("name")]
public string Name { get; set; }
[StringLength(512)]
[JsonPropertyName("displayName")]
public string DisplayName { get; set; }
[StringLength(2000)]
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("code")]
public string Code { get; set; }
[JsonPropertyName("isActive")]
public bool IsActive { get; set; }
[JsonPropertyName("compilationStatus")]
public string CompilationStatus { get; set; }
[JsonPropertyName("lastCompilationError")]
public string LastCompilationError { get; set; }
[JsonPropertyName("lastSuccessfulCompilation")]
public DateTime? LastSuccessfulCompilation { get; set; }
[JsonPropertyName("version")]
public int Version { get; set; }
[JsonPropertyName("codeHash")]
public string CodeHash { get; set; }
[JsonPropertyName("primaryEntityType")]
public string PrimaryEntityType { get; set; }
[JsonPropertyName("controllerName")]
public string ControllerName { get; set; }
}
/// <summary>
/// Kod derleme sonucu DTO'su
/// </summary>
public class CompileResultDto
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
[JsonPropertyName("errors")]
public List<CompilationErrorDto> Errors { get; set; }
[JsonPropertyName("compilationTimeMs")]
public long CompilationTimeMs { get; set; }
[JsonPropertyName("hasWarnings")]
public bool HasWarnings { get; set; }
[JsonPropertyName("warnings")]
public List<string> Warnings { get; set; }
}
/// <summary>
/// Tek bir derleme hatası
/// </summary>
public class CompilationErrorDto
{
[JsonPropertyName("code")]
public string Code { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonPropertyName("line")]
public int Line { get; set; }
[JsonPropertyName("column")]
public int Column { get; set; }
[JsonPropertyName("severity")]
public string Severity { get; set; }
[JsonPropertyName("fileName")]
public string FileName { get; set; }
}
/// <summary>
/// Kod test etme DTO'su
/// </summary>
public class TestCompileRequestDto
{
[JsonPropertyName("code")]
public string Code { get; set; }
}
/// <summary>
/// AppService yayınlama DTO'su
/// </summary>
public class PublishAppServiceRequestDto
{
[StringLength(256)]
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("code")]
public string Code { get; set; }
[StringLength(512)]
[JsonPropertyName("displayName")]
public string DisplayName { get; set; }
[StringLength(2000)]
[JsonPropertyName("description")]
public string Description { get; set; }
[StringLength(256)]
[JsonPropertyName("primaryEntityType")]
public string PrimaryEntityType { get; set; }
}
/// <summary>
/// AppService yayınlama sonucu
/// </summary>
public class PublishResultDto
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
[JsonPropertyName("appServiceId")]
public Guid? AppServiceId { get; set; }
[JsonPropertyName("swaggerUrl")]
public string SwaggerUrl { get; set; }
[JsonPropertyName("generatedEndpoints")]
public List<string> GeneratedEndpoints { get; set; }
[JsonPropertyName("controllerName")]
public string ControllerName { get; set; }
}

View file

@ -0,0 +1,65 @@
using System;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Kurs.Platform.DeveloperKit;
/// <summary>
/// Dynamic AppService yönetimi için application service interface
/// </summary>
public interface IDynamicServiceManager : IApplicationService
{
/// <summary>
/// C# kodunu test compile eder (herhangi bir assembly yüklemez)
/// </summary>
/// <param name="request">Test edilecek kod</param>
/// <returns>Derleme sonucu</returns>
Task<CompileResultDto> TestCompileAsync(TestCompileRequestDto request);
/// <summary>
/// AppService'i yayınlar (compile eder, veritabanına kaydeder ve runtime'da yükler)
/// </summary>
/// <param name="request">Yayınlanacak AppService bilgileri</param>
/// <returns>Yayınlama sonucu</returns>
Task<PublishResultDto> PublishAsync(PublishAppServiceRequestDto request);
/// <summary>
/// Mevcut tenant'ın AppService tanımlarını listeler
/// </summary>
/// <param name="input">Sayfalama parametreleri</param>
/// <returns>AppService listesi</returns>
Task<PagedResultDto<DynamicServiceDto>> GetListAsync(PagedAndSortedResultRequestDto input);
/// <summary>
/// Belirli bir AppService tanımını getirir
/// </summary>
/// <param name="id">AppService ID</param>
/// <returns>AppService tanımı</returns>
Task<DynamicServiceDto> GetAsync(Guid id);
/// <summary>
/// AppService tanımını siler ve runtime'dan kaldırır
/// </summary>
/// <param name="id">AppService ID</param>
Task DeleteAsync(Guid id);
/// <summary>
/// AppService'i aktif/pasif yapar
/// </summary>
/// <param name="id">AppService ID</param>
/// <param name="isActive">Aktif durumu</param>
Task SetActiveAsync(Guid id, bool isActive);
/// <summary>
/// Tüm aktif AppService'leri yeniden yükler (uygulama restart'tan sonra)
/// </summary>
Task ReloadAllActiveServicesAsync();
/// <summary>
/// Belirli bir AppService'i yeniden derler ve yükler
/// </summary>
/// <param name="id">AppService ID</param>
/// <returns>Yeniden derleme sonucu</returns>
Task<CompileResultDto> RecompileAsync(Guid id);
}

View file

@ -27,5 +27,8 @@ public class DeveloperKitAutoMapperProfile : Profile
// Migration mappings // Migration mappings
CreateMap<ApiMigration, ApiMigrationDto>(); CreateMap<ApiMigration, ApiMigrationDto>();
CreateMap<CreateUpdateApiMigrationDto, ApiMigration>(); CreateMap<CreateUpdateApiMigrationDto, ApiMigration>();
CreateMap<DynamicService, DynamicServiceDto>()
.ForMember(dest => dest.CompilationStatus, opt => opt.MapFrom(src => src.CompilationStatus.ToString()));
} }
} }

View file

@ -0,0 +1,399 @@
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 Kurs.Platform.DeveloperKit;
namespace Kurs.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.Threading.Thread",
"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",
"Thread",
"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);
// ApplicationService türevlerini bul
var appServiceTypes = assembly.GetTypes()
.Where(t => IsApplicationServiceType(t))
.ToList();
_logger.LogInformation("Tenant {TenantId} için {Count} ApplicationService bulundu",
tenantId, appServiceTypes.Count);
// Tenant assembly'leri listesine ekle
_tenantAssemblies.AddOrUpdate(tenantId,
new List<Assembly> { assembly },
(key, existing) => { existing.Add(assembly); return existing; });
// Background service'e assembly kaydı isteği gönder
// Bu, DI container ve MVC conventional controller sistemine kaydetmek için
NotifyAssemblyRegistration?.Invoke(tenantId, assembly, assemblyName);
_logger.LogInformation("Tenant {TenantId} için assembly başarıyla yüklendi: {AssemblyName}",
tenantId, assemblyName);
return new CompileResultDto
{
Success = true,
CompilationTimeMs = validateResult.CompilationTimeMs
};
}
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("Kurs.Platform") ||
a.FullName.Contains("Microsoft.AspNetCore")))
.ToList();
foreach (var assembly in abpAssemblies)
{
references.Add(MetadataReference.CreateFromFile(assembly.Location));
}
_logger.LogDebug("Toplam {Count} referans eklendi", references.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "ABP referansları eklenirken hata");
}
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 (Exception ex)
{
_logger.LogWarning(ex, "Tip kontrolü sırasında hata: {Type}", type?.FullName);
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;
}
}

View file

@ -0,0 +1,375 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Microsoft.AspNetCore.Authorization;
using Kurs.Platform.Entities;
using Kurs.Platform.DeveloperKit;
namespace Kurs.Platform.DynamicServices;
/// <summary>
/// Dynamic AppService yönetimi için ApplicationService implementasyonu
/// </summary>
[Authorize] // Sadece giriş yapmış kullanıcılar erişebilir
public class DynamicServiceManager : ApplicationService, IDynamicServiceManager
{
private readonly IRepository<DynamicService, Guid> _dynamicAppServiceRepository;
private readonly DynamicServiceCompiler _compiler;
private readonly ICurrentTenant _currentTenant;
public DynamicServiceManager(
IRepository<DynamicService, Guid> dynamicAppServiceRepository,
DynamicServiceCompiler compiler,
ICurrentTenant currentTenant)
{
_dynamicAppServiceRepository = dynamicAppServiceRepository;
_compiler = compiler;
_currentTenant = currentTenant;
}
/// <summary>
/// C# kodunu test compile eder
/// </summary>
[Authorize(PlatformConsts.DynamicServices.TestCompile)]
public async Task<CompileResultDto> TestCompileAsync(TestCompileRequestDto request)
{
Logger.LogInformation("Test compile başlatıldı. Tenant: {TenantId}", _currentTenant.Id);
try
{
var result = await _compiler.CompileAndValidateAsync(request.Code, _currentTenant.Id);
Logger.LogInformation("Test compile tamamlandı. Başarılı: {Success}, Tenant: {TenantId}",
result.Success, _currentTenant.Id);
return result;
}
catch (Exception ex)
{
Logger.LogError(ex, "Test compile sırasında hata. Tenant: {TenantId}", _currentTenant.Id);
throw;
}
}
/// <summary>
/// AppService'i yayınlar
/// </summary>
[Authorize(PlatformConsts.DynamicServices.Publish)]
public async Task<PublishResultDto> PublishAsync(PublishAppServiceRequestDto request)
{
Logger.LogInformation("AppService yayınlama başlatıldı. Ad: {Name}, Tenant: {TenantId}",
request.Name, _currentTenant.Id);
try
{
// Önce kodu test compile et
var compileResult = await _compiler.CompileAndValidateAsync(request.Code, _currentTenant.Id);
if (!compileResult.Success)
{
return new PublishResultDto
{
Success = false,
ErrorMessage = compileResult.ErrorMessage
};
}
// Aynı isimde AppService var mı kontrol et
var existingService = await _dynamicAppServiceRepository
.FirstOrDefaultAsync(x => x.Name == request.Name && x.TenantId == _currentTenant.Id);
DynamicService appService;
if (existingService != null)
{
// Mevcut servisi güncelle
existingService.UpdateCode(request.Code);
existingService.DisplayName = request.DisplayName;
existingService.Description = request.Description;
existingService.PrimaryEntityType = request.PrimaryEntityType;
appService = await _dynamicAppServiceRepository.UpdateAsync(existingService);
Logger.LogInformation("Mevcut AppService güncellendi. ID: {Id}", appService.Id);
}
else
{
// Yeni servis oluştur
appService = new DynamicService(
GuidGenerator.Create(),
request.Name,
request.Code,
_currentTenant.Id)
{
DisplayName = request.DisplayName,
Description = request.Description,
PrimaryEntityType = request.PrimaryEntityType,
ControllerName = GenerateControllerName(request.Name)
};
appService = await _dynamicAppServiceRepository.InsertAsync(appService);
Logger.LogInformation("Yeni AppService oluşturuldu. ID: {Id}", appService.Id);
}
// Runtime'da derle ve yükle
var assemblyName = $"DynamicAppService_{appService.Id}_{appService.Version}";
var loadResult = await _compiler.CompileAndRegisterForTenantAsync(
_currentTenant.Id ?? Guid.Empty,
request.Code,
assemblyName);
if (loadResult.Success)
{
// Başarılı derleme durumunu kaydet
appService.MarkCompilationSuccess();
await _dynamicAppServiceRepository.UpdateAsync(appService);
var result = new PublishResultDto
{
Success = true,
AppServiceId = appService.Id,
ControllerName = appService.ControllerName,
SwaggerUrl = "/swagger/index.html",
GeneratedEndpoints = GenerateEndpointList(request.Name)
};
Logger.LogInformation("AppService başarıyla yayınlandı. ID: {Id}, Controller: {Controller}",
appService.Id, appService.ControllerName);
return result;
}
else
{
// Derleme hatası durumunu kaydet
appService.MarkCompilationError(loadResult.ErrorMessage);
await _dynamicAppServiceRepository.UpdateAsync(appService);
return new PublishResultDto
{
Success = false,
ErrorMessage = loadResult.ErrorMessage,
AppServiceId = appService.Id
};
}
}
catch (Exception ex)
{
Logger.LogError(ex, "AppService yayınlama sırasında hata. Ad: {Name}, Tenant: {TenantId}",
request.Name, _currentTenant.Id);
return new PublishResultDto
{
Success = false,
ErrorMessage = $"Yayınlama sırasında beklenmeyen hata: {ex.Message}"
};
}
}
/// <summary>
/// AppService listesini getirir
/// </summary>
[Authorize(PlatformConsts.DynamicServices.DynamicAppServices)]
public async Task<PagedResultDto<DynamicServiceDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{
Logger.LogDebug("AppService listesi istendi. Tenant: {TenantId}", _currentTenant.Id);
var queryable = await _dynamicAppServiceRepository.GetQueryableAsync();
// Tenant filtresi otomatik uygulanır (IMultiTenant)
var query = queryable.WhereIf(!string.IsNullOrEmpty(input.Sorting),
x => x.CreationTime.ToString().Contains(input.Sorting ?? ""));
var totalCount = await AsyncExecuter.CountAsync(query);
var items = await AsyncExecuter.ToListAsync(
query.OrderByDescending(x => x.CreationTime)
.Skip(input.SkipCount)
.Take(input.MaxResultCount)
);
var dtos = ObjectMapper.Map<List<DynamicService>, List<DynamicServiceDto>>(items);
Logger.LogDebug("AppService listesi döndürüldü. Toplam: {Total}, Dönen: {Count}",
totalCount, dtos.Count);
return new PagedResultDto<DynamicServiceDto>(totalCount, dtos);
}
/// <summary>
/// Belirli bir AppService'i getirir
/// </summary>
[Authorize(PlatformConsts.DynamicServices.ViewCode)]
public async Task<DynamicServiceDto> GetAsync(Guid id)
{
Logger.LogDebug("AppService detayı istendi. ID: {Id}, Tenant: {TenantId}", id, _currentTenant.Id);
var appService = await _dynamicAppServiceRepository.GetAsync(id);
var dto = ObjectMapper.Map<DynamicService, DynamicServiceDto>(appService);
return dto;
}
/// <summary>
/// AppService'i siler
/// </summary>
[Authorize(PlatformConsts.DynamicServices.Delete)]
public async Task DeleteAsync(Guid id)
{
Logger.LogInformation("AppService silme işlemi başlatıldı. ID: {Id}, Tenant: {TenantId}",
id, _currentTenant.Id);
var appService = await _dynamicAppServiceRepository.GetAsync(id);
// TODO: Runtime'dan assembly'yi kaldırma işlemi
// (AssemblyLoadContext.Unload() çağrısı)
await _dynamicAppServiceRepository.DeleteAsync(id);
Logger.LogInformation("AppService silindi. ID: {Id}", id);
}
/// <summary>
/// AppService'i aktif/pasif yapar
/// </summary>
[Authorize(PlatformConsts.DynamicServices.Manage)]
public async Task SetActiveAsync(Guid id, bool isActive)
{
Logger.LogInformation("AppService aktiflik durumu değiştiriliyor. ID: {Id}, Aktif: {Active}",
id, isActive);
var appService = await _dynamicAppServiceRepository.GetAsync(id);
appService.IsActive = isActive;
await _dynamicAppServiceRepository.UpdateAsync(appService);
Logger.LogInformation("AppService aktiflik durumu güncellendi. ID: {Id}, Aktif: {Active}",
id, isActive);
}
/// <summary>
/// Tüm aktif AppService'leri yeniden yükler
/// </summary>
[Authorize(PlatformConsts.DynamicServices.Manage)]
public async Task ReloadAllActiveServicesAsync()
{
Logger.LogInformation("Tüm aktif AppService'ler yeniden yükleniyor. Tenant: {TenantId}",
_currentTenant.Id);
var activeServices = await _dynamicAppServiceRepository
.GetListAsync(x => x.IsActive && x.CompilationStatus == CompilationStatus.Success);
var successCount = 0;
var errorCount = 0;
foreach (var service in activeServices)
{
try
{
var assemblyName = $"DynamicAppService_{service.Id}_{service.Version}";
var result = await _compiler.CompileAndRegisterForTenantAsync(
_currentTenant.Id ?? Guid.Empty,
service.Code,
assemblyName);
if (result.Success)
{
successCount++;
Logger.LogDebug("AppService yeniden yüklendi. ID: {Id}, Ad: {Name}",
service.Id, service.Name);
}
else
{
errorCount++;
service.MarkCompilationError(result.ErrorMessage);
await _dynamicAppServiceRepository.UpdateAsync(service);
Logger.LogWarning("AppService yeniden yüklenemedi. ID: {Id}, Hata: {Error}",
service.Id, result.ErrorMessage);
}
}
catch (Exception ex)
{
errorCount++;
Logger.LogError(ex, "AppService yeniden yükleme hatası. ID: {Id}", service.Id);
service.MarkCompilationError(ex.Message);
await _dynamicAppServiceRepository.UpdateAsync(service);
}
}
Logger.LogInformation("AppService yeniden yükleme tamamlandı. Başarılı: {Success}, Hatalı: {Error}, Toplam: {Total}",
successCount, errorCount, activeServices.Count);
}
/// <summary>
/// Belirli bir AppService'i yeniden derler
/// </summary>
[Authorize(PlatformConsts.DynamicServices.Manage)]
public async Task<CompileResultDto> RecompileAsync(Guid id)
{
Logger.LogInformation("AppService yeniden derleniyor. ID: {Id}", id);
var appService = await _dynamicAppServiceRepository.GetAsync(id);
var assemblyName = $"DynamicAppService_{appService.Id}_{appService.Version + 1}";
var result = await _compiler.CompileAndRegisterForTenantAsync(
_currentTenant.Id ?? Guid.Empty,
appService.Code,
assemblyName);
if (result.Success)
{
appService.MarkCompilationSuccess();
appService.Version++;
}
else
{
appService.MarkCompilationError(result.ErrorMessage);
}
await _dynamicAppServiceRepository.UpdateAsync(appService);
Logger.LogInformation("AppService yeniden derleme tamamlandı. ID: {Id}, Başarılı: {Success}",
id, result.Success);
return result;
}
/// <summary>
/// Controller adını oluşturur
/// </summary>
private string GenerateControllerName(string serviceName)
{
// DynamicCustomerAppService -> DynamicCustomer
var controllerName = serviceName;
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);
}
return controllerName;
}
/// <summary>
/// Örnek endpoint listesi oluşturur
/// </summary>
private List<string> GenerateEndpointList(string serviceName)
{
var controllerName = GenerateControllerName(serviceName);
return new List<string>
{
$"/api/app/{controllerName.ToLowerInvariant()}",
$"/api/app/{controllerName.ToLowerInvariant()}/{{id}}",
$"/api/app/{controllerName.ToLowerInvariant()}/list"
};
}
}

View file

@ -26,6 +26,9 @@
<PackageReference Include="Volo.Abp.PermissionManagement.Application" Version="9.0.2" /> <PackageReference Include="Volo.Abp.PermissionManagement.Application" Version="9.0.2" />
<PackageReference Include="Volo.Abp.TenantManagement.Application" Version="9.0.2" /> <PackageReference Include="Volo.Abp.TenantManagement.Application" Version="9.0.2" />
<PackageReference Include="Volo.Abp.FeatureManagement.Application" Version="9.0.2" /> <PackageReference Include="Volo.Abp.FeatureManagement.Application" Version="9.0.2" />
<!-- Dynamic Code Compilation -->
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -9937,6 +9937,12 @@
"en": "fields", "en": "fields",
"tr": "alan" "tr": "alan"
}, },
{
"resourceName": "Platform",
"key": "App.DeveloperKit.DynamicServices",
"en": "Dynamic Services",
"tr": "Dinamik Hizmetler"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKit.Migration.Generating", "key": "App.DeveloperKit.Migration.Generating",

View file

@ -350,6 +350,15 @@
"App.DeveloperKit.Endpoints" "App.DeveloperKit.Endpoints"
] ]
}, },
{
"key": "admin.developerkit.dynamic-services",
"path": "/admin/developerkit/dynamic-services",
"componentPath": "@/views/developerKit/DynamicServicePage",
"routeType": "protected",
"authority": [
"App.DeveloperKit.DynamicServices"
]
},
{ {
"key": "admin.developerkit.components", "key": "admin.developerkit.components",
"path": "/admin/developerkit/components", "path": "/admin/developerkit/components",
@ -1646,6 +1655,16 @@
"RequiredPermissionName": "App.DeveloperKit.CustomEndpoints", "RequiredPermissionName": "App.DeveloperKit.CustomEndpoints",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.DynamicServices",
"DisplayName": "App.DeveloperKit.DynamicServices",
"Order": 5,
"Url": "/admin/developerkit/dynamic-services",
"Icon": "FcCommandLine",
"RequiredPermissionName": "App.DeveloperKit.DynamicServices",
"IsDisabled": false
},
{ {
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.Components", "Code": "App.DeveloperKit.Components",

View file

@ -2335,6 +2335,78 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices",
"ParentName": "App.DeveloperKit",
"DisplayName": "App.DeveloperKit.DynamicServices",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.Create",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "Create",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.Update",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "Update",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.Delete",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "Delete",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.Manage",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "Manage",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.TestCompile",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "TestCompile",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.Publish",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "Publish",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.DynamicServices.ViewCode",
"ParentName": "App.DeveloperKit.DynamicServices",
"DisplayName": "ViewCode",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.Reports.Categories", "Name": "App.Reports.Categories",

View file

@ -145,5 +145,6 @@ public enum TableNameEnum
Partner, Partner,
PartnerBank, PartnerBank,
PartnerCertificate, PartnerCertificate,
PartnerContact PartnerContact,
DynamicService
} }

View file

@ -71,6 +71,20 @@ public static class PlatformConsts
public const string MenuGroup = "MenuGroup"; public const string MenuGroup = "MenuGroup";
} }
public static class DynamicServices
{
public const string GroupName = "DynamicServices";
public const string DynamicAppServices = GroupName + ".DynamicAppServices";
public const string Create = DynamicAppServices + ".Create";
public const string Edit = DynamicAppServices + ".Edit";
public const string Delete = DynamicAppServices + ".Delete";
public const string Manage = DynamicAppServices + ".Manage";
public const string TestCompile = DynamicAppServices + ".TestCompile";
public const string Publish = DynamicAppServices + ".Publish";
public const string ViewCode = DynamicAppServices + ".ViewCode";
}
public static class AbpIdentity public static class AbpIdentity
{ {
public const string GroupName = $"{Prefix.Abp}.Identity"; public const string GroupName = $"{Prefix.Abp}.Identity";

View file

@ -12,6 +12,7 @@ public static class TableNameResolver
_map = new(StringComparer.OrdinalIgnoreCase) _map = new(StringComparer.OrdinalIgnoreCase)
{ {
// 🔹 MODULE TABLOLARI // 🔹 MODULE TABLOLARI
{ nameof(TableNameEnum.DynamicService), (TablePrefix.PlatformByName, MenuPrefix.Platform) },
{ nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Platform) }, { nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Platform) },
{ nameof(TableNameEnum.Language), (TablePrefix.PlatformByName, MenuPrefix.Platform) }, { nameof(TableNameEnum.Language), (TablePrefix.PlatformByName, MenuPrefix.Platform) },
{ nameof(TableNameEnum.LanguageKey), (TablePrefix.PlatformByName, MenuPrefix.Platform) }, { nameof(TableNameEnum.LanguageKey), (TablePrefix.PlatformByName, MenuPrefix.Platform) },

View file

@ -0,0 +1,174 @@
using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Kurs.Platform.Entities;
/// <summary>
/// Tenant bazında dinamik olarak tanımlanmış AppService'lerin kod ve meta verilerini saklar
/// </summary>
public class DynamicService : FullAuditedEntity<Guid>, IMultiTenant
{
/// <summary>
/// Tenant ID - IMultiTenant implementasyonu
/// </summary>
public Guid? TenantId { get; set; }
/// <summary>
/// AppService'in benzersiz adı (örn: "DynamicCustomerService")
/// </summary>
[Required]
[StringLength(256)]
public string Name { get; set; }
/// <summary>
/// AppService'in kullanıcı dostu başlığı
/// </summary>
[StringLength(512)]
public string? DisplayName { get; set; }
/// <summary>
/// AppService açıklaması
/// </summary>
[StringLength(2000)]
public string? Description { get; set; }
/// <summary>
/// Tam C# sınıf kodu (using'ler ve namespace dahil)
/// </summary>
[Required]
public string Code { get; set; }
/// <summary>
/// AppService aktif mi?
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Son derleme durumu
/// </summary>
public CompilationStatus CompilationStatus { get; set; }
/// <summary>
/// Son derleme hatası (varsa)
/// </summary>
public string? LastCompilationError { get; set; }
/// <summary>
/// Son başarılı derleme zamanı
/// </summary>
public DateTime? LastSuccessfulCompilation { get; set; }
/// <summary>
/// Kod versiyonu (her güncelleme ile artan numara)
/// </summary>
public int Version { get; set; }
/// <summary>
/// Kod hash'i (değişiklik kontrolü için)
/// </summary>
[StringLength(64)]
public string? CodeHash { get; set; }
/// <summary>
/// AppService'in kullandığı ana entity türü (opsiyonel)
/// </summary>
[StringLength(256)]
public string? PrimaryEntityType { get; set; }
/// <summary>
/// Swagger'da görünecek controller adı
/// </summary>
[StringLength(256)]
public string? ControllerName { get; set; }
protected DynamicService()
{
// EF Core constructor
}
public DynamicService(
Guid id,
string name,
string code,
Guid? tenantId = null) : base(id)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(name), maxLength: 256);
Code = Check.NotNullOrWhiteSpace(code, nameof(code));
TenantId = tenantId;
IsActive = true;
CompilationStatus = CompilationStatus.Pending;
Version = 1;
}
/// <summary>
/// Kodu günceller ve versiyonu artırır
/// </summary>
public void UpdateCode(string newCode)
{
Check.NotNullOrWhiteSpace(newCode, nameof(newCode));
if (Code != newCode)
{
Code = newCode;
Version++;
CompilationStatus = CompilationStatus.Pending;
LastCompilationError = null;
CodeHash = GenerateCodeHash(newCode);
}
}
/// <summary>
/// Derleme başarısını işaretler
/// </summary>
public void MarkCompilationSuccess()
{
CompilationStatus = CompilationStatus.Success;
LastCompilationError = null;
LastSuccessfulCompilation = DateTime.UtcNow;
}
/// <summary>
/// Derleme hatasını işaretler
/// </summary>
public void MarkCompilationError(string error)
{
CompilationStatus = CompilationStatus.Failed;
LastCompilationError = error;
}
private string GenerateCodeHash(string code)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(code));
return Convert.ToHexString(hash);
}
}
/// <summary>
/// Derleme durumu enum'u
/// </summary>
public enum CompilationStatus
{
/// <summary>
/// Henüz derlenmedi
/// </summary>
Pending = 0,
/// <summary>
/// Derleme başarılı
/// </summary>
Success = 1,
/// <summary>
/// Derleme başarısız
/// </summary>
Failed = 2,
/// <summary>
/// Derleme işlemi devam ediyor
/// </summary>
InProgress = 3
}

View file

@ -26,6 +26,7 @@ using System.Text.Json;
using static Kurs.Platform.PlatformConsts; using static Kurs.Platform.PlatformConsts;
using static Kurs.Settings.SettingsConsts; using static Kurs.Settings.SettingsConsts;
using Kurs.Platform.Enums; using Kurs.Platform.Enums;
using Kurs.Platform.Domain.DeveloperKit;
namespace Kurs.Platform.EntityFrameworkCore; namespace Kurs.Platform.EntityFrameworkCore;
@ -63,6 +64,7 @@ public class PlatformDbContext :
public DbSet<ApiEndpoint> GeneratedEndpoints { get; set; } public DbSet<ApiEndpoint> GeneratedEndpoints { get; set; }
public DbSet<CustomEndpoint> CustomEndpoints { get; set; } public DbSet<CustomEndpoint> CustomEndpoints { get; set; }
public DbSet<CustomComponent> CustomComponents { get; set; } public DbSet<CustomComponent> CustomComponents { get; set; }
public DbSet<DynamicService> DynamicServices { get; set; }
public DbSet<ReportTemplate> ReportTemplates { get; set; } public DbSet<ReportTemplate> ReportTemplates { get; set; }
public DbSet<ReportParameter> ReportParameters { get; set; } public DbSet<ReportParameter> ReportParameters { get; set; }
public DbSet<ReportGenerated> ReportGenerated { get; set; } public DbSet<ReportGenerated> ReportGenerated { get; set; }
@ -100,6 +102,8 @@ public class PlatformDbContext :
public DbSet<Contact> Contacts { get; set; } public DbSet<Contact> Contacts { get; set; }
public DbSet<Sector> Sectors { get; set; } public DbSet<Sector> Sectors { get; set; }
public DbSet<SkillType> SkillTypes { get; set; } public DbSet<SkillType> SkillTypes { get; set; }
// Dynamic Services
public DbSet<Skill> Skills { get; set; } public DbSet<Skill> Skills { get; set; }
public DbSet<SkillLevel> SkillLevels { get; set; } public DbSet<SkillLevel> SkillLevels { get; set; }
public DbSet<UomCategory> UomCategories { get; set; } public DbSet<UomCategory> UomCategories { get; set; }
@ -2444,5 +2448,24 @@ public class PlatformDbContext :
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.LogEntry)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.LogEntry)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
}); });
builder.Entity<DynamicService>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.DynamicService)), Prefix.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name).IsRequired().HasMaxLength(256);
b.Property(x => x.DisplayName).HasMaxLength(512);
b.Property(x => x.Description).HasMaxLength(2000);
b.Property(x => x.Code).IsRequired().HasColumnType("text"); // Uzun C# kod metni için
b.Property(x => x.IsActive).IsRequired().HasDefaultValue(true);
b.Property(x => x.CompilationStatus).IsRequired().HasConversion<string>().HasMaxLength(20);
b.Property(x => x.LastCompilationError).HasColumnType("text"); // Uzun hata mesajları için
b.Property(x => x.LastSuccessfulCompilation).IsRequired(false);
b.Property(x => x.Version).IsRequired().HasDefaultValue(1);
b.Property(x => x.CodeHash).HasMaxLength(64);
b.Property(x => x.PrimaryEntityType).HasMaxLength(256);
b.Property(x => x.ControllerName).HasMaxLength(256);
});
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Kurs.Platform.Migrations
{
/// <inheritdoc />
public partial class DynamicService : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Plat_H_DynamicService",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Name = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
Description = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
Code = table.Column<string>(type: "text", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
CompilationStatus = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
LastCompilationError = table.Column<string>(type: "text", nullable: true),
LastSuccessfulCompilation = table.Column<DateTime>(type: "datetime2", nullable: true),
Version = table.Column<int>(type: "int", nullable: false, defaultValue: 1),
CodeHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: true),
PrimaryEntityType = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
ControllerName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Plat_H_DynamicService", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Plat_H_DynamicService");
}
}
}

View file

@ -3554,6 +3554,100 @@ namespace Kurs.Platform.Migrations
b.ToTable("Adm_T_Document", (string)null); b.ToTable("Adm_T_Document", (string)null);
}); });
modelBuilder.Entity("Kurs.Platform.Entities.DynamicService", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("text");
b.Property<string>("CodeHash")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("CompilationStatus")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("ControllerName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(2000)
.HasColumnType("nvarchar(2000)");
b.Property<string>("DisplayName")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(true);
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<string>("LastCompilationError")
.HasColumnType("text");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<DateTime?>("LastSuccessfulCompilation")
.HasColumnType("datetime2");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PrimaryEntityType")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.Property<int>("Version")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasDefaultValue(1);
b.HasKey("Id");
b.ToTable("Plat_H_DynamicService", (string)null);
});
modelBuilder.Entity("Kurs.Platform.Entities.EducationStatus", b => modelBuilder.Entity("Kurs.Platform.Entities.EducationStatus", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")

View file

@ -0,0 +1,194 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.DependencyInjection;
namespace Kurs.Platform.DynamicServices
{
/// <summary>
/// Dinamik assembly'leri runtime'da DI ve MVC sistemine kaydeden background service
/// </summary>
public class DynamicAssemblyRegistrationService : BackgroundService, ITransientDependency
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<DynamicAssemblyRegistrationService> _logger;
private readonly ApplicationPartManager _partManager;
private readonly IOptions<AbpAspNetCoreMvcOptions> _mvcOptions;
// Bekleyen assembly kayıt istekleri
private static readonly Queue<AssemblyRegistrationRequest> _pendingRegistrations = new();
private static readonly object _lock = new object();
public DynamicAssemblyRegistrationService(
IServiceProvider serviceProvider,
ILogger<DynamicAssemblyRegistrationService> logger,
ApplicationPartManager partManager,
IOptions<AbpAspNetCoreMvcOptions> mvcOptions)
{
_serviceProvider = serviceProvider;
_logger = logger;
_partManager = partManager;
_mvcOptions = mvcOptions;
}
/// <summary>
/// Yeni assembly kaydı istemi
/// </summary>
public static void RequestAssemblyRegistration(Guid tenantId, Assembly assembly, string assemblyName)
{
lock (_lock)
{
_pendingRegistrations.Enqueue(new AssemblyRegistrationRequest
{
TenantId = tenantId,
Assembly = assembly,
AssemblyName = assemblyName,
RequestTime = DateTime.UtcNow
});
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Dynamic Assembly Registration Service başlatıldı");
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessPendingRegistrations();
// 5 saniye bekle
await Task.Delay(5000, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Assembly registration service hatası");
}
}
_logger.LogInformation("Dynamic Assembly Registration Service durduruluyor");
}
private async Task ProcessPendingRegistrations()
{
var requests = new List<AssemblyRegistrationRequest>();
lock (_lock)
{
while (_pendingRegistrations.Count > 0)
{
requests.Add(_pendingRegistrations.Dequeue());
}
}
if (requests.Count == 0)
return;
_logger.LogDebug("İşlenecek {Count} assembly kaydı var", requests.Count);
foreach (var request in requests)
{
try
{
await RegisterAssembly(request);
}
catch (Exception ex)
{
_logger.LogError(ex, "Assembly kaydı başarısız. Tenant: {TenantId}, Assembly: {Assembly}",
request.TenantId, request.AssemblyName);
}
}
}
private async Task RegisterAssembly(AssemblyRegistrationRequest request)
{
_logger.LogInformation("Assembly kaydediliyor. Tenant: {TenantId}, Assembly: {Assembly}",
request.TenantId, request.AssemblyName);
// ApplicationService türlerini bul
var appServiceTypes = request.Assembly.GetTypes()
.Where(t => IsApplicationServiceType(t))
.ToList();
if (appServiceTypes.Count == 0)
{
_logger.LogWarning("Assembly'de ApplicationService bulunamadı: {Assembly}", request.AssemblyName);
return;
}
// Dependency Injection Container'a ekle
using (var scope = _serviceProvider.CreateScope())
{
var services = scope.ServiceProvider.GetRequiredService<IServiceCollection>();
foreach (var serviceType in appServiceTypes)
{
// Transient olarak kaydet
services.AddTransient(serviceType);
_logger.LogDebug("DI'ya eklendi: {ServiceType}", serviceType.FullName);
}
}
// MVC Application Parts'a ekle (Controllers için)
var assemblyPart = new AssemblyPart(request.Assembly);
_partManager.ApplicationParts.Add(assemblyPart);
// ABP Conventional Controllers'a ekle
_mvcOptions.Value.ConventionalControllers.Create(request.Assembly);
_logger.LogInformation("Assembly başarıyla kaydedildi. Tenant: {TenantId}, Assembly: {Assembly}, Service Count: {Count}",
request.TenantId, request.AssemblyName, appServiceTypes.Count);
await Task.CompletedTask;
}
private bool IsApplicationServiceType(Type type)
{
try
{
return !type.IsAbstract &&
!type.IsInterface &&
type.IsClass &&
(type.Name.EndsWith("AppService") || type.Name.EndsWith("ApplicationService")) &&
HasApplicationServiceBase(type);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Tip kontrolü sırasında hata: {Type}", type?.FullName);
return false;
}
}
private bool HasApplicationServiceBase(Type type)
{
var currentType = type.BaseType;
while (currentType != null)
{
if (currentType.Name.Contains("ApplicationService"))
{
return true;
}
currentType = currentType.BaseType;
}
return false;
}
private class AssemblyRegistrationRequest
{
public Guid TenantId { get; set; }
public Assembly Assembly { get; set; }
public string AssemblyName { get; set; }
public DateTime RequestTime { get; set; }
}
}
}

View file

@ -49,6 +49,7 @@ using Volo.Abp.Security.Claims;
using Volo.Abp.Swashbuckle; using Volo.Abp.Swashbuckle;
using Volo.Abp.UI.Navigation.Urls; using Volo.Abp.UI.Navigation.Urls;
using Volo.Abp.VirtualFileSystem; using Volo.Abp.VirtualFileSystem;
using Kurs.Platform.DynamicServices;
using static Kurs.Platform.PlatformConsts; using static Kurs.Platform.PlatformConsts;
using static Kurs.Settings.SettingsConsts; using static Kurs.Settings.SettingsConsts;
@ -120,6 +121,8 @@ public class PlatformHttpApiHostModule : AbpModule
ConfigureHangfire(context, configuration); ConfigureHangfire(context, configuration);
ConfigureBlobStoring(configuration); ConfigureBlobStoring(configuration);
ConfigureAuditing(); ConfigureAuditing();
ConfigureDynamicServices(context);
context.Services.AddSignalR(); context.Services.AddSignalR();
@ -352,6 +355,15 @@ public class PlatformHttpApiHostModule : AbpModule
}); });
} }
private void ConfigureDynamicServices(ServiceConfigurationContext context)
{
// Dynamic AppService Background Service
context.Services.AddHostedService<DynamicAssemblyRegistrationService>();
// Roslyn Compiler Servisleri
context.Services.AddSingleton<DynamicServiceCompiler>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context) public override void OnApplicationInitialization(ApplicationInitializationContext context)
{ {
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kurs.Platform.Classrooms; using Kurs.Platform.Classrooms;
using Kurs.Platform.Enums; using Kurs.Platform.Enums;
using Kurs.Platform.DynamicServices;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -146,6 +147,9 @@ public class Program
var app = builder.Build(); var app = builder.Build();
app.UseCors("Dynamic"); app.UseCors("Dynamic");
// Dynamic Assembly Registration Delegate Setup
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
await app.InitializeApplicationAsync(); await app.InitializeApplicationAsync();
await app.RunAsync(); await app.RunAsync();
return 0; return 0;

View file

@ -0,0 +1,97 @@
using System;
using System.Threading.Tasks;
using Kurs.Platform.DeveloperKit;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc;
namespace Kurs.Platform.DynamicServices
{
/// <summary>
/// Dynamic AppService yönetimi HTTP API Controller
/// </summary>
[Area("app")]
[Route("api/app/dynamic-app-service")]
public class DynamicAppServiceController : AbpController
{
private readonly IDynamicServiceManager _dynamicAppServiceManager;
public DynamicAppServiceController(IDynamicServiceManager dynamicAppServiceManager)
{
_dynamicAppServiceManager = dynamicAppServiceManager;
}
/// <summary>
/// Kod test derlemesi
/// </summary>
[HttpPost("test-compile")]
public async Task<CompileResultDto> TestCompileAsync(TestCompileRequestDto request)
{
return await _dynamicAppServiceManager.TestCompileAsync(request);
}
/// <summary>
/// AppService yayınlama
/// </summary>
[HttpPost("publish")]
public async Task<PublishResultDto> PublishAsync(PublishAppServiceRequestDto request)
{
return await _dynamicAppServiceManager.PublishAsync(request);
}
/// <summary>
/// AppService listesi
/// </summary>
[HttpGet]
public async Task<PagedResultDto<DynamicServiceDto>> GetListAsync(
[FromQuery] PagedAndSortedResultRequestDto input)
{
return await _dynamicAppServiceManager.GetListAsync(input);
}
/// <summary>
/// AppService detayı
/// </summary>
[HttpGet("{id}")]
public async Task<DynamicServiceDto> GetAsync(Guid id)
{
return await _dynamicAppServiceManager.GetAsync(id);
}
/// <summary>
/// AppService silme
/// </summary>
[HttpDelete("{id}")]
public async Task DeleteAsync(Guid id)
{
await _dynamicAppServiceManager.DeleteAsync(id);
}
/// <summary>
/// AppService aktiflik durumu
/// </summary>
[HttpPut("{id}/active")]
public async Task SetActiveAsync(Guid id, [FromBody] bool isActive)
{
await _dynamicAppServiceManager.SetActiveAsync(id, isActive);
}
/// <summary>
/// Tüm aktif servisleri yeniden yükle
/// </summary>
[HttpPost("reload-all")]
public async Task ReloadAllActiveServicesAsync()
{
await _dynamicAppServiceManager.ReloadAllActiveServicesAsync();
}
/// <summary>
/// AppService yeniden derleme
/// </summary>
[HttpPost("{id}/recompile")]
public async Task<CompileResultDto> RecompileAsync(Guid id)
{
return await _dynamicAppServiceManager.RecompileAsync(id);
}
}
}

View file

@ -0,0 +1,628 @@
import React, { useState, useRef, useEffect } from 'react'
import { Editor } from '@monaco-editor/react'
import {
FaPlay,
FaUpload,
FaCode,
FaCheckCircle,
FaExclamationCircle,
FaSpinner,
FaCopy,
FaExternalLinkAlt,
FaTrash,
FaSync,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import {
dynamicServiceService,
type CompileResult,
type PublishResult,
type DynamicServiceDto,
postTestCompile,
TestCompileRequestDto,
} from '@/services/dynamicService.service'
const DynamicAppServiceEditor: React.FC = () => {
// State
const [code, setCode] = useState('')
const [serviceName, setServiceName] = useState('')
const [displayName, setDisplayName] = useState('')
const [description, setDescription] = useState('')
const [primaryEntityType, setPrimaryEntityType] = useState('')
const [isCompiling, setIsCompiling] = useState(false)
const [isPublishing, setIsPublishing] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [compileResult, setCompileResult] = useState<CompileResult | null>(null)
const [publishResult, setPublishResult] = useState<PublishResult | null>(null)
const [services, setServices] = useState<DynamicServiceDto[]>([])
const [selectedService, setSelectedService] = useState<DynamicServiceDto | null>(null)
const [showServiceList, setShowServiceList] = useState(true)
const { translate } = useLocalization()
// Template kod
const defaultTemplate = `using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Microsoft.AspNetCore.Authorization;
namespace DynamicServices
{
[Authorize]
public class DynamicCustomerAppService : ApplicationService
{
// Repository injection örneği (kendi entity'nizi kullanın)
// private readonly IRepository<Customer, Guid> _customerRepository;
// public DynamicCustomerAppService(IRepository<Customer, Guid> customerRepository)
// {
// _customerRepository = customerRepository;
// }
public virtual async Task<string> GetHelloWorldAsync()
{
return await Task.FromResult("Hello World from Dynamic AppService!");
}
public virtual async Task<List<string>> GetSampleDataAsync()
{
return await Task.FromResult(new List<string>
{
"Item 1",
"Item 2",
"Item 3"
});
}
// Repository kullanım örneği:
// public virtual async Task<List<Customer>> GetCustomersAsync()
// {
// return await _customerRepository.GetListAsync();
// }
}
}`
// Component mount
useEffect(() => {
setCode(defaultTemplate)
loadServices()
}, [])
// Monaco Editor ayarları
const editorOptions = {
fontSize: 14,
lineNumbers: 'on' as const,
roundedSelection: false,
scrollBeyondLastLine: false,
automaticLayout: true,
minimap: { enabled: false },
folding: true,
wordWrap: 'on' as const,
theme: 'vs-dark',
}
// Servisleri yükle
const loadServices = async () => {
try {
setIsLoading(true)
const response = await dynamicServiceService.getList()
setServices(response.items || [])
} catch (error) {
console.error('Servisler yüklenirken hata:', error)
} finally {
setIsLoading(false)
}
}
// Test compile
const handleTestCompile = async () => {
if (!code.trim()) {
alert('Lütfen kod girin')
return
}
try {
setIsCompiling(true)
setCompileResult(null)
console.log('Test compile code:', code)
const input = { code: code } as TestCompileRequestDto
const result = await postTestCompile(input)
setCompileResult(result.data)
} catch (error: any) {
console.error('Test compile error:', error)
console.error('Error response:', error.response?.data)
setCompileResult({
success: false,
errorMessage: error.response?.data?.message || 'Derleme sırasında hata oluştu',
compilationTimeMs: 0,
hasWarnings: false,
errors: [],
})
} finally {
setIsCompiling(false)
}
}
// Publish
const handlePublish = async () => {
if (!code.trim() || !serviceName.trim()) {
alert('Lütfen kod ve servis adını girin')
return
}
try {
setIsPublishing(true)
setPublishResult(null)
const requestData = {
name: serviceName,
code: code,
displayName: displayName,
description: description,
primaryEntityType: primaryEntityType,
}
console.log('Publish request data:', requestData)
const result = await dynamicServiceService.publish(requestData)
setPublishResult(result)
if (result.success) {
await loadServices() // Listeyi yenile
}
} catch (error: any) {
console.error('Publish error:', error)
console.error('Error response:', error.response?.data)
setPublishResult({
success: false,
errorMessage: error.response?.data?.message || 'Yayınlama sırasında hata oluştu',
})
} finally {
setIsPublishing(false)
}
}
// Servisi yükle
const loadService = async (service: DynamicServiceDto) => {
try {
const data = await dynamicServiceService.getById(service.id)
setSelectedService(data)
setCode(data.code)
setServiceName(data.name)
setDisplayName(data.displayName || '')
setDescription(data.description || '')
setPrimaryEntityType(data.primaryEntityType || '')
setCompileResult(null)
setPublishResult(null)
} catch (error) {
console.error('Servis yüklenirken hata:', error)
}
}
// Servisi sil
const deleteService = async (serviceId: string) => {
if (!confirm('Bu servisi silmek istediğinizden emin misiniz?')) {
return
}
try {
await dynamicServiceService.delete(serviceId)
await loadServices()
if (selectedService?.id === serviceId) {
setSelectedService(null)
setCode(defaultTemplate)
setServiceName('')
setDisplayName('')
setDescription('')
setPrimaryEntityType('')
}
} catch (error) {
console.error('Servis silinirken hata:', error)
alert('Servis silinirken hata oluştu')
}
}
// Yeni servis
const newService = () => {
setSelectedService(null)
setCode(defaultTemplate)
setServiceName('')
setDisplayName('')
setDescription('')
setPrimaryEntityType('')
setCompileResult(null)
setPublishResult(null)
}
// Swagger aç
const openSwagger = () => {
window.open(`${import.meta.env.VITE_API_URL}/swagger/index.html`, '_blank')
}
// Kodu kopyala
const copyCode = () => {
navigator.clipboard.writeText(code)
alert('Kod panoya kopyalandı')
}
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="mx-auto">
{/* Header */}
<div className="bg-white rounded-lg shadow-sm border p-6 mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 mb-2">Dynamic AppService Editor</h1>
<p className="text-gray-600">
C# kod yazarak dinamik AppService'ler oluşturun ve yayınlayın
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={openSwagger}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
<FaExternalLinkAlt className="w-4 h-4" />
Swagger
</button>
<button
onClick={() => setShowServiceList(!showServiceList)}
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
>
<FaCode className="w-4 h-4" />
{showServiceList ? 'Listeyi Gizle' : 'Listeyi Göster'}
</button>
</div>
</div>
</div>
<div className="grid grid-cols-12 gap-6">
{/* Service List */}
{showServiceList && (
<div className="col-span-12 lg:col-span-4">
<div className="bg-white rounded-lg shadow-sm border">
<div className="p-4 border-b">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Mevcut Servisler</h3>
<div className="flex gap-2">
<button
onClick={newService}
className="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700"
>
<FaCode className="w-4 h-4 inline mr-1" />
Yeni
</button>
<button
onClick={loadServices}
className="bg-gray-600 text-white px-3 py-1 rounded text-sm hover:bg-gray-700"
>
<FaSync className="w-4 h-4" />
</button>
</div>
</div>
</div>
<div className="max-h-96 overflow-y-auto">
{isLoading ? (
<div className="p-4 text-center">
<FaSpinner className="w-6 h-6 animate-spin mx-auto mb-2" />
Yükleniyor...
</div>
) : services.length > 0 ? (
services.map((service) => (
<div
key={service.id}
className={`p-3 border-b hover:bg-gray-50 cursor-pointer ${
selectedService?.id === service.id
? 'bg-blue-50 border-l-4 border-l-blue-500'
: ''
}`}
onClick={() => loadService(service)}
>
<div className="flex items-center justify-between">
<div className="flex-1">
<h4 className="font-medium text-gray-900">{service.name}</h4>
{service.displayName && (
<p className="text-sm text-gray-600">{service.displayName}</p>
)}
<div className="flex items-center gap-2 mt-1">
<span
className={`px-2 py-1 text-xs rounded ${
service.compilationStatus === 'Success'
? 'bg-green-100 text-green-800'
: service.compilationStatus === 'Failed'
? 'bg-red-100 text-red-800'
: 'bg-yellow-100 text-yellow-800'
}`}
>
{service.compilationStatus}
</span>
<span className="text-xs text-gray-500">v{service.version}</span>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation()
deleteService(service.id)
}}
className="text-red-600 hover:text-red-800 p-1"
>
<FaTrash className="w-4 h-4" />
</button>
</div>
</div>
))
) : (
<div className="p-4 text-center text-gray-500">Henüz servis yok</div>
)}
</div>
</div>
</div>
)}
{/* Main Editor */}
<div className={`col-span-12 ${showServiceList ? 'lg:col-span-8' : ''}`}>
{/* Service Info Form */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Servis Adı *
</label>
<input
type="text"
value={serviceName}
onChange={(e) => setServiceName(e.target.value)}
placeholder="ör: DynamicCustomerAppService"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Görünen Ad</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="ör: Müşteri Yönetimi"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">ıklama</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Bu servisin ne yaptığınııklayın..."
rows={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ana Entity Türü
</label>
<input
type="text"
value={primaryEntityType}
onChange={(e) => setPrimaryEntityType(e.target.value)}
placeholder="ör: Customer"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="bg-white rounded-lg shadow-sm border p-4 mb-4">
<div className="flex flex-wrap items-center gap-3">
<button
onClick={handleTestCompile}
disabled={isCompiling || !code.trim()}
className="flex items-center gap-2 bg-orange-600 text-white px-4 py-2 rounded-lg hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isCompiling ? (
<FaSpinner className="w-4 h-4 animate-spin" />
) : (
<FaPlay className="w-4 h-4" />
)}
Test Compile
</button>
<button
onClick={handlePublish}
disabled={isPublishing || !code.trim() || !serviceName.trim()}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isPublishing ? (
<FaSpinner className="w-4 h-4 animate-spin" />
) : (
<FaUpload className="w-4 h-4" />
)}
Publish
</button>
<button
onClick={copyCode}
className="flex items-center gap-2 bg-gray-600 text-white px-4 py-2 rounded-lg hover:bg-gray-700 transition-colors"
>
<FaCopy className="w-4 h-4" />
Kopyala
</button>
</div>
</div>
{/* Results */}
{(compileResult || publishResult) && (
<div className="space-y-4 mb-4">
{/* Compile Result */}
{compileResult && (
<div
className={`rounded-lg border p-4 ${
compileResult.success
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-center gap-2 mb-2">
{compileResult.success ? (
<FaCheckCircle className="w-5 h-5 text-green-600" />
) : (
<FaExclamationCircle className="w-5 h-5 text-red-600" />
)}
<h4
className={`font-medium ${
compileResult.success ? 'text-green-800' : 'text-red-800'
}`}
>
Derleme {compileResult.success ? 'Başarılı' : 'Başarısız'}
</h4>
<span className="text-sm text-gray-600">
({compileResult.compilationTimeMs}ms)
</span>
</div>
{!compileResult.success &&
compileResult.errors &&
compileResult.errors.length > 0 && (
<div className="space-y-2">
{compileResult.errors.map((error, index) => (
<div key={index} className="bg-white rounded p-3 border">
<div className="flex items-start gap-2">
<span className="text-red-600 font-mono text-sm">{error.code}</span>
<div className="flex-1">
<p className="text-sm text-gray-800">{error.message}</p>
<p className="text-xs text-gray-600 mt-1">
Satır {error.line}, Sütun {error.column}
</p>
</div>
</div>
</div>
))}
</div>
)}
{compileResult.hasWarnings && compileResult.warnings && (
<div className="mt-3">
<h5 className="text-sm font-medium text-yellow-800 mb-2">Uyarılar:</h5>
<ul className="list-disc list-inside space-y-1">
{compileResult.warnings.map((warning, index) => (
<li key={index} className="text-sm text-yellow-700">
{warning}
</li>
))}
</ul>
</div>
)}
</div>
)}
{/* Publish Result */}
{publishResult && (
<div
className={`rounded-lg border p-4 ${
publishResult.success
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-center gap-2 mb-2">
{publishResult.success ? (
<FaCheckCircle className="w-5 h-5 text-green-600" />
) : (
<FaExclamationCircle className="w-5 h-5 text-red-600" />
)}
<h4
className={`font-medium ${
publishResult.success ? 'text-green-800' : 'text-red-800'
}`}
>
Yayınlama {publishResult.success ? 'Başarılı' : 'Başarısız'}
</h4>
</div>
{publishResult.success && (
<div className="space-y-2">
<p className="text-green-700 text-sm">
AppService başarıyla yayınlandı ve Swagger'a eklendi.
</p>
{publishResult.controllerName && (
<p className="text-sm text-gray-600">
Controller:{' '}
<code className="bg-gray-100 px-2 py-1 rounded">
{publishResult.controllerName}
</code>
</p>
)}
{publishResult.generatedEndpoints &&
publishResult.generatedEndpoints.length > 0 && (
<div>
<h5 className="text-sm font-medium text-gray-700 mb-1">
Oluşturulan Endpoint'ler:
</h5>
<ul className="list-disc list-inside space-y-1">
{publishResult.generatedEndpoints.map((endpoint, index) => (
<li key={index} className="text-sm text-gray-600 font-mono">
{endpoint}
</li>
))}
</ul>
</div>
)}
<div className="flex gap-2 mt-3">
<button
onClick={openSwagger}
className="flex items-center gap-1 bg-blue-600 text-white px-3 py-1 rounded text-sm hover:bg-blue-700"
>
<FaExternalLinkAlt className="w-3 h-3" />
Swagger'da Gör
</button>
</div>
</div>
)}
{!publishResult.success && publishResult.errorMessage && (
<p className="text-red-700 text-sm">{publishResult.errorMessage}</p>
)}
</div>
)}
</div>
)}
{/* Monaco Editor */}
<div className="bg-white rounded-lg shadow-sm border overflow-hidden">
<div className="p-3 bg-gray-50 border-b flex items-center justify-between">
<h3 className="font-medium text-gray-700">C# Code Editor</h3>
<div className="flex items-center gap-2 text-sm text-gray-600">
<span>Lines: {code.split('\n').length}</span>
<span>|</span>
<span>Characters: {code.length}</span>
</div>
</div>
<div style={{ height: '600px' }}>
<Editor
defaultLanguage="csharp"
value={code}
onChange={(value) => setCode(value || '')}
options={editorOptions}
theme="vs-dark"
/>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default DynamicAppServiceEditor

View file

@ -43,6 +43,7 @@ export const ROUTES_ENUM = {
componentsNew: '/admin/developerkit/components/new', componentsNew: '/admin/developerkit/components/new',
componentsView: '/admin/developerkit/components/view/:id', componentsView: '/admin/developerkit/components/view/:id',
componentsEdit: '/admin/developerkit/components/edit/:id', componentsEdit: '/admin/developerkit/components/edit/:id',
dynamicServices: '/admin/developerkit/dynamic-services',
}, },
reports: { reports: {
generator: '/admin/reports/generator', generator: '/admin/reports/generator',

View file

@ -0,0 +1,182 @@
import apiService from './api.service'
export interface CompilationError {
code: string
message: string
line: number
column: number
severity: string
fileName?: string
}
export interface CompileResult {
success: boolean
errorMessage?: string
errors?: CompilationError[]
compilationTimeMs: number
hasWarnings: boolean
warnings?: string[]
}
export interface PublishResult {
success: boolean
errorMessage?: string
appServiceId?: string
swaggerUrl?: string
generatedEndpoints?: string[]
controllerName?: string
}
export interface DynamicServiceDto {
id: string
name: string
displayName: string
description: string
code: string
isActive: boolean
compilationStatus: string
lastCompilationError?: string
lastSuccessfulCompilation?: string
version: number
codeHash?: string
primaryEntityType?: string
controllerName?: string
creationTime: string
}
export interface TestCompileDto {
code: string
}
export interface TestCompileRequestDto {
Code: string
}
export interface PublishDto {
name: string
code: string
displayName?: string
description?: string
primaryEntityType?: string
}
export interface DynamicAppServiceListResult {
items: DynamicServiceDto[]
totalCount: number
}
export const postTestCompile = (input: TestCompileRequestDto) =>
apiService.fetchData<CompileResult>({
method: 'POST',
url: `/api/app/dynamic-app-service/test-compile`,
data: input as any,
})
class DynamicServiceService {
private readonly baseUrl = '/api/app/dynamic-app-service'
/**
* Tüm dynamic app service'leri listele
*/
async getList(): Promise<DynamicAppServiceListResult> {
const response = await apiService.fetchData<DynamicAppServiceListResult>({
url: this.baseUrl,
method: 'GET',
})
return response.data
}
/**
* ID'ye göre dynamic app service detayını getir
*/
async getById(id: string): Promise<DynamicServiceDto> {
const response = await apiService.fetchData<DynamicServiceDto>({
url: `${this.baseUrl}/${id}`,
method: 'GET',
})
return response.data
}
/**
* Yeni dynamic app service oluştur
*/
async create(data: DynamicServiceDto): Promise<DynamicServiceDto> {
const response = await apiService.fetchData<DynamicServiceDto>({
url: this.baseUrl,
method: 'POST',
data: data as any,
})
return response.data
}
/**
* Dynamic app service güncelle
*/
async update(id: string, data: Partial<DynamicServiceDto>): Promise<DynamicServiceDto> {
const response = await apiService.fetchData<DynamicServiceDto>({
url: `${this.baseUrl}/${id}`,
method: 'PUT',
data: data as any,
})
return response.data
}
/**
* Dynamic app service sil
*/
async delete(id: string): Promise<void> {
await apiService.fetchData<void>({
url: `${this.baseUrl}/${id}`,
method: 'DELETE',
})
}
/**
* Kodu test et (compile)
*/
async testCompile(data: TestCompileDto): Promise<CompileResult> {
console.log('DynamicServiceService.testCompile called with data:', data)
const response = await apiService.fetchData<CompileResult>({
url: `${this.baseUrl}/test-compile`,
method: 'POST',
data: data as any,
})
return response.data
}
/**
* Dynamic app service'i yayınla
*/
async publish(data: PublishDto): Promise<PublishResult> {
const response = await apiService.fetchData<PublishResult>({
url: `${this.baseUrl}/publish`,
method: 'POST',
data: data as any,
})
return response.data
}
/**
* Dynamic app service'i aktif/pasif yap
*/
async toggleActive(id: string, isActive: boolean): Promise<void> {
await apiService.fetchData<void>({
url: `${this.baseUrl}/${id}/toggle-active`,
method: 'POST',
data: { isActive } as any,
})
}
/**
* Compilation durumunu getir
*/
async getCompilationStatus(id: string): Promise<{ status: string; error?: string }> {
const response = await apiService.fetchData<{ status: string; error?: string }>({
url: `${this.baseUrl}/${id}/compilation-status`,
method: 'GET',
})
return response.data
}
}
export const dynamicServiceService = new DynamicServiceService()

View file

@ -0,0 +1,19 @@
import React from 'react'
import DynamicAppServiceEditor from '@/components/developerKit/DynamicAppServiceEditor'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet'
const DynamicServicePage: React.FC = () => {
const { translate } = useLocalization()
return (
<>
<Helmet>
<title>Dynamic Services - Developer Kit</title>
</Helmet>
<DynamicAppServiceEditor />
</>
)
}
export default DynamicServicePage