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
|
||||
CreateMap<ApiMigration, ApiMigrationDto>();
|
||||
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.TenantManagement.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>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -9937,6 +9937,12 @@
|
|||
"en": "fields",
|
||||
"tr": "alan"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.DynamicServices",
|
||||
"en": "Dynamic Services",
|
||||
"tr": "Dinamik Hizmetler"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.Migration.Generating",
|
||||
|
|
|
|||
|
|
@ -350,6 +350,15 @@
|
|||
"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",
|
||||
"path": "/admin/developerkit/components",
|
||||
|
|
@ -1646,6 +1655,16 @@
|
|||
"RequiredPermissionName": "App.DeveloperKit.CustomEndpoints",
|
||||
"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",
|
||||
"Code": "App.DeveloperKit.Components",
|
||||
|
|
|
|||
|
|
@ -2335,6 +2335,78 @@
|
|||
"MultiTenancySide": 3,
|
||||
"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",
|
||||
"Name": "App.Reports.Categories",
|
||||
|
|
|
|||
|
|
@ -145,5 +145,6 @@ public enum TableNameEnum
|
|||
Partner,
|
||||
PartnerBank,
|
||||
PartnerCertificate,
|
||||
PartnerContact
|
||||
PartnerContact,
|
||||
DynamicService
|
||||
}
|
||||
|
|
@ -71,6 +71,20 @@ public static class PlatformConsts
|
|||
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 const string GroupName = $"{Prefix.Abp}.Identity";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public static class TableNameResolver
|
|||
_map = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
// 🔹 MODULE TABLOLARI
|
||||
{ nameof(TableNameEnum.DynamicService), (TablePrefix.PlatformByName, MenuPrefix.Platform) },
|
||||
{ nameof(TableNameEnum.LogEntry), (TablePrefix.PlatformByName, MenuPrefix.Platform) },
|
||||
{ nameof(TableNameEnum.Language), (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.Settings.SettingsConsts;
|
||||
using Kurs.Platform.Enums;
|
||||
using Kurs.Platform.Domain.DeveloperKit;
|
||||
|
||||
namespace Kurs.Platform.EntityFrameworkCore;
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ public class PlatformDbContext :
|
|||
public DbSet<ApiEndpoint> GeneratedEndpoints { get; set; }
|
||||
public DbSet<CustomEndpoint> CustomEndpoints { get; set; }
|
||||
public DbSet<CustomComponent> CustomComponents { get; set; }
|
||||
public DbSet<DynamicService> DynamicServices { get; set; }
|
||||
public DbSet<ReportTemplate> ReportTemplates { get; set; }
|
||||
public DbSet<ReportParameter> ReportParameters { get; set; }
|
||||
public DbSet<ReportGenerated> ReportGenerated { get; set; }
|
||||
|
|
@ -100,6 +102,8 @@ public class PlatformDbContext :
|
|||
public DbSet<Contact> Contacts { get; set; }
|
||||
public DbSet<Sector> Sectors { get; set; }
|
||||
public DbSet<SkillType> SkillTypes { get; set; }
|
||||
|
||||
// Dynamic Services
|
||||
public DbSet<Skill> Skills { get; set; }
|
||||
public DbSet<SkillLevel> SkillLevels { 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.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);
|
||||
});
|
||||
|
||||
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 =>
|
||||
{
|
||||
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.UI.Navigation.Urls;
|
||||
using Volo.Abp.VirtualFileSystem;
|
||||
using Kurs.Platform.DynamicServices;
|
||||
using static Kurs.Platform.PlatformConsts;
|
||||
using static Kurs.Settings.SettingsConsts;
|
||||
|
||||
|
|
@ -120,6 +121,8 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
ConfigureHangfire(context, configuration);
|
||||
ConfigureBlobStoring(configuration);
|
||||
ConfigureAuditing();
|
||||
|
||||
ConfigureDynamicServices(context);
|
||||
|
||||
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)
|
||||
{
|
||||
var app = context.GetApplicationBuilder();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Threading.Tasks;
|
||||
using Kurs.Platform.Classrooms;
|
||||
using Kurs.Platform.Enums;
|
||||
using Kurs.Platform.DynamicServices;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
|
@ -146,6 +147,9 @@ public class Program
|
|||
|
||||
var app = builder.Build();
|
||||
app.UseCors("Dynamic");
|
||||
|
||||
// Dynamic Assembly Registration Delegate Setup
|
||||
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
|
||||
await app.InitializeApplicationAsync();
|
||||
await app.RunAsync();
|
||||
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',
|
||||
componentsView: '/admin/developerkit/components/view/:id',
|
||||
componentsEdit: '/admin/developerkit/components/edit/:id',
|
||||
dynamicServices: '/admin/developerkit/dynamic-services',
|
||||
},
|
||||
reports: {
|
||||
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