├── img ├── example1.jpg └── blazor-kernel.svg ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .gitmodules ├── src └── BlazorInteractive │ ├── BlazorMarkdown.cs │ ├── SyntaxNodeExtensions.cs │ ├── VirtualRazorProjectFileSystem.cs │ ├── Repl.cs │ ├── KernelExtensions.cs │ ├── BlazorKernelExtension.cs │ ├── BlazorKernel.cs │ ├── BlazorInteractive.csproj │ ├── BlazorExtensions.cs │ └── BlazorCompilationService.cs ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── LICENSE ├── tests ├── BlazorInteractive.Tests │ ├── KernelExtensions.cs │ ├── BlazorInteractive.Tests.csproj │ ├── BlazorInteractiveTests.cs │ └── TestUtility.cs ├── integration-test.dib └── integration-test.ipynb ├── .editorconfig ├── .devcontainer └── devcontainer.json ├── README.md ├── blazor-interactive.sln └── .gitignore /img/example1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plbonneville/BlazorInteractive/HEAD/img/example1.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dotnet-test-explorer.testProjectPath": "**/*Tests.@(csproj|vbproj|fsproj)" 3 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/BlazorRepl"] 2 | path = src/BlazorRepl 3 | url = https://github.com/BlazorRepl/BlazorRepl.git 4 | -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorMarkdown.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorInteractive; 2 | 3 | /// 4 | /// Type used to register a markdown formatter for Blazor. 5 | /// 6 | internal sealed record BlazorMarkdown(string Value, string ComponentName) 7 | { 8 | public override string ToString() => Value; 9 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src/BlazorInteractive" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: nuget 10 | directory: "/tests/BlazorInteractive.Tests" 11 | schedule: 12 | interval: daily 13 | time: "10:00" 14 | open-pull-requests-limit: 10 15 | - package-ecosystem: nuget 16 | directory: "/tests/BlazorInteractive.IntegrationTests" 17 | schedule: 18 | interval: daily 19 | time: "10:00" 20 | open-pull-requests-limit: 10 21 | -------------------------------------------------------------------------------- /src/BlazorInteractive/SyntaxNodeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | 6 | namespace BlazorInteractive; 7 | 8 | internal static class SyntaxNodeExtensions 9 | { 10 | public static string RemoveNamespace(this SyntaxNode root) => 11 | root 12 | .ChildNodes() 13 | .Where(node => node is not QualifiedNameSyntax) 14 | .Aggregate(new StringBuilder(), (sb, node) => 15 | { 16 | if (node is NamespaceDeclarationSyntax @namespace) 17 | { 18 | return sb.Append(RemoveNamespace(@namespace)); 19 | } 20 | 21 | return sb.Append(node.ToFullString()); 22 | }) 23 | .ToString(); 24 | } -------------------------------------------------------------------------------- /src/BlazorInteractive/VirtualRazorProjectFileSystem.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorRepl.Core 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Razor.Language; 6 | 7 | internal class VirtualRazorProjectFileSystem : RazorProjectFileSystem 8 | { 9 | public override IEnumerable EnumerateItems(string basePath) 10 | { 11 | this.NormalizeAndEnsureValidPath(basePath); 12 | return Enumerable.Empty(); 13 | } 14 | 15 | ////[Obsolete] 16 | public override RazorProjectItem GetItem(string path) => this.GetItem(path, fileKind: null); 17 | 18 | 19 | public override RazorProjectItem GetItem(string path, string fileKind) 20 | { 21 | this.NormalizeAndEnsureValidPath(path); 22 | return new NotFoundProjectItem(string.Empty, path); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pier-Luc Bonneville 4 | All rights reserved. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/BlazorInteractive.Tests/bin/Debug/net5.0/BlazorInteractive.Tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/BlazorInteractive.Tests", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /tests/BlazorInteractive.Tests/KernelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.DotNet.Interactive.CSharp; 2 | using Microsoft.DotNet.Interactive.FSharp; 3 | using Microsoft.DotNet.Interactive.PackageManagement; 4 | 5 | namespace BlazorInteractive.Tests; 6 | 7 | public static class KernelExtensions 8 | { 9 | public static CSharpKernel UseNugetDirective(this CSharpKernel kernel, bool forceRestore = false) 10 | { 11 | kernel.UseNugetDirective((k, resolvedPackageReference) => 12 | { 13 | k.AddAssemblyReferences(resolvedPackageReference 14 | .SelectMany(r => r.AssemblyPaths)); 15 | return Task.CompletedTask; 16 | }, forceRestore); 17 | 18 | return kernel; 19 | } 20 | 21 | public static FSharpKernel UseNugetDirective(this FSharpKernel kernel, bool forceRestore = false) 22 | { 23 | kernel.UseNugetDirective((k, resolvedPackageReference) => 24 | { 25 | var resolvedAssemblies = resolvedPackageReference 26 | .SelectMany(r => r.AssemblyPaths); 27 | 28 | var packageRoots = resolvedPackageReference 29 | .Select(r => r.PackageRoot); 30 | 31 | 32 | k.AddAssemblyReferencesAndPackageRoots(resolvedAssemblies, packageRoots); 33 | return Task.CompletedTask; 34 | }, forceRestore); 35 | 36 | return kernel; 37 | } 38 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | env: 12 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | with: 21 | submodules: true 22 | 23 | - name: Setup dotnet 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: 8.0.100 27 | 28 | - name: Install dependencies 29 | run: dotnet restore ./src/BlazorInteractive 30 | 31 | - name: Install dependencies for unit tests 32 | run: dotnet restore ./tests/BlazorInteractive.Tests 33 | 34 | - name: Build 35 | run: dotnet build --nologo --configuration Release --no-restore ./src/BlazorInteractive 36 | 37 | - name: Test 38 | run: dotnet test --nologo --no-restore --verbosity normal ./tests/BlazorInteractive.Tests 39 | 40 | - name: Pack NuGet package 41 | run: dotnet pack --configuration Release --no-build ./src/BlazorInteractive 42 | 43 | - name: Publish NuGet package 44 | if: startsWith(github.ref, 'refs/heads/main') 45 | run: dotnet nuget push ./src/BlazorInteractive/bin/Release/*.nupkg --api-key $NUGET_API_KEY --source https://api.nuget.org/v3/index.json --skip-duplicate 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | [*.{c,c++,cc,cp,cpp,cu,cuh,cxx,h,hh,hpp,hxx,inc,inl,ino,ipp,mpp,proto,tpp}] 3 | indent_style=tab 4 | indent_size=tab 5 | tab_width=4 6 | 7 | [*.{asax,ascx,aspx,cs,cshtml,css,htm,html,js,jsx,master,razor,skin,ts,tsx,vb,xaml,xamlx,xoml}] 8 | indent_style=space 9 | indent_size=4 10 | tab_width=4 11 | 12 | [*.{appxmanifest,build,config,csproj,dbml,discomap,dtd,json,jsproj,lsproj,njsproj,nuspec,proj,props,resjson,resw,resx,StyleCop,targets,tasks,vbproj,xml,xsd}] 13 | indent_style=space 14 | indent_size=2 15 | tab_width=2 16 | 17 | [*] 18 | 19 | # Microsoft .NET properties 20 | csharp_new_line_before_members_in_object_initializers=false 21 | csharp_preferred_modifier_order=public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion 22 | csharp_style_var_elsewhere=true:hint 23 | csharp_style_var_for_built_in_types=true:hint 24 | csharp_style_var_when_type_is_apparent=true:hint 25 | dotnet_style_predefined_type_for_locals_parameters_members=true:hint 26 | dotnet_style_predefined_type_for_member_access=true:hint 27 | dotnet_style_qualification_for_event=false:warning 28 | dotnet_style_qualification_for_field=false:warning 29 | dotnet_style_qualification_for_method=false:warning 30 | dotnet_style_qualification_for_property=false:warning 31 | dotnet_style_require_accessibility_modifiers=for_non_interface_members:hint 32 | -------------------------------------------------------------------------------- /src/BlazorInteractive/Repl.cs: -------------------------------------------------------------------------------- 1 | using BlazorRepl.Core; 2 | 3 | namespace BlazorInteractive; 4 | 5 | internal class BlazorAssemblyCompiler 6 | { 7 | public BlazorAssemblyCompiler(string code, string componentName) 8 | { 9 | CodeFiles.Add($"{componentName}.razor", new CodeFile { Content = code, Path = $"{componentName}.razor" }); 10 | } 11 | 12 | public BlazorCompilationService CompilationService { get; init; } 13 | 14 | public IReadOnlyCollection Diagnostics { get; private set; } = Array.Empty(); 15 | 16 | private IDictionary CodeFiles { get; } = new Dictionary(); 17 | 18 | public async Task<(CompileToAssemblyResult, string)> CompileAsync() 19 | { 20 | string code = null; 21 | CompileToAssemblyResult compilationResult = null; 22 | 23 | try 24 | { 25 | (compilationResult, code) = await CompilationService.CompileToAssemblyAsync( 26 | CodeFiles.Values, 27 | _ => Task.CompletedTask); 28 | 29 | Diagnostics = compilationResult.Diagnostics.OrderByDescending(x => x.Severity).ThenBy(x => x.Code).ToList(); 30 | } 31 | catch (Exception e) 32 | { 33 | throw new Exception("Error while compiling the code.", e); 34 | } 35 | 36 | return (compilationResult, code); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "C# (.NET)", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm", 7 | 8 | // Features to add to the dev container. More info: https://containers.dev/features. 9 | "features": { 10 | "ghcr.io/devcontainers/features/powershell:1": {} 11 | }, 12 | 13 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 14 | // "forwardPorts": [5000, 5001], 15 | // "portsAttributes": { 16 | // "5001": { 17 | // "protocol": "https" 18 | // } 19 | // } 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | // "postCreateCommand": "dotnet restore", 23 | 24 | // Configure tool-specific properties. 25 | "customizations": { 26 | "vscode": { 27 | "extensions": [ 28 | "formulahendry.dotnet-test-explorer", 29 | "ms-dotnettools.csharp", 30 | "ms-dotnettools.dotnet-interactive-vscode", 31 | "ms-toolsai.jupyter" 32 | ] 33 | } 34 | } 35 | 36 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 37 | // "remoteUser": "root" 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/BlazorInteractive.Tests/BlazorInteractive.Tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/BlazorInteractive.Tests/BlazorInteractive.Tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/BlazorInteractive.Tests/BlazorInteractive.Tests.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /src/BlazorInteractive/KernelExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Net.Http.Json; 3 | using System.Reflection; 4 | 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 7 | using Microsoft.DotNet.Interactive; 8 | using Microsoft.DotNet.Interactive.Commands; 9 | using Microsoft.DotNet.Interactive.CSharp; 10 | 11 | namespace BlazorInteractive; 12 | 13 | internal static class KernelExtensions 14 | { 15 | private static readonly Assembly[] _references = 16 | { 17 | typeof(ComponentBase).Assembly, // Microsoft.AspNetCore.Components 18 | typeof(WebAssemblyHost).Assembly, // Microsoft.AspNetCore.Components.WebAssembly 19 | typeof(DataType).Assembly, // System.ComponentModel.DataAnnotations 20 | typeof(JsonContent).Assembly // System.Net.Http.Json 21 | }; 22 | 23 | internal static Task LoadRequiredAssemblies(this CompositeKernel kernel) 24 | => LoadRequiredAssemblies(kernel.FindKernelByName("csharp") as CSharpKernel); 25 | 26 | private static async Task LoadRequiredAssemblies(this CSharpKernel csharpKernel) 27 | { 28 | var rDirectives = string.Join(Environment.NewLine, _references.Select(a => $"#r \"{a.Location}\"")); 29 | 30 | await csharpKernel.SendAsync(new SubmitCode($"{rDirectives}"), CancellationToken.None).ConfigureAwait(false); 31 | //await csharpKernel.SendAsync(new SubmitCode($"{rDirectives}{Environment.NewLine}{usings}"), CancellationToken.None).ConfigureAwait(false); 32 | } 33 | } -------------------------------------------------------------------------------- /tests/BlazorInteractive.Tests/BlazorInteractive.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | false 8 | 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | all 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/integration-test.dib: -------------------------------------------------------------------------------- 1 | #!meta 2 | 3 | {"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"name":"blazor"},{"aliases":["c#","cs"],"languageName":"C#","name":"csharp"},{"aliases":["f#","fs"],"languageName":"F#","name":"fsharp"},{"aliases":[],"languageName":"HTML","name":"html"},{"aliases":[],"languageName":"http","name":"httpRequest"},{"aliases":["js"],"languageName":"JavaScript","name":"javascript"},{"aliases":[],"languageName":"KQL","name":"kql"},{"aliases":[],"languageName":"Mermaid","name":"mermaid"},{"aliases":["powershell"],"languageName":"PowerShell","name":"pwsh"},{"aliases":[],"languageName":"SQL","name":"sql"},{"aliases":[],"name":"value"}]}} 4 | 5 | #!pwsh 6 | 7 | # 0. Clean-up the bin and obj folders 8 | Get-ChildItem .. -Include bin,obj -Recurse | Remove-Item -Force -Recurse 9 | 10 | # 2. Pack up the NuGet package. Note, you should increment the version because the previous one, once installed, will be in your NuGet cache 11 | $version = [System.DateTime]::Now.ToString("yyyy.MM.dd.HHmmss") 12 | dotnet pack /p:PackageVersion=$version ../src/BlazorInteractive/BlazorInteractive.csproj 13 | 14 | # 3. Check that the package is there 15 | Get-ChildItem .. -Recurse *.nupkg 16 | 17 | #!csharp 18 | 19 | #i nuget:/workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release 20 | #r "nuget:BlazorInteractive" 21 | 22 | #!blazor 23 | 24 |

Hello @name

25 | 26 | @code { 27 | string name = "Alice"; 28 | } 29 | 30 | #!csharp 31 | 32 | #!blazor --name Counter 33 |

Counter

34 | 35 |

36 | Current count: @currentCount 37 |

38 | 39 | 40 | 41 | @code { 42 | public int currentCount = 0; 43 | 44 | void IncrementCount() 45 | { 46 | currentCount++; 47 | } 48 | } 49 | 50 | #!csharp 51 | 52 | var componentName = typeof(Counter).Name; 53 | componentName 54 | -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorKernelExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Html; 2 | using Microsoft.DotNet.Interactive; 3 | using Microsoft.DotNet.Interactive.Commands; 4 | using Microsoft.DotNet.Interactive.Formatting; 5 | 6 | namespace BlazorInteractive; 7 | 8 | public sealed class BlazorKernelExtension : IKernelExtension, IStaticContentSource 9 | { 10 | public string Name => "Blazor"; 11 | 12 | public async Task OnLoadAsync(Kernel kernel) 13 | { 14 | if (kernel is not CompositeKernel compositeKernel) 15 | { 16 | throw new InvalidOperationException("The Blazor kernel can only be added into a CompositeKernel."); 17 | } 18 | 19 | // Add a BlazorKernel as a child kernel to the CompositeKernel 20 | compositeKernel.Add(new BlazorKernel()); 21 | 22 | await compositeKernel 23 | .UseBlazor() 24 | .LoadRequiredAssemblies(); 25 | 26 | var message = new HtmlString( 27 | """ 28 |
29 | Compile and render Razor components (.razor) in .NET Interactive Notebooks. 30 |

This extension adds a new kernel that can render Blazor markdown.

31 | 32 |
33 |                     
34 |             #!blazor
35 |             

Counter

36 | 37 |

38 | Current count: @currentCount 39 |

40 | 41 | @code { 42 | int currentCount = 0; 43 | }
44 |
45 | 46 |

This extension also adds the compiled component as a type to the interactive workspace.

47 | 48 |

Options:

49 |
    50 |
  • -n, --name     The Razor component's (.razor) type name. The default value is __Main
  • 51 |
52 |
53 | """); 54 | 55 | var formattedValue = new FormattedValue( 56 | HtmlFormatter.MimeType, 57 | message.ToDisplayString(HtmlFormatter.MimeType)); 58 | 59 | await compositeKernel.SendAsync(new DisplayValue(formattedValue, Guid.NewGuid().ToString())); 60 | } 61 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET Interactive Notebooks Blazor Extension 2 | 3 | [![NuGet version (BlazorInteractive)](https://img.shields.io/nuget/v/BlazorInteractive.svg)](https://www.nuget.org/packages/BlazorInteractive/) 4 | [![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/plbonneville/BlazorInteractive/blob/main/LICENSE) 5 | 6 | Compile and render Razor components (.razor) in .NET Interactive Notebooks. 7 | 8 | ## Table of content 9 | 10 | - [Get started](#get-started) 11 | - [Usage](#usageexamples) 12 | - [How to compile this project](#how-to-compile-this-project) 13 | 14 | ## Get started 15 | 16 | To get started with Blazor in .NET Interactive Notebooks, first install the `BlazorInteractive` NuGet package. 17 | 18 | In a new `C# (.NET Interactive)` cell enter and run the following: 19 | 20 | ``` 21 | #r "nuget: BlazorInteractive, 1.2.0" 22 | ``` 23 | 24 | ## Usage/Examples 25 | 26 | Using the `#!blazor` magic command your code cell will be parsed by a Blazor engine and the results displayed using the `"txt/html"` mime type. 27 | 28 | ```razor 29 | #!blazor 30 |

Hello @name

31 | 32 | @code { 33 | string name = "Alice"; 34 | } 35 | ``` 36 | 37 | ![hello world](img/example1.jpg) 38 | 39 | ### Naming your components 40 | 41 | You can name the generated Razor components using the `-n` or `--name` options. 42 | 43 | ```razor 44 | #!blazor --name Counter 45 |

Counter

46 | 47 |

48 | Current count: @currentCount 49 |

50 | 51 | 52 | 53 | @code { 54 | public int currentCount = 0; 55 | 56 | void IncrementCount() 57 | { 58 | currentCount++; 59 | } 60 | } 61 | ``` 62 | 63 | You can then use the component as any other class in the next code cells: 64 | 65 | ```csharp 66 | var componentName = typeof(Counter).Name; 67 | componentName 68 | ``` 69 | 70 | ```csharp 71 | var counter = new Counter(); 72 | counter.currentCount 73 | ``` 74 | 75 | ## Sequence of commands and events 76 | 77 | Here is an overview of the sequence of commands and events: 78 | 79 | ![sequence of commands and events](https://github.com/plbonneville/BlazorInteractive/blob/main/img/blazor-kernel.svg) 80 | 81 | ## Contributing 82 | 83 | Contributions are always welcome! 84 | 85 | ### How to compile this project 86 | 87 | Since this project requires a git submodule, you'll need to initialize and update the [Blazor REPL](https://github.com/BlazorRepl/BlazorRepl) submodule. 88 | 89 | #### On the first `git clone`: 90 | 91 | ``` 92 | git clone --recurse-submodules -j8 https://github.com/plbonneville/BlazorInteractive.git 93 | ``` 94 | 95 | #### If you already have the repository cloned, run: 96 | 97 | ``` 98 | git submodule init 99 | git submodule update 100 | ``` 101 | 102 | ## Built with 103 | 104 | - [Blazor REPL](https://github.com/BlazorRepl/BlazorRepl) 105 | - [bUnit](https://github.com/bUnit-dev/bUnit) 106 | - [.NET Interactive ](https://github.com/dotnet/interactive) 107 | -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorKernel.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using System.CommandLine.NamingConventionBinder; 4 | using System.CommandLine.Parsing; 5 | 6 | using Microsoft.DotNet.Interactive; 7 | using Microsoft.DotNet.Interactive.Commands; 8 | 9 | namespace BlazorInteractive; 10 | 11 | /// 12 | /// A that renders Blazor markup. 13 | /// 14 | internal sealed class BlazorKernel : Kernel, IKernelCommandHandler 15 | { 16 | private const string DefaultComponentName = "__Main"; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// A new instance. 22 | public BlazorKernel() : base("blazor") 23 | { 24 | } 25 | 26 | private string ComponentName { get; set; } = DefaultComponentName; 27 | 28 | public Task HandleAsync(SubmitCode command, KernelInvocationContext context) 29 | { 30 | var markdown = new BlazorMarkdown(command.Code, ComponentName); 31 | context.Display(markdown); 32 | return Task.CompletedTask; 33 | } 34 | 35 | public override ChooseKernelDirective ChooseKernelDirective 36 | { 37 | get 38 | { 39 | var nameOption = new Option( 40 | new[] { "-n", "--name" }, 41 | "The Razor component's (.razor) type name. The default value is '__Main'") 42 | { 43 | IsRequired = false 44 | }; 45 | 46 | return new BlazorKernelOptionsDirective(this) 47 | { 48 | nameOption, 49 | //fromFileOption, 50 | }; 51 | } 52 | } 53 | 54 | private class BlazorKernelOptionsDirective : ChooseKernelDirective 55 | { 56 | private readonly BlazorKernel _kernel; 57 | 58 | public BlazorKernelOptionsDirective(BlazorKernel kernel, string description = "Compile and render Razor components (.razor).") : base(kernel, description) 59 | { 60 | _kernel = kernel; 61 | } 62 | 63 | protected override async Task Handle(KernelInvocationContext kernelInvocationContext, InvocationContext commandLineInvocationContext) 64 | { 65 | var options = BlazorDirectiveOptions.Create(commandLineInvocationContext.ParseResult); 66 | 67 | //if (options.FromFile is { } fromFile) 68 | //{ 69 | // var value = await File.ReadAllTextAsync(fromFile.FullName); 70 | // await _kernel.StoreValueAsync(value, options, kernelInvocationContext); 71 | //} 72 | 73 | if (options.Name is { }) 74 | { 75 | _kernel.ComponentName = options.Name; 76 | } 77 | 78 | await base.Handle(kernelInvocationContext, commandLineInvocationContext); 79 | } 80 | } 81 | 82 | private class BlazorDirectiveOptions 83 | { 84 | private static readonly ModelBinder ModelBinder = new(); 85 | 86 | public static BlazorDirectiveOptions Create(ParseResult parseResult) 87 | { 88 | var invocationContext = new InvocationContext(parseResult); 89 | var bindingContext = invocationContext.BindingContext; 90 | 91 | return ModelBinder.CreateInstance(bindingContext) as BlazorDirectiveOptions; 92 | } 93 | 94 | public string Name { get; set; } 95 | 96 | //public FileInfo FromFile { get; set; } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorInteractive.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Library 4 | net8.0 5 | enable 6 | true 7 | true 8 | Renders Blazor markup in dotnet-interactive notebooks. 9 | 1.2.0 10 | $(NoWarn);NU5100,NU5104,IDE0003 11 | true 12 | true 13 | false 14 | Pier-Luc Bonneville 15 | MIT 16 | https://github.com/plbonneville/BlazorInteractive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Encodings.Web; 2 | 3 | using Bunit; 4 | using Microsoft.AspNetCore.Components; 5 | using Microsoft.AspNetCore.Html; 6 | using Microsoft.CodeAnalysis.CSharp; 7 | using Microsoft.DotNet.Interactive; 8 | using Microsoft.DotNet.Interactive.Commands; 9 | using Microsoft.DotNet.Interactive.CSharp; 10 | using Microsoft.DotNet.Interactive.Formatting; 11 | 12 | namespace BlazorInteractive; 13 | 14 | internal static class BlazorExtensions 15 | { 16 | /// 17 | /// Registers the type as a formatter. 18 | /// 19 | /// A . 20 | /// A reference to this instance after the operation has completed. 21 | public static T UseBlazor(this T kernel) where T : Kernel 22 | { 23 | Formatter.Register((markdown, writer) => 24 | { 25 | // Formatter.Register() doesn't support async formatters yet. 26 | // Prevent SynchronizationContext-induced deadlocks given the following sync-over-async code. 27 | ExecutionContext.SuppressFlow(); 28 | 29 | try 30 | { 31 | Task.Run(async () => 32 | { 33 | var (assemblyBytes, code) = await GenerateAssemblyAndCodeFile(markdown); 34 | 35 | var html = GenerateHtml(assemblyBytes, markdown.ComponentName); 36 | html.WriteTo(writer, HtmlEncoder.Default); 37 | 38 | AddComponentTypeToInteractiveWorkspace(kernel, code); 39 | }) 40 | .Wait(); 41 | } 42 | finally 43 | { 44 | ExecutionContext.RestoreFlow(); 45 | } 46 | }, HtmlFormatter.MimeType); 47 | 48 | return kernel; 49 | } 50 | 51 | private static async Task<(byte[] assemblyBytes, string code)> GenerateAssemblyAndCodeFile(BlazorMarkdown markdown) 52 | { 53 | var blazorCompilationService = new BlazorCompilationService(); 54 | await blazorCompilationService.InitializeAsync(); 55 | 56 | var blazorAssemblyCompiler = new BlazorAssemblyCompiler(markdown.ToString(), markdown.ComponentName) 57 | { 58 | CompilationService = blazorCompilationService 59 | }; 60 | 61 | var (compileToAssemblyResult, code) = await blazorAssemblyCompiler.CompileAsync(); 62 | 63 | return (compileToAssemblyResult.AssemblyBytes, code); 64 | } 65 | 66 | private static void AddComponentTypeToInteractiveWorkspace(Kernel kernel, string code) 67 | { 68 | var tree = CSharpSyntaxTree.ParseText(code); 69 | var root = tree.GetCompilationUnitRoot(); 70 | var codeWithoutNamespace = root.RemoveNamespace(); 71 | 72 | var csharpKernel = kernel.FindKernelByName("csharp") as CSharpKernel; 73 | 74 | csharpKernel.DeferCommand(new SubmitCode(codeWithoutNamespace)); 75 | } 76 | 77 | private static IHtmlContent GenerateHtml(byte[] assemblyBytes, string componentName) 78 | { 79 | var assembly = AppDomain.CurrentDomain.Load(assemblyBytes); 80 | 81 | var type = assembly.GetType($"BlazorRepl.UserComponents.{componentName}"); 82 | 83 | using var ctx = new TestContext(); 84 | var method = typeof(TestContext).GetMethods().First(x => x.Name == nameof(TestContext.RenderComponent)); 85 | var generic = method.MakeGenericMethod(type); 86 | var results = generic.Invoke(ctx, new object[] { Array.Empty() }); 87 | var cut = results as IRenderedComponent; 88 | 89 | var markup = cut.Markup; 90 | 91 | var id = "blazorExtension" + Guid.NewGuid().ToString("N"); 92 | 93 | var html = $""" 94 |
{markup}
95 | """.ToHtmlContent(); 96 | 97 | return html; 98 | } 99 | } -------------------------------------------------------------------------------- /blazor-interactive.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.6.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{613EF67B-FFB0-4C36-A182-3DB50FAD7E31}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorInteractive", "src\BlazorInteractive\BlazorInteractive.csproj", "{A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{199EB841-975A-4AD2-888F-0069FAE90207}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorInteractive.Tests", "tests\BlazorInteractive.Tests\BlazorInteractive.Tests.csproj", "{01CD8BE9-5946-4ECF-AB12-C13733BD198D}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A3E6E66-BA9C-457E-BC2A-214D64685226}" 15 | ProjectSection(SolutionItems) = preProject 16 | .gitignore = .gitignore 17 | README.md = README.md 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|x64 = Release|x64 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|x64.ActiveCfg = Debug|Any CPU 33 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|x64.Build.0 = Debug|Any CPU 34 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Debug|x86.Build.0 = Debug|Any CPU 36 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|x64.ActiveCfg = Release|Any CPU 39 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|x64.Build.0 = Release|Any CPU 40 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|x86.ActiveCfg = Release|Any CPU 41 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7}.Release|x86.Build.0 = Release|Any CPU 42 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|x64.Build.0 = Debug|Any CPU 46 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Debug|x86.Build.0 = Debug|Any CPU 48 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|x64.ActiveCfg = Release|Any CPU 51 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|x64.Build.0 = Release|Any CPU 52 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|x86.ActiveCfg = Release|Any CPU 53 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D}.Release|x86.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {A69401AE-183E-41B5-BDEF-BE8AEA8D19A7} = {613EF67B-FFB0-4C36-A182-3DB50FAD7E31} 60 | {01CD8BE9-5946-4ECF-AB12-C13733BD198D} = {199EB841-975A-4AD2-888F-0069FAE90207} 61 | EndGlobalSection 62 | GlobalSection(ExtensibilityGlobals) = postSolution 63 | SolutionGuid = {52169C88-A241-48ED-93DF-ED15925961A3} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /tests/integration-test.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "dotnet_interactive": { 8 | "language": "pwsh" 9 | } 10 | }, 11 | "outputs": [ 12 | { 13 | "name": "stdout", 14 | "output_type": "stream", 15 | "text": [ 16 | "MSBuild version 17.8.3+195e7f5a3 for .NET\n", 17 | " Determining projects to restore...\n", 18 | " Restored /workspaces/BlazorInteractive/src/BlazorInteractive/BlazorInteractive.csproj (in 540 ms).\n", 19 | " BlazorInteractive -> /workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release/net8.0/BlazorInteractive.dll\n", 20 | " The package BlazorInteractive.2023.12.13.15524 is missing a readme. Go to https://aka.ms/nuget/authoring-best-practices/readme to learn why package readmes are important.\n", 21 | " Successfully created package '/workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release/BlazorInteractive.2023.12.13.15524.nupkg'.\n", 22 | "\n", 23 | " Directory: /workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release\n", 24 | "\n", 25 | "\u001b[32;1mUnixMode \u001b[0m\u001b[32;1m User\u001b[0m\u001b[32;1m Group \u001b[0m\u001b[32;1m LastWriteTime\u001b[0m\u001b[32;1m Size\u001b[0m\u001b[32;1m Name\u001b[0m\n", 26 | "\u001b[32;1m-------- \u001b[0m \u001b[32;1m ----\u001b[0m \u001b[32;1m----- \u001b[0m \u001b[32;1m -------------\u001b[0m \u001b[32;1m ----\u001b[0m \u001b[32;1m----\u001b[0m\n", 27 | "-rw-r--r-- vscode vscode 12/13/2023 01:55 53883 \u001b[31;1mBlazorInteractive.2023.12.13.15524.n\u001b[0m\n", 28 | " \u001b[31;1mupkg\u001b[0m\n", 29 | "\n" 30 | ] 31 | } 32 | ], 33 | "source": [ 34 | "# 0. Clean-up the bin and obj folders\n", 35 | "Get-ChildItem .. -Include bin,obj -Recurse | Remove-Item -Force -Recurse\n", 36 | "\n", 37 | "# 2. Pack up the NuGet package. Note, you should increment the version because the previous one, once installed, will be in your NuGet cache\n", 38 | "$version = [System.DateTime]::Now.ToString(\"yyyy.MM.dd.HHmmss\")\n", 39 | "dotnet pack /p:PackageVersion=$version ../src/BlazorInteractive/BlazorInteractive.csproj\n", 40 | "\n", 41 | "# 3. Check that the package is there\n", 42 | "Get-ChildItem .. -Recurse *.nupkg" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 2, 48 | "metadata": { 49 | "dotnet_interactive": { 50 | "language": "csharp" 51 | } 52 | }, 53 | "outputs": [ 54 | { 55 | "data": { 56 | "text/html": [ 57 | "
Restore sources
  • /workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release
Installed Packages
  • BlazorInteractive, 2023.12.13.15524
" 58 | ] 59 | }, 60 | "metadata": {}, 61 | "output_type": "display_data" 62 | }, 63 | { 64 | "data": { 65 | "text/plain": [ 66 | "Loading extensions from `/home/vscode/.nuget/packages/blazorinteractive/2023.12.13.15524/lib/net8.0/BlazorInteractive.dll`" 67 | ] 68 | }, 69 | "metadata": {}, 70 | "output_type": "display_data" 71 | }, 72 | { 73 | "data": { 74 | "text/html": [ 75 | "
\r\n", 76 | " Compile and render Razor components (.razor) in .NET Interactive Notebooks.\r\n", 77 | "

This extension adds a new kernel that can render Blazor markdown.

\r\n", 78 | "\r\n", 79 | "
\r\n",
 80 |        "        \r\n",
 81 |        "#!blazor\r\n",
 82 |        "

Counter

\r\n", 83 | "\r\n", 84 | "

\r\n", 85 | " Current count: @currentCount\r\n", 86 | "

\r\n", 87 | "\r\n", 88 | "@code {\r\n", 89 | " int currentCount = 0;\r\n", 90 | "}
\r\n", 91 | "
\r\n", 92 | "\r\n", 93 | "

This extension also adds the compiled component as a type to the interactive workspace.

\r\n", 94 | "\r\n", 95 | "

Options:

\r\n", 96 | "
    \r\n", 97 | "
  • -n, --name     The Razor component's (.razor) type name. The default value is __Main
  • \r\n", 98 | "
\r\n", 99 | "
" 100 | ] 101 | }, 102 | "metadata": {}, 103 | "output_type": "display_data" 104 | } 105 | ], 106 | "source": [ 107 | "#i nuget:/workspaces/BlazorInteractive/src/BlazorInteractive/bin/Release\n", 108 | "#r \"nuget:BlazorInteractive\"" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 3, 114 | "metadata": { 115 | "dotnet_interactive": { 116 | "language": "csharp" 117 | } 118 | }, 119 | "outputs": [ 120 | { 121 | "data": { 122 | "text/html": [ 123 | "

Hello Alice

" 124 | ] 125 | }, 126 | "metadata": {}, 127 | "output_type": "display_data" 128 | } 129 | ], 130 | "source": [ 131 | "#!blazor\n", 132 | "

Hello @name

\n", 133 | "\n", 134 | "@code {\n", 135 | " string name = \"Alice\";\n", 136 | "}" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": 4, 142 | "metadata": { 143 | "dotnet_interactive": { 144 | "language": "csharp" 145 | } 146 | }, 147 | "outputs": [ 148 | { 149 | "data": { 150 | "text/html": [ 151 | "

Counter

\n", 152 | "\n", 153 | "

\n", 154 | " Current count: 0

\n", 155 | "\n", 156 | "
" 157 | ] 158 | }, 159 | "metadata": {}, 160 | "output_type": "display_data" 161 | } 162 | ], 163 | "source": [ 164 | "#!blazor --name Counter\n", 165 | "

Counter

\n", 166 | "\n", 167 | "

\n", 168 | " Current count: @currentCount\n", 169 | "

\n", 170 | "\n", 171 | "\n", 172 | "\n", 173 | "@code {\n", 174 | " public int currentCount = 0;\n", 175 | "\n", 176 | " void IncrementCount()\n", 177 | " {\n", 178 | " currentCount++;\n", 179 | " }\n", 180 | "}" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 5, 186 | "metadata": { 187 | "dotnet_interactive": { 188 | "language": "csharp" 189 | } 190 | }, 191 | "outputs": [ 192 | { 193 | "data": { 194 | "text/plain": [ 195 | "Counter" 196 | ] 197 | }, 198 | "metadata": {}, 199 | "output_type": "display_data" 200 | } 201 | ], 202 | "source": [ 203 | "var componentName = typeof(Counter).Name;\n", 204 | "componentName" 205 | ] 206 | } 207 | ], 208 | "metadata": { 209 | "kernelspec": { 210 | "display_name": ".NET (C#)", 211 | "language": "C#", 212 | "name": ".net-csharp" 213 | }, 214 | "language_info": { 215 | "name": "polyglot-notebook" 216 | }, 217 | "polyglot_notebook": { 218 | "kernelInfo": { 219 | "defaultKernelName": "csharp", 220 | "items": [ 221 | { 222 | "aliases": [], 223 | "name": ".NET" 224 | }, 225 | { 226 | "aliases": [], 227 | "name": "blazor" 228 | }, 229 | { 230 | "aliases": [ 231 | "C#", 232 | "c#" 233 | ], 234 | "languageName": "C#", 235 | "name": "csharp" 236 | }, 237 | { 238 | "aliases": [ 239 | "F#", 240 | "f#" 241 | ], 242 | "languageName": "F#", 243 | "name": "fsharp" 244 | }, 245 | { 246 | "aliases": [], 247 | "languageName": "HTML", 248 | "name": "html" 249 | }, 250 | { 251 | "aliases": [], 252 | "languageName": "KQL", 253 | "name": "kql" 254 | }, 255 | { 256 | "aliases": [], 257 | "languageName": "Mermaid", 258 | "name": "mermaid" 259 | }, 260 | { 261 | "aliases": [ 262 | "powershell" 263 | ], 264 | "languageName": "PowerShell", 265 | "name": "pwsh" 266 | }, 267 | { 268 | "aliases": [], 269 | "languageName": "SQL", 270 | "name": "sql" 271 | }, 272 | { 273 | "aliases": [], 274 | "name": "value" 275 | }, 276 | { 277 | "aliases": [ 278 | "frontend" 279 | ], 280 | "name": "vscode" 281 | } 282 | ] 283 | } 284 | } 285 | }, 286 | "nbformat": 4, 287 | "nbformat_minor": 2 288 | } 289 | -------------------------------------------------------------------------------- /tests/BlazorInteractive.Tests/BlazorInteractiveTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.DotNet.Interactive; 3 | using Microsoft.DotNet.Interactive.Commands; 4 | using Microsoft.DotNet.Interactive.CSharp; 5 | using Microsoft.DotNet.Interactive.Events; 6 | using Microsoft.DotNet.Interactive.FSharp; 7 | using Microsoft.DotNet.Interactive.Tests.Utility; 8 | using Xunit; 9 | 10 | namespace BlazorInteractive.Tests; 11 | 12 | public sealed class BlazorInteractiveTests : IDisposable 13 | { 14 | private readonly Kernel _kernel; 15 | 16 | public BlazorInteractiveTests() 17 | { 18 | // Prevent SynchronizationContext-induced deadlocks given the following sync-over-async code. 19 | ExecutionContext.SuppressFlow(); 20 | 21 | try 22 | { 23 | _kernel = new CompositeKernel 24 | { 25 | new CSharpKernel().UseNugetDirective(), 26 | new FSharpKernel().UseNugetDirective() 27 | }; 28 | 29 | Task.Run(async () => 30 | { 31 | var extension = new BlazorKernelExtension(); 32 | await extension.OnLoadAsync(_kernel); 33 | }) 34 | .Wait(); 35 | 36 | KernelEvents = _kernel.KernelEvents.ToSubscribedList(); 37 | } 38 | finally 39 | { 40 | ExecutionContext.RestoreFlow(); 41 | } 42 | } 43 | 44 | private SubscribedList KernelEvents { get; } 45 | 46 | public void Dispose() 47 | { 48 | _kernel?.Dispose(); 49 | KernelEvents?.Dispose(); 50 | GC.SuppressFinalize(this); 51 | } 52 | 53 | [Fact] 54 | public async Task It_is_registered_as_a_directive() 55 | { 56 | // Arrange 57 | using var events = _kernel.KernelEvents.ToSubscribedList(); 58 | 59 | // Act 60 | await _kernel.SubmitCodeAsync("#!blazor"); 61 | 62 | // Assert 63 | KernelEvents 64 | .Should() 65 | .ContainSingle() 66 | .Which.Command.Should().BeOfType() 67 | .Which.Code.Should().Be("#!blazor"); 68 | } 69 | 70 | [Fact] 71 | public async Task It_formats_BlazorCode_as_html() 72 | { 73 | // Arrange 74 | using var events = _kernel.KernelEvents.ToSubscribedList(); 75 | 76 | // Act 77 | await _kernel.SubmitCodeAsync( 78 | """ 79 | #!blazor 80 |

hello world

81 | """); 82 | 83 | // Assert 84 | events 85 | .Should() 86 | .ContainSingle() 87 | .Which 88 | .FormattedValues 89 | .Should() 90 | .ContainSingle(v => v.MimeType == "text/html"); 91 | } 92 | 93 | [Fact] 94 | public async Task It_interprets_BlazorCode() 95 | { 96 | // Arrange 97 | const string code = 98 | """ 99 | #!blazor 100 |

Hello world @name!

101 | 102 | @code { 103 | string name = "Alice"; 104 | } 105 | """; 106 | 107 | using var events = _kernel.KernelEvents.ToSubscribedList(); 108 | 109 | // Act 110 | await _kernel.SubmitCodeAsync(code); 111 | 112 | // Assert 113 | events 114 | .Should() 115 | .ContainSingle() 116 | .Which 117 | .FormattedValues 118 | .Should() 119 | .ContainSingle(v => v.MimeType == "text/html") 120 | .Which 121 | .Value 122 | .Should() 123 | .Contain("Hello world Alice!"); 124 | } 125 | 126 | [Fact] 127 | public async Task It_interpret_BlazorCode_with_a_method() 128 | { 129 | // Arrange 130 | const string code = 131 | """ 132 | #!blazor 133 |

Counter

134 | 135 |

136 | Current count: @currentCount 137 |

138 | 139 | 140 | 141 | @code { 142 | int currentCount = 0; 143 | 144 | void IncrementCount() 145 | { 146 | currentCount++; 147 | } 148 | } 149 | """; 150 | 151 | using var events = _kernel.KernelEvents.ToSubscribedList(); 152 | 153 | // Act 154 | await _kernel.SubmitCodeAsync(code); 155 | 156 | // Assert 157 | KernelEvents 158 | .Should() 159 | .ContainSingle() 160 | .Which 161 | .FormattedValues 162 | .Should() 163 | .ContainSingle(v => v.MimeType == "text/html") 164 | .Which 165 | .Value 166 | .Should() 167 | .Contain("Current count: 0") 168 | .And 169 | .Contain("Click me"); 170 | } 171 | 172 | [Fact] 173 | public async Task It_renders_html_and_not_html_encoded_html() 174 | { 175 | // Arrange 176 | using var events = _kernel.KernelEvents.ToSubscribedList(); 177 | 178 | // Act 179 | await _kernel.SubmitCodeAsync( 180 | """ 181 | #!blazor 182 |

hello world

183 | """); 184 | 185 | await Task.Delay(1000); 186 | 187 | // Assert 188 | KernelEvents 189 | .Should() 190 | .ContainSingle() 191 | .Which 192 | .FormattedValues 193 | .Should() 194 | .ContainSingle(v => v.MimeType == "text/html") 195 | .Which 196 | .Value 197 | .Should() 198 | .Contain("

hello world

"); 199 | } 200 | 201 | [Fact] 202 | public async Task It_can_reference_a_component_defined_in_previous_compilation() 203 | { 204 | // Arrange 205 | await _kernel.SubmitCodeAsync( 206 | """ 207 | #!blazor 208 |

hello world

209 | """); 210 | 211 | using var events = _kernel.KernelEvents.ToSubscribedList(); 212 | 213 | // Act 214 | await _kernel.SubmitCodeAsync( 215 | """ 216 | #!csharp 217 | typeof(__Main).Name 218 | """); 219 | 220 | // Assert 221 | events 222 | .Should() 223 | .ContainSingle() 224 | .Which 225 | .FormattedValues 226 | .Should() 227 | .ContainSingle(v => v.MimeType == "text/plain") 228 | .Which 229 | .Value 230 | .Should() 231 | .Be("__Main"); 232 | } 233 | 234 | [Fact] 235 | public async Task It_can_instantiate_a_component_defined_in_previous_compilation() 236 | { 237 | // Arrange 238 | await _kernel.SubmitCodeAsync( 239 | """ 240 | #!blazor 241 |

hello world

242 | """); 243 | 244 | using var events = _kernel.KernelEvents.ToSubscribedList(); 245 | 246 | // Act 247 | await _kernel.SubmitCodeAsync( 248 | """ 249 | #!csharp 250 | var component = new __Main(); 251 | """); 252 | 253 | // Assert 254 | events 255 | .Should() 256 | .ContainSingle() 257 | .Which 258 | .Command.As() 259 | .Code 260 | .Should() 261 | .Be( 262 | """ 263 | #!csharp 264 | var component = new __Main(); 265 | """); 266 | } 267 | 268 | [Fact] 269 | public async Task It_can_reference_a_named_component_defined_in_previous_compilation() 270 | { 271 | // Arrange 272 | await _kernel.SubmitCodeAsync( 273 | """ 274 | #!blazor --name HelloWorld 275 |

hello world

276 | """); 277 | 278 | using var events = _kernel.KernelEvents.ToSubscribedList(); 279 | 280 | // Act 281 | await _kernel.SubmitCodeAsync( 282 | """ 283 | #!csharp 284 | typeof(HelloWorld).Name 285 | """); 286 | 287 | // Assert 288 | events 289 | .Should() 290 | .ContainSingle() 291 | .Which 292 | .FormattedValues 293 | .Should() 294 | .ContainSingle(v => v.MimeType == "text/plain") 295 | .Which 296 | .Value 297 | .Should() 298 | .Be("HelloWorld"); 299 | } 300 | 301 | [Fact] 302 | public async Task It_can_instantiate_a_named_component_defined_in_previous_compilation() 303 | { 304 | // Arrange 305 | await _kernel.SubmitCodeAsync( 306 | """ 307 | #!blazor -n HelloWorld 308 |

hello world

309 | """); 310 | 311 | using var events = _kernel.KernelEvents.ToSubscribedList(); 312 | 313 | // Act 314 | await _kernel.SubmitCodeAsync( 315 | """ 316 | #!csharp 317 | var component = new HelloWorld(); 318 | """); 319 | 320 | // Assert 321 | events 322 | .Should() 323 | .ContainSingle() 324 | .Which 325 | .Command.As() 326 | .Code 327 | .Should() 328 | .Be( 329 | """ 330 | #!csharp 331 | var component = new HelloWorld(); 332 | """); 333 | } 334 | } -------------------------------------------------------------------------------- /tests/BlazorInteractive.Tests/TestUtility.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | using System; 5 | using System.Collections; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using System.Diagnostics; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | 12 | using FluentAssertions; 13 | using FluentAssertions.Collections; 14 | using FluentAssertions.Equivalency; 15 | using FluentAssertions.Execution; 16 | using FluentAssertions.Primitives; 17 | 18 | using Microsoft.DotNet.Interactive.Commands; 19 | using Microsoft.DotNet.Interactive.Events; 20 | using Microsoft.DotNet.Interactive.Formatting.TabularData; 21 | using Microsoft.DotNet.Interactive.Parsing; 22 | using Microsoft.DotNet.Interactive.Connection; 23 | 24 | namespace Microsoft.DotNet.Interactive.Tests.Utility; 25 | 26 | [DebuggerStepThrough] 27 | public static class AssertionExtensions 28 | { 29 | public static GenericCollectionAssertions AllSatisfy( 30 | this GenericCollectionAssertions assertions, 31 | Action assert) 32 | { 33 | using var _ = new AssertionScope(); 34 | 35 | foreach (var item in assertions.Subject) 36 | { 37 | assert(item); 38 | } 39 | 40 | return assertions; 41 | } 42 | 43 | public static void BeEquivalentToRespectingRuntimeTypes( 44 | this GenericCollectionAssertions assertions, 45 | params object[] expectations) 46 | { 47 | assertions.BeEquivalentTo(expectations, o => o.RespectingRuntimeTypes()); 48 | } 49 | 50 | public static void BeEquivalentToRespectingRuntimeTypes( 51 | this ObjectAssertions assertions, 52 | TExpectation expectation, 53 | Func, EquivalencyAssertionOptions> config = null) 54 | { 55 | assertions.BeEquivalentTo(expectation, o => 56 | { 57 | if (config is { }) 58 | { 59 | return config.Invoke(o).RespectingRuntimeTypes(); 60 | } 61 | else 62 | { 63 | return o.RespectingRuntimeTypes(); 64 | } 65 | }); 66 | } 67 | 68 | public static void BeJsonEquivalentTo(this StringAssertions assertion, T expected) 69 | { 70 | var obj = System.Text.Json.JsonSerializer.Deserialize(assertion.Subject, expected.GetType()); 71 | obj.Should().BeEquivalentToRespectingRuntimeTypes(expected); 72 | } 73 | 74 | public static AndConstraint> BeEquivalentSequenceTo( 75 | this GenericCollectionAssertions assertions, 76 | params object[] expectedValues) 77 | { 78 | var actualValues = assertions.Subject.ToArray(); 79 | 80 | actualValues 81 | .Select(a => a?.GetType()) 82 | .Should() 83 | .BeEquivalentTo(expectedValues.Select(e => e?.GetType())); 84 | 85 | using (new AssertionScope()) 86 | { 87 | foreach (var tuple in actualValues 88 | .Zip(expectedValues, (actual, expected) => (actual, expected)) 89 | .Where(t => t.expected is null || t.expected.GetType().GetProperties().Any())) 90 | { 91 | tuple.actual 92 | .Should() 93 | .BeEquivalentToRespectingRuntimeTypes(tuple.expected); 94 | } 95 | } 96 | 97 | return new AndConstraint>(assertions); 98 | } 99 | 100 | public static AndConstraint>> BeEquivalentSequenceTo( 101 | this StringCollectionAssertions assertions, 102 | params string[] expectedValues) 103 | { 104 | return assertions.ContainInOrder(expectedValues).And.BeEquivalentTo(expectedValues); 105 | } 106 | 107 | public static AndWhichConstraint ContainSingle( 108 | this GenericCollectionAssertions should, 109 | Func where = null) 110 | where T : KernelCommand 111 | { 112 | T subject; 113 | 114 | if (where is null) 115 | { 116 | should.ContainSingle(e => e is T); 117 | 118 | subject = should.Subject 119 | .OfType() 120 | .Single(); 121 | } 122 | else 123 | { 124 | should.ContainSingle(e => e is T && where((T)e)); 125 | 126 | subject = should.Subject 127 | .OfType() 128 | .Where(where) 129 | .Single(); 130 | } 131 | 132 | return new AndWhichConstraint(subject.Should(), subject); 133 | } 134 | 135 | public static AndWhichConstraint ContainSingle( 136 | this GenericCollectionAssertions should, 137 | Func where = null) 138 | where T : SyntaxNodeOrToken 139 | { 140 | T subject; 141 | 142 | if (where is null) 143 | { 144 | should.ContainSingle(e => e is T); 145 | 146 | subject = should.Subject 147 | .OfType() 148 | .Single(); 149 | } 150 | else 151 | { 152 | should.ContainSingle(e => e is T && where((T)e)); 153 | 154 | subject = should.Subject 155 | .OfType() 156 | .Where(where) 157 | .Single(); 158 | } 159 | 160 | return new AndWhichConstraint(subject.Should(), subject); 161 | } 162 | 163 | public static AndWhichConstraint ContainSingle( 164 | this GenericCollectionAssertions should, 165 | Func where = null) 166 | where T : SyntaxNode 167 | { 168 | T subject; 169 | 170 | if (where is null) 171 | { 172 | should.ContainSingle(e => e is T); 173 | 174 | subject = should.Subject 175 | .OfType() 176 | .Single(); 177 | } 178 | else 179 | { 180 | should.ContainSingle(e => e is T && where((T)e)); 181 | 182 | subject = should.Subject 183 | .OfType() 184 | .Where(where) 185 | .Single(); 186 | } 187 | 188 | return new AndWhichConstraint(subject.Should(), subject); 189 | } 190 | 191 | public static AndWhichConstraint ContainSingle( 192 | this GenericCollectionAssertions should, 193 | Func where = null) 194 | where T : KernelEvent 195 | { 196 | T subject; 197 | 198 | if (where is null) 199 | { 200 | should.ContainSingle(e => e is T); 201 | 202 | subject = should.Subject 203 | .OfType() 204 | .Single(); 205 | } 206 | else 207 | { 208 | should.ContainSingle(e => e is T && where((T)e)); 209 | 210 | subject = should.Subject 211 | .OfType() 212 | .Where(where) 213 | .Single(); 214 | } 215 | 216 | return new AndWhichConstraint(subject.Should(), subject); 217 | } 218 | 219 | public static AndWhichConstraint ContainSingle( 220 | this GenericCollectionAssertions should, 221 | Func where = null) 222 | where T : IKernelEventEnvelope 223 | { 224 | T subject; 225 | 226 | if (where is null) 227 | { 228 | should.ContainSingle(e => e is T); 229 | 230 | subject = should.Subject 231 | .OfType() 232 | .Single(); 233 | } 234 | else 235 | { 236 | should.ContainSingle(e => e is T && where((T)e)); 237 | 238 | subject = should.Subject 239 | .OfType() 240 | .Where(where) 241 | .Single(); 242 | } 243 | 244 | return new AndWhichConstraint(subject.Should(), subject); 245 | } 246 | 247 | public static AndConstraint> NotContainErrors( 248 | this GenericCollectionAssertions should) => 249 | should 250 | .NotContain(e => e is ErrorProduced) 251 | .And 252 | .NotContain(e => e is CommandFailed); 253 | 254 | public static AndWhichConstraint EventuallyContainSingle( 255 | this GenericCollectionAssertions should, 256 | Func where = null, 257 | int timeout = 3000) 258 | where T : KernelEvent 259 | { 260 | return Task.Run(async () => 261 | { 262 | if (where is null) 263 | { 264 | where = _ => true; 265 | } 266 | 267 | var startTime = DateTime.UtcNow; 268 | var endTime = startTime + TimeSpan.FromMilliseconds(timeout); 269 | while (DateTime.UtcNow < endTime) 270 | { 271 | if (should.Subject.OfType().Any(where)) 272 | { 273 | break; 274 | } 275 | 276 | await Task.Delay(200); 277 | } 278 | 279 | return should.ContainSingle(where); 280 | }).Result; 281 | } 282 | } 283 | 284 | public static class ObservableExtensions 285 | { 286 | public static SubscribedList ToSubscribedList(this IObservable source) 287 | { 288 | return new SubscribedList(source); 289 | } 290 | } 291 | 292 | public class SubscribedList : IReadOnlyList, IDisposable 293 | { 294 | private ImmutableArray _list = ImmutableArray.Empty; 295 | private readonly IDisposable _subscription; 296 | 297 | public SubscribedList(IObservable source) 298 | { 299 | _subscription = source.Subscribe(x => { _list = _list.Add(x); }); 300 | } 301 | 302 | public IEnumerator GetEnumerator() 303 | { 304 | return ((IEnumerable)_list).GetEnumerator(); 305 | } 306 | 307 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 308 | 309 | public int Count => _list.Length; 310 | 311 | public T this[int index] => _list[index]; 312 | 313 | public void Dispose() => _subscription.Dispose(); 314 | } 315 | 316 | internal static class TestUtility 317 | { 318 | internal static TabularDataResource ShouldDisplayTabularDataResourceWhich( 319 | this SubscribedList events) 320 | { 321 | events.Should().NotContainErrors(); 322 | 323 | return events 324 | .Should() 325 | .ContainSingle(e => e.Value is DataExplorer) 326 | .Which 327 | .Value 328 | .As>() 329 | .Data; 330 | } 331 | } -------------------------------------------------------------------------------- /src/BlazorInteractive/BlazorCompilationService.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Net.Http.Json; 3 | using System.Reflection; 4 | using System.Runtime; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | 8 | using BlazorRepl.Core; 9 | using Microsoft.AspNetCore.Components.Routing; 10 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 11 | using Microsoft.AspNetCore.Razor.Language; 12 | using Microsoft.CodeAnalysis; 13 | using Microsoft.CodeAnalysis.CSharp; 14 | using Microsoft.CodeAnalysis.Razor; 15 | 16 | namespace BlazorInteractive; 17 | 18 | internal class BlazorCompilationService 19 | { 20 | public const string DefaultRootNamespace = "BlazorRepl.UserComponents"; 21 | 22 | private const string WorkingDirectory = "/BlazorRepl/"; 23 | private const string DefaultImports = 24 | """ 25 | @using System.ComponentModel.DataAnnotations 26 | @using System.Linq 27 | @using System.Net.Http 28 | @using System.Net.Http.Json 29 | @using Microsoft.AspNetCore.Components.Forms 30 | @using Microsoft.AspNetCore.Components.Routing 31 | @using Microsoft.AspNetCore.Components.Web 32 | @using Microsoft.JSInterop 33 | """; 34 | 35 | private static readonly CSharpParseOptions CSharpParseOptions = new(LanguageVersion.Preview); 36 | private static readonly RazorProjectFileSystem RazorProjectFileSystem = new VirtualRazorProjectFileSystem(); 37 | 38 | // Creating the initial compilation + reading references is taking a lot of time without caching 39 | // so making sure it doesn't happen for each run. 40 | private CSharpCompilation baseCompilation; 41 | 42 | public BlazorCompilationService() 43 | { 44 | } 45 | 46 | public async Task InitializeAsync() 47 | { 48 | if (this.baseCompilation != null) 49 | { 50 | return; 51 | } 52 | 53 | var referenceAssemblyRoots = new[] 54 | { 55 | typeof(AssemblyTargetedPatchBandAttribute).Assembly, // System.Private.CoreLib 56 | typeof(Uri).Assembly, // System.Private.Uri 57 | typeof(Console).Assembly, // System.Console 58 | typeof(IQueryable).Assembly, // System.Linq.Expressions 59 | typeof(HttpClient).Assembly, // System.Net.Http 60 | typeof(HttpClientJsonExtensions).Assembly, // System.Net.Http.Json 61 | typeof(RequiredAttribute).Assembly, // System.ComponentModel.Annotations 62 | typeof(Regex).Assembly, // System.Text.RegularExpressions 63 | typeof(NavLink).Assembly, // Microsoft.AspNetCore.Components.Web 64 | typeof(WebAssemblyHostBuilder).Assembly, // Microsoft.AspNetCore.Components.WebAssembly 65 | }; 66 | 67 | // Get all required assemblies 68 | var baseAssemblies = CompilationService 69 | .BaseAssemblyPackageVersionMappings 70 | .Select(package => Assembly.Load(package.Key)); 71 | 72 | var referenceAssemblies = AppDomain.CurrentDomain.GetAssemblies() 73 | .Union(baseAssemblies) 74 | .Where(x => !x.IsDynamic) 75 | .Where(x => !string.IsNullOrWhiteSpace(x.Location)) 76 | .Select(assembly => MetadataReference.CreateFromFile(assembly.Location, MetadataReferenceProperties.Assembly)) 77 | .ToList(); 78 | 79 | this.baseCompilation = CSharpCompilation.Create( 80 | "BlazorRepl.UserComponents", 81 | references: referenceAssemblies, 82 | options: new CSharpCompilationOptions( 83 | OutputKind.DynamicallyLinkedLibrary, 84 | optimizationLevel: OptimizationLevel.Release, 85 | concurrentBuild: false, 86 | //// Warnings CS1701 and CS1702 are disabled when compiling in VS too 87 | specificDiagnosticOptions: new[] 88 | { 89 | new KeyValuePair("CS1701", ReportDiagnostic.Suppress), 90 | new KeyValuePair("CS1702", ReportDiagnostic.Suppress), 91 | })); 92 | 93 | await Task.CompletedTask; 94 | } 95 | 96 | public async Task<(CompileToAssemblyResult, string)> CompileToAssemblyAsync( 97 | ICollection codeFiles, 98 | Func updateStatusFunc) // TODO: try convert to event 99 | { 100 | if (codeFiles == null) 101 | { 102 | throw new ArgumentNullException(nameof(codeFiles)); 103 | } 104 | 105 | this.ThrowIfNotInitialized(); 106 | 107 | var cSharpResults = await this.CompileToCSharpAsync(codeFiles, updateStatusFunc); 108 | 109 | var result = this.CompileToAssembly(cSharpResults); 110 | 111 | return (result, cSharpResults[0].Code); 112 | } 113 | 114 | private static RazorProjectItem CreateRazorProjectItem(string fileName, string fileContent) 115 | { 116 | var fullPath = WorkingDirectory + fileName; 117 | 118 | // File paths in Razor are always of the form '/a/b/c.razor' 119 | var filePath = fileName; 120 | if (!filePath.StartsWith('/')) 121 | { 122 | filePath = '/' + filePath; 123 | } 124 | 125 | fileContent = fileContent.Replace("\r", string.Empty); 126 | 127 | return new VirtualProjectItem( 128 | WorkingDirectory, 129 | filePath, 130 | fullPath, 131 | fileName, 132 | FileKinds.Component, 133 | Encoding.UTF8.GetBytes(fileContent.TrimStart())); 134 | } 135 | 136 | private static RazorProjectEngine CreateRazorProjectEngine(IReadOnlyList references) => 137 | RazorProjectEngine.Create(RazorConfiguration.Default, RazorProjectFileSystem, b => 138 | { 139 | b.SetRootNamespace(DefaultRootNamespace); 140 | b.AddDefaultImports(DefaultImports); 141 | 142 | // Features that use Roslyn are mandatory for components 143 | CompilerFeatures.Register(b); 144 | 145 | b.Features.Add(new CompilationTagHelperFeature()); 146 | b.Features.Add(new DefaultMetadataReferenceFeature { References = references }); 147 | }); 148 | 149 | private CompileToAssemblyResult CompileToAssembly(IReadOnlyList cSharpResults) 150 | { 151 | if (cSharpResults.Any(r => r.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error))) 152 | { 153 | return new CompileToAssemblyResult { Diagnostics = cSharpResults.SelectMany(r => r.Diagnostics).ToList() }; 154 | } 155 | 156 | var syntaxTrees = new SyntaxTree[cSharpResults.Count]; 157 | for (var i = 0; i < cSharpResults.Count; i++) 158 | { 159 | var cSharpResult = cSharpResults[i]; 160 | syntaxTrees[i] = CSharpSyntaxTree.ParseText(cSharpResult.Code, CSharpParseOptions, cSharpResult.FilePath); 161 | } 162 | 163 | var finalCompilation = this.baseCompilation.AddSyntaxTrees(syntaxTrees); 164 | 165 | var compilationDiagnostics = finalCompilation.GetDiagnostics().Where(d => d.Severity > DiagnosticSeverity.Info); 166 | 167 | var result = new CompileToAssemblyResult 168 | { 169 | Compilation = finalCompilation, 170 | Diagnostics = compilationDiagnostics 171 | .Select(CompilationDiagnostic.FromCSharpDiagnostic) 172 | .Concat(cSharpResults.SelectMany(r => r.Diagnostics)) 173 | .ToList(), 174 | }; 175 | 176 | if (result.Diagnostics.All(x => x.Severity != DiagnosticSeverity.Error)) 177 | { 178 | using var peStream = new MemoryStream(); 179 | finalCompilation.Emit(peStream); 180 | 181 | result.AssemblyBytes = peStream.ToArray(); 182 | } 183 | 184 | return result; 185 | } 186 | 187 | private async Task> CompileToCSharpAsync( 188 | ICollection codeFiles, 189 | Func updateStatusFunc) 190 | { 191 | await (updateStatusFunc?.Invoke("Preparing Project") ?? Task.CompletedTask); 192 | 193 | // The first phase won't include any metadata references for component discovery. This mirrors what the build does. 194 | var projectEngine = CreateRazorProjectEngine(Array.Empty()); 195 | 196 | // Result of generating declarations 197 | var declarations = new CompileToCSharpResult[codeFiles.Count]; 198 | var index = 0; 199 | foreach (var codeFile in codeFiles) 200 | { 201 | if (codeFile.Type == CodeFileType.Razor) 202 | { 203 | var projectItem = CreateRazorProjectItem(codeFile.Path, codeFile.Content); 204 | 205 | var codeDocument = projectEngine.ProcessDeclarationOnly(projectItem); 206 | var cSharpDocument = codeDocument.GetCSharpDocument(); 207 | 208 | declarations[index] = new CompileToCSharpResult 209 | { 210 | FilePath = codeFile.Path, 211 | ProjectItem = projectItem, 212 | Code = cSharpDocument.GeneratedCode, 213 | Diagnostics = cSharpDocument.Diagnostics.Select(CompilationDiagnostic.FromRazorDiagnostic).ToList(), 214 | }; 215 | } 216 | else 217 | { 218 | declarations[index] = new CompileToCSharpResult 219 | { 220 | FilePath = codeFile.Path, 221 | Code = codeFile.Content, 222 | Diagnostics = Enumerable.Empty(), // Will actually be evaluated later 223 | }; 224 | } 225 | 226 | index++; 227 | } 228 | 229 | // Result of doing 'temp' compilation 230 | var tempAssembly = this.CompileToAssembly(declarations); 231 | if (tempAssembly.Diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error)) 232 | { 233 | return new[] { new CompileToCSharpResult { Diagnostics = tempAssembly.Diagnostics } }; 234 | } 235 | 236 | await (updateStatusFunc?.Invoke("Compiling Assembly") ?? Task.CompletedTask); 237 | 238 | // Add the 'temp' compilation as a metadata reference 239 | var references = new List(this.baseCompilation.References) { tempAssembly.Compilation.ToMetadataReference() }; 240 | projectEngine = CreateRazorProjectEngine(references); 241 | 242 | var results = new CompileToCSharpResult[declarations.Length]; 243 | for (index = 0; index < declarations.Length; index++) 244 | { 245 | var declaration = declarations[index]; 246 | var isRazorDeclaration = declaration.ProjectItem != null; 247 | 248 | if (isRazorDeclaration) 249 | { 250 | var codeDocument = projectEngine.Process(declaration.ProjectItem); 251 | var cSharpDocument = codeDocument.GetCSharpDocument(); 252 | 253 | results[index] = new CompileToCSharpResult 254 | { 255 | FilePath = declaration.FilePath, 256 | ProjectItem = declaration.ProjectItem, 257 | Code = cSharpDocument.GeneratedCode, 258 | Diagnostics = cSharpDocument.Diagnostics.Select(CompilationDiagnostic.FromRazorDiagnostic).ToList(), 259 | }; 260 | } 261 | else 262 | { 263 | results[index] = declaration; 264 | } 265 | } 266 | 267 | return results; 268 | } 269 | 270 | private void ThrowIfNotInitialized() 271 | { 272 | if (this.baseCompilation == null) 273 | { 274 | throw new InvalidOperationException( 275 | $"{nameof(BlazorCompilationService)} is not initialized. Please call {nameof(this.InitializeAsync)} to initialize it."); 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/csharp,dotnetcore,visualstudio,visualstudiocode,rider 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp,dotnetcore,visualstudio,visualstudiocode,rider 4 | 5 | ### Csharp ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Ww][Ii][Nn]32/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # ASP.NET Scaffolding 71 | ScaffoldingReadMe.txt 72 | 73 | # StyleCop 74 | StyleCopReport.xml 75 | 76 | # Files built by Visual Studio 77 | *_i.c 78 | *_p.c 79 | *_h.h 80 | *.ilk 81 | *.meta 82 | *.obj 83 | *.iobj 84 | *.pch 85 | *.pdb 86 | *.ipdb 87 | *.pgc 88 | *.pgd 89 | *.rsp 90 | *.sbr 91 | *.tlb 92 | *.tli 93 | *.tlh 94 | *.tmp 95 | *.tmp_proj 96 | *_wpftmp.csproj 97 | *.log 98 | *.vspscc 99 | *.vssscc 100 | .builds 101 | *.pidb 102 | *.svclog 103 | *.scc 104 | 105 | # Chutzpah Test files 106 | _Chutzpah* 107 | 108 | # Visual C++ cache files 109 | ipch/ 110 | *.aps 111 | *.ncb 112 | *.opendb 113 | *.opensdf 114 | *.sdf 115 | *.cachefile 116 | *.VC.db 117 | *.VC.VC.opendb 118 | 119 | # Visual Studio profiler 120 | *.psess 121 | *.vsp 122 | *.vspx 123 | *.sap 124 | 125 | # Visual Studio Trace Files 126 | *.e2e 127 | 128 | # TFS 2012 Local Workspace 129 | $tf/ 130 | 131 | # Guidance Automation Toolkit 132 | *.gpState 133 | 134 | # ReSharper is a .NET coding add-in 135 | _ReSharper*/ 136 | *.[Rr]e[Ss]harper 137 | *.DotSettings.user 138 | 139 | # TeamCity is a build add-in 140 | _TeamCity* 141 | 142 | # DotCover is a Code Coverage Tool 143 | *.dotCover 144 | 145 | # AxoCover is a Code Coverage Tool 146 | .axoCover/* 147 | !.axoCover/settings.json 148 | 149 | # Coverlet is a free, cross platform Code Coverage Tool 150 | coverage*.[ji][sn][of][no] 151 | coverage*.xml 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | ### DotnetCore ### 369 | # .NET Core build folders 370 | bin/ 371 | obj/ 372 | 373 | # Common node modules locations 374 | /node_modules 375 | /wwwroot/node_modules 376 | 377 | ### Rider ### 378 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 379 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 380 | 381 | # User-specific stuff 382 | .idea/**/workspace.xml 383 | .idea/**/tasks.xml 384 | .idea/**/usage.statistics.xml 385 | .idea/**/dictionaries 386 | .idea/**/shelf 387 | 388 | # Generated files 389 | .idea/**/contentModel.xml 390 | 391 | # Sensitive or high-churn files 392 | .idea/**/dataSources/ 393 | .idea/**/dataSources.ids 394 | .idea/**/dataSources.local.xml 395 | .idea/**/sqlDataSources.xml 396 | .idea/**/dynamic.xml 397 | .idea/**/uiDesigner.xml 398 | .idea/**/dbnavigator.xml 399 | 400 | # Gradle 401 | .idea/**/gradle.xml 402 | .idea/**/libraries 403 | 404 | # Gradle and Maven with auto-import 405 | # When using Gradle or Maven with auto-import, you should exclude module files, 406 | # since they will be recreated, and may cause churn. Uncomment if using 407 | # auto-import. 408 | # .idea/artifacts 409 | # .idea/compiler.xml 410 | # .idea/jarRepositories.xml 411 | # .idea/modules.xml 412 | # .idea/*.iml 413 | # .idea/modules 414 | # *.iml 415 | # *.ipr 416 | 417 | # CMake 418 | cmake-build-*/ 419 | 420 | # Mongo Explorer plugin 421 | .idea/**/mongoSettings.xml 422 | 423 | # File-based project format 424 | *.iws 425 | 426 | # IntelliJ 427 | out/ 428 | 429 | # mpeltonen/sbt-idea plugin 430 | .idea_modules/ 431 | 432 | # JIRA plugin 433 | atlassian-ide-plugin.xml 434 | 435 | # Cursive Clojure plugin 436 | .idea/replstate.xml 437 | 438 | # Crashlytics plugin (for Android Studio and IntelliJ) 439 | com_crashlytics_export_strings.xml 440 | crashlytics.properties 441 | crashlytics-build.properties 442 | fabric.properties 443 | 444 | # Editor-based Rest Client 445 | .idea/httpRequests 446 | 447 | # Android studio 3.1+ serialized cache file 448 | .idea/caches/build_file_checksums.ser 449 | 450 | ### VisualStudioCode ### 451 | .vscode/* 452 | !.vscode/settings.json 453 | !.vscode/tasks.json 454 | !.vscode/launch.json 455 | !.vscode/extensions.json 456 | *.code-workspace 457 | 458 | ### VisualStudioCode Patch ### 459 | # Ignore all local history of files 460 | .history 461 | .ionide 462 | 463 | ### VisualStudio ### 464 | 465 | # User-specific files 466 | 467 | # User-specific files (MonoDevelop/Xamarin Studio) 468 | 469 | # Mono auto generated files 470 | 471 | # Build results 472 | 473 | # Visual Studio 2015/2017 cache/options directory 474 | # Uncomment if you have tasks that create the project's static files in wwwroot 475 | 476 | # Visual Studio 2017 auto generated files 477 | 478 | # MSTest test Results 479 | 480 | # NUnit 481 | 482 | # Build Results of an ATL Project 483 | 484 | # Benchmark Results 485 | 486 | # .NET Core 487 | 488 | # ASP.NET Scaffolding 489 | 490 | # StyleCop 491 | 492 | # Files built by Visual Studio 493 | 494 | # Chutzpah Test files 495 | 496 | # Visual C++ cache files 497 | 498 | # Visual Studio profiler 499 | 500 | # Visual Studio Trace Files 501 | 502 | # TFS 2012 Local Workspace 503 | 504 | # Guidance Automation Toolkit 505 | 506 | # ReSharper is a .NET coding add-in 507 | 508 | # TeamCity is a build add-in 509 | 510 | # DotCover is a Code Coverage Tool 511 | 512 | # AxoCover is a Code Coverage Tool 513 | 514 | # Coverlet is a free, cross platform Code Coverage Tool 515 | 516 | # Visual Studio code coverage results 517 | 518 | # NCrunch 519 | 520 | # MightyMoose 521 | 522 | # Web workbench (sass) 523 | 524 | # Installshield output folder 525 | 526 | # DocProject is a documentation generator add-in 527 | 528 | # Click-Once directory 529 | 530 | # Publish Web Output 531 | # Note: Comment the next line if you want to checkin your web deploy settings, 532 | # but database connection strings (with potential passwords) will be unencrypted 533 | 534 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 535 | # checkin your Azure Web App publish settings, but sensitive information contained 536 | # in these scripts will be unencrypted 537 | 538 | # NuGet Packages 539 | # NuGet Symbol Packages 540 | # The packages folder can be ignored because of Package Restore 541 | # except build/, which is used as an MSBuild target. 542 | # Uncomment if necessary however generally it will be regenerated when needed 543 | # NuGet v3's project.json files produces more ignorable files 544 | 545 | # Microsoft Azure Build Output 546 | 547 | # Microsoft Azure Emulator 548 | 549 | # Windows Store app package directories and files 550 | 551 | # Visual Studio cache files 552 | # files ending in .cache can be ignored 553 | # but keep track of directories ending in .cache 554 | 555 | # Others 556 | 557 | # Including strong name files can present a security risk 558 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 559 | 560 | # Since there are multiple workflows, uncomment next line to ignore bower_components 561 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 562 | 563 | # RIA/Silverlight projects 564 | 565 | # Backup & report files from converting an old project file 566 | # to a newer Visual Studio version. Backup files are not needed, 567 | # because we have git ;-) 568 | 569 | # SQL Server files 570 | 571 | # Business Intelligence projects 572 | 573 | # Microsoft Fakes 574 | 575 | # GhostDoc plugin setting file 576 | 577 | # Node.js Tools for Visual Studio 578 | 579 | # Visual Studio 6 build log 580 | 581 | # Visual Studio 6 workspace options file 582 | 583 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 584 | 585 | # Visual Studio LightSwitch build output 586 | 587 | # Paket dependency manager 588 | 589 | # FAKE - F# Make 590 | 591 | # CodeRush personal settings 592 | 593 | # Python Tools for Visual Studio (PTVS) 594 | 595 | # Cake - Uncomment if you are using it 596 | # tools/** 597 | # !tools/packages.config 598 | 599 | # Tabs Studio 600 | 601 | # Telerik's JustMock configuration file 602 | 603 | # BizTalk build output 604 | 605 | # OpenCover UI analysis results 606 | 607 | # Azure Stream Analytics local run output 608 | 609 | # MSBuild Binary and Structured Log 610 | 611 | # NVidia Nsight GPU debugger configuration file 612 | 613 | # MFractors (Xamarin productivity tool) working folder 614 | 615 | # Local History for Visual Studio 616 | 617 | # BeatPulse healthcheck temp database 618 | 619 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 620 | 621 | # Ionide (cross platform F# VS Code tools) working folder 622 | 623 | # Fody - auto-generated XML schema 624 | 625 | ### VisualStudio Patch ### 626 | # Additional files built by Visual Studio 627 | *.tlog 628 | 629 | # End of https://www.toptal.com/developers/gitignore/api/csharp,dotnetcore,visualstudio,visualstudiocode,rider 630 | -------------------------------------------------------------------------------- /img/blazor-kernel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Composite kernel
Composite kernel
Client
Client
C# kernel
C# kernel
Blazor kernel
Blazor kernel
DisplayedValueProduced
Display...
SubmitCode: #!blazor
SubmitCode: #!blazor
CommandSucceeded
Command...
CodeSubmissionReceived
CodeSub...
CompleteCodeSubmissionReceived
Complet...
SubmitCode: #!blazor
SubmitCode: #!blazor
SubmitCode: #!csharp
SubmitCode: #!csharp
<h1>Hello Alice</h1>
<h1>Hello Alice</h1>
#!blazor --name HelloWorld
<h1>Hello @name</h1>

@code {
    string name = "Alice";
}
#!blazor --name HelloWorld...
#!csharp
public partial class HelloWorld
    : ComponentBase
{
    ...
}
#!csharp...
Viewer does not support full SVG 1.1
--------------------------------------------------------------------------------