diff --git a/src/DebugHost/Program.cs b/src/DebugHost/Program.cs index f33a0f7..137aeea 100644 --- a/src/DebugHost/Program.cs +++ b/src/DebugHost/Program.cs @@ -5,17 +5,33 @@ using Microsoft.Diagnostics.Runtime; -foreach (var dumpPath in Directory.GetFiles(@"D:\dumps", "*.dmp")) -{ - ProcessFile(dumpPath); -} +// foreach (var dumpPath in Directory.GetFiles(@"D:\dumps", "*.dmp")) +// { +// ProcessFile(dumpPath); +// } + +ProcessFile(@"D:\dbg\dump_20230507_155200.dmp"); static void ProcessFile(string filePath) { Console.WriteLine($"Processing dump: {filePath}"); var runtimeContext = new RuntimeContext(filePath); - WriteWebRequests(runtimeContext); + WriteDictionary(runtimeContext); + + void WriteDictionary(RuntimeContext runtimeContext1) + { + var obj = runtimeContext.EnumerateObjects(null) + .Where(obj => !obj.IsNull && obj.Type.Name.StartsWith("System.Collections.Generic.Dictionary TargetObject.ReadField("count"); + public int Count => TargetObject.ReadField(CountFieldName); public int Version => TargetObject.ReadField("version"); - // TODO add TKey - // TODO add TValue + public ulong KeyMethodTable { get; } + public ulong ValueMethodTable { get; } public DictionaryProxy(RuntimeContext context, ClrObject targetObject) : base(context, targetObject) { + (KeyMethodTable, ValueMethodTable) = GetMethodTables(); } public DictionaryProxy(RuntimeContext context, ulong address) : base(context, address) { + (KeyMethodTable, ValueMethodTable) = GetMethodTables(); + } + + private string CountFieldName => Context.IsCoreRuntime ? "_count" : "count"; + private string EntriesFieldName => Context.IsCoreRuntime ? "_entries" : "entries"; + + private (ulong, ulong) GetMethodTables() + { + // ulong keyMt = 0; + // ulong valueMt = 0; + // + // foreach (var genericParameter in TargetObject.Type!.EnumerateGenericParameters()) + // { + // if (keyMt == 0) + // { + // foreach (var module in Context.Heap.Runtime.EnumerateModules()) + // { + // foreach ((ulong methodTable, int token) in module.EnumerateTypeRefToMethodTableMap()) + // { + // if (token == genericParameter.MetadataToken) + // { + // keyMt = methodTable; + // } + // } + // } + // } + // } + + ClrObject entries = TargetObject.ReadObjectField(EntriesFieldName); + if (entries.IsNull) + { + return (0, 0); + } + var entriesArray = entries + .AsArray(); + + var nextField = entriesArray.Type.ComponentType!.GetFieldByName("next")!; + var keyField = entriesArray.Type.ComponentType!.GetFieldByName("key")!; + var valueField = entriesArray.Type.ComponentType!.GetFieldByName("value")!; + + return (keyField.Type.MethodTable, valueField.Type.MethodTable); } private IEnumerable> EnumerateKeyValuePairs(Func> kvpBuilder) @@ -54,6 +96,63 @@ private IEnumerable> EnumerateKeyValuePairs> EnumerateItems() + { + int count = TargetObject.ReadField(Context.IsCoreRuntime ? "_count" : "count"); + if (count == 0) + yield break; + + var entries = TargetObject.ReadObjectField(EntriesFieldName) + .AsArray(); + + var nextField = entries.Type.ComponentType!.GetFieldByName("next")!; + var keyField = entries.Type.ComponentType!.GetFieldByName("key")!; + var valueField = entries.Type.ComponentType!.GetFieldByName("value")!; + + // TODO return + // keyField.Type.MethodTable + // valueField.Type.MethodTable + + for (int entryIndex = 0; entryIndex < count; entryIndex++) + { + var entry = entries.GetStructValue(entryIndex); + + int next = nextField.Read(entry.Address, true); + if (next < -1) + { + continue; + } + + Item key; + Item value; + if (keyField.IsObjectReference) + { + // IClrValue + var entryKey = keyField.ReadObject(entry.Address, true); + key = new Item(entryKey.Address, entryKey.Type?.IsString ?? false ? entryKey.AsString() : null); + } + else + { + var entryKey = keyField.ReadStruct(entry.Address, true); + key = new Item(entryKey.Address, null); + } + + if (valueField.IsObjectReference) + { + // IClrValue + var entryValue = valueField.ReadObject(entry.Address, true); + value = new Item(entryValue.Address, entryValue.Type?.IsString ?? false ? entryValue.AsString() : null); + } + else + { + var entryValue = valueField.ReadStruct(entry.Address, true); + value = new Item(entryValue.Address, null); + } + + yield return new KeyValuePair(key, value); + } + } + private KeyValuePair ReadStringString( ulong elementAddress, ClrInstanceField keyField, @@ -107,4 +206,6 @@ public void Dump(ILogger logger) Console.WriteLine($"{kvp.Key} = {kvp.Value}"); } } + + public record struct Item(ulong Address, string? Value); } \ No newline at end of file diff --git a/src/Heartbeat/ClientApp/api.yml b/src/Heartbeat/ClientApp/api.yml index a2a60ea..6b2a810 100644 --- a/src/Heartbeat/ClientApp/api.yml +++ b/src/Heartbeat/ClientApp/api.yml @@ -351,6 +351,40 @@ paths: application/json: schema: $ref: '#/components/schemas/ProblemDetails' + '/api/dump/object/{address}/as-dictionary': + get: + tags: + - Dump + operationId: GetClrObjectAsDictionary + parameters: + - name: address + in: path + required: true + style: simple + schema: + type: integer + format: int64 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/DictionaryInfo' + '204': + description: No Content + '404': + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '500': + description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' '/api/dump/object/{address}/as-jwt': get: tags: @@ -568,6 +602,51 @@ components: - AsyncPinnedHandle - SizedRefHandle type: string + DictionaryInfo: + required: + - count + - items + - keyMethodTable + - valueMethodTable + type: object + properties: + count: + type: integer + format: int32 + keyMethodTable: + type: integer + format: int64 + valueMethodTable: + type: integer + format: int64 + items: + type: array + items: + $ref: '#/components/schemas/DictionaryItemDictionaryItemKeyValuePair' + additionalProperties: false + DictionaryItem: + required: + - address + type: object + properties: + address: + type: integer + format: int64 + value: + type: string + nullable: true + additionalProperties: false + DictionaryItemDictionaryItemKeyValuePair: + required: + - key + - value + type: object + properties: + key: + $ref: '#/components/schemas/DictionaryItem' + value: + $ref: '#/components/schemas/DictionaryItem' + additionalProperties: false DumpInfo: required: - architecture diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asDictionary/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asDictionary/index.ts new file mode 100644 index 0000000..e3874f2 --- /dev/null +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/asDictionary/index.ts @@ -0,0 +1,45 @@ +/* tslint:disable */ +/* eslint-disable */ +// Generated by Microsoft Kiota +import { createDictionaryInfoFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, serializeProblemDetails, type DictionaryInfo, type ProblemDetails } from '../../../../../models/'; +import { BaseRequestBuilder, HttpMethod, RequestInformation, type Parsable, type ParsableFactory, type RequestAdapter, type RequestConfiguration, type RequestOption } from '@microsoft/kiota-abstractions'; + +/** + * Builds and executes requests for operations under /api/dump/object/{address}/as-dictionary + */ +export class AsDictionaryRequestBuilder extends BaseRequestBuilder { + /** + * Instantiates a new AsDictionaryRequestBuilder and sets the default values. + * @param pathParameters The raw url or the Url template parameters for the request. + * @param requestAdapter The request adapter to use to execute the requests. + */ + public constructor(pathParameters: Record | string | undefined, requestAdapter: RequestAdapter) { + super(pathParameters, requestAdapter, "{+baseurl}/api/dump/object/{address}/as-dictionary", (x, y) => new AsDictionaryRequestBuilder(x, y)); + } + /** + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a Promise of DictionaryInfo + */ + public get(requestConfiguration?: RequestConfiguration | undefined) : Promise { + const requestInfo = this.toGetRequestInformation( + requestConfiguration + ); + const errorMapping = { + "404": createProblemDetailsFromDiscriminatorValue, + "500": createProblemDetailsFromDiscriminatorValue, + } as Record>; + return this.requestAdapter.sendAsync(requestInfo, createDictionaryInfoFromDiscriminatorValue, errorMapping); + } + /** + * @param requestConfiguration Configuration for the request such as headers, query parameters, and middleware options. + * @returns a RequestInformation + */ + public toGetRequestInformation(requestConfiguration?: RequestConfiguration | undefined) : RequestInformation { + const requestInfo = new RequestInformation(HttpMethod.GET, this.urlTemplate, this.pathParameters); + requestInfo.configure(requestConfiguration); + requestInfo.headers.tryAdd("Accept", "application/json"); + return requestInfo; + } +} +/* tslint:enable */ +/* eslint-enable */ diff --git a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts index 7115ace..638078f 100644 --- a/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts +++ b/src/Heartbeat/ClientApp/src/client/api/dump/object/item/index.ts @@ -3,6 +3,7 @@ // Generated by Microsoft Kiota import { createGetClrObjectResultFromDiscriminatorValue, createProblemDetailsFromDiscriminatorValue, deserializeIntoProblemDetails, serializeProblemDetails, type GetClrObjectResult, type ProblemDetails } from '../../../../models/'; import { AsArrayRequestBuilder } from './asArray/'; +import { AsDictionaryRequestBuilder } from './asDictionary/'; import { AsJwtRequestBuilder } from './asJwt/'; import { FieldsRequestBuilder } from './fields/'; import { RootsRequestBuilder } from './roots/'; @@ -18,6 +19,12 @@ export class WithAddressItemRequestBuilder extends BaseRequestBuilder { clrObjectRootPath.root = n.getObjectValue(createRootInfoFromDiscriminatorValue); }, } } +export function deserializeIntoDictionaryInfo(dictionaryInfo: DictionaryInfo | undefined = {} as DictionaryInfo) : Record void> { + return { + "count": n => { dictionaryInfo.count = n.getNumberValue(); }, + "items": n => { dictionaryInfo.items = n.getCollectionOfObjectValues(createDictionaryItemDictionaryItemKeyValuePairFromDiscriminatorValue); }, + "keyMethodTable": n => { dictionaryInfo.keyMethodTable = n.getNumberValue(); }, + "valueMethodTable": n => { dictionaryInfo.valueMethodTable = n.getNumberValue(); }, + } +} +export function deserializeIntoDictionaryItem(dictionaryItem: DictionaryItem | undefined = {} as DictionaryItem) : Record void> { + return { + "address": n => { dictionaryItem.address = n.getNumberValue(); }, + "value": n => { dictionaryItem.value = n.getStringValue(); }, + } +} +export function deserializeIntoDictionaryItemDictionaryItemKeyValuePair(dictionaryItemDictionaryItemKeyValuePair: DictionaryItemDictionaryItemKeyValuePair | undefined = {} as DictionaryItemDictionaryItemKeyValuePair) : Record void> { + return { + "key": n => { dictionaryItemDictionaryItemKeyValuePair.key = n.getObjectValue(createDictionaryItemFromDiscriminatorValue); }, + "value": n => { dictionaryItemDictionaryItemKeyValuePair.value = n.getObjectValue(createDictionaryItemFromDiscriminatorValue); }, + } +} export function deserializeIntoDumpInfo(dumpInfo: DumpInfo | undefined = {} as DumpInfo) : Record void> { return { "architecture": n => { dumpInfo.architecture = n.getEnumValue(ArchitectureObject); }, @@ -305,6 +334,44 @@ export function deserializeIntoStringInfo(stringInfo: StringInfo | undefined = { "value": n => { stringInfo.value = n.getStringValue(); }, } } +export interface DictionaryInfo extends Parsable { + /** + * The count property + */ + count?: number; + /** + * The items property + */ + items?: DictionaryItemDictionaryItemKeyValuePair[]; + /** + * The keyMethodTable property + */ + keyMethodTable?: number; + /** + * The valueMethodTable property + */ + valueMethodTable?: number; +} +export interface DictionaryItem extends Parsable { + /** + * The address property + */ + address?: number; + /** + * The value property + */ + value?: string; +} +export interface DictionaryItemDictionaryItemKeyValuePair extends Parsable { + /** + * The key property + */ + key?: DictionaryItem; + /** + * The value property + */ + value?: DictionaryItem; +} export interface DumpInfo extends Parsable { /** * The architecture property @@ -571,6 +638,20 @@ export function serializeClrObjectRootPath(writer: SerializationWriter, clrObjec writer.writeCollectionOfObjectValues("pathItems", clrObjectRootPath.pathItems, serializeRootPathItem); writer.writeObjectValue("root", clrObjectRootPath.root, serializeRootInfo); } +export function serializeDictionaryInfo(writer: SerializationWriter, dictionaryInfo: DictionaryInfo | undefined = {} as DictionaryInfo) : void { + writer.writeNumberValue("count", dictionaryInfo.count); + writer.writeCollectionOfObjectValues("items", dictionaryInfo.items, serializeDictionaryItemDictionaryItemKeyValuePair); + writer.writeNumberValue("keyMethodTable", dictionaryInfo.keyMethodTable); + writer.writeNumberValue("valueMethodTable", dictionaryInfo.valueMethodTable); +} +export function serializeDictionaryItem(writer: SerializationWriter, dictionaryItem: DictionaryItem | undefined = {} as DictionaryItem) : void { + writer.writeNumberValue("address", dictionaryItem.address); + writer.writeStringValue("value", dictionaryItem.value); +} +export function serializeDictionaryItemDictionaryItemKeyValuePair(writer: SerializationWriter, dictionaryItemDictionaryItemKeyValuePair: DictionaryItemDictionaryItemKeyValuePair | undefined = {} as DictionaryItemDictionaryItemKeyValuePair) : void { + writer.writeObjectValue("key", dictionaryItemDictionaryItemKeyValuePair.key, serializeDictionaryItem); + writer.writeObjectValue("value", dictionaryItemDictionaryItemKeyValuePair.value, serializeDictionaryItem); +} export function serializeDumpInfo(writer: SerializationWriter, dumpInfo: DumpInfo | undefined = {} as DumpInfo) : void { writer.writeEnumValue("architecture", dumpInfo.architecture); writer.writeBooleanValue("canWalkHeap", dumpInfo.canWalkHeap); diff --git a/src/Heartbeat/ClientApp/src/lib/handleFetchData/index.ts b/src/Heartbeat/ClientApp/src/lib/handleFetchData/index.ts index 7c5ec4d..16e00f3 100644 --- a/src/Heartbeat/ClientApp/src/lib/handleFetchData/index.ts +++ b/src/Heartbeat/ClientApp/src/lib/handleFetchData/index.ts @@ -13,7 +13,7 @@ export default function fetchData(load: () => Promise, const getErrorMessage = () => { // ProblemDetails - return error.detail ?? error.title + return error?.exception?.details ?? error.detail ?? error.title ?? error; } diff --git a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx index 12ad343..93c0876 100644 --- a/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx +++ b/src/Heartbeat/ClientApp/src/pages/clrObject/ClrObject.tsx @@ -4,7 +4,7 @@ import {DataGrid, GridColDef, GridRenderCellParams, GridToolbar} from '@mui/x-da import {Stack} from "@mui/material"; import {TabbedShowLayout} from "react-admin"; -import {ClrObjectArrayItem, ClrObjectField, ClrObjectRootPath, GetClrObjectResult, JwtInfo} from '../../client/models'; +import {ClrObjectArrayItem, ClrObjectField, ClrObjectRootPath, DictionaryInfo, GetClrObjectResult, JwtInfo} from '../../client/models'; import {useStateWithLoading} from "../../hooks/useStateWithLoading"; import {useNotifyError} from "../../hooks/useNotifyError"; @@ -38,6 +38,7 @@ export const ClrObject = () => { const [fields, setFields, isFieldsLoading, setIsFieldsLoading] = useStateWithLoading() const [roots, setRoots, isRootsLoading, setIsRootsLoading] = useStateWithLoading() const [arrayItems, setArrayItems, isArrayItemsLoading, setArrayItemsLoading] = useStateWithLoading() + const [dictionary, setDictionary, isDictionaryLoading, setDictionaryLoading] = useStateWithLoading() const [jwt, setJwt, isJwtLoading, setJwtLoading] = useStateWithLoading() useEffect(() => { @@ -76,6 +77,15 @@ export const ClrObject = () => { fetchData(fetchArrayItems, setArrayItems, setArrayItemsLoading, notifyError) }, [address, notify]); + useEffect(() => { + const fetchDictionary = async () => { + const client = getClient(); + return await client.api.dump.object.byAddress(address).asDictionary.get() + } + + fetchData(fetchDictionary, setDictionary, setDictionaryLoading, notifyError) + }, [address, notify]); + useEffect(() => { const fetchJwt = async () => { const client = getClient(); @@ -115,7 +125,6 @@ export const ClrObject = () => { {getChildrenContent(clrObject)} - {/* TODO move each tab to a separate component */} @@ -126,6 +135,9 @@ export const ClrObject = () => { + @@ -263,6 +275,80 @@ const ArrayTabContent = (props: { isLoading: boolean, arrayItems?: ClrObjectArra ); } +const DictionaryTabContent = (props: { isLoading: boolean, dictionary?: DictionaryInfo }) => { + + // TODO show char[] as string + // TODO show byte[] as utf8 string + const getDictionaryItemsContent = (dictionary?: DictionaryInfo) => { + if (!dictionary) + return undefined; + + const GetGrid = () => { + if (!dictionary || dictionary.items!.length === 0) + return (<>); + + const columns: GridColDef[] = [ + { + ...objectAddressColumn, + field: 'key.address', + headerName: 'Key address', + valueGetter: params => params.row.key.address + }, + { + ...objectAddressColumn, + field: 'value.address', + headerName: 'Value address', + valueGetter: params => params.row.value.address + }, + { + field: 'key.value', + headerName: 'Key', + valueGetter: params => params.row.key.value, + flex: 0.5 + }, + { + field: 'value.value', + headerName: 'Value', + valueGetter: params => params.row.value.value, + flex: 1 + }, + ]; + + return ( + row.key!.address!} + columns={columns} + rowHeight={25} + pageSizeOptions={[20, 50, 100]} + density='compact' + slots={{toolbar: GridToolbar}} + initialState={{ + pagination: {paginationModel: {pageSize: 20}}, + }} + /> + ); + } + + const propertyRows: PropertyRow[] = [ + {title: 'Count', value: dictionary.count!}, + // {title: 'Key MT', value: renderMethodTableLink(dictionary.keyMethodTable)}, + // {title: 'Value MT', value: renderMethodTableLink(dictionary.keyMethodTable)}, + ] + + return ( + + + {GetGrid()} + + ); + } + + return ( + {getDictionaryItemsContent(props.dictionary)} + ); +} + const JwtTabContent = (props: { isLoading: boolean, jwt?: JwtInfo }) => { const getRootsContent = (jwt?: JwtInfo) => { if (!jwt) diff --git a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs index 315da5b..1e4e1ef 100644 --- a/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs +++ b/src/Heartbeat/Endpoints/EndpointJsonSerializerContext.cs @@ -7,6 +7,7 @@ namespace Heartbeat.Host.Endpoints; [JsonSerializable(typeof(GetObjectInstancesResult))] [JsonSerializable(typeof(GetClrObjectResult))] [JsonSerializable(typeof(JwtInfo))] +[JsonSerializable(typeof(DictionaryInfo))] [JsonSerializable(typeof(Module[]))] [JsonSerializable(typeof(ClrObjectField[]))] [JsonSerializable(typeof(List))] diff --git a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs index b9fb4fe..d78809b 100644 --- a/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs +++ b/src/Heartbeat/Endpoints/EndpointRouteBuilderExtensions.cs @@ -62,6 +62,12 @@ public static void MapDumpEndpoints(this IEndpointRouteBuilder app) .Produces(StatusCodes.Status500InternalServerError) .WithName(nameof(RouteHandlers.GetClrObjectAsArray)); + dumpGroup.MapGet("object/{address}/as-dictionary", RouteHandlers.GetClrObjectAsDictionary) + .Produces(StatusCodes.Status204NoContent) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status500InternalServerError) + .WithName(nameof(RouteHandlers.GetClrObjectAsDictionary)); + dumpGroup.MapGet("object/{address}/as-jwt", RouteHandlers.GetClrObjectAsJwt) .Produces(StatusCodes.Status204NoContent) .Produces(StatusCodes.Status404NotFound) diff --git a/src/Heartbeat/Endpoints/Models.cs b/src/Heartbeat/Endpoints/Models.cs index d095b1d..7ef9fa1 100644 --- a/src/Heartbeat/Endpoints/Models.cs +++ b/src/Heartbeat/Endpoints/Models.cs @@ -44,6 +44,9 @@ public record ClrObjectField( public record struct ClrObjectArrayItem(int Index, ulong Address, string? Value); +public record DictionaryInfo(int Count, ulong KeyMethodTable, ulong ValueMethodTable, IEnumerable> Items); +public record struct DictionaryItem(ulong Address, string? Value); + public record Module(ulong Address, ulong Size, string? Name); public record HeapSegment(ulong Start, ulong End, GCSegmentKind Kind) diff --git a/src/Heartbeat/Endpoints/RouteHandlers.cs b/src/Heartbeat/Endpoints/RouteHandlers.cs index 4cd33a6..bc0b8b6 100644 --- a/src/Heartbeat/Endpoints/RouteHandlers.cs +++ b/src/Heartbeat/Endpoints/RouteHandlers.cs @@ -282,7 +282,7 @@ public static Results>, NotFound> GetClrObjectRoots([ return TypedResults.Ok(result); } - + public static Results>, NoContent, NotFound> GetClrObjectAsArray([FromServices] RuntimeContext context, ulong address) { var clrObject = context.Heap.GetObject(address); @@ -296,16 +296,49 @@ public static Results>, NoContent, NotFound> ArrayProxy arrayProxy = new(context, clrObject); // TODO var str = arrayProxy.AsStringValue(); - + var items = arrayProxy.EnumerateArrayElements() .Select(e => new ClrObjectArrayItem(e.Index, e.Address, e.Value)); - + return TypedResults.Ok(items); } return TypedResults.NoContent(); } + public static Results, NoContent, NotFound> GetClrObjectAsDictionary([FromServices] RuntimeContext context, ulong address) + { + var clrObject = context.Heap.GetObject(address); + if (clrObject.Type == null) + { + return TypedResults.NotFound(); + } + + var typeName = clrObject.Type.Name ?? string.Empty; + var isDictType = + typeName.StartsWith("System.Collections.Generic.Dictionary") + && !typeName.EndsWith("+Entry[]"); + + if (!isDictType) + { + return TypedResults.NoContent(); + } + + var proxy = new DictionaryProxy(context, clrObject); + var items = proxy.EnumerateItems() + .Select(kvp => new KeyValuePair( + MapItem(kvp.Key), + MapItem(kvp.Value))); + + var result = new DictionaryInfo(proxy.Count, proxy.KeyMethodTable, proxy.ValueMethodTable, items); + return TypedResults.Ok(result); + + static DictionaryItem MapItem(DictionaryProxy.Item item) + { + return new DictionaryItem(item.Address, item.Value); + } + } + public static Results, NoContent, NotFound> GetClrObjectAsJwt([FromServices] RuntimeContext context, ulong address) { var clrObject = context.Heap.GetObject(address);