Initial commit
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Seed default parameter sets
|
||||
/// </summary>
|
||||
public static class DefaultDataSeeder
|
||||
{
|
||||
public static async Task SeedAsync(TuningDbContext context)
|
||||
{
|
||||
// Seed default parameter set if not exists
|
||||
if (!await context.ParameterSets.AnyAsync(p => p.IsDefault))
|
||||
{
|
||||
var defaultParams = new NavigationParameterSet
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Default",
|
||||
Description = "Default parameter set",
|
||||
IsDefault = true,
|
||||
Version = 1
|
||||
};
|
||||
|
||||
context.ParameterSets.Add(defaultParams);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.NavigationTune.Data;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using RobotNet10.NavigationTune.Scenarios;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for test scenarios
|
||||
/// </summary>
|
||||
public interface IScenarioRepository
|
||||
{
|
||||
Task<TestScenario?> GetByIdAsync(Guid id);
|
||||
Task<List<TestScenario>> GetAllAsync();
|
||||
Task<List<TestScenario>> GetDefaultScenariosAsync();
|
||||
Task<TestScenarioEntity?> GetEntityByIdAsync(Guid id);
|
||||
Task<List<TestScenarioEntity>> GetAllEntitiesAsync();
|
||||
Task<List<TestScenarioEntity>> GetDefaultScenarioEntitiesAsync();
|
||||
Task<Guid> SaveAsync(TestScenario scenario);
|
||||
Task UpdateAsync(TestScenario scenario);
|
||||
Task DeleteAsync(Guid id);
|
||||
}
|
||||
|
||||
public class ScenarioRepository(TuningDbContext context) : IScenarioRepository
|
||||
{
|
||||
public async Task<TestScenario?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var entity = await context.TestScenarios.FindAsync(id);
|
||||
return entity?.ToTestScenario();
|
||||
}
|
||||
|
||||
public async Task<List<TestScenario>> GetAllAsync()
|
||||
{
|
||||
var entities = await context.TestScenarios
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return [.. entities.Select(e => e.ToTestScenario())];
|
||||
}
|
||||
|
||||
public async Task<List<TestScenario>> GetDefaultScenariosAsync()
|
||||
{
|
||||
var entities = await context.TestScenarios
|
||||
.Where(s => s.IsDefault)
|
||||
.OrderBy(s => s.Name)
|
||||
.ToListAsync();
|
||||
|
||||
return [.. entities.Select(e => e.ToTestScenario())];
|
||||
}
|
||||
|
||||
public async Task<TestScenarioEntity?> GetEntityByIdAsync(Guid id)
|
||||
{
|
||||
return await context.TestScenarios.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<List<TestScenarioEntity>> GetAllEntitiesAsync()
|
||||
{
|
||||
return await context.TestScenarios
|
||||
.OrderByDescending(s => s.CreatedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TestScenarioEntity>> GetDefaultScenarioEntitiesAsync()
|
||||
{
|
||||
return await context.TestScenarios
|
||||
.Where(s => s.IsDefault)
|
||||
.OrderBy(s => s.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<Guid> SaveAsync(TestScenario scenario)
|
||||
{
|
||||
var entity = TestScenarioEntity.FromTestScenario(scenario);
|
||||
context.TestScenarios.Add(entity);
|
||||
await context.SaveChangesAsync();
|
||||
return entity.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TestScenario scenario)
|
||||
{
|
||||
var existing = await context.TestScenarios.FindAsync(scenario.Id) ?? throw new InvalidOperationException($"Scenario with ID {scenario.Id} not found.");
|
||||
var updated = TestScenarioEntity.FromTestScenario(scenario);
|
||||
existing.Name = updated.Name;
|
||||
existing.Description = updated.Description;
|
||||
existing.Type = updated.Type;
|
||||
existing.IsDefault = updated.IsDefault;
|
||||
existing.ConfigJson = updated.ConfigJson;
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var entity = await context.TestScenarios.FindAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
context.TestScenarios.Remove(entity);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet10.NavigationTune.Shared.Interfaces;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for test runs
|
||||
/// </summary>
|
||||
public class TestRepository(TuningDbContext context) : ITestRepository
|
||||
{
|
||||
public async Task<TestRun?> GetByIdAsync(Guid id)
|
||||
{
|
||||
var testRun = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.Include(r => r.SafetyViolations)
|
||||
.FirstOrDefaultAsync(r => r.Id == id);
|
||||
|
||||
if (testRun != null)
|
||||
{
|
||||
// Load scenario and parameter set separately
|
||||
var scenarioEntity = await context.TestScenarios.FindAsync(testRun.ScenarioId);
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = await context.ParameterSets.FindAsync(testRun.ParameterSetId);
|
||||
}
|
||||
|
||||
return testRun;
|
||||
}
|
||||
|
||||
public async Task<List<TestRun>> GetAllAsync()
|
||||
{
|
||||
var testRuns = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.OrderByDescending(r => r.StartTime)
|
||||
.ToListAsync();
|
||||
|
||||
// Load scenarios and parameter sets
|
||||
foreach (var testRun in testRuns)
|
||||
{
|
||||
var scenarioEntity = await context.TestScenarios.FindAsync(testRun.ScenarioId);
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = await context.ParameterSets.FindAsync(testRun.ParameterSetId);
|
||||
}
|
||||
|
||||
return testRuns;
|
||||
}
|
||||
|
||||
public async Task<int> GetCountAsync()
|
||||
{
|
||||
return await context.TestRuns.CountAsync();
|
||||
}
|
||||
|
||||
public async Task<List<TestRun>> GetPagedAsync(int skip, int take)
|
||||
{
|
||||
var testRuns = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.OrderByDescending(r => r.StartTime)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
var scenarioIds = testRuns.Select(r => r.ScenarioId).Distinct().ToList();
|
||||
var parameterSetIds = testRuns.Select(r => r.ParameterSetId).Distinct().ToList();
|
||||
|
||||
var scenarios = await context.TestScenarios
|
||||
.Where(s => scenarioIds.Contains(s.Id))
|
||||
.ToListAsync();
|
||||
var parameterSets = await context.ParameterSets
|
||||
.Where(p => parameterSetIds.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var testRun in testRuns)
|
||||
{
|
||||
var scenarioEntity = scenarios.FirstOrDefault(s => s.Id == testRun.ScenarioId);
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = parameterSets.FirstOrDefault(p => p.Id == testRun.ParameterSetId);
|
||||
}
|
||||
|
||||
return testRuns;
|
||||
}
|
||||
|
||||
public async Task<List<TestRun>> GetByScenarioAsync(Guid scenarioId)
|
||||
{
|
||||
var testRuns = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.Where(r => r.ScenarioId == scenarioId)
|
||||
.OrderByDescending(r => r.StartTime)
|
||||
.ToListAsync();
|
||||
|
||||
// Load scenarios and parameter sets
|
||||
var scenarioEntity = await context.TestScenarios.FindAsync(scenarioId);
|
||||
foreach (var testRun in testRuns)
|
||||
{
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = await context.ParameterSets.FindAsync(testRun.ParameterSetId);
|
||||
}
|
||||
|
||||
return testRuns;
|
||||
}
|
||||
|
||||
public async Task<List<TestRun>> GetByParameterSetAsync(Guid parameterSetId)
|
||||
{
|
||||
var testRuns = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.Where(r => r.ParameterSetId == parameterSetId)
|
||||
.OrderByDescending(r => r.StartTime)
|
||||
.ToListAsync();
|
||||
|
||||
// Load scenarios and parameter sets
|
||||
var parameterSet = await context.ParameterSets.FindAsync(parameterSetId);
|
||||
foreach (var testRun in testRuns)
|
||||
{
|
||||
var scenarioEntity = await context.TestScenarios.FindAsync(testRun.ScenarioId);
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = parameterSet;
|
||||
}
|
||||
|
||||
return testRuns;
|
||||
}
|
||||
|
||||
public async Task<List<TestRun>> GetByDateRangeAsync(DateTime from, DateTime to)
|
||||
{
|
||||
var testRuns = await context.TestRuns
|
||||
.Include(r => r.Metrics)
|
||||
.Where(r => r.StartTime >= from && r.StartTime <= to)
|
||||
.OrderByDescending(r => r.StartTime)
|
||||
.ToListAsync();
|
||||
|
||||
// Load scenarios and parameter sets
|
||||
var scenarioIds = testRuns.Select(r => r.ScenarioId).Distinct().ToList();
|
||||
var parameterSetIds = testRuns.Select(r => r.ParameterSetId).Distinct().ToList();
|
||||
|
||||
var scenarios = await context.TestScenarios
|
||||
.Where(s => scenarioIds.Contains(s.Id))
|
||||
.ToListAsync();
|
||||
var parameterSets = await context.ParameterSets
|
||||
.Where(p => parameterSetIds.Contains(p.Id))
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var testRun in testRuns)
|
||||
{
|
||||
var scenarioEntity = scenarios.FirstOrDefault(s => s.Id == testRun.ScenarioId);
|
||||
if (scenarioEntity != null)
|
||||
testRun.Scenario = scenarioEntity.ToTestScenario();
|
||||
|
||||
testRun.ParameterSet = parameterSets.FirstOrDefault(p => p.Id == testRun.ParameterSetId);
|
||||
}
|
||||
|
||||
return testRuns;
|
||||
}
|
||||
|
||||
public async Task<Guid> SaveAsync(TestRun testRun)
|
||||
{
|
||||
context.TestRuns.Add(testRun);
|
||||
await context.SaveChangesAsync();
|
||||
return testRun.Id;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TestRun testRun)
|
||||
{
|
||||
var entry = context.Entry(testRun);
|
||||
if (entry.State == EntityState.Detached)
|
||||
context.TestRuns.Update(testRun);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update test run from execution result without loading/replacing navigation properties,
|
||||
/// to avoid DbUpdateConcurrencyException when entity was created in another scope.
|
||||
/// </summary>
|
||||
public async Task UpdateFromResultAsync(
|
||||
Guid testRunId,
|
||||
TestStatus status,
|
||||
DateTime? endTime,
|
||||
double duration,
|
||||
string? errorMessage,
|
||||
TestMetrics? metrics,
|
||||
List<SafetyViolation>? safetyViolations)
|
||||
{
|
||||
var testRun = await context.TestRuns
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(r => r.Id == testRunId);
|
||||
if (testRun == null)
|
||||
return;
|
||||
|
||||
testRun.Status = status;
|
||||
testRun.EndTime = endTime;
|
||||
testRun.Duration = duration;
|
||||
testRun.ErrorMessage = errorMessage;
|
||||
context.TestRuns.Update(testRun);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var existingMetrics = await context.TestMetrics.Where(m => m.TestRunId == testRunId).ToListAsync();
|
||||
if (existingMetrics.Count > 0)
|
||||
context.TestMetrics.RemoveRange(existingMetrics);
|
||||
|
||||
var existingViolations = await context.SafetyViolations.Where(v => v.TestRunId == testRunId).ToListAsync();
|
||||
if (existingViolations.Count > 0)
|
||||
context.SafetyViolations.RemoveRange(existingViolations);
|
||||
|
||||
if (metrics != null)
|
||||
{
|
||||
metrics.TestRunId = testRunId;
|
||||
if (metrics.Id == default)
|
||||
metrics.Id = Guid.NewGuid();
|
||||
context.TestMetrics.Add(metrics);
|
||||
}
|
||||
|
||||
if (safetyViolations != null && safetyViolations.Count > 0)
|
||||
{
|
||||
foreach (var v in safetyViolations)
|
||||
{
|
||||
v.TestRunId = testRunId;
|
||||
if (v.Id == default)
|
||||
v.Id = Guid.NewGuid();
|
||||
}
|
||||
context.SafetyViolations.AddRange(safetyViolations);
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var testRun = await GetByIdAsync(id);
|
||||
if (testRun != null)
|
||||
{
|
||||
context.TestRuns.Remove(testRun);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteManyAsync(IEnumerable<Guid> ids)
|
||||
{
|
||||
var idList = ids.Distinct().ToList();
|
||||
if (idList.Count == 0)
|
||||
return;
|
||||
|
||||
var toRemove = await context.TestRuns
|
||||
.Where(r => idList.Contains(r.Id))
|
||||
.ToListAsync();
|
||||
if (toRemove.Count > 0)
|
||||
{
|
||||
context.TestRuns.RemoveRange(toRemove);
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Text.Json;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
using StraightLineScenario = RobotNet10.NavigationTune.Scenarios.StraightLineScenario;
|
||||
using CircleScenario = RobotNet10.NavigationTune.Scenarios.CircleScenario;
|
||||
using CustomPathScenario = RobotNet10.NavigationTune.Scenarios.CustomPathScenario;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Entity for storing TestScenario in database
|
||||
/// Since TestScenario is abstract, we store config as JSON
|
||||
/// </summary>
|
||||
public class TestScenarioEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public TrajectoryType Type { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// JSON config for scenario-specific properties
|
||||
/// </summary>
|
||||
public string ConfigJson { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Convert to TestScenario instance
|
||||
/// </summary>
|
||||
public TestScenario ToTestScenario()
|
||||
{
|
||||
return Type switch
|
||||
{
|
||||
TrajectoryType.StraightLine => JsonSerializer.Deserialize<StraightLineScenario>(ConfigJson)
|
||||
?? new StraightLineScenario { Id = Id, Name = Name, Description = Description, Type = Type, CreatedAt = CreatedAt, IsDefault = IsDefault },
|
||||
TrajectoryType.Circle => JsonSerializer.Deserialize<CircleScenario>(ConfigJson)
|
||||
?? new CircleScenario { Id = Id, Name = Name, Description = Description, Type = Type, CreatedAt = CreatedAt, IsDefault = IsDefault },
|
||||
TrajectoryType.Custom => JsonSerializer.Deserialize<CustomPathScenario>(ConfigJson)
|
||||
?? new CustomPathScenario { Id = Id, Name = Name, Description = Description, Type = Type, CreatedAt = CreatedAt, IsDefault = IsDefault },
|
||||
_ => throw new NotSupportedException($"Scenario type {Type} is not supported")
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create from TestScenario
|
||||
/// </summary>
|
||||
public static TestScenarioEntity FromTestScenario(TestScenario scenario)
|
||||
{
|
||||
return new TestScenarioEntity
|
||||
{
|
||||
Id = scenario.Id,
|
||||
Name = scenario.Name,
|
||||
Description = scenario.Description,
|
||||
Type = scenario.Type,
|
||||
CreatedAt = scenario.CreatedAt,
|
||||
IsDefault = scenario.IsDefault,
|
||||
ConfigJson = scenario.Type switch
|
||||
{
|
||||
TrajectoryType.StraightLine => JsonSerializer.Serialize((StraightLineScenario)scenario),
|
||||
TrajectoryType.Circle => JsonSerializer.Serialize((CircleScenario)scenario),
|
||||
TrajectoryType.Custom => JsonSerializer.Serialize((CustomPathScenario)scenario),
|
||||
_ => "{}"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using System.Text.Json;
|
||||
using RobotNet10.NavigationTune.Shared.Models;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Database context for tuning system
|
||||
/// </summary>
|
||||
public class TuningDbContext(DbContextOptions<TuningDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<NavigationParameterSet> ParameterSets { get; set; }
|
||||
public DbSet<TestScenarioEntity> TestScenarios { get; set; }
|
||||
public DbSet<TestRun> TestRuns { get; set; }
|
||||
public DbSet<TestMetrics> TestMetrics { get; set; }
|
||||
public DbSet<SafetyViolation> SafetyViolations { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// JSON converter for complex types
|
||||
var jsonOptions = new JsonSerializerOptions { WriteIndented = false };
|
||||
|
||||
// ParameterSets - Configure complex types as JSON
|
||||
modelBuilder.Entity<NavigationParameterSet>(entity =>
|
||||
{
|
||||
entity.ToTable("parameter_sets");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
entity.Property(e => e.CreatedAt).IsRequired();
|
||||
entity.Property(e => e.UpdatedAt);
|
||||
entity.Property(e => e.IsDefault);
|
||||
entity.Property(e => e.Version);
|
||||
entity.Property(e => e.ControllerType).HasConversion<int>(); // Store enum as int
|
||||
|
||||
// Store complex types as JSON
|
||||
entity.Property(e => e.MovePidConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<PIDConfig>(v, jsonOptions) ?? new PIDConfig())
|
||||
.HasColumnType("TEXT"); // SQLite uses TEXT, PostgreSQL will use jsonb
|
||||
|
||||
entity.Property(e => e.RotatePidConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<PIDConfig>(v, jsonOptions) ?? new PIDConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.PurePursuitConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<PurePursuitConfig>(v, jsonOptions) ?? new PurePursuitConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.StanleyConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<StanleyConfig>(v, jsonOptions) ?? new StanleyConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.EstimatorConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<VelocityEstimatorConfig>(v, jsonOptions) ?? new VelocityEstimatorConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.SignalConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<VelocitySignalProcessingConfig>(v, jsonOptions) ?? new VelocitySignalProcessingConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.MotorDynamicsConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<MotorDynamicsConfig>(v, jsonOptions) ?? new MotorDynamicsConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.Property(e => e.NavigationConfig)
|
||||
.HasConversion(
|
||||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||||
v => JsonSerializer.Deserialize<NavigationConfig>(v, jsonOptions) ?? new NavigationConfig())
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
entity.HasIndex(e => e.Name);
|
||||
entity.HasIndex(e => e.IsDefault);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
});
|
||||
|
||||
// TestScenarios - Store config as JSON since TestScenario is abstract
|
||||
modelBuilder.Entity<TestScenarioEntity>(entity =>
|
||||
{
|
||||
entity.ToTable("test_scenarios");
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.Property(e => e.Name).IsRequired().HasMaxLength(200);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
entity.Property(e => e.Type).IsRequired();
|
||||
entity.Property(e => e.ConfigJson).IsRequired().HasColumnType("TEXT");
|
||||
entity.Property(e => e.CreatedAt).IsRequired();
|
||||
entity.Property(e => e.IsDefault);
|
||||
entity.HasIndex(e => e.Type);
|
||||
entity.HasIndex(e => e.Name);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
});
|
||||
|
||||
// TestRuns
|
||||
modelBuilder.Entity<TestRun>(entity =>
|
||||
{
|
||||
entity.ToTable("test_runs");
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
// Foreign keys
|
||||
entity.Property(e => e.ScenarioId).IsRequired();
|
||||
entity.Property(e => e.ParameterSetId).IsRequired();
|
||||
|
||||
// Navigation properties - Ignore since we load separately to avoid circular dependencies
|
||||
entity.Ignore(e => e.Scenario);
|
||||
entity.Ignore(e => e.ParameterSet);
|
||||
|
||||
// Properties
|
||||
entity.Property(e => e.StartTime).IsRequired();
|
||||
entity.Property(e => e.EndTime);
|
||||
entity.Property(e => e.Status).IsRequired().HasConversion<int>();
|
||||
entity.Property(e => e.Duration);
|
||||
entity.Property(e => e.Notes).HasMaxLength(1000);
|
||||
entity.Property(e => e.ErrorMessage).HasMaxLength(1000);
|
||||
|
||||
// Relationships
|
||||
entity.HasOne(e => e.Metrics)
|
||||
.WithOne()
|
||||
.HasForeignKey<TestMetrics>(m => m.TestRunId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasMany(e => e.SafetyViolations)
|
||||
.WithOne()
|
||||
.HasForeignKey(v => v.TestRunId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// Indexes
|
||||
entity.HasIndex(e => e.ScenarioId);
|
||||
entity.HasIndex(e => e.ParameterSetId);
|
||||
entity.HasIndex(e => e.StartTime);
|
||||
entity.HasIndex(e => e.Status);
|
||||
entity.HasIndex(e => new { e.ScenarioId, e.ParameterSetId });
|
||||
});
|
||||
|
||||
// TestMetrics
|
||||
modelBuilder.Entity<TestMetrics>(entity =>
|
||||
{
|
||||
entity.ToTable("test_metrics");
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
// Foreign key
|
||||
entity.Property(e => e.TestRunId).IsRequired();
|
||||
|
||||
// All metrics are double/double
|
||||
entity.Property(e => e.CrossTrackErrorRMS);
|
||||
entity.Property(e => e.CrossTrackErrorPeak);
|
||||
entity.Property(e => e.CrossTrackErrorMean);
|
||||
entity.Property(e => e.CrossTrackErrorStdDev);
|
||||
entity.Property(e => e.HeadingErrorRMS);
|
||||
entity.Property(e => e.HeadingErrorPeak);
|
||||
entity.Property(e => e.GoalPositionError);
|
||||
entity.Property(e => e.GoalHeadingError);
|
||||
entity.Property(e => e.VelocityStdDev);
|
||||
entity.Property(e => e.AccelerationStdDev);
|
||||
entity.Property(e => e.PathLengthRatio);
|
||||
entity.Property(e => e.CompletionTime);
|
||||
entity.Property(e => e.AverageSpeed);
|
||||
entity.Property(e => e.MaxSpeed);
|
||||
entity.Property(e => e.OverallScore);
|
||||
entity.Property(e => e.TrackingScore);
|
||||
entity.Property(e => e.SmoothnessScore);
|
||||
entity.Property(e => e.EfficiencyScore);
|
||||
entity.Property(e => e.PassedCriteria);
|
||||
|
||||
// Unique index on TestRunId (one-to-one relationship)
|
||||
entity.HasIndex(e => e.TestRunId).IsUnique();
|
||||
});
|
||||
|
||||
// SafetyViolations
|
||||
modelBuilder.Entity<SafetyViolation>(entity =>
|
||||
{
|
||||
entity.ToTable("safety_violations");
|
||||
entity.HasKey(e => e.Id);
|
||||
|
||||
// Foreign key
|
||||
entity.Property(e => e.TestRunId).IsRequired();
|
||||
|
||||
// Properties
|
||||
entity.Property(e => e.Timestamp).IsRequired();
|
||||
entity.Property(e => e.Type).IsRequired().HasConversion<int>();
|
||||
entity.Property(e => e.Severity).IsRequired().HasConversion<int>();
|
||||
entity.Property(e => e.Value);
|
||||
entity.Property(e => e.Threshold);
|
||||
entity.Property(e => e.Message).IsRequired().HasMaxLength(500);
|
||||
|
||||
// Indexes
|
||||
entity.HasIndex(e => e.TestRunId);
|
||||
entity.HasIndex(e => e.Timestamp);
|
||||
entity.HasIndex(e => new { e.TestRunId, e.Timestamp });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace RobotNet10.NavigationTune.Data;
|
||||
|
||||
public static class TuningDbExtensions
|
||||
{
|
||||
extension (IServiceProvider serviceProvider)
|
||||
{
|
||||
public async Task SeedTuningDbAsync()
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
|
||||
using var appDb = scope.ServiceProvider.GetRequiredService<TuningDbContext>();
|
||||
|
||||
await appDb.Database.MigrateAsync();
|
||||
await appDb.Database.EnsureCreatedAsync();
|
||||
await appDb.SaveChangesAsync();
|
||||
await DefaultDataSeeder.SeedAsync(appDb);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user