DynamicServiceManager
This commit is contained in:
parent
f91955a42e
commit
ff088fd9d7
25 changed files with 16424 additions and 1 deletions
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -145,5 +145,6 @@ public enum TableNameEnum
|
||||||
Partner,
|
Partner,
|
||||||
PartnerBank,
|
PartnerBank,
|
||||||
PartnerCertificate,
|
PartnerCertificate,
|
||||||
PartnerContact
|
PartnerContact,
|
||||||
|
DynamicService
|
||||||
}
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13841
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251104205840_DynamicService.Designer.cs
generated
Normal file
13841
api/src/Kurs.Platform.EntityFrameworkCore/Migrations/20251104205840_DynamicService.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
628
ui/src/components/developerKit/DynamicAppServiceEditor.tsx
Normal file
628
ui/src/components/developerKit/DynamicAppServiceEditor.tsx
Normal 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">Açıklama</label>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Bu servisin ne yaptığını açı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
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
182
ui/src/services/dynamicService.service.ts
Normal file
182
ui/src/services/dynamicService.service.ts
Normal 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()
|
||||||
19
ui/src/views/developerKit/DynamicServicePage.tsx
Normal file
19
ui/src/views/developerKit/DynamicServicePage.tsx
Normal 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
|
||||||
Loading…
Reference in a new issue