This commit is contained in:
2025-11-09 02:08:31 +07:00
parent 1ed58d1a98
commit 7dba29c66f
57 changed files with 8362 additions and 359 deletions

View File

@@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Managing.Infrastructure.Databases.Migrations
{
/// <inheritdoc />
public partial class AddJobTypeAndGeneticRequestIdToBacktestJobs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BacktestJobs",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
BundleRequestId = table.Column<Guid>(type: "uuid", nullable: true),
UserId = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
JobType = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
Priority = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
ConfigJson = table.Column<string>(type: "jsonb", nullable: false),
StartDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
EndDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ProgressPercentage = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
AssignedWorkerId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
LastHeartbeat = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
StartedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CompletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ResultJson = table.Column<string>(type: "jsonb", nullable: true),
ErrorMessage = table.Column<string>(type: "text", nullable: true),
RequestId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
GeneticRequestId = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BacktestJobs", x => x.Id);
table.ForeignKey(
name: "FK_BacktestJobs_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "idx_assigned_worker",
table: "BacktestJobs",
columns: new[] { "AssignedWorkerId", "Status" });
migrationBuilder.CreateIndex(
name: "idx_bundle_request",
table: "BacktestJobs",
column: "BundleRequestId");
migrationBuilder.CreateIndex(
name: "idx_genetic_request",
table: "BacktestJobs",
column: "GeneticRequestId");
migrationBuilder.CreateIndex(
name: "idx_status_jobtype_priority_created",
table: "BacktestJobs",
columns: new[] { "Status", "JobType", "Priority", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "idx_user_status",
table: "BacktestJobs",
columns: new[] { "UserId", "Status" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BacktestJobs");
}
}
}

View File

@@ -706,6 +706,95 @@ namespace Managing.Infrastructure.Databases.Migrations
b.ToTable("Indicators");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("AssignedWorkerId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<Guid?>("BundleRequestId")
.HasColumnType("uuid");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("ConfigJson")
.IsRequired()
.HasColumnType("jsonb");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("ErrorMessage")
.HasColumnType("text");
b.Property<string>("GeneticRequestId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<int>("JobType")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<DateTime?>("LastHeartbeat")
.HasColumnType("timestamp with time zone");
b.Property<int>("Priority")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<int>("ProgressPercentage")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<string>("RequestId")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("ResultJson")
.HasColumnType("jsonb");
b.Property<DateTime>("StartDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("StartedAt")
.HasColumnType("timestamp with time zone");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("BundleRequestId")
.HasDatabaseName("idx_bundle_request");
b.HasIndex("GeneticRequestId")
.HasDatabaseName("idx_genetic_request");
b.HasIndex("AssignedWorkerId", "Status")
.HasDatabaseName("idx_assigned_worker");
b.HasIndex("UserId", "Status")
.HasDatabaseName("idx_user_status");
b.HasIndex("Status", "JobType", "Priority", "CreatedAt")
.HasDatabaseName("idx_status_jobtype_priority_created");
b.ToTable("BacktestJobs");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b =>
{
b.Property<int>("Id")
@@ -1494,6 +1583,17 @@ namespace Managing.Infrastructure.Databases.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.JobEntity", b =>
{
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.SetNull)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Managing.Infrastructure.Databases.PostgreSql.Entities.MoneyManagementEntity", b =>
{
b.HasOne("Managing.Infrastructure.Databases.PostgreSql.Entities.UserEntity", "User")

View File

@@ -0,0 +1,67 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Managing.Infrastructure.Databases.PostgreSql.Entities;
[Table("BacktestJobs")]
public class JobEntity
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public Guid Id { get; set; }
public Guid? BundleRequestId { get; set; }
[Required]
public int UserId { get; set; }
[Required]
public int Status { get; set; } // BacktestJobStatus enum as int
[Required]
public int JobType { get; set; } // JobType enum as int
[Required]
public int Priority { get; set; } = 0;
[Required]
[Column(TypeName = "jsonb")]
public string ConfigJson { get; set; } = string.Empty;
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
[Required]
public int ProgressPercentage { get; set; } = 0;
[MaxLength(255)]
public string? AssignedWorkerId { get; set; }
public DateTime? LastHeartbeat { get; set; }
[Required]
public DateTime CreatedAt { get; set; }
public DateTime? StartedAt { get; set; }
public DateTime? CompletedAt { get; set; }
[Column(TypeName = "jsonb")]
public string? ResultJson { get; set; }
[Column(TypeName = "text")]
public string? ErrorMessage { get; set; }
[MaxLength(255)]
public string? RequestId { get; set; }
[MaxLength(255)]
public string? GeneticRequestId { get; set; }
// Navigation property
public UserEntity? User { get; set; }
}

View File

@@ -27,6 +27,7 @@ public class ManagingDbContext : DbContext
public DbSet<GeneticRequestEntity> GeneticRequests { get; set; }
public DbSet<BacktestEntity> Backtests { get; set; }
public DbSet<BundleBacktestRequestEntity> BundleBacktestRequests { get; set; }
public DbSet<JobEntity> Jobs { get; set; }
// Trading entities
public DbSet<ScenarioEntity> Scenarios { get; set; }
@@ -231,6 +232,45 @@ public class ManagingDbContext : DbContext
entity.HasIndex(e => new { e.UserId, e.Name, e.Version });
});
// Configure BacktestJob entity
modelBuilder.Entity<JobEntity>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.Id).ValueGeneratedNever(); // GUIDs are generated by application
entity.Property(e => e.UserId).IsRequired();
entity.Property(e => e.Status).IsRequired();
entity.Property(e => e.JobType).IsRequired().HasDefaultValue(0); // 0 = Backtest
entity.Property(e => e.Priority).IsRequired().HasDefaultValue(0);
entity.Property(e => e.ConfigJson).IsRequired().HasColumnType("jsonb");
entity.Property(e => e.StartDate).IsRequired();
entity.Property(e => e.EndDate).IsRequired();
entity.Property(e => e.ProgressPercentage).IsRequired().HasDefaultValue(0);
entity.Property(e => e.AssignedWorkerId).HasMaxLength(255);
entity.Property(e => e.ResultJson).HasColumnType("jsonb");
entity.Property(e => e.ErrorMessage).HasColumnType("text");
entity.Property(e => e.RequestId).HasMaxLength(255);
entity.Property(e => e.GeneticRequestId).HasMaxLength(255);
entity.Property(e => e.CreatedAt).IsRequired();
// Indexes for efficient job claiming and queries
entity.HasIndex(e => new { e.Status, e.JobType, e.Priority, e.CreatedAt })
.HasDatabaseName("idx_status_jobtype_priority_created");
entity.HasIndex(e => e.BundleRequestId)
.HasDatabaseName("idx_bundle_request");
entity.HasIndex(e => new { e.AssignedWorkerId, e.Status })
.HasDatabaseName("idx_assigned_worker");
entity.HasIndex(e => new { e.UserId, e.Status })
.HasDatabaseName("idx_user_status");
entity.HasIndex(e => e.GeneticRequestId)
.HasDatabaseName("idx_genetic_request");
// Configure relationship with User
entity.HasOne(e => e.User)
.WithMany()
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
});
// Configure Scenario entity
modelBuilder.Entity<ScenarioEntity>(entity =>
{

View File

@@ -0,0 +1,485 @@
using Managing.Application.Abstractions.Repositories;
using Managing.Domain.Backtests;
using Managing.Infrastructure.Databases.PostgreSql.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using static Managing.Common.Enums;
namespace Managing.Infrastructure.Databases.PostgreSql;
public class PostgreSqlJobRepository : IBacktestJobRepository
{
private readonly ManagingDbContext _context;
private readonly ILogger<PostgreSqlJobRepository> _logger;
public PostgreSqlJobRepository(
ManagingDbContext context,
ILogger<PostgreSqlJobRepository> logger)
{
_context = context;
_logger = logger;
}
public async Task<BacktestJob> CreateAsync(BacktestJob job)
{
var entity = MapToEntity(job);
_context.Jobs.Add(entity);
await _context.SaveChangesAsync();
return MapToDomain(entity);
}
public async Task<BacktestJob?> ClaimNextJobAsync(string workerId, JobType? jobType = null)
{
// Use execution strategy to support retry with transactions
// FOR UPDATE SKIP LOCKED ensures only one worker can claim a specific job
var strategy = _context.Database.CreateExecutionStrategy();
return await strategy.ExecuteAsync(async () =>
{
await using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Build SQL query with optional job type filter
var sql = @"
SELECT * FROM ""BacktestJobs""
WHERE ""Status"" = {0}";
var parameters = new List<object> { (int)BacktestJobStatus.Pending };
if (jobType.HasValue)
{
sql += @" AND ""JobType"" = {1}";
parameters.Add((int)jobType.Value);
}
sql += @"
ORDER BY ""Priority"" DESC, ""CreatedAt"" ASC
LIMIT 1
FOR UPDATE SKIP LOCKED";
// Use raw SQL with FromSqlRaw to get the next job with row-level locking
var job = await _context.Jobs
.FromSqlRaw(sql, parameters.ToArray())
.FirstOrDefaultAsync();
if (job == null)
{
await transaction.RollbackAsync();
return null;
}
// Update the job status atomically
job.Status = (int)BacktestJobStatus.Running;
job.AssignedWorkerId = workerId;
job.StartedAt = DateTime.UtcNow;
job.LastHeartbeat = DateTime.UtcNow;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
return MapToDomain(job);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Error claiming job for worker {WorkerId}", workerId);
throw;
}
});
}
public async Task UpdateAsync(BacktestJob job)
{
// Use AsTracking() to enable change tracking since DbContext uses NoTracking by default
var entity = await _context.Jobs
.AsTracking()
.FirstOrDefaultAsync(e => e.Id == job.Id);
if (entity == null)
{
_logger.LogWarning("Job {JobId} not found for update", job.Id);
return;
}
// Update entity properties
entity.Status = (int)job.Status;
entity.JobType = (int)job.JobType;
entity.ProgressPercentage = job.ProgressPercentage;
entity.AssignedWorkerId = job.AssignedWorkerId;
entity.LastHeartbeat = job.LastHeartbeat;
entity.StartedAt = job.StartedAt;
entity.CompletedAt = job.CompletedAt;
entity.ResultJson = job.ResultJson;
entity.ErrorMessage = job.ErrorMessage;
entity.RequestId = job.RequestId;
entity.GeneticRequestId = job.GeneticRequestId;
entity.Priority = job.Priority;
await _context.SaveChangesAsync();
}
public async Task<IEnumerable<BacktestJob>> GetByBundleRequestIdAsync(Guid bundleRequestId)
{
var entities = await _context.Jobs
.Where(j => j.BundleRequestId == bundleRequestId)
.ToListAsync();
return entities.Select(MapToDomain);
}
public async Task<IEnumerable<BacktestJob>> GetByUserIdAsync(int userId)
{
var entities = await _context.Jobs
.Where(j => j.UserId == userId)
.ToListAsync();
return entities.Select(MapToDomain);
}
/// <summary>
/// Gets all running jobs assigned to a specific worker
/// </summary>
public async Task<IEnumerable<BacktestJob>> GetRunningJobsByWorkerIdAsync(string workerId)
{
var entities = await _context.Jobs
.Where(j => j.AssignedWorkerId == workerId && j.Status == (int)BacktestJobStatus.Running)
.ToListAsync();
return entities.Select(MapToDomain);
}
public async Task<IEnumerable<BacktestJob>> GetByGeneticRequestIdAsync(string geneticRequestId)
{
var entities = await _context.Jobs
.Where(j => j.GeneticRequestId == geneticRequestId)
.ToListAsync();
return entities.Select(MapToDomain);
}
public async Task<(IEnumerable<BacktestJob> Jobs, int TotalCount)> GetPaginatedAsync(
int page,
int pageSize,
string sortBy = "CreatedAt",
string sortOrder = "desc",
BacktestJobStatus? status = null,
JobType? jobType = null,
int? userId = null,
string? workerId = null,
Guid? bundleRequestId = null)
{
var query = _context.Jobs.AsQueryable();
// Apply filters
if (status.HasValue)
{
query = query.Where(j => j.Status == (int)status.Value);
}
if (jobType.HasValue)
{
query = query.Where(j => j.JobType == (int)jobType.Value);
}
if (userId.HasValue)
{
query = query.Where(j => j.UserId == userId.Value);
}
if (!string.IsNullOrEmpty(workerId))
{
query = query.Where(j => j.AssignedWorkerId == workerId);
}
if (bundleRequestId.HasValue)
{
query = query.Where(j => j.BundleRequestId == bundleRequestId.Value);
}
// Get total count before pagination
var totalCount = await query.CountAsync();
// Apply sorting
query = sortBy.ToLower() switch
{
"createdat" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.CreatedAt)
: query.OrderByDescending(j => j.CreatedAt),
"startedat" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.StartedAt)
: query.OrderByDescending(j => j.StartedAt),
"completedat" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.CompletedAt)
: query.OrderByDescending(j => j.CompletedAt),
"priority" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.Priority)
: query.OrderByDescending(j => j.Priority),
"status" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.Status)
: query.OrderByDescending(j => j.Status),
"jobtype" => sortOrder.ToLower() == "asc"
? query.OrderBy(j => j.JobType)
: query.OrderByDescending(j => j.JobType),
_ => query.OrderByDescending(j => j.CreatedAt) // Default sort
};
// Apply pagination
var entities = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var jobs = entities.Select(MapToDomain);
return (jobs, totalCount);
}
public async Task<BacktestJob?> GetByIdAsync(Guid jobId)
{
var entity = await _context.Jobs
.FirstOrDefaultAsync(j => j.Id == jobId);
return entity != null ? MapToDomain(entity) : null;
}
public async Task<IEnumerable<BacktestJob>> GetStaleJobsAsync(int timeoutMinutes = 5)
{
var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes);
var entities = await _context.Jobs
.Where(j => j.Status == (int)BacktestJobStatus.Running &&
(j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold))
.ToListAsync();
return entities.Select(MapToDomain);
}
public async Task<int> ResetStaleJobsAsync(int timeoutMinutes = 5)
{
var timeoutThreshold = DateTime.UtcNow.AddMinutes(-timeoutMinutes);
// Use AsTracking() to enable change tracking since DbContext uses NoTracking by default
var staleJobs = await _context.Jobs
.AsTracking()
.Where(j => j.Status == (int)BacktestJobStatus.Running &&
(j.LastHeartbeat == null || j.LastHeartbeat < timeoutThreshold))
.ToListAsync();
foreach (var job in staleJobs)
{
job.Status = (int)BacktestJobStatus.Pending;
job.AssignedWorkerId = null;
job.LastHeartbeat = null;
}
var count = staleJobs.Count;
if (count > 0)
{
await _context.SaveChangesAsync();
_logger.LogInformation("Reset {Count} stale jobs back to Pending status", count);
}
return count;
}
public async Task<JobSummary> GetSummaryAsync()
{
// Use ADO.NET directly for aggregation queries to avoid EF Core mapping issues
var connection = _context.Database.GetDbConnection();
await connection.OpenAsync();
try
{
var statusCounts = new List<StatusCountResult>();
var jobTypeCounts = new List<JobTypeCountResult>();
var statusTypeCounts = new List<StatusTypeCountResult>();
var totalJobs = 0;
// Query 1: Status summary
var statusSummarySql = @"
SELECT ""Status"", COUNT(*) as Count
FROM ""BacktestJobs""
GROUP BY ""Status""
ORDER BY ""Status""";
using (var command = connection.CreateCommand())
{
command.CommandText = statusSummarySql;
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
statusCounts.Add(new StatusCountResult
{
Status = reader.GetInt32(0),
Count = reader.GetInt32(1)
});
}
}
}
// Query 2: Job type summary
var jobTypeSummarySql = @"
SELECT ""JobType"", COUNT(*) as Count
FROM ""BacktestJobs""
GROUP BY ""JobType""
ORDER BY ""JobType""";
using (var command = connection.CreateCommand())
{
command.CommandText = jobTypeSummarySql;
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
jobTypeCounts.Add(new JobTypeCountResult
{
JobType = reader.GetInt32(0),
Count = reader.GetInt32(1)
});
}
}
}
// Query 3: Status + Job type summary
var statusTypeSummarySql = @"
SELECT ""Status"", ""JobType"", COUNT(*) as Count
FROM ""BacktestJobs""
GROUP BY ""Status"", ""JobType""
ORDER BY ""Status"", ""JobType""";
using (var command = connection.CreateCommand())
{
command.CommandText = statusTypeSummarySql;
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
statusTypeCounts.Add(new StatusTypeCountResult
{
Status = reader.GetInt32(0),
JobType = reader.GetInt32(1),
Count = reader.GetInt32(2)
});
}
}
}
// Query 4: Total count
var totalCountSql = @"
SELECT COUNT(*) as Count
FROM ""BacktestJobs""";
using (var command = connection.CreateCommand())
{
command.CommandText = totalCountSql;
var result = await command.ExecuteScalarAsync();
totalJobs = result != null ? Convert.ToInt32(result) : 0;
}
return new JobSummary
{
StatusCounts = statusCounts.Select(s => new JobStatusCount
{
Status = (BacktestJobStatus)s.Status,
Count = s.Count
}).ToList(),
JobTypeCounts = jobTypeCounts.Select(j => new JobTypeCount
{
JobType = (JobType)j.JobType,
Count = j.Count
}).ToList(),
StatusTypeCounts = statusTypeCounts.Select(st => new JobStatusTypeCount
{
Status = (BacktestJobStatus)st.Status,
JobType = (JobType)st.JobType,
Count = st.Count
}).ToList(),
TotalJobs = totalJobs
};
}
finally
{
await connection.CloseAsync();
}
}
// Helper classes for raw SQL query results
private class StatusCountResult
{
public int Status { get; set; }
public int Count { get; set; }
}
private class JobTypeCountResult
{
public int JobType { get; set; }
public int Count { get; set; }
}
private class StatusTypeCountResult
{
public int Status { get; set; }
public int JobType { get; set; }
public int Count { get; set; }
}
private class TotalCountResult
{
public int Count { get; set; }
}
private static JobEntity MapToEntity(BacktestJob job)
{
return new JobEntity
{
Id = job.Id,
BundleRequestId = job.BundleRequestId,
UserId = job.UserId,
Status = (int)job.Status,
JobType = (int)job.JobType,
Priority = job.Priority,
ConfigJson = job.ConfigJson,
StartDate = job.StartDate,
EndDate = job.EndDate,
ProgressPercentage = job.ProgressPercentage,
AssignedWorkerId = job.AssignedWorkerId,
LastHeartbeat = job.LastHeartbeat,
CreatedAt = job.CreatedAt,
StartedAt = job.StartedAt,
CompletedAt = job.CompletedAt,
ResultJson = job.ResultJson,
ErrorMessage = job.ErrorMessage,
RequestId = job.RequestId,
GeneticRequestId = job.GeneticRequestId
};
}
private static BacktestJob MapToDomain(JobEntity entity)
{
return new BacktestJob
{
Id = entity.Id,
BundleRequestId = entity.BundleRequestId,
UserId = entity.UserId,
Status = (BacktestJobStatus)entity.Status,
JobType = (JobType)entity.JobType,
Priority = entity.Priority,
ConfigJson = entity.ConfigJson,
StartDate = entity.StartDate,
EndDate = entity.EndDate,
ProgressPercentage = entity.ProgressPercentage,
AssignedWorkerId = entity.AssignedWorkerId,
LastHeartbeat = entity.LastHeartbeat,
CreatedAt = entity.CreatedAt,
StartedAt = entity.StartedAt,
CompletedAt = entity.CompletedAt,
ResultJson = entity.ResultJson,
ErrorMessage = entity.ErrorMessage,
RequestId = entity.RequestId,
GeneticRequestId = entity.GeneticRequestId
};
}
}