chore: initial git load of code space

This commit is contained in:
Thom Lamb
2026-05-12 08:52:33 -05:00
parent 9abada692f
commit 5e467bcc9c
384 changed files with 65960 additions and 2 deletions
@@ -0,0 +1,39 @@
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Abstractions;
/// <summary>
/// Interface for converting between QueryBreakdown and QueryBreakdownEntity for Entity Framework Core integration.
/// </summary>
public interface IQueryBreakdownMapper
{
/// <summary>
/// Converts a QueryBreakdown (SQL Tools) to a QueryBreakdownEntity (EF Core).
/// </summary>
/// <param name="queryBreakdown">The QueryBreakdown to convert.</param>
/// <returns>A QueryBreakdownEntity that can be persisted to the database.</returns>
QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown);
/// <summary>
/// Converts a QueryBreakdownEntity (EF Core) back to a QueryBreakdown (SQL Tools).
/// </summary>
/// <param name="entity">The QueryBreakdownEntity to convert.</param>
/// <returns>A QueryBreakdown instance with all clauses and parameters restored.</returns>
QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity);
/// <summary>
/// Converts a QueryBreakdown to a QueryBreakdownEntity with related entities (parameters and WITH clauses).
/// </summary>
/// <param name="queryBreakdown">The QueryBreakdown to convert.</param>
/// <returns>A QueryBreakdownEntity with all related entities populated.</returns>
(QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown);
/// <summary>
/// Converts a QueryBreakdownEntity with related entities back to a QueryBreakdown.
/// </summary>
/// <param name="entity">The QueryBreakdownEntity with navigation properties loaded.</param>
/// <returns>A fully reconstructed QueryBreakdown instance.</returns>
QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity);
}
@@ -0,0 +1,125 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Configurations;
/// <summary>
/// Entity Framework Core configuration for the QueryBreakdownEntity.
/// Defines the table structure, relationships, and constraints.
/// </summary>
public class QueryBreakdownEntityConfiguration : IEntityTypeConfiguration<QueryBreakdownEntity>
{
/// <summary>
/// Configures the QueryBreakdownEntity for Entity Framework Core.
/// </summary>
/// <param name="builder">The entity type builder.</param>
public void Configure(EntityTypeBuilder<QueryBreakdownEntity> builder)
{
builder.ToTable("QueryBreakdowns");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
// Configure SELECT clause
builder.Property(e => e.SelectClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.SelectClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure FROM clause
builder.Property(e => e.FromClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.FromClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure WHERE clause
builder.Property(e => e.WhereClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.WhereClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure GROUP BY clause
builder.Property(e => e.GroupByClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.GroupByClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure HAVING clause
builder.Property(e => e.HavingClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.HavingClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure ORDER BY clause
builder.Property(e => e.OrderByClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.OrderByClauseComment)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure WITH clause (CTEs)
builder.Property(e => e.WithClause)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure raw SQL
builder.Property(e => e.RawSql)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure JSON properties
builder.Property(e => e.SetupClausesJson)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.FinishClausesJson)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.ParametersJson)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
// Configure timestamps
builder.Property(e => e.CreatedAt)
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
builder.Property(e => e.UpdatedAt)
.HasColumnType("datetime2")
.HasDefaultValueSql("GETUTCDATE()");
// Configure relationships
builder.HasMany<QueryParameterEntity>()
.WithOne(p => p.QueryBreakdownEntity)
.HasForeignKey(p => p.QueryBreakdownEntityId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasMany<WithClauseEntity>()
.WithOne(w => w.QueryBreakdownEntity)
.HasForeignKey(w => w.QueryBreakdownEntityId)
.OnDelete(DeleteBehavior.Cascade);
// Create indexes for common queries
builder.HasIndex(e => e.CreatedAt);
builder.HasIndex(e => e.UpdatedAt);
}
}
@@ -0,0 +1,42 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Configurations;
/// <summary>
/// Entity Framework Core configuration for the QueryParameterEntity.
/// </summary>
public class QueryParameterEntityConfiguration : IEntityTypeConfiguration<QueryParameterEntity>
{
/// <summary>
/// Configures the QueryParameterEntity for Entity Framework Core.
/// </summary>
/// <param name="builder">The entity type builder.</param>
public void Configure(EntityTypeBuilder<QueryParameterEntity> builder)
{
builder.ToTable("QueryParameters");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.QueryBreakdownEntityId)
.IsRequired();
builder.Property(e => e.ParameterName)
.HasColumnType("nvarchar(256)")
.IsRequired();
builder.Property(e => e.ParameterValue)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.ParameterTypeName)
.HasColumnType("nvarchar(256)")
.IsRequired(false);
// Create index for faster lookups
builder.HasIndex(e => new { e.QueryBreakdownEntityId, e.ParameterName })
.IsUnique();
}
}
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Configurations;
/// <summary>
/// Entity Framework Core configuration for the WithClauseEntity.
/// </summary>
public class WithClauseEntityConfiguration : IEntityTypeConfiguration<WithClauseEntity>
{
/// <summary>
/// Configures the WithClauseEntity for Entity Framework Core.
/// </summary>
/// <param name="builder">The entity type builder.</param>
public void Configure(EntityTypeBuilder<WithClauseEntity> builder)
{
builder.ToTable("WithClauses");
builder.HasKey(e => e.Id);
builder.Property(e => e.Id).ValueGeneratedOnAdd();
builder.Property(e => e.QueryBreakdownEntityId)
.IsRequired();
builder.Property(e => e.CteName)
.HasColumnType("nvarchar(256)")
.IsRequired();
builder.Property(e => e.ColumnList)
.HasColumnType("nvarchar(max)")
.IsRequired(false);
builder.Property(e => e.CteDefinition)
.HasColumnType("nvarchar(max)")
.IsRequired();
builder.Property(e => e.OrderIndex)
.IsRequired();
// Create index for ordering and lookups
builder.HasIndex(e => new { e.QueryBreakdownEntityId, e.OrderIndex });
}
}
@@ -0,0 +1,122 @@
namespace Strata.SqlTools.EFCore.Models;
/// <summary>
/// Represents a SQL query breakdown entity for Entity Framework Core mapping.
/// This entity encapsulates the query components (SELECT, FROM, WHERE, etc.)
/// and is designed to be compatible with EF Core DbContext and database models.
/// </summary>
public class QueryBreakdownEntity
{
/// <summary>
/// Gets or sets the unique identifier for this query breakdown.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the SELECT clause of the query.
/// </summary>
public string? SelectClause { get; set; }
/// <summary>
/// Gets or sets the comment for the SELECT clause.
/// </summary>
public string? SelectClauseComment { get; set; }
/// <summary>
/// Gets or sets the FROM clause of the query.
/// </summary>
public string? FromClause { get; set; }
/// <summary>
/// Gets or sets the comment for the FROM clause.
/// </summary>
public string? FromClauseComment { get; set; }
/// <summary>
/// Gets or sets the WHERE clause of the query.
/// </summary>
public string? WhereClause { get; set; }
/// <summary>
/// Gets or sets the comment for the WHERE clause.
/// </summary>
public string? WhereClauseComment { get; set; }
/// <summary>
/// Gets or sets the GROUP BY clause of the query.
/// </summary>
public string? GroupByClause { get; set; }
/// <summary>
/// Gets or sets the comment for the GROUP BY clause.
/// </summary>
public string? GroupByClauseComment { get; set; }
/// <summary>
/// Gets or sets the HAVING clause of the query.
/// </summary>
public string? HavingClause { get; set; }
/// <summary>
/// Gets or sets the comment for the HAVING clause.
/// </summary>
public string? HavingClauseComment { get; set; }
/// <summary>
/// Gets or sets the ORDER BY clause of the query.
/// </summary>
public string? OrderByClause { get; set; }
/// <summary>
/// Gets or sets the comment for the ORDER BY clause.
/// </summary>
public string? OrderByClauseComment { get; set; }
/// <summary>
/// Gets or sets the WITH clause (Common Table Expressions) as a JSON string.
/// </summary>
public string? WithClause { get; set; }
/// <summary>
/// Gets or sets the raw/original SQL statement before parsing and breakdown.
/// </summary>
public string? RawSql { get; set; }
/// <summary>
/// Gets or sets the setup clauses as a JSON string.
/// These are clauses to execute before the main statement.
/// </summary>
public string? SetupClausesJson { get; set; }
/// <summary>
/// Gets or sets the finish clauses as a JSON string.
/// These are clauses to execute after the main statement.
/// </summary>
public string? FinishClausesJson { get; set; }
/// <summary>
/// Gets or sets the parameters as a JSON string.
/// Contains parameter names and their values.
/// </summary>
public string? ParametersJson { get; set; }
/// <summary>
/// Gets or sets the timestamp when this entity was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the timestamp when this entity was last updated.
/// </summary>
public DateTime UpdatedAt { get; set; }
/// <summary>
/// Navigation property for the related query parameters.
/// </summary>
public virtual ICollection<QueryParameterEntity> Parameters { get; set; } = new List<QueryParameterEntity>();
/// <summary>
/// Navigation property for the related WITH clauses (CTEs).
/// </summary>
public virtual ICollection<WithClauseEntity> WithClauses { get; set; } = new List<WithClauseEntity>();
}
@@ -0,0 +1,38 @@
namespace Strata.SqlTools.EFCore.Models;
/// <summary>
/// Represents a query parameter entity for use with Entity Framework Core.
/// Stores query parameter names and their values with type information.
/// </summary>
public class QueryParameterEntity
{
/// <summary>
/// Gets or sets the unique identifier for this parameter.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the identifier of the parent query breakdown entity.
/// </summary>
public int QueryBreakdownEntityId { get; set; }
/// <summary>
/// Gets or sets the parameter name (e.g., "@ParameterName" or "ParameterName").
/// </summary>
public string ParameterName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the parameter value as a string representation.
/// </summary>
public string? ParameterValue { get; set; }
/// <summary>
/// Gets or sets the CLR type name of the parameter value for deserialization.
/// </summary>
public string? ParameterTypeName { get; set; }
/// <summary>
/// Navigation property to the parent QueryBreakdownEntity.
/// </summary>
public virtual QueryBreakdownEntity? QueryBreakdownEntity { get; set; }
}
@@ -0,0 +1,42 @@
namespace Strata.SqlTools.EFCore.Models;
/// <summary>
/// Represents a WITH clause (Common Table Expression) entity for Entity Framework Core mapping.
/// </summary>
public class WithClauseEntity
{
/// <summary>
/// Gets or sets the unique identifier for this WITH clause.
/// </summary>
public int Id { get; set; }
/// <summary>
/// Gets or sets the identifier of the parent query breakdown entity.
/// </summary>
public int QueryBreakdownEntityId { get; set; }
/// <summary>
/// Gets or sets the name of the CTE (Common Table Expression).
/// </summary>
public string CteName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the column list for the CTE (optional).
/// </summary>
public string? ColumnList { get; set; }
/// <summary>
/// Gets or sets the definition/query of the CTE.
/// </summary>
public string CteDefinition { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the order of this CTE in the WITH clause.
/// </summary>
public int OrderIndex { get; set; }
/// <summary>
/// Navigation property to the parent QueryBreakdownEntity.
/// </summary>
public virtual QueryBreakdownEntity? QueryBreakdownEntity { get; set; }
}
+240
View File
@@ -0,0 +1,240 @@
# Strata.SqlTools.EFCore
> Entity Framework Core integration and support for Strata.SqlTools QueryBreakdown functionality
This project provides seamless integration between the Strata.SqlTools query analysis framework and Entity Framework Core, allowing you to persist, query, and manage `QueryBreakdown` objects within your existing EF Core DbContext.
## Key Features
- **EF Core Integration**: Map QueryBreakdown objects directly to your DbContext
- **Entity Models**: Fully normalized entity models for QueryBreakdownEntity, QueryParameterEntity, and WithClauseEntity
- **Automatic Mapping**: IQueryBreakdownMapper for converting between SQL Tools and EF Core models
- **Repository Pattern**: IQueryBreakdownRepository for simplified CRUD operations
- **DbContext Extensions**: Easy-to-use extension methods for DbContext integration
- **JSON Serialization**: Intelligent serialization of complex types (parameters, clauses) to JSON for efficient storage
## Installation
Add the NuGet package reference:
```xml
<PackageReference Include="Strata.SqlTools.EFCore" Version="1.0.0" />
```
Or via the .NET CLI:
```bash
dotnet add package Strata.SqlTools.EFCore
```
## Quick Start
### 1. Configure Your DbContext
Add the QueryBreakdown entities to your DbContext:
```csharp
using Microsoft.EntityFrameworkCore;
using Strata.SqlTools.EFCore.Models;
using Strata.SqlTools.EFCore.Configurations;
public class YourDbContext : DbContext
{
public DbSet<QueryBreakdownEntity> QueryBreakdowns { get; set; }
public DbSet<QueryParameterEntity> QueryParameters { get; set; }
public DbSet<WithClauseEntity> WithClauses { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure QueryBreakdown entities
modelBuilder.ConfigureQueryBreakdownEntities();
}
}
```
### 2. Register Services
Register the mapper and repository in your dependency injection container:
```csharp
services.AddScoped<IQueryBreakdownMapper, QueryBreakdownMapper>();
services.AddScoped<IQueryBreakdownRepository>(
provider => new QueryBreakdownRepository(
provider.GetRequiredService<YourDbContext>(),
provider.GetRequiredService<IQueryBreakdownMapper>()
)
);
```
### 3. Use the Repository
Inject and use the repository in your application:
```csharp
public class QueryService
{
private readonly IQueryBreakdownRepository _repository;
public QueryService(IQueryBreakdownRepository repository)
{
_repository = repository;
}
public async Task SaveQueryAsync(QueryBreakdown queryBreakdown)
{
int id = await _repository.AddAsync(queryBreakdown);
Console.WriteLine($"Query saved with ID: {id}");
}
public async Task<QueryBreakdown?> GetQueryAsync(int id)
{
return await _repository.GetByIdAsync(id);
}
public async Task<List<QueryBreakdown>> GetAllQueriesAsync()
{
return await _repository.GetAllAsync();
}
public async Task UpdateQueryAsync(int id, QueryBreakdown queryBreakdown)
{
await _repository.UpdateAsync(id, queryBreakdown);
}
public async Task DeleteQueryAsync(int id)
{
bool deleted = await _repository.DeleteAsync(id);
Console.WriteLine(deleted ? "Query deleted." : "Query not found.");
}
}
```
## Entity Models
### QueryBreakdownEntity
The main entity that represents a SQL query breakdown:
- **Id**: Primary key
- **SelectClause**: The SELECT clause
- **FromClause**: The FROM clause
- **WhereClause**: The WHERE clause
- **GroupByClause**: The GROUP BY clause
- **HavingClause**: The HAVING clause
- **OrderByClause**: The ORDER BY clause
- **WithClause**: Common Table Expressions (CTEs)
- **RawSql**: Original SQL statement
- **SetupClausesJson**: JSON serialized setup clauses
- **FinishClausesJson**: JSON serialized finish clauses
- **ParametersJson**: JSON serialized parameters
- **CreatedAt**: Creation timestamp
- **UpdatedAt**: Last update timestamp
#### Related Entities
- **QueryParameterEntity**: Represents parameters used in the query
- **WithClauseEntity**: Represents individual Common Table Expressions (CTEs)
## Mapper Interface
The `IQueryBreakdownMapper` provides the following operations:
```csharp
public interface IQueryBreakdownMapper
{
QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown);
QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity);
(QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown);
QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity);
}
```
## Repository Interface
The `IQueryBreakdownRepository` provides the following operations:
```csharp
public interface IQueryBreakdownRepository
{
Task<int> AddAsync(QueryBreakdown queryBreakdown);
Task<QueryBreakdown?> GetByIdAsync(int id);
Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id);
Task<List<QueryBreakdown>> GetAllAsync();
Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync();
Task UpdateAsync(int id, QueryBreakdown queryBreakdown);
Task<bool> DeleteAsync(int id);
Task<int> GetCountAsync();
}
```
## Database Schema
The project includes three main tables:
### QueryBreakdowns Table
Stores the main query breakdown information
### QueryParameters Table
Stores individual query parameters with foreign key to QueryBreakdowns
### WithClauses Table
Stores Common Table Expressions with foreign key to QueryBreakdowns
## DbContext Extension Methods
```csharp
// Configure query breakdown entities during model creation
modelBuilder.ConfigureQueryBreakdownEntities();
// Get queryable sets from context
var queryBreakdowns = dbContext.GetQueryBreakdowns();
var parameters = dbContext.GetQueryParameters();
var withClauses = dbContext.GetWithClauses();
// Get a query breakdown with related data
var entity = await dbContext.GetQueryBreakdownWithRelatedDataAsync(id);
```
## Advanced Usage
### Custom Entity Configuration
If you need to customize the entity configuration, you can create your own configuration classes that implement `IEntityTypeConfiguration<T>`:
```csharp
public class CustomQueryBreakdownConfiguration : IEntityTypeConfiguration<QueryBreakdownEntity>
{
public void Configure(EntityTypeBuilder<QueryBreakdownEntity> builder)
{
// Apply custom configuration
builder.ToTable("CustomQueryBreakdowns", "dbo");
// ... other configurations
}
}
```
### Working with Existing DbContext
If you already have an existing DbContext, simply:
1. Add the DbSets for QueryBreakdown entities
2. Call `modelBuilder.ConfigureQueryBreakdownEntities()` in `OnModelCreating`
3. Create a migration: `dotnet ef migrations add AddQueryBreakdownEntities`
4. Update the database: `dotnet ef database update`
## Dependencies
- **Microsoft.EntityFrameworkCore** (8.0.0+)
- **Microsoft.EntityFrameworkCore.Relational** (8.0.0+)
- **Strata.SqlTools** (1.0.0+)
- **Strata.SqlTools.SqlServer** (1.0.0+)
## License
MIT
## Support
For issues, feature requests, or questions, please visit the [GitHub repository](https://github.com/stratadecision/sql-builder).
@@ -0,0 +1,74 @@
using Microsoft.EntityFrameworkCore;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Services;
/// <summary>
/// Extension methods for DbContext to support QueryBreakdown entities.
/// </summary>
public static class DbContextExtensions
{
/// <summary>
/// Adds an entity configuration to the ModelBuilder for QueryBreakdown-related entities.
/// Call this in your DbContext.OnModelCreating method.
/// </summary>
/// <param name="modelBuilder">The ModelBuilder instance.</param>
/// <returns>The ModelBuilder instance for fluent chaining.</returns>
public static ModelBuilder ConfigureQueryBreakdownEntities(this ModelBuilder modelBuilder)
{
ArgumentNullException.ThrowIfNull(modelBuilder);
modelBuilder.ApplyConfiguration(new Configurations.QueryBreakdownEntityConfiguration());
modelBuilder.ApplyConfiguration(new Configurations.QueryParameterEntityConfiguration());
modelBuilder.ApplyConfiguration(new Configurations.WithClauseEntityConfiguration());
return modelBuilder;
}
/// <summary>
/// Gets a queryable set of QueryBreakdownEntity instances from the DbContext.
/// </summary>
/// <param name="context">The DbContext instance.</param>
/// <returns>An IQueryable of QueryBreakdownEntity.</returns>
public static IQueryable<QueryBreakdownEntity> GetQueryBreakdowns(this DbContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Set<QueryBreakdownEntity>();
}
/// <summary>
/// Gets a queryable set of QueryParameterEntity instances from the DbContext.
/// </summary>
/// <param name="context">The DbContext instance.</param>
/// <returns>An IQueryable of QueryParameterEntity.</returns>
public static IQueryable<QueryParameterEntity> GetQueryParameters(this DbContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Set<QueryParameterEntity>();
}
/// <summary>
/// Gets a queryable set of WithClauseEntity instances from the DbContext.
/// </summary>
/// <param name="context">The DbContext instance.</param>
/// <returns>An IQueryable of WithClauseEntity.</returns>
public static IQueryable<WithClauseEntity> GetWithClauses(this DbContext context)
{
ArgumentNullException.ThrowIfNull(context);
return context.Set<WithClauseEntity>();
}
/// <summary>
/// Includes query breakdown related data and returns a single QueryBreakdownEntity by ID.
/// </summary>
/// <param name="context">The DbContext instance.</param>
/// <param name="id">The ID of the QueryBreakdownEntity to retrieve.</param>
/// <returns>The QueryBreakdownEntity with related entities included, or null if not found.</returns>
public static async Task<QueryBreakdownEntity?> GetQueryBreakdownWithRelatedDataAsync(this DbContext context, int id)
{
ArgumentNullException.ThrowIfNull(context);
return await context.Set<QueryBreakdownEntity>()
.FirstOrDefaultAsync(q => q.Id == id);
}
}
@@ -0,0 +1,279 @@
using System.Collections;
using System.Text.Json;
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.EFCore.Abstractions;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Services;
/// <summary>
/// Implementation of IQueryBreakdownMapper for converting between QueryBreakdown and QueryBreakdownEntity.
/// </summary>
public class QueryBreakdownMapper : IQueryBreakdownMapper
{
/// <summary>
/// Converts a QueryBreakdown (SQL Tools) to a QueryBreakdownEntity (EF Core).
/// </summary>
public QueryBreakdownEntity MapToEntity(QueryBreakdown queryBreakdown)
{
ArgumentNullException.ThrowIfNull(queryBreakdown);
var entity = new QueryBreakdownEntity
{
SelectClause = queryBreakdown.SelectClause?.Clause,
SelectClauseComment = queryBreakdown.SelectClause?.Comment,
FromClause = queryBreakdown.FromClause?.Clause,
FromClauseComment = queryBreakdown.FromClause?.Comment,
WhereClause = queryBreakdown.WhereClause?.Clause,
WhereClauseComment = queryBreakdown.WhereClause?.Comment,
GroupByClause = queryBreakdown.GroupByClause?.Clause,
GroupByClauseComment = queryBreakdown.GroupByClause?.Comment,
HavingClause = queryBreakdown.HavingClause?.Clause,
HavingClauseComment = queryBreakdown.HavingClause?.Comment,
OrderByClause = queryBreakdown.OrderByClause?.Clause,
OrderByClauseComment = queryBreakdown.OrderByClause?.Comment,
WithClause = queryBreakdown.GetWithClauseValue(),
RawSql = queryBreakdown.RawSql,
SetupClausesJson = SerializeList(queryBreakdown.SetupClauses),
FinishClausesJson = SerializeArrayList(queryBreakdown.FinishClauses),
ParametersJson = SerializeDictionary(queryBreakdown.Parameters),
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
return entity;
}
/// <summary>
/// Converts a QueryBreakdownEntity (EF Core) back to a QueryBreakdown (SQL Tools).
/// </summary>
public QueryBreakdown MapToDomainModel(QueryBreakdownEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
var queryBreakdown = new QueryBreakdown();
// Set clause properties
if (!string.IsNullOrEmpty(entity.SelectClause))
{
queryBreakdown.SelectClause.Clause = entity.SelectClause;
queryBreakdown.SelectClause.Comment = entity.SelectClauseComment;
}
if (!string.IsNullOrEmpty(entity.FromClause))
{
queryBreakdown.FromClause.Clause = entity.FromClause;
queryBreakdown.FromClause.Comment = entity.FromClauseComment;
}
if (!string.IsNullOrEmpty(entity.WhereClause))
{
queryBreakdown.WhereClause.Clause = entity.WhereClause;
queryBreakdown.WhereClause.Comment = entity.WhereClauseComment;
}
if (!string.IsNullOrEmpty(entity.GroupByClause))
{
queryBreakdown.GroupByClause.Clause = entity.GroupByClause;
queryBreakdown.GroupByClause.Comment = entity.GroupByClauseComment;
}
if (!string.IsNullOrEmpty(entity.HavingClause))
{
queryBreakdown.HavingClause.Clause = entity.HavingClause;
queryBreakdown.HavingClause.Comment = entity.HavingClauseComment;
}
if (!string.IsNullOrEmpty(entity.OrderByClause))
{
queryBreakdown.OrderByClause.Clause = entity.OrderByClause;
queryBreakdown.OrderByClause.Comment = entity.OrderByClauseComment;
}
if (!string.IsNullOrEmpty(entity.WithClause))
{
queryBreakdown.SetWithClauseValue(entity.WithClause);
}
if (!string.IsNullOrEmpty(entity.RawSql))
{
queryBreakdown.RawSql = entity.RawSql;
}
// Restore setup clauses
if (!string.IsNullOrEmpty(entity.SetupClausesJson))
{
var setupClauses = DeserializeList(entity.SetupClausesJson);
queryBreakdown.SetupClauses.Clear();
foreach (var clause in setupClauses)
{
queryBreakdown.SetupClauses.Add(clause);
}
}
// Restore finish clauses
if (!string.IsNullOrEmpty(entity.FinishClausesJson))
{
var finishClauses = DeserializeArrayList(entity.FinishClausesJson);
queryBreakdown.FinishClauses.Clear();
foreach (var clause in finishClauses)
{
queryBreakdown.FinishClauses.Add(clause);
}
}
// Restore parameters
if (!string.IsNullOrEmpty(entity.ParametersJson))
{
var parameters = DeserializeDictionary(entity.ParametersJson);
queryBreakdown.Parameters.Clear();
foreach (var kvp in parameters)
{
queryBreakdown.Parameters[kvp.Key] = kvp.Value;
}
}
return queryBreakdown;
}
/// <summary>
/// Converts a QueryBreakdown to a QueryBreakdownEntity with related entities.
/// </summary>
public (QueryBreakdownEntity Entity, List<QueryParameterEntity> Parameters, List<WithClauseEntity> WithClauses) MapToEntityWithRelations(QueryBreakdown queryBreakdown)
{
ArgumentNullException.ThrowIfNull(queryBreakdown);
var entity = MapToEntity(queryBreakdown);
// Map parameters
var parameterEntities = new List<QueryParameterEntity>();
foreach (var param in queryBreakdown.ParameterList)
{
parameterEntities.Add(new QueryParameterEntity
{
ParameterName = param.Name,
ParameterValue = param.Value?.ToString(),
ParameterTypeName = param.Value?.GetType().FullName
});
}
// Map WITH clauses
var withClauseEntities = new List<WithClauseEntity>();
int orderIndex = 0;
foreach (var withClause in queryBreakdown.WithClauses)
{
withClauseEntities.Add(new WithClauseEntity
{
CteName = withClause.TableName,
ColumnList = withClause.Clause,
CteDefinition = withClause.Sql?.SelectClause?.Clause ?? string.Empty,
OrderIndex = orderIndex++
});
}
return (entity, parameterEntities, withClauseEntities);
}
/// <summary>
/// Converts a QueryBreakdownEntity with related entities back to a QueryBreakdown.
/// </summary>
public QueryBreakdown MapToDomainModelWithRelations(QueryBreakdownEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
var queryBreakdown = MapToDomainModel(entity);
// Reconstruct Parameters dictionary from parameter entities if they are loaded
if (entity.Parameters != null && entity.Parameters.Count > 0)
{
queryBreakdown.Parameters.Clear();
foreach (var paramEntity in entity.Parameters)
{
// Store with @ prefix to match how AddParameter works
var key = paramEntity.ParameterName.StartsWith('@')
? paramEntity.ParameterName
: $"@{paramEntity.ParameterName}";
// Deserialize value if type information is available
object? value = paramEntity.ParameterValue;
if (!string.IsNullOrEmpty(paramEntity.ParameterTypeName) && !string.IsNullOrEmpty(paramEntity.ParameterValue))
{
var type = Type.GetType(paramEntity.ParameterTypeName);
if (type != null)
{
try
{
value = Convert.ChangeType(paramEntity.ParameterValue, type);
}
catch
{
// If conversion fails, use string value
value = paramEntity.ParameterValue;
}
}
}
queryBreakdown.Parameters[key] = value;
}
}
return queryBreakdown;
}
#region Helper Methods
internal static string SerializeList(List<string> list)
{
return JsonSerializer.Serialize(list);
}
internal static List<string> DeserializeList(string json)
{
return JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
}
internal static string SerializeArrayList(ArrayList list)
{
var stringList = new List<string>();
foreach (var item in list)
{
stringList.Add(item?.ToString() ?? string.Empty);
}
return JsonSerializer.Serialize(stringList);
}
internal static ArrayList DeserializeArrayList(string json)
{
var stringList = JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
var arrayList = new ArrayList();
foreach (var item in stringList)
{
arrayList.Add(item);
}
return arrayList;
}
internal static string SerializeDictionary(Dictionary<string, object> dict)
{
var stringDict = new Dictionary<string, string>();
foreach (var kvp in dict)
{
stringDict[kvp.Key] = kvp.Value?.ToString() ?? string.Empty;
}
return JsonSerializer.Serialize(stringDict);
}
internal static Dictionary<string, object> DeserializeDictionary(string json)
{
var stringDict = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new Dictionary<string, string>();
var result = new Dictionary<string, object>();
foreach (var kvp in stringDict)
{
result[kvp.Key] = kvp.Value;
}
return result;
}
#endregion
}
@@ -0,0 +1,243 @@
using Microsoft.EntityFrameworkCore;
using Strata.SqlTools.Breakdowns.SqlServer;
using Strata.SqlTools.EFCore.Abstractions;
using Strata.SqlTools.EFCore.Models;
namespace Strata.SqlTools.EFCore.Services;
/// <summary>
/// Interface for a generic repository pattern for QueryBreakdown entities.
/// Provides a simplified API for common database operations.
/// </summary>
public interface IQueryBreakdownRepository
{
/// <summary>
/// Adds a new QueryBreakdown to the repository and saves changes.
/// </summary>
/// <param name="queryBreakdown">The QueryBreakdown to add.</param>
/// <returns>The ID of the added entity.</returns>
Task<int> AddAsync(QueryBreakdown queryBreakdown);
/// <summary>
/// Retrieves a QueryBreakdown by ID and converts it from the entity.
/// </summary>
/// <param name="id">The ID of the QueryBreakdown entity.</param>
/// <returns>The QueryBreakdown, or null if not found.</returns>
Task<QueryBreakdown?> GetByIdAsync(int id);
/// <summary>
/// Retrieves a QueryBreakdownEntity by ID.
/// </summary>
/// <param name="id">The ID of the entity.</param>
/// <returns>The QueryBreakdownEntity, or null if not found.</returns>
Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id);
/// <summary>
/// Gets all QueryBreakdowns.
/// </summary>
/// <returns>A list of all QueryBreakdowns.</returns>
Task<List<QueryBreakdown>> GetAllAsync();
/// <summary>
/// Gets all QueryBreakdownEntities.
/// </summary>
/// <returns>A list of all QueryBreakdownEntities.</returns>
Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync();
/// <summary>
/// Updates an existing QueryBreakdown and saves changes.
/// </summary>
/// <param name="id">The ID of the entity to update.</param>
/// <param name="queryBreakdown">The updated QueryBreakdown.</param>
Task UpdateAsync(int id, QueryBreakdown queryBreakdown);
/// <summary>
/// Deletes a QueryBreakdown by ID and saves changes.
/// </summary>
/// <param name="id">The ID of the entity to delete.</param>
/// <returns>True if the entity was deleted; false if not found.</returns>
Task<bool> DeleteAsync(int id);
/// <summary>
/// Gets the count of all QueryBreakdown entities.
/// </summary>
/// <returns>The count of entities.</returns>
Task<int> GetCountAsync();
}
/// <summary>
/// Implementation of IQueryBreakdownRepository for managing QueryBreakdown entities in Entity Framework Core.
/// </summary>
public class QueryBreakdownRepository : IQueryBreakdownRepository
{
private readonly DbContext _context;
private readonly IQueryBreakdownMapper _mapper;
/// <summary>
/// Initializes a new instance of the QueryBreakdownRepository.
/// </summary>
/// <param name="context">The EF Core DbContext.</param>
/// <param name="mapper">The mapper for converting between QueryBreakdown and QueryBreakdownEntity.</param>
public QueryBreakdownRepository(DbContext context, IQueryBreakdownMapper mapper)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(mapper);
_context = context;
_mapper = mapper;
}
/// <summary>
/// Adds a new QueryBreakdown to the repository and saves changes.
/// </summary>
public async Task<int> AddAsync(QueryBreakdown queryBreakdown)
{
ArgumentNullException.ThrowIfNull(queryBreakdown);
var (entity, parameters, withClauses) = _mapper.MapToEntityWithRelations(queryBreakdown);
// Add the main entity
_context.Set<QueryBreakdownEntity>().Add(entity);
await _context.SaveChangesAsync();
// Add related entities with foreign key set
foreach (var param in parameters)
{
param.QueryBreakdownEntityId = entity.Id;
_context.Set<QueryParameterEntity>().Add(param);
}
foreach (var withClause in withClauses)
{
withClause.QueryBreakdownEntityId = entity.Id;
_context.Set<WithClauseEntity>().Add(withClause);
}
await _context.SaveChangesAsync();
return entity.Id;
}
/// <summary>
/// Retrieves a QueryBreakdown by ID and converts it from the entity.
/// </summary>
public async Task<QueryBreakdown?> GetByIdAsync(int id)
{
var entity = await _context.Set<QueryBreakdownEntity>()
.Include(e => e.Parameters)
.Include(e => e.WithClauses)
.FirstOrDefaultAsync(e => e.Id == id);
return entity != null ? _mapper.MapToDomainModelWithRelations(entity) : null;
}
/// <summary>
/// Retrieves a QueryBreakdownEntity by ID.
/// </summary>
public async Task<QueryBreakdownEntity?> GetEntityByIdAsync(int id)
{
return await _context.Set<QueryBreakdownEntity>()
.FirstOrDefaultAsync(e => e.Id == id);
}
/// <summary>
/// Gets all QueryBreakdowns.
/// </summary>
public async Task<List<QueryBreakdown>> GetAllAsync()
{
var entities = await _context.Set<QueryBreakdownEntity>().ToListAsync();
return entities.ConvertAll(e => _mapper.MapToDomainModel(e));
}
/// <summary>
/// Gets all QueryBreakdownEntities.
/// </summary>
public async Task<List<QueryBreakdownEntity>> GetAllEntitiesAsync()
{
return await _context.Set<QueryBreakdownEntity>().ToListAsync();
}
/// <summary>
/// Updates an existing QueryBreakdown and saves changes.
/// </summary>
public async Task UpdateAsync(int id, QueryBreakdown queryBreakdown)
{
ArgumentNullException.ThrowIfNull(queryBreakdown);
var entity = await _context.Set<QueryBreakdownEntity>().FirstOrDefaultAsync(e => e.Id == id);
if (entity == null)
{
throw new InvalidOperationException($"QueryBreakdown with ID {id} not found.");
}
var updatedEntity = _mapper.MapToEntity(queryBreakdown);
// Update the main entity
entity.SelectClause = updatedEntity.SelectClause;
entity.SelectClauseComment = updatedEntity.SelectClauseComment;
entity.FromClause = updatedEntity.FromClause;
entity.FromClauseComment = updatedEntity.FromClauseComment;
entity.WhereClause = updatedEntity.WhereClause;
entity.WhereClauseComment = updatedEntity.WhereClauseComment;
entity.GroupByClause = updatedEntity.GroupByClause;
entity.GroupByClauseComment = updatedEntity.GroupByClauseComment;
entity.HavingClause = updatedEntity.HavingClause;
entity.HavingClauseComment = updatedEntity.HavingClauseComment;
entity.OrderByClause = updatedEntity.OrderByClause;
entity.OrderByClauseComment = updatedEntity.OrderByClauseComment;
entity.WithClause = updatedEntity.WithClause;
entity.RawSql = updatedEntity.RawSql;
entity.SetupClausesJson = updatedEntity.SetupClausesJson;
entity.FinishClausesJson = updatedEntity.FinishClausesJson;
entity.ParametersJson = updatedEntity.ParametersJson;
entity.UpdatedAt = DateTime.UtcNow;
// Delete and recreate related entities
var existingParameters = _context.Set<QueryParameterEntity>().Where(p => p.QueryBreakdownEntityId == id);
_context.Set<QueryParameterEntity>().RemoveRange(existingParameters);
var existingWithClauses = _context.Set<WithClauseEntity>().Where(w => w.QueryBreakdownEntityId == id);
_context.Set<WithClauseEntity>().RemoveRange(existingWithClauses);
var (_, parameters, withClauses) = _mapper.MapToEntityWithRelations(queryBreakdown);
foreach (var param in parameters)
{
param.QueryBreakdownEntityId = id;
_context.Set<QueryParameterEntity>().Add(param);
}
foreach (var withClause in withClauses)
{
withClause.QueryBreakdownEntityId = id;
_context.Set<WithClauseEntity>().Add(withClause);
}
await _context.SaveChangesAsync();
}
/// <summary>
/// Deletes a QueryBreakdown by ID and saves changes.
/// </summary>
public async Task<bool> DeleteAsync(int id)
{
var entity = await _context.Set<QueryBreakdownEntity>().FirstOrDefaultAsync(e => e.Id == id);
if (entity == null)
{
return false;
}
_context.Set<QueryBreakdownEntity>().Remove(entity);
await _context.SaveChangesAsync();
return true;
}
/// <summary>
/// Gets the count of all QueryBreakdown entities.
/// </summary>
public async Task<int> GetCountAsync()
{
return await _context.Set<QueryBreakdownEntity>().CountAsync();
}
}
@@ -0,0 +1,52 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- NuGet Package Metadata -->
<PackageId>Strata.SqlTools.EFCore</PackageId>
<Version>1.0.0</Version>
<Authors>Strata Decision Technology</Authors>
<Company>Strata Decision Technology</Company>
<Product>Strata SQL Utilities - EF Core</Product>
<Description>Entity Framework Core integration and support for Strata.SqlTools QueryBreakdown functionality, allowing seamless mapping of SQL query breakdowns onto existing DbContext and database models.</Description>
<PackageTags>sql;efcore;entity-framework;query-builder;database;orm</PackageTags>
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>Initial release with EF Core integration for QueryBreakdown functionality.</PackageReleaseNotes>
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
<!-- Build Configuration -->
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
<!-- Code Analysis -->
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" />
</ItemGroup>
</Project>