* refactor: Remove obsolete test conversion scripts * feat: Add Phase 2 Data Model operations and integration - Introduced new helper methods for safe COM iteration and relationship finding. - Created separate result type classes for Data Model operations, adhering to architectural guidelines. - Implemented core interface methods for reading and writing Data Model entities, including measures and relationships. - Added comprehensive integration tests for new operations, ensuring full coverage. - Updated MCP server to integrate new Data Model actions and commands. - Enhanced CLI with new commands for Data Model management, including create/update operations for measures and relationships. - Revised README and server description to reflect new capabilities and tools. * refactor: Remove obsolete temporary files related to data model and test analysis * feat: Add CLI commands for Phase 2 TableCommands features - Added GetStructuredReference command with region and optional column support - Added Sort command for single-column sorting (asc/desc) - Added SortMulti command for multi-level sorting (max 3 columns) - All commands include Spectre.Console formatted output with tables - Usage help messages with examples for all three commands - Routing added to Program.cs for table-get-structured-reference, table-sort, table-sort-multi - Fixed Spectre.Console markup escaping for optional parameters [[]] - Commands follow existing CLI patterns with batch API and error handling * test: Add comprehensive integration tests for Phase 2 TableCommands - Created TableCommandsTests.cs with 17 integration tests - Phase 1 lifecycle tests: List, Info (2 tests) - Phase 2 structured reference tests: All regions, with/without columns, error handling (7 tests) - Phase 2 sorting tests: Single/multi-column, validation, error handling (8 tests) - All tests passing (17/17) - Test data: SalesTable with Region, Product, Amount, Date columns - Verifies Excel COM behavior: absolute references (\\), structured ref format - Tests require Excel installation (integration tests with ExcelSession) * feat: Phase 2 TableCommands - Structured References and Sorting Phase 2 adds structured reference helper and table sorting capabilities. NEW FEATURES: - Structured References: GetStructuredReferenceAsync returns both range address and Excel structured reference formula - Table Sorting: SortAsync with single-column and multi-column (max 3 levels) support CORE IMPLEMENTATION (src/ExcelMcp.Core/): - Added TableRegion enum (All, Data, Headers, Totals, ThisRow) - Added TableStructuredReferenceResult class (9 properties) - Added TableSortColumn class for multi-column sort specifications - Created TableCommands.StructuredReferences.cs (~183 lines) - GetStructuredReferenceAsync with BuildStructuredReference and GetRegionRange helpers - Supports all 5 table regions with optional column filtering - Created TableCommands.Sort.cs (~228 lines) - SortAsync(single column) - simple ascending/descending sort - SortAsync(multi-column) - up to 3 sort levels per Excel limitation - Updated ITableCommands interface (21 total methods) MCP SERVER INTEGRATION (src/ExcelMcp.McpServer/): - Added 3 new actions to TableTool: get-structured-reference, sort, sort-multi - Added handlers with full error handling and validation - JSON deserialization for multi-column sort parameters DOCUMENTATION (docs/COMMANDS.md): - Added Structured Reference Operations section with examples - Added Sort Operations section with single and multi-column examples - Added workflow hints for both features BUILD VERIFICATION: - Full solution builds successfully (8 projects, 0 errors, 0 warnings) - All async patterns use #pragma warning disable CS1998 for synchronous COM interop INTEGRATION NOTES: - Structured references bridge TableCommands (structure) and RangeCommands (data operations) - Table sorting preserves headers and totals row - CLI integration deferred (MCP Server is primary interface) Phase 2 Status: COMPLETE - Production ready for MCP Server usage * fix: Remove merge conflict markers from cherry-pick --------- Co-authored-by: Stefan Broenner <stefan.broenner@microsoft.comm>
15 KiB
Excel Table (ListObject) API Specification
Comprehensive specification for Excel Table operations - reviewing current implementation and future refactoring needs
Executive Summary
This specification reviews the current TableCommands implementation to determine:
- What functionality already exists
- What overlaps with RangeCommands
- What should be refactored or removed
- What's missing that should be added
Key Questions to Answer
- Does TableCommands duplicate RangeCommands? - Data operations on tables
- What's the proper division of responsibilities? - Table structure vs data operations
- Should ReadDataAsync/AppendRowsAsync move to RangeCommands? - Data operations
- What table-specific features are missing? - Structured references, filters, slicers?
Current TableCommands Implementation
Interface Review (ITableCommands.cs)
Lifecycle Operations:
- ✅
ListAsync- List all tables in workbook - ✅
CreateAsync- Create table from range with headers/style - ✅
RenameAsync- Rename table - ✅
DeleteAsync- Delete table (convert back to range) - ✅
GetInfoAsync- Get detailed table information
Structure Operations:
- ✅
ResizeAsync- Resize table to new range - ✅
ToggleTotalsAsync- Show/hide totals row - ✅
SetColumnTotalAsync- Set totals function for column - ✅
SetStyleAsync- Change table style
Data Operations: ⚠️ POTENTIAL OVERLAP WITH RANGECOMMANDS
- ✅
ReadDataAsync- Read table data - ✅
AppendRowsAsync- Append rows to table
Data Model Integration:
- ✅
AddToDataModelAsync- Add table to Power Pivot
Excel Table (ListObject) Capabilities
What is an Excel Table?
Excel Tables (ListObject COM objects) are structured ranges with metadata:
- Named references (e.g., "SalesTable")
- Column headers with names
- Automatic expansion when data added
- Built-in filtering and sorting UI
- Table styles and formatting
- Totals row with aggregate functions
- Structured references in formulas (
[@ColumnName]) - Can be added to Data Model for relationships
Excel COM API - ListObject Operations
1. Table Lifecycle
// Create table
dynamic listObjects = sheet.ListObjects;
dynamic table = listObjects.Add(
SourceType: xlSrcRange,
Source: sheet.Range["A1:D100"],
XlListObjectHasHeaders: xlYes
);
table.Name = "SalesTable";
// Delete table (convert to range, preserve data)
table.Unlist();
// Delete table (remove everything)
table.Delete();
2. Table Properties
// Basic properties
string name = table.Name;
string range = table.Range.Address;
bool hasHeaders = table.ShowHeaders;
bool hasTotals = table.ShowTotals;
string style = table.TableStyle.Name;
// Row counts
int totalRows = table.Range.Rows.Count; // Including header/totals
int dataRows = table.DataBodyRange?.Rows.Count ?? 0; // Data only
// Column operations
int columnCount = table.ListColumns.Count;
dynamic column = table.ListColumns.Item(1); // or by name
string columnName = column.Name;
3. Table Resize
// Resize table to new range
table.Resize(sheet.Range["A1:E200"]);
4. Totals Row
// Show/hide totals row
table.ShowTotals = true;
// Set totals function for column
dynamic column = table.ListColumns.Item("Amount");
column.TotalsCalculation = xlTotalsCalculationSum; // Sum
column.TotalsCalculation = xlTotalsCalculationAverage; // Average
column.TotalsCalculation = xlTotalsCalculationCount; // Count
column.TotalsCalculation = xlTotalsCalculationMax; // Max
column.TotalsCalculation = xlTotalsCalculationMin; // Min
column.TotalsCalculation = xlTotalsCalculationStdDev; // Std Dev
column.TotalsCalculation = xlTotalsCalculationVar; // Variance
column.TotalsCalculation = xlTotalsCalculationCustom; // Custom formula
5. Table Styles
// Built-in styles
table.TableStyle = workbook.TableStyles.Item("TableStyleMedium2");
// Or by name
table.TableStyle = "TableStyleLight9";
6. AutoFilter (Filtering)
// Tables automatically have AutoFilter
dynamic autoFilter = table.AutoFilter;
// Apply filter to column
autoFilter.Range.AutoFilter(
Field: 2, // Column index (1-based)
Criteria1: "USA",
Operator: xlFilterValues
);
// Clear filters
autoFilter.ShowAllData();
// Check if filtered
bool isFiltered = table.ShowAutoFilter;
7. Data Operations
// Read data (values only, no headers)
dynamic dataBodyRange = table.DataBodyRange;
object[,] values = dataBodyRange?.Value2;
// Read entire table (including headers)
object[,] allData = table.Range.Value2;
// Append row (table auto-expands)
dynamic newRow = table.ListRows.Add();
newRow.Range.Value2 = new object[,] { { val1, val2, val3 } };
// Insert row at position
dynamic insertedRow = table.ListRows.Add(Position: 5);
8. Data Model Integration
// Add to Data Model (Power Pivot)
table.TableObject = table; // Make it a "proper" table
// Then use Power Pivot to add to model
Overlap Analysis: TableCommands vs RangeCommands
Current Overlap
| Operation | TableCommands | RangeCommands | Verdict |
|---|---|---|---|
| Read data | ReadDataAsync |
GetValuesAsync |
⚠️ OVERLAP - RangeCommands can read table ranges |
| Write data | AppendRowsAsync |
SetValuesAsync |
⚠️ OVERLAP - RangeCommands can write to table ranges |
| Clear data | ❌ Not implemented | ClearContentsAsync |
✅ RangeCommands handles this |
| Format cells | ❌ Not implemented | SetNumberFormatAsync, SetFontAsync, etc. |
✅ RangeCommands handles this |
Key Insight: Tables ARE Ranges with Metadata
Excel Tables are fundamentally ranges with additional structure:
- Underlying cells = regular range
- Table structure = metadata layer (headers, totals, style, filters)
Proposed Division:
- TableCommands = Table structure and metadata (lifecycle, totals, filters, styles)
- RangeCommands = Data operations on any range (including table data ranges)
Proposed Refactoring Strategy
Option 1: Remove Data Operations from TableCommands
Remove from TableCommands:
- ❌
ReadDataAsync→ UseRangeCommands.GetValuesAsync(batch, sheetName, "TableName[#Data]") - ❌
AppendRowsAsync→ UseRangeCommands.SetValuesAsync+ResizeAsync
Keep in TableCommands:
- ✅ All lifecycle operations (List, Create, Rename, Delete, GetInfo)
- ✅ All structure operations (Resize, ToggleTotals, SetColumnTotal, SetStyle)
- ✅ Data Model integration (AddToDataModel)
- ✅ Filter operations (if added)
Benefits:
- Clear separation: Table structure vs data operations
- Users learn ONE API for data (RangeCommands)
- TableCommands focuses on table-specific features
Challenges:
- Users need to know table structured references (
TableName[#Data]) - Auto-expansion on append requires manual resize
Option 2: Keep Data Operations but Delegate to RangeCommands Internally
Keep current interface:
- ✅
ReadDataAsync- Internally calls RangeCommands - ✅
AppendRowsAsync- Internally calls RangeCommands + auto-resize
Benefits:
- User-friendly API (no need to know structured references)
- Auto-expansion handled automatically
- Backwards compatible
Challenges:
- Duplication of functionality
- Two ways to do the same thing
Option 3: Hybrid Approach (RECOMMENDED)
TableCommands focuses on table-specific operations:
- ✅ Lifecycle: List, Create, Rename, Delete, GetInfo
- ✅ Structure: Resize, ToggleTotals, SetColumnTotal, SetStyle
- ✅ Table-specific data:
AppendRowsAsync(auto-expansion feature) - ✅ Filters: Apply, clear, get filter state
- ✅ Data Model: AddToDataModel
- ❌ Remove:
ReadDataAsync- Use RangeCommands instead
Rationale:
AppendRowsAsynchas table-specific behavior (auto-expansion) - KEEPReadDataAsyncis just range read with no table-specific logic - REMOVE- Filters are table-specific (AutoFilter object) - ADD
- Data operations (format, copy, etc.) - Use RangeCommands
Missing Table Features
1. Filter Operations ⭐ HIGH PRIORITY
// Apply filter to column
Task<OperationResult> ApplyFilterAsync(IExcelBatch batch, string tableName, string columnName, string criteria);
// Apply multiple criteria filter
Task<OperationResult> ApplyFilterAsync(IExcelBatch batch, string tableName, string columnName, List<string> values);
// Clear filters
Task<OperationResult> ClearFiltersAsync(IExcelBatch batch, string tableName);
// Get filter state
Task<TableFilterResult> GetFiltersAsync(IExcelBatch batch, string tableName);
Excel COM:
dynamic autoFilter = table.AutoFilter;
autoFilter.Range.AutoFilter(Field: 2, Criteria1: "USA");
autoFilter.ShowAllData(); // Clear all filters
2. Structured Reference Support ⭐ MEDIUM PRIORITY
// Get structured reference for table regions
Task<OperationResult> GetStructuredReferenceAsync(IExcelBatch batch, string tableName, TableRegion region);
public enum TableRegion
{
All, // TableName[#All]
Data, // TableName[#Data]
Headers, // TableName[#Headers]
Totals, // TableName[#Totals]
ThisRow // TableName[@]
}
3. Column Operations ⭐ MEDIUM PRIORITY
// Add column to table
Task<OperationResult> AddColumnAsync(IExcelBatch batch, string tableName, string columnName, int? position = null);
// Remove column from table
Task<OperationResult> RemoveColumnAsync(IExcelBatch batch, string tableName, string columnName);
// Rename column
Task<OperationResult> RenameColumnAsync(IExcelBatch batch, string tableName, string oldName, string newName);
Excel COM:
dynamic newColumn = table.ListColumns.Add(Position: 3);
newColumn.Name = "NewColumn";
table.ListColumns.Item("OldColumn").Delete();
4. Sort Operations ⭐ LOW PRIORITY (RangeCommands has Sort)
Tables can use standard Range.Sort(), so RangeCommands.SortAsync works on table ranges.
5. Data Validation on Columns ⭐ LOW PRIORITY (RangeCommands has Validation)
RangeCommands validation operations work on table column ranges.
6. Slicers ⭐ FUTURE ENHANCEMENT
// Add slicer for table column
Task<OperationResult> AddSlicerAsync(IExcelBatch batch, string tableName, string columnName);
Slicers are complex UI objects - defer to future phase.
Recommended TableCommands Refactoring
Phase 1: Remove Duplication (THIS PHASE)
Remove from TableCommands:
- ❌
ReadDataAsync- Users should useRangeCommands.GetValuesAsync(batch, sheetName, "TableName[#Data]")- Document migration: "Use RangeCommands to read table data"
- Provide examples in docs
Keep in TableCommands:
2. ✅ AppendRowsAsync - Table-specific auto-expansion behavior
- This is unique to tables (auto-resize when data added)
- Cannot be easily replicated with RangeCommands alone
Update Documentation: 3. Document that RangeCommands works with table ranges 4. Provide examples of table structured references
Phase 2: Add Missing Features (FUTURE)
Filter Operations:
ApplyFilterAsync- Apply filter to columnClearFiltersAsync- Clear all filtersGetFiltersAsync- Get current filter state
Column Operations:
4. AddColumnAsync - Add column to table
5. RemoveColumnAsync - Remove column
6. RenameColumnAsync - Rename column
Structured References:
7. GetStructuredReferenceAsync - Get range address for table regions
Implementation Details
Current TableCommands Methods Review
✅ KEEP - Table Lifecycle
ListAsync- List all tablesCreateAsync- Create table from rangeRenameAsync- Rename tableDeleteAsync- Delete tableGetInfoAsync- Get table details
✅ KEEP - Table Structure
ResizeAsync- Resize tableToggleTotalsAsync- Show/hide totals rowSetColumnTotalAsync- Set totals functionSetStyleAsync- Change table style
✅ KEEP - Table-Specific Data
AppendRowsAsync- Append with auto-expansion
✅ KEEP - Data Model
AddToDataModelAsync- Add to Power Pivot
❌ REMOVE - Data Operations (Use RangeCommands)
ReadDataAsync- Duplicate of RangeCommands.GetValuesAsync
Migration Guide for Users
Before (TableCommands.ReadDataAsync)
var result = await tableCommands.ReadDataAsync(batch, "SalesTable");
List<List<object?>> data = result.Data;
After (RangeCommands.GetValuesAsync)
// Option 1: Read data only (no headers)
var result = await rangeCommands.GetValuesAsync(batch, "Sales", "SalesTable[#Data]");
List<List<object?>> data = result.Values;
// Option 2: Read everything (headers + data)
var result = await rangeCommands.GetValuesAsync(batch, "Sales", "SalesTable[#All]");
// Option 3: If you don't know the sheet name
var tableInfo = await tableCommands.GetInfoAsync(batch, "SalesTable");
var result = await rangeCommands.GetValuesAsync(batch, tableInfo.SheetName, "SalesTable[#Data]");
Table Structured References
Excel Tables support structured references:
TableName[#All]- Entire table including headers and totalsTableName[#Data]- Data rows only (no headers or totals)TableName[#Headers]- Header row onlyTableName[#Totals]- Totals row onlyTableName[[ColumnName]]- Specific columnTableName[@]- Current row (in formulas)
Summary
Current State
- TableCommands has 12 operations
- 1 operation (
ReadDataAsync) duplicates RangeCommands - 1 operation (
AppendRowsAsync) has table-specific behavior worth keeping - Missing important table features: filters, column management
Proposed Changes
Phase 1 - Remove Duplication:
- ❌ Delete
ReadDataAsync- Use RangeCommands instead - ✅ Keep
AppendRowsAsync- Table-specific auto-expansion - 📝 Update documentation with migration guide and structured reference examples
Phase 2 - Add Missing Features (Future): 4. Add filter operations (Apply, Clear, Get) 5. Add column operations (Add, Remove, Rename) 6. Add structured reference helper
Architecture Principle
TableCommands = Table structure and metadata
- Lifecycle (create, rename, delete, list)
- Structure (resize, totals, styles, columns)
- Table-specific behaviors (auto-expansion on append, filters)
- Data Model integration
RangeCommands = Data operations on any range
- Read/write values and formulas
- Formatting and styling
- Copy, clear, insert, delete
- Works on table ranges via structured references
This maintains clear separation of concerns and prevents duplication!
Open Questions for Review
-
Should we remove
ReadDataAsyncin Phase 1?- Pro: Eliminates duplication, encourages unified API
- Con: Breaking change, users need to learn structured references
-
Should
AppendRowsAsyncaccept 2D arrays instead of CSV?- Pro: Consistent with RangeCommands (no CSV in Core/MCP)
- Con: Breaking change
-
Should we add filter operations in Phase 1 or Phase 2?
- Filters are common table operations
- But adds scope to refactoring
-
Should we support table slicers?
- Complex UI objects
- Defer to future phase?
Next Step: Review this specification and decide on Phase 1 scope before implementation!