{"id":18462683,"url":"https://github.com/extism/dotnet-pdk","last_synced_at":"2025-08-21T22:31:22.695Z","repository":{"id":64343154,"uuid":"575108845","full_name":"extism/dotnet-pdk","owner":"extism","description":"Extism Plug-in Development Kit (PDK) for C# and F#","archived":false,"fork":false,"pushed_at":"2024-12-05T14:22:05.000Z","size":12444,"stargazers_count":45,"open_issues_count":1,"forks_count":6,"subscribers_count":7,"default_branch":"main","last_synced_at":"2024-12-16T02:52:10.814Z","etag":null,"topics":["csharp","dotnet","fsharp","plugins","wasi","wasm"],"latest_commit_sha":null,"homepage":"https://extism.org","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/extism.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2022-12-06T19:21:55.000Z","updated_at":"2024-12-08T15:35:03.000Z","dependencies_parsed_at":"2024-01-15T10:33:43.670Z","dependency_job_id":"c044d97f-8147-48e4-a1c0-1bf38167b283","html_url":"https://github.com/extism/dotnet-pdk","commit_stats":null,"previous_names":["extism/dotnet-pdk"],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fdotnet-pdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fdotnet-pdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fdotnet-pdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/extism%2Fdotnet-pdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/extism","download_url":"https://codeload.github.com/extism/dotnet-pdk/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230537062,"owners_count":18241515,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","dotnet","fsharp","plugins","wasi","wasm"],"created_at":"2024-11-06T09:04:03.150Z","updated_at":"2024-12-20T05:08:50.927Z","avatar_url":"https://github.com/extism.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# .NET PDK\n\nThis library can be used to write Extism\n[Plug-ins](https://extism.org/docs/concepts/plug-in) in C# and F#.\n\n\u003e NOTE: This is an experimental PDK. We'd love to hear your feedback.\n\n## Prerequisites\n\n1. .NET SDK 8: https://dotnet.microsoft.com/en-us/download/dotnet/8.0\n2. WASI Workload:\n\n```\ndotnet workload install wasi-experimental\n```\n\n3. Extract [WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases) into\n   local file system and set the `WASI_SDK_PATH` environment variable to point\n   to it\n\n## Install\n\nCreate a new project and add this nuget package to your project:\n\n```\ndotnet new wasiconsole -o MyPlugin\n# OR, for F#: dotnet new console -o MyPlugin -lang F#\ncd MyPlugin\ndotnet add package Extism.Pdk\n```\n\nUpdate your `MyPlugin.csproj`/`MyPlugin.fsproj` as follows:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n  \u003cPropertyGroup\u003e\n    \u003cTargetFramework\u003enet8.0\u003c/TargetFramework\u003e\n    \u003cRuntimeIdentifier\u003ewasi-wasm\u003c/RuntimeIdentifier\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n  \u003c/PropertyGroup\u003e\n\u003c/Project\u003e\n```\n\n## Getting Started\n\nThe goal of writing an Extism plug-in is to compile your C#/F# code to a Wasm\nmodule with exported functions that the host application can invoke. The first\nthing you should understand is creating an export. Let's write a simple program\nthat exports a greet function which will take a name as a string and return a\ngreeting string. Paste this into your Program.cs/Program.fs:\n\nC#:\n\n```csharp\nusing System;\nusing System.Runtime.InteropServices;\nusing System.Text.Json;\nusing Extism;\n\nnamespace MyPlugin;\npublic class Functions\n{\n    public static void Main()\n    {\n        // Note: a `Main` method is required for the app to compile\n    }\n\n    [UnmanagedCallersOnly(EntryPoint = \"greet\")]\n    public static int Greet()\n    {\n        var name = Pdk.GetInputString();\n        var greeting = $\"Hello, {name}!\";\n        Pdk.SetOutput(greeting);\n\n        return 0;\n    }\n}\n```\n\nF#:\n\n```fsharp\nmodule MyPlugin\n\nopen System\nopen System.Runtime.InteropServices\nopen System.Text.Json\nopen Extism\n\n[\u003cUnmanagedCallersOnly(EntryPoint = \"greet\")\u003e]\nlet Greet () : int32 =\n    let name = Pdk.GetInputString()\n    let greeting = $\"Hello, {name}!\"\n    Pdk.SetOutput(greeting)\n    0\n    \n[\u003cEntryPoint\u003e]\nlet Main args  =\n    // Note: an `EntryPoint` function is required for the app to compile\n    0\n```\n\nSome things to note about this code:\n\n1. The `[UnmanagedCallersOnly(EntryPoint = \"greet\")]` is required, this marks\n   the `Greet` function as an export with the name `greet` that can be called by\n   the host. `EntryPoint` is optional.\n1. We need a `Main` but it's unused. If you do want to use it, it's exported as\n   a function called `_start`.\n1. Exports in the .NET PDK are coded to the raw ABI. You get parameters from the\n   host by calling `Pdk.GetInput*` functions and you send returns back with the\n   `Pdk.SetOutput` functions.\n1. An Extism export expects an `Int32` return code. `0` is success and `1` is a\n   failure.\n\nCompile with this command:\n\n```\ndotnet build\n```\n\nThis will create a `MyPlugin.wasm` file in\n`bin/Debug/net8.0/wasi-wasm/AppBundle`. Now, you can try out your plugin by\nusing any of the\n[Extism SDKs](https://extism.org/docs/category/integrate-into-your-codebase) or\nby using [Extism CLI](https://extism.org/docs/install)'s `run` command:\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm greet --input \"Benjamin\" --wasi\n# =\u003e Hello, Benjamin!\n```\n\n\u003e **Note:** Currently wasi must be provided for all .NET plug-ins even if they\n\u003e don't need system access.\n\n## More Exports: Error Handling\n\nSuppose we want to re-write our greeting function to never greet Benjamis. We\ncan use `Pdk.SetError`:\n\nC#:\n\n```csharp\n[UnmanagedCallersOnly(EntryPoint = \"greet\")]\npublic static int Greet()\n{\n    var name = Pdk.GetInputString();\n    if (name == \"Benjamin\")\n    {\n        Pdk.SetError(\"Sorry, we don't greet Benjamins!\");\n        return 1;\n    }\n\n    var greeting = $\"Hello, {name}!\";\n    Pdk.SetOutput(greeting);\n\n    return 0;\n}\n```\n\nF#:\n\n```fsharp\n[\u003cUnmanagedCallersOnly(EntryPoint = \"greet\")\u003e]\nlet Greet () =\n    let name = Pdk.GetInputString()\n    if name = \"Benjamin\" then\n        Pdk.SetError(\"Sorry, we don't greet Benjamins!\")\n        1\n    else\n        let greeting = $\"Hello, {name}!\"\n        Pdk.SetOutput(greeting)\n        0\n```\n\nNow when we try again:\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm greet --input=\"Benjamin\" --wasi\n# =\u003e Error: Sorry, we don't greet Benjamins!\necho $? # print last status code\n# =\u003e 1\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm greet --input=\"Zach\" --wasi\n# =\u003e Hello, Zach!\necho $?\n# =\u003e 0\n```\n\nWe can also throw a normal .NET Exception:\n\n```\nvar name = Pdk.GetInputString();\nif (name == \"Benjamin\")\n{\n    throw new ArgumentException(\"Sorry, we don't greet Benjamins!\");\n}\n```\n\nNow when we try again:\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm greet --input=\"Benjamin\" --wasi\n# =\u003e Error: System.ArgumentException: Sorry, we don't greet Benjamins!\n   at MyPlugin.Functions.Greet()\n```\n\n## Json\n\nExtism export functions simply take bytes in and bytes out. Those can be\nwhatever you want them to be. A common and simple way to get more complex types\nto and from the host is with json:\n\nC#:\n\n```csharp\n[JsonSerializable(typeof(Add))]\n[JsonSerializable(typeof(Sum))]\npublic partial class SourceGenerationContext : JsonSerializerContext {}\n\npublic record Add(int a, int b);\npublic record Sum(int Result);\n\npublic static class Functions\n{\n    [UnmanagedCallersOnly]\n    public static int add()\n    {\n        var parameters = Pdk.GetInputJson(SourceGenerationContext.Default.Add);\n        var sum = new Sum(parameters.a + parameters.b);\n        Pdk.SetOutputJson(sum, SourceGenerationContext.Default.Sum);\n        return 0;\n    }\n}\n```\n\nF#:\n\n```fsharp\n[\u003cUnmanagedCallersOnly\u003e]\nlet add () =\n    let inputJson = Pdk.GetInputString()\n    let jsonData = JsonDocument.Parse(inputJson).RootElement\n    let a = jsonData.GetProperty(\"a\").GetInt32()\n    let b = jsonData.GetProperty(\"b\").GetInt32()\n    let result = a + b\n    let outputJson = $\"{{ \\\"Result\\\": {result} }}\"\n    \n    Pdk.SetOutput(outputJson)\n    0\n```\n\n**Note:** For F#, please make sure the\n[System.Text.Json](https://www.nuget.org/packages/System.Text.Json) NuGet\npackage is installed in your project.\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm --wasi add --input='{\"a\": 20, \"b\": 21}'\n# =\u003e {\"Result\":41}\n```\n\n**Note:** When enabling trimming, make sure you use the\n[source generation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation)\nas reflection is disabled in that mode.\n\n## Configs\n\nConfigs are key-value pairs that can be passed in by the host when creating a\nplug-in. These can be useful to statically configure the plug-in with some data\nthat exists across every function call. Here is a trivial example using\nPdk.TryGetConfig:\n\nC#:\n\n```csharp\n[UnmanagedCallersOnly(EntryPoint = \"greet\")]\npublic static int Greet()\n{\n    if (!Pdk.TryGetConfig(\"user\", out var user)) {\n        throw new InvalidOperationException(\"This plug-in requires a 'user' key in the config\");\n    }\n\n    var greeting = $\"Hello, {user}!\";\n    Pdk.SetOutput(greeting);\n\n    return 0;\n}\n```\n\nF#:\n\n```fsharp\n[\u003cUnmanagedCallersOnly(EntryPoint = \"greet\")\u003e]\nlet Greet () =\n    match Pdk.TryGetConfig \"user\" with\n    | true, user -\u003e\n        let greeting = $\"Hello, {user}!\"\n        Pdk.SetOutput(greeting)\n        0\n    | false, _ -\u003e\n        failwith \"This plug-in requires a 'user' key in the config\"\n```\n\nTo test it, the [Extism CLI](https://github.com/extism/cli) has a --config\noption that lets you pass in key=value pairs:\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm --wasi greet --config user=Benjamin\n# =\u003e Hello, Benjamin!\n```\n\n## Variables\n\nVariables are another key-value mechanism but it's a mutable data store that\nwill persist across function calls. These variables will persist as long as the\nhost has loaded and not freed the plug-in.\n\nC#:\n\n```csharp\n[UnmanagedCallersOnly]\npublic static int count()\n{\n    int count = 0;\n    if (Pdk.TryGetVar(\"count\", out var memoryBlock))\n    {\n        count = BitConverter.ToInt32(memoryBlock.ReadBytes());\n    }\n    count += 1;\n    Pdk.SetVar(\"count\", BitConverter.GetBytes(count));\n    Pdk.SetOutput(count.ToString());\n    return 0;\n}\n```\n\nF#:\n\n```fsharp\n[\u003cUnmanagedCallersOnly\u003e]\nlet count () =\n\n    let count =\n        match Pdk.TryGetVar \"count\" with\n        | true, buffer -\u003e\n            BitConverter.ToInt32(buffer.ReadBytes())\n        | false, _ -\u003e\n            0\n    \n    let count = count + 1\n\n    Pdk.SetVar(\"count\", BitConverter.GetBytes(count))\n    Pdk.SetOutput(count.ToString())\n    \n    0\n```\n\nFrom [Extism CLI](https://github.com/extism/cli):\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm --wasi count --loop 3\n1\n2\n3\n```\n\n## HTTP\n\nSometimes it is useful to let a plug-in make HTTP calls:\n\nC#:\n\n```csharp\n[UnmanagedCallersOnly]\npublic static int http_get()\n{\n    // create an HTTP Request (withuot relying on WASI), set headers as needed\n    var request = new HttpRequest(\"https://jsonplaceholder.typicode.com/todos/1\")\n    {\n        Method = HttpMethod.GET,\n    };\n    request.Headers.Add(\"some-name\", \"some-value\");\n    request.Headers.Add(\"another\", \"again\");\n    var response = Pdk.SendRequest(request);\n    Pdk.SetOutput(response.Body);\n    return 0;\n}\n```\n\nF#:\n\n```fsharp\n[\u003cUnmanagedCallersOnly\u003e]\nlet http_get () =\n    let request = HttpRequest(\"https://jsonplaceholder.typicode.com/todos/1\")\n    request.Headers.Add(\"some-name\", \"some-value\")\n    request.Headers.Add(\"another\", \"again\")\n\n    let response = Pdk.SendRequest(request)\n    Pdk.SetOutput(response.Body)\n    \n    0\n```\n\nFrom [Extism CLI](https://github.com/extism/cli):\n\n```\nextism call .\\bin\\Debug\\net8.0\\wasi-wasm\\AppBundle\\MyPlugin.wasm --wasi http_get --allow-host='*.typicode.com'\n{\n  \"userId\": 1,\n  \"id\": 1,\n  \"title\": \"delectus aut autem\",\n  \"completed\": false\n}\n```\n\n\u003e **NOTE**: `HttpClient` doesn't work in Wasm yet.\n\n## Imports (Host Functions)\n\nLike any other code module, Wasm not only let's you export functions to the\noutside world, you can import them too. Host Functions allow a plug-in to import\nfunctions defined in the host. For example, if you host application is written\nin Go, it can pass a Go function down to your Go plug-in where you can invoke\nit.\n\nThis topic can get fairly complicated and we have not yet fully abstracted the\nWasm knowledge you need to do this correctly. So we recommend reading our\n[concept doc on Host Functions](https://extism.org/docs/concepts/host-functions)\nbefore you get started.\n\n### A Simple Example\n\nHost functions have a similar interface as exports. You just need to declare\nthem as extern on the top of your `Program.cs`/`Program.fs`. You only declare\nthe interface as it is the host's responsibility to provide the implementation:\n\nC#:\n\n```csharp\n[DllImport(\"extism\", EntryPoint = \"a_go_func\")]\npublic static extern ulong GoFunc(ulong offset);\n[UnmanagedCallersOnly]\npublic static int hello_from_go()\n{\n    var message = \"An argument to send to Go\";\n    using var block = Pdk.Allocate(message);\n    var ptr = GoFunc(block.Offset);\n    var response = MemoryBlock.Find(ptr).ReadString();\n    Pdk.SetOutput(response);\n    return 0;\n}\n```\n\nF#:\n\n```fsharp\n[\u003cDllImport(\"extism\", EntryPoint = \"a_go_func\")\u003e]\nextern uint64 GoFunc(uint64 offset)\n\n[\u003cUnmanagedCallersOnly\u003e]\nlet hello_from_go () =\n    let message = \"An argument to send to Go\"\n    use block = Pdk.Allocate(message)\n\n    let ptr = GoFunc(block.Offset)\n    let response = MemoryBlock.Find ptr\n    Pdk.SetOutput(response)\n    \n    0\n```\n\n### Testing it out\n\nWe can't really test this from the Extism CLI as something must provide the\nimplementation. So let's write out the Go side here. Check out the\n[docs for Host SDKs](https://extism.org/docs/concepts/host-sdk) to implement a\nhost function in a language of your choice.\n\n```go\nctx := context.Background()\nconfig := extism.PluginConfig{\n    EnableWasi: true,\n}\n\ngo_func := extism.NewHostFunctionWithStack(\n    \"a_go_func\",\n    func(ctx context.Context, p *extism.CurrentPlugin, stack []uint64) {\n        input, err := p.ReadString(stack[0])\n        if err != nil {\n            panic(err)\n        }\n\n        fmt.Println(\"Hello from Go!\")\n\n        offs, err := p.WriteString(input + \"!\")\n        if err != nil {\n            panic(err)\n        }\n\n        stack[0] = offs\n    },\n    []api.ValueType{api.ValueTypeI64},\n    []api.ValueType{api.ValueTypeI64},\n)\n```\n\nNow when we load the plug-in we pass the host function:\n\n```go\nmanifest := extism.Manifest{\n    Wasm: []extism.Wasm{\n        extism.WasmFile{\n            Path: \"/path/to/plugin.wasm\",\n        },\n    },\n}\n\nplugin, err := extism.NewPlugin(ctx, manifest, config, []extism.HostFunction{go_func})\n\nif err != nil {\n    fmt.Printf(\"Failed to initialize plugin: %v\\n\", err)\n    os.Exit(1)\n}\n\n_, out, err := plugin.Call(\"hello_from_go\", []byte(\"Hello, World!\"))\nfmt.Println(string(out))\n```\n\n```bash\ngo run .\n# =\u003e Hello from Go!\n# =\u003e An argument to send to Go!\n```\n\n### Referenced Assemblies\n\nMethods in referenced assemblies that are decorated with `[DllImport]` and\n`[UnmanagedCallersOnly]` are imported and exported respectively.\n\n**Note:** The library imports/exports are ignored if the app doesn't call at\nleast one method from the library.\n\nFor example, if we have a library that contains this class:\n\n```csharp\nnamespace MessagingBot.Pdk;\npublic class Events\n{\n    // This function will be imported  by all WASI apps that reference this library\n    [DllImport(\"env\", EntryPoint = \"send_message\")]\n    public static extern void SendMessage(ulong offset);\n\n    // You can wrap the imports in your own functions to make them easier to use\n    public static void SendMessage(string message)\n    {\n        using var block = Extism.Pdk.Allocate(message);\n        SendMessage(block.Offset);\n    }\n\n    // This function will be exported by all WASI apps that reference this library\n    [UnmanagedCallersOnly]\n    public static void message_received()\n    {\n        var message = Extism.Pdk.GetInputString();\n        // TODO: do stuff with message\n    }\n}\n```\n\nThen, we can reference the library in a WASI app and use the functions:\n\n```csharp\nusing MessagingBot.Pdk;\n\nEvents.SendMessage(\"Hello World!\");\n```\n\nThis is useful when you want to provide a common set of imports and exports that\nare specific to your use case.\n\n### Optimize Size\n\nNormally, the .NET runtime is very conservative when trimming and includes a lot\nof metadata for debugging and exception purposes. We have enabled some options\nin Release mode by default that would make the resulting binary smaller (6mb for\na hello world sample vs 20mb in debug mode).\n\nIf you have imports in referenced assemblies, make sure\n[you mark them as roots](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options?pivots=dotnet-7-0#root-assemblies)\nso that they don't get trimmed:\n\n```xml\n\u003cItemGroup\u003e\n    \u003cTrimmerRootAssembly Include=\"SampleLib\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\nAnd then, run:\n\n```\ndotnet publish -c Release\n```\n\nNow, you'll have a smaller `.wasm` file in\n`bin\\Release\\net8.0\\wasi-wasm\\AppBundle`.\n\nFor more details, refer to\n[the official documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options?pivots=dotnet-7-0#trimming-framework-library-features).\n\n## Generating Bindings\n\nIt's often very useful to define a schema to describe the function signatures\nand types you want to use between Extism SDK and PDK languages.\n\n[XTP Bindgen](https://github.com/dylibso/xtp-bindgen) is an open source\nframework to generate PDK bindings for Extism plug-ins. It's used by the\n[XTP Platform](https://www.getxtp.com/), but can be used outside of the platform\nto define any Extism compatible plug-in system.\n\n### 1. Install the `xtp` CLI.\n\nSee installation instructions\n[here](https://docs.xtp.dylibso.com/docs/cli#installation).\n\n### 2. Create a schema using our OpenAPI-inspired IDL:\n\n```yaml\nversion: v1-draft\nexports: \n  CountVowels:\n      input: \n          type: string\n          contentType: text/plain; charset=utf-8\n      output:\n          $ref: \"#/components/schemas/VowelReport\"\n          contentType: application/json\n# components.schemas defined in example-schema.yaml...\n```\n\n\u003e See an example in [example-schema.yaml](./example-schema.yaml), or a full\n\u003e \"kitchen sink\" example on\n\u003e [the docs page](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema/).\n\n### 3. Generate bindings to use from your plugins:\n\n```\nxtp plugin init --schema-file ./example-schema.yaml\n    1. TypeScript                      \n    2. Go                              \n    3. Rust                            \n    4. Python                          \n  \u003e 5. C#                              \n    6. Zig                             \n    7. C++                             \n    8. GitHub Template                 \n    9. Local Template\n```\n\nThis will create an entire boilerplate plugin project for you to get started\nwith:\n\n```csharp\n/// \u003creturns\u003eThe result of counting vowels on the Vowels input.\u003c/returns\u003e\npublic static VowelReport CountVowels(string input)\n{\n    // TODO: fill out your implementation here\n    throw new NotImplementedException();\n}\n```\n\nImplement the empty function(s), and run `xtp plugin build` to compile your\nplugin.\n\n\u003e For more information about XTP Bindgen, see the\n\u003e [dylibso/xtp-bindgen](https://github.com/dylibso/xtp-bindgen) repository and\n\u003e the official\n\u003e [XTP Schema documentation](https://docs.xtp.dylibso.com/docs/concepts/xtp-schema).\n\n## Reach Out!\n\nHave a question or just want to drop in and say hi?\n[Hop on the Discord](https://extism.org/discord)!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextism%2Fdotnet-pdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fextism%2Fdotnet-pdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fextism%2Fdotnet-pdk/lists"}