using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using D365MetadataBridge.Services;
using D365MetadataBridge.Models;
namespace D365MetadataBridge.Protocol
{
///
/// Routes incoming requests to the appropriate service method.
///
public class RequestDispatcher
{
private readonly MetadataReadService? _metadataService;
private readonly MetadataWriteService? _writeService;
private readonly CrossReferenceService? _xrefService;
private readonly AxDbSqlService _axdbService;
public RequestDispatcher(MetadataReadService? metadataService, MetadataWriteService? writeService, CrossReferenceService? xrefService, AxDbSqlService? axdbService = null)
{
_metadataService = metadataService;
_writeService = writeService;
_xrefService = xrefService;
_axdbService = axdbService ?? new AxDbSqlService(new AxDbSqlOptions());
}
public Task Dispatch(BridgeRequest request)
{
try
{
switch (request.Method.ToLowerInvariant())
{
// === Health ===
case "ping":
return Task.FromResult(BridgeResponse.CreateSuccess(request.Id, "pong"));
// Standalone, read-only AxDB SQL debug operations; they never touch the metadata provider.
case "axdbstatus":
case "axdbschema":
case "axdbquery":
return HandleAxDb(request);
// === Metadata Read ===
case "readtable":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("tableName")
?? throw new ArgumentException("Missing parameter: tableName");
return _metadataService!.ReadTable(name);
});
case "readclass":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("className")
?? throw new ArgumentException("Missing parameter: className");
return _metadataService!.ReadClass(name);
});
case "readenum":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("enumName")
?? throw new ArgumentException("Missing parameter: enumName");
return _metadataService!.ReadEnum(name);
});
case "readedt":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("edtName")
?? throw new ArgumentException("Missing parameter: edtName");
return _metadataService!.ReadEdt(name);
});
case "readform":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("formName")
?? throw new ArgumentException("Missing parameter: formName");
return _metadataService!.ReadForm(name);
});
case "readquery":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("queryName")
?? throw new ArgumentException("Missing parameter: queryName");
return _metadataService!.ReadQuery(name);
});
case "readview":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("viewName")
?? throw new ArgumentException("Missing parameter: viewName");
return _metadataService!.ReadView(name);
});
case "readdataentity":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("entityName")
?? throw new ArgumentException("Missing parameter: entityName");
return _metadataService!.ReadDataEntity(name);
});
case "readreport":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("reportName")
?? throw new ArgumentException("Missing parameter: reportName");
return _metadataService!.ReadReport(name);
});
case "getmethodsource":
return HandleMetadata(request, () =>
{
var className = request.GetStringParam("className")
?? throw new ArgumentException("Missing parameter: className");
var methodName = request.GetStringParam("methodName")
?? throw new ArgumentException("Missing parameter: methodName");
return _metadataService!.GetMethodSource(className, methodName);
});
// === Search ===
case "searchobjects":
return HandleMetadata(request, () =>
{
// The TS bridge client sends the type filter under "objectType";
// accept both keys so the filter is honored either way (and never
// silently downgraded to an unfiltered "all" search).
var type = request.GetStringParam("type")
?? request.GetStringParam("objectType")
?? "all";
var query = request.GetStringParam("query")
?? throw new ArgumentException("Missing parameter: query");
var maxResults = request.GetIntParam("maxResults") ?? 50;
return _metadataService!.SearchObjects(type, query, maxResults);
});
case "listobjects":
return HandleMetadata(request, () =>
{
var type = request.GetStringParam("type")
?? throw new ArgumentException("Missing parameter: type");
return _metadataService!.ListObjects(type);
});
// === Security Artifacts (Phase 6) ===
case "readsecurityprivilege":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("name")
?? throw new ArgumentException("Missing parameter: name");
return _metadataService!.ReadSecurityPrivilege(name);
});
case "readsecurityduty":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("name")
?? throw new ArgumentException("Missing parameter: name");
return _metadataService!.ReadSecurityDuty(name);
});
case "readsecurityrole":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("name")
?? throw new ArgumentException("Missing parameter: name");
return _metadataService!.ReadSecurityRole(name);
});
// === Menu Items (Phase 6) ===
case "readmenuitem":
return HandleMetadata(request, () =>
{
var name = request.GetStringParam("name")
?? throw new ArgumentException("Missing parameter: name");
var itemType = request.GetStringParam("itemType") ?? "any";
return _metadataService!.ReadMenuItem(name, itemType);
});
// === Table Extensions (Phase 6) ===
case "readtableextensions":
return HandleMetadata(request, () =>
{
var baseTableName = request.GetStringParam("baseTableName")
?? throw new ArgumentException("Missing parameter: baseTableName");
return _metadataService!.ReadTableExtensions(baseTableName);
});
// === Code Completion (Phase 6) ===
case "getcompletionmembers":
return HandleMetadata(request, () =>
{
var symbolName = request.GetStringParam("symbolName")
?? throw new ArgumentException("Missing parameter: symbolName");
return _metadataService!.GetCompletionMembers(symbolName);
});
// === Cross-References ===
case "findreferences":
return HandleXref(request, () =>
{
var objectPath = request.GetStringParam("objectPath")
?? request.GetStringParam("targetName")
?? throw new ArgumentException("Missing parameter: objectPath or targetName");
return _xrefService!.FindReferences(objectPath);
});
case "getxrefschema":
return HandleXref(request, () =>
{
return _xrefService!.GetSchemaInfo();
});
case "samplexrefrows":
return HandleXref(request, () =>
{
var tableName = request.GetStringParam("tableName") ?? "References";
return _xrefService!.SampleRows(tableName);
});
// === Extension / Event xref queries (Phase 6) ===
case "findextensionclasses":
return HandleXref(request, () =>
{
var baseClassName = request.GetStringParam("baseClassName")
?? throw new ArgumentException("Missing parameter: baseClassName");
return _xrefService!.FindExtensionClasses(baseClassName);
});
case "findeventsubscribers":
return HandleXref(request, () =>
{
var targetName = request.GetStringParam("targetName")
?? throw new ArgumentException("Missing parameter: targetName");
var eventNameFilter = request.GetStringParam("eventName");
var handlerTypeFilter = request.GetStringParam("handlerType");
return _xrefService!.FindEventSubscribers(targetName, eventNameFilter, handlerTypeFilter);
});
case "findapiusagecallers":
return HandleXref(request, () =>
{
var apiName = request.GetStringParam("apiName")
?? throw new ArgumentException("Missing parameter: apiName");
var limit = request.GetIntParam("limit") ?? 200;
return _xrefService!.FindApiUsageCallers(apiName, limit);
});
// === Capabilities ===
case "getcapabilities":
return HandleMetadata(request, () =>
{
return _metadataService!.GetCapabilities();
});
// === Form Pattern Discovery ===
case "discoverformpatterns":
return HandleMetadata(request, () =>
{
return _metadataService!.DiscoverFormPatterns();
});
// === Info ===
case "getinfo":
return Task.FromResult(BridgeResponse.CreateSuccess(request.Id, new
{
version = "1.0.0",
metadataAvailable = _metadataService != null,
xrefAvailable = _xrefService != null,
writeAvailable = _writeService != null,
axdbSqlAvailable = _axdbService.Enabled,
capabilities = new[]
{
"ping", "readTable", "readClass", "readEnum", "readEdt",
"readForm", "readQuery", "readView", "readDataEntity",
"readReport", "getMethodSource", "searchObjects",
"listObjects", "findReferences", "getInfo",
"validateObject", "resolveObjectInfo", "refreshProvider",
"createObject", "addMethod", "removeMethod", "addField",
"modifyField", "renameField", "removeField", "replaceAllFields",
"addIndex", "removeIndex", "addRelation", "removeRelation",
"addFieldGroup", "removeFieldGroup", "addFieldToFieldGroup",
"addEnumValue", "modifyEnumValue", "removeEnumValue",
"addControl", "addDataSource",
"setProperty", "replaceCode",
"getCapabilities", "discoverFormPatterns",
"findExtensionClasses", "findEventSubscribers", "findApiUsageCallers"
}.Concat(_axdbService.Enabled ? new[] { "axdbSql", "axdbStatus", "axdbSchema", "axdbQuery" } : Array.Empty()).ToArray()
}));
// === Write-support (validate / resolve / refresh) ===
case "validateobject":
return HandleMetadata(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing parameter: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing parameter: objectName");
return _metadataService!.ValidateObject(objectType, objectName);
});
case "resolveobjectinfo":
return HandleMetadata(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing parameter: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing parameter: objectName");
return _metadataService!.ResolveObjectInfo(objectType, objectName);
});
case "refreshprovider":
return HandleMetadata(request, () =>
{
return _metadataService!.RefreshProvider();
});
// === Write Operations (via MetadataWriteService) ===
case "createobject":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var modelName = request.GetStringParam("modelName")
?? throw new ArgumentException("Missing: modelName");
switch (objectType.ToLowerInvariant())
{
case "class":
case "class-extension":
return _writeService!.CreateClass(objectName, modelName,
request.GetStringParam("declaration"),
request.GetParam>("methods"),
request.GetDictParam("properties"));
case "table":
return _writeService!.CreateTable(objectName, modelName,
request.GetParam>("fields"),
request.GetParam>("fieldGroups"),
request.GetParam>("indexes"),
request.GetParam>("relations"),
request.GetParam>("methods"),
request.GetDictParam("properties"));
case "enum":
return _writeService!.CreateEnum(objectName, modelName,
request.GetParam>("values"),
request.GetDictParam("properties"));
case "edt":
return _writeService!.CreateEdt(objectName, modelName,
request.GetDictParam("properties"));
case "query":
return _writeService!.CreateQuery(objectName, modelName,
request.GetDictParam("properties"));
case "view":
return _writeService!.CreateView(objectName, modelName,
request.GetParam>("fields"),
request.GetDictParam("properties"));
case "menu-item-action":
return _writeService!.CreateMenuItemAction(objectName, modelName,
request.GetDictParam("properties"));
case "menu-item-display":
return _writeService!.CreateMenuItemDisplay(objectName, modelName,
request.GetDictParam("properties"));
case "menu-item-output":
return _writeService!.CreateMenuItemOutput(objectName, modelName,
request.GetDictParam("properties"));
case "security-privilege":
return _writeService!.CreateSecurityPrivilege(objectName, modelName,
request.GetDictParam("properties"));
case "security-duty":
return _writeService!.CreateSecurityDuty(objectName, modelName,
request.GetDictParam("properties"));
case "security-role":
return _writeService!.CreateSecurityRole(objectName, modelName,
request.GetDictParam("properties"));
case "table-extension":
return _writeService!.CreateTableExtension(objectName, modelName,
request.GetParam>("fields"),
request.GetParam>("fieldGroups"),
request.GetParam>("indexes"),
request.GetParam>("relations"),
request.GetParam>("methods"),
request.GetDictParam("properties"));
case "form-extension":
return _writeService!.CreateFormExtension(objectName, modelName,
request.GetParam>("methods"),
request.GetDictParam("properties"));
case "enum-extension":
return _writeService!.CreateEnumExtension(objectName, modelName,
request.GetParam>("values"),
request.GetDictParam("properties"));
case "form":
return _writeService!.CreateForm(objectName, modelName,
request.GetParam>("methods"),
request.GetDictParam("properties"));
case "menu":
return _writeService!.CreateMenu(objectName, modelName,
request.GetDictParam("properties"));
default:
throw new ArgumentException($"createObject not supported for '{objectType}' via bridge — use XML fallback");
}
}, true);
case "createsmarttable":
return HandleWrite(request, () =>
{
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var modelName = request.GetStringParam("modelName")
?? throw new ArgumentException("Missing: modelName");
return _writeService!.CreateSmartTable(
objectName, modelName,
request.GetStringParam("tableGroup"),
request.GetStringParam("tableType"),
request.GetStringParam("label"),
request.GetParam>("fields"),
request.GetParam>("extraFieldGroups"),
request.GetParam>("indexes"),
request.GetParam>("relations"),
request.GetParam>("methods"),
request.GetDictParam("extraProperties"));
}, true);
case "addmethod":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var methodName = request.GetStringParam("methodName")
?? throw new ArgumentException("Missing: methodName");
var source = request.GetStringParam("sourceCode")
?? throw new ArgumentException("Missing: sourceCode");
return _writeService!.AddMethod(objectType, objectName, methodName, source);
});
case "addfield":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var fieldName = request.GetStringParam("fieldName")
?? throw new ArgumentException("Missing: fieldName");
// dataField/dataSource select the data-entity-extension mapped-field
// path inside AddField; both are absent for table/table-extension.
// This is the single-op RPC that BridgeClient.addField() calls —
// it must forward the same parameters as the batch-modify case below.
// `type`/`extendedDataType` are the spellings the create path's
// fields[] uses for the same two things; accepting only the
// add-field spelling silently defaulted them (String, no EDT).
return _writeService!.AddField(tableName, fieldName,
request.GetStringParam("fieldType") ?? request.GetStringParam("type") ?? "String",
request.GetStringParam("edt") ?? request.GetStringParam("extendedDataType"),
request.GetBoolParam("mandatory") ?? false,
request.GetStringParam("label"),
request.GetStringParam("dataField"),
request.GetStringParam("dataSource"),
request.GetStringParam("fieldGroupName"));
});
case "setproperty":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var propertyPath = request.GetStringParam("propertyPath")
?? throw new ArgumentException("Missing: propertyPath");
var propertyValue = request.GetStringParam("propertyValue")
?? throw new ArgumentException("Missing: propertyValue");
return _writeService!.SetProperty(objectType, objectName, propertyPath, propertyValue);
});
case "replacecode":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var oldCode = request.GetStringParam("oldCode")
?? throw new ArgumentException("Missing: oldCode");
var newCode = request.GetStringParam("newCode")
?? throw new ArgumentException("Missing: newCode");
return _writeService!.ReplaceCode(objectType, objectName,
request.GetStringParam("methodName"), oldCode, newCode);
});
case "removemethod":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var methodName = request.GetStringParam("methodName")
?? throw new ArgumentException("Missing: methodName");
return _writeService!.RemoveMethod(objectType, objectName, methodName);
});
case "addindex":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var indexName = request.GetStringParam("indexName")
?? throw new ArgumentException("Missing: indexName");
return _writeService!.AddIndex(tableName, indexName,
request.GetParam>("fields"),
request.GetBoolParam("allowDuplicates") ?? false,
request.GetBoolParam("alternateKey") ?? false);
});
case "removeindex":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var indexName = request.GetStringParam("indexName")
?? throw new ArgumentException("Missing: indexName");
return _writeService!.RemoveIndex(tableName, indexName);
});
// This is the single-op RPC that BridgeClient.addFullTextIndex()/
// addTableMapping() actually call — it must carry the same four cases
// HandleBatchModify() has below, or every real request dies with
// "-32601 Unknown method" no matter how correct the C# write path is.
case "addfulltextindex":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var indexName = request.GetStringParam("indexName")
?? throw new ArgumentException("Missing: indexName");
return _writeService!.AddFullTextIndex(tableName, indexName,
request.GetParam>("fields"));
});
case "removefulltextindex":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var indexName = request.GetStringParam("indexName")
?? throw new ArgumentException("Missing: indexName");
return _writeService!.RemoveFullTextIndex(tableName, indexName);
});
case "addtablemapping":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var mapName = request.GetStringParam("mapName")
?? throw new ArgumentException("Missing: mapName");
return _writeService!.AddTableMapping(tableName, mapName,
request.GetStringParam("mappingTable"),
request.GetParam>("connections"));
});
case "removetablemapping":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var mapName = request.GetStringParam("mapName")
?? throw new ArgumentException("Missing: mapName");
return _writeService!.RemoveTableMapping(tableName, mapName);
});
case "addrelation":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var relationName = request.GetStringParam("relationName")
?? throw new ArgumentException("Missing: relationName");
var relatedTable = request.GetStringParam("relatedTable")
?? throw new ArgumentException("Missing: relatedTable");
return _writeService!.AddRelation(tableName, relationName, relatedTable,
request.GetParam>("constraints"),
request.GetStringParam("relationCardinality") ?? request.GetStringParam("cardinality"),
request.GetStringParam("relatedTableCardinality"),
request.GetStringParam("relationshipType"));
});
case "removerelation":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var relationName = request.GetStringParam("relationName")
?? throw new ArgumentException("Missing: relationName");
return _writeService!.RemoveRelation(tableName, relationName);
});
case "addfieldgroup":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var groupName = request.GetStringParam("fieldGroupName")
?? request.GetStringParam("groupName")
?? throw new ArgumentException("Missing: fieldGroupName");
return _writeService!.AddFieldGroup(tableName, groupName,
request.GetStringParam("label"),
request.GetParam>("fields"));
});
case "removefieldgroup":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var groupName = request.GetStringParam("fieldGroupName")
?? request.GetStringParam("groupName")
?? throw new ArgumentException("Missing: fieldGroupName");
return _writeService!.RemoveFieldGroup(tableName, groupName);
});
case "addfieldtofieldgroup":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var groupName = request.GetStringParam("fieldGroupName")
?? request.GetStringParam("groupName")
?? throw new ArgumentException("Missing: fieldGroupName");
var fieldName = request.GetStringParam("fieldName")
?? throw new ArgumentException("Missing: fieldName");
// This is the single-op RPC BridgeClient.addFieldToFieldGroup() actually
// calls — dropping extendBaseFieldGroup here silently reverts every
// caller to the extension-owns-the-group path (see HandleBatchModify()
// below, which forwards it correctly).
return _writeService!.AddFieldToFieldGroup(tableName, groupName, fieldName,
request.GetBoolParam("extendBaseFieldGroup") ?? false);
});
case "modifyfield":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var fieldName = request.GetStringParam("fieldName")
?? throw new ArgumentException("Missing: fieldName");
return _writeService!.ModifyField(tableName, fieldName,
request.GetDictParam("properties"));
});
case "renamefield":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var oldName = request.GetStringParam("fieldName")
?? request.GetStringParam("oldName")
?? throw new ArgumentException("Missing: fieldName");
var newName = request.GetStringParam("fieldNewName")
?? request.GetStringParam("newName")
?? throw new ArgumentException("Missing: fieldNewName");
return _writeService!.RenameField(tableName, oldName, newName);
});
case "removefield":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var fieldName = request.GetStringParam("fieldName")
?? throw new ArgumentException("Missing: fieldName");
return _writeService!.RemoveField(tableName, fieldName);
});
case "replaceallfields":
return HandleWrite(request, () =>
{
var tableName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var fields = request.GetParam>("fields")
?? throw new ArgumentException("Missing: fields");
return _writeService!.ReplaceAllFields(tableName, fields);
});
case "addenumvalue":
return HandleWrite(request, () =>
{
var enumName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var valueName = request.GetStringParam("enumValueName")
?? request.GetStringParam("valueName")
?? throw new ArgumentException("Missing: enumValueName");
var value = request.GetIntParam("enumValue")
?? request.GetIntParam("value")
?? throw new ArgumentException("Missing: enumValue");
return _writeService!.AddEnumValue(enumName, valueName, value,
request.GetStringParam("label"),
request.GetStringParam("countryRegionCodes"));
});
case "modifyenumvalue":
return HandleWrite(request, () =>
{
var enumName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var valueName = request.GetStringParam("enumValueName")
?? request.GetStringParam("valueName")
?? throw new ArgumentException("Missing: enumValueName");
return _writeService!.ModifyEnumValue(enumName, valueName,
request.GetDictParam("properties"));
});
case "removeenumvalue":
return HandleWrite(request, () =>
{
var enumName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var valueName = request.GetStringParam("enumValueName")
?? request.GetStringParam("valueName")
?? throw new ArgumentException("Missing: enumValueName");
return _writeService!.RemoveEnumValue(enumName, valueName);
});
case "addcontrol":
return HandleWrite(request, () =>
{
var formName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var controlName = request.GetStringParam("controlName")
?? throw new ArgumentException("Missing: controlName");
var parentControl = request.GetStringParam("parentControl")
?? throw new ArgumentException("Missing: parentControl");
var controlType = request.GetStringParam("controlType") ?? "String";
return _writeService!.AddControl(formName, controlName, parentControl, controlType,
request.GetStringParam("controlDataSource"),
request.GetStringParam("controlDataField"),
request.GetStringParam("label"));
});
case "adddatasource":
return HandleWrite(request, () =>
{
var objectType = request.GetStringParam("objectType")
?? throw new ArgumentException("Missing: objectType");
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
var dsName = request.GetStringParam("dataSourceName")
?? request.GetStringParam("dsName")
?? throw new ArgumentException("Missing: dataSourceName");
var table = request.GetStringParam("dataSourceTable")
?? request.GetStringParam("table")
?? throw new ArgumentException("Missing: dataSourceTable");
return _writeService!.AddDataSource(objectType, objectName, dsName, table,
request.GetStringParam("joinSource"),
request.GetStringParam("linkType"));
});
case "addfieldmodification":
return HandleWrite(request, () =>
{
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
return _writeService!.AddFieldModification(objectName,
request.GetStringParam("fieldName")
?? throw new ArgumentException("Missing: fieldName"),
request.GetStringParam("fieldLabel") ?? request.GetStringParam("label"),
request.GetBoolParam("fieldMandatory") ?? request.GetBoolParam("mandatory"));
});
case "addmenuitemtomenu":
return HandleWrite(request, () =>
{
var objectName = request.GetStringParam("objectName")
?? throw new ArgumentException("Missing: objectName");
return _writeService!.AddMenuItemToMenu(objectName,
request.GetStringParam("menuItemToAdd")
?? request.GetStringParam("menuItemName")
?? throw new ArgumentException("Missing: menuItemToAdd"),
request.GetStringParam("menuItemToAddType")
?? request.GetStringParam("menuItemType")
?? "display");
});
// === Batch Modify (multiple operations in one call) ===
case "batchmodify":
return HandleBatchModify(request);
default:
return Task.FromResult(
BridgeResponse.CreateError(request.Id, -32601, $"Unknown method: {request.Method}"));
}
}
catch (Exception ex)
{
return Task.FromResult(
BridgeResponse.CreateError(request.Id, -32603, $"Dispatch error: {ex.Message}"));
}
}
private async Task HandleAxDb(BridgeRequest request)
{
var result = await _axdbService.Handle(request.Method, request.Params).ConfigureAwait(false);
return BridgeResponse.CreateSuccess(request.Id, result);
}
///
/// Methods that only READ metadata, and may therefore run concurrently with
/// each other.
///
/// Measured on this VM: six reads issued together took 674 ms against 689 ms
/// issued one at a time — a 1.02x "speedup", because the stdio loop awaited each
/// Dispatch before reading the next line. Every fan-out the TS side performs
/// (get_object_info objects[], search queries[], prepare's five probes) was
/// therefore pipelining into a queue of one.
///
/// The list is explicit and the DEFAULT IS FALSE: anything not named here —
/// including a method added later — keeps the old exclusive behaviour. A write
/// misfiled as a read would corrupt metadata; a read misfiled as a write is
/// merely as slow as it is today.
///
private static readonly HashSet ConcurrentSafeReads =
new HashSet(StringComparer.OrdinalIgnoreCase)
{
"ping", "getcapabilities", "getinfo", "getxrefschema",
// Typed readers
"readtable", "readclass", "readenum", "readedt", "readform", "readquery",
"readview", "readreport", "readdataentity", "readmenuitem",
"readsecurityprivilege", "readsecurityduty", "readsecurityrole",
"readtableextensions",
// Short aliases of the same readers
"table", "class", "enum", "edt", "form", "query", "view", "menu",
// Lookup / search
"searchobjects", "listobjects", "resolveobjectinfo", "getcompletionmembers",
"getmethodsource", "validateobject", "discoverformpatterns",
// Cross-reference queries
"findreferences", "findeventsubscribers", "findextensionclasses",
"findapiusagecallers", "samplexrefrows",
// AxDB SQL: its own connection per call, no metadata provider, read-only. A query may
// run up to its 45 s budget; exclusive, it would hold every metadata call behind it.
"axdbstatus", "axdbschema", "axdbquery",
};
/// True when may run alongside other reads.
public static bool IsConcurrentSafeRead(string? method)
=> !string.IsNullOrEmpty(method) && ConcurrentSafeReads.Contains(method!);
private Task HandleMetadata(BridgeRequest request, Func