{ADCES] Discutii si Networking
Timp de discutii / networking / profesional / neprofesional.
Va invitam pe toti
Șoseaua Grozăvești 82 · București
How to find us
Quantic Pub. Telefon personal 0728200034
Timp de discutii / networking / profesional / neprofesional.
Va invitam pe toti
Șoseaua Grozăvești 82 · București
How to find us
Quantic Pub. Telefon personal 0728200034
Timp de discutii / networking / profesional / neprofesional.
Va invitam pe toti
Șoseaua Grozăvești 82 · București
How to find us
Quantic Pub. Telefon personal 0728200034
The Watch2 NuGet package project now has interfaces, making it test-friendly and ready for action.
To make life easier, we’ve sprinkled some Dependency Injection (DI) magic throughout the project.
Classes have been refactored to embrace constructor injection, ensuring all necessary interfaces are on board.
The DI container has been set up in `Program.cs`, making dependency resolution a breeze. The tests have also been given a makeover to fit the new DI style.
This is a never-ending journey, but hey, it’s a pretty good start!
The program.cs now looks like this:
var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider(); var console = serviceProvider.GetRequiredService<IConsoleWrapper>(); var processManager = serviceProvider.GetRequiredService<ProcessManager>(); var startInfo = serviceProvider.GetRequiredService<IProcessStartInfo>(); await processManager.StartProcessAsync(args, console, startInfo); void ConfigureServices(IServiceCollection services) { services.AddSingleton<IConsoleWrapper, ConsoleWrapper>(); services.AddSingleton<ProcessManager, ProcessManager>(); services.AddSingleton<IProcessStartInfo>(provider => new ProcessStartInfoWrapper { FileName = "dotnet", Arguments = "watch " + string.Join(' ', args), WorkingDirectory = Environment.CurrentDirectory, RedirectStandardOutput = true, RedirectStandardInput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }); services.AddLogging(loggingBuilder => { loggingBuilder.ClearProviders(); loggingBuilder.SetMinimumLevel(LogLevel.Trace); loggingBuilder.AddNLog("nlog.config"); }); }
Now I want to do build and auto-deployment as a Nuget Package( at https://www.nuget.org/packages/watch2 )
The most simple build action on GitHub Actions is simple example of how to do it.
name: .NET on: push: branches: [ main ] pull_request: branches: [ main ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup .NET 8 uses: actions/setup-dotnet@v3 with: dotnet-version: 8.0.x - name: build run: | cd src cd Watch2 dotnet restore dotnet build
However, I want to do more than that. I want to build , test and deploy on demand the Nuget package to Nuget.org.
However , the building should be done first on local – and then with GitHub Actions – the deployment should be done.
For this I do like https://github.com/xt0rted/dotnet-run-script
This is how I use to do test and pack, by using a global.json file
{ "scripts": { "build": "dotnet build --configuration Release", "test": "dotnet test --configuration Release", "ci": "dotnet r build && dotnet r test", "pack":"dotnet r ci && cd Watch2 && dotnet pack -o ../../nugetPackages" } }
So the github action now looks like this
name: compile and deploy with tag v* on: push: branches: [ "main" ] tags: [ 'v*' ] # Listen for tag pushes that match version tags pull_request: branches: [ "main" ] jobs: build: # runs-on: windows-latest runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup .NET uses: actions/setup-dotnet@v3 with: dotnet-version: 8.0.x - name: Restore dependencies run: | dotnet tool install --global PowerShell cd src cd Watch2 dotnet restore - name: Build run: | cd src cd Watch2 dotnet tool restore dotnet r pack - name: 'Upload nuget' uses: actions/upload-artifact@v4 with: name: Nuget_${{github.run_number}} path: src/nugetPackages retention-days: 1 - name: push to nuget if: startsWith(github.ref, 'refs/tags/v') # This line ensures the step runs only if a tag version is present run: | dir src/nugetPackages/*.* echo '1' dir src/nugetPackages/*.symbols.nupkg echo '2' cd src cd nugetPackages dotnet nuget push "*.symbols.nupkg" --api-key ${{ secrets.NUGET_KEY }} --source https://api.nuget.org/v3/index.json
More, we should add to github Settings => Secrets and variables => Actions secrets a new secret with name NUGET_KEY
The build and test are made automatically . More, if I want to deploy also, then
1. I modify the version in the csproj file
<PropertyGroup> <Version>8.2024.1102.950</Version> </PropertyGroup>
2. I push the changes to the repository with a tag ( e.g. v8.2024.1102.950)
So this achieves the CI/CD basic .
The Watch2 just intercepts the dotnet watch, adds a clear console, and a delay to give a breather between running the app and the tests.
So I need to have tests – if I want to be sure what the software does and to modify it without summoning the coding gremlins. More importantly, the output from dotnet watch could be different – so I need more tests .
The solution evolved from
to
The main points learned from the process of refactoring:
1. GitHub Copilot is like having a coding sidekick. Some modifications I made manually, but most of the code was generated by my new best friend.
2. For each class that doesn’t have an interface (like ProcessStartInfo, Process, Console), a Wrapper interface must be created. Because who doesn’t love wrapping things up?
3. It is very hard to make interfaces dependent just on interfaces – not on implementations.
4. Static variables could impact the test !
5. The Rocks nuget package really rocks for generating mocks
[assembly: Rock(typeof(IProcessWrapper), BuildType.Create)] [assembly: Rock(typeof(IConsoleWrapper), BuildType.Create)] [assembly: Rock(typeof(IDataReceivedEventArgs), BuildType.Make)] [TestMethod] public void TestRestart() { // Arrange var mockProcess = new IProcessWrapperCreateExpectations(); var mockConsole = new IConsoleWrapperCreateExpectations(); var mockDataReceivedEventArgs = new IDataReceivedEventArgsMakeExpectations(); mockProcess.Methods.Kill().ExpectedCallCount ( 1); mockConsole.Methods.WriteLine(Rocks.Arg.Any<string>()).Callback(it=> { }); // Act (new ProcessManager()).HandleOutput(("Waiting for a file to change before"), mockConsole.Instance()); // Assert mockProcess.Verify(); }
The first attempt to writing the code was very fast – and, surely, not testable. (Speedy Gonzales would be proud!)
Some observations:
1. If you start any process inside a console application, you should intercept the cancel signal and stop the process.
This is done by using the CancelKeyPress event of the Console class.
This is important because the user can cancel the process by pressing Ctrl+C.
(Because who doesn’t love a good Ctrl+C in the morning?)
Console.CancelKeyPress += delegate { // call methods to clean up Kill(proc); };
2. If you want to stop the dotnet watch after it detects a change in the file system, you cannot. You need to kill the process and start it again. So, you need to kill the process and start it again.
(It’s like a phoenix, it must die to be reborn!)
3. Spectre.Console is a really good library to create console applications with a lot of features. I used it to create messages with different colors and styles.
(Because plain text is so last century.)
4. The code is very simple and easy to understand. I used the Process class to start the dotnet watch process and the Console class to write messages to the console.
(Simple and easy, just like making a cup of coffee… if you have a coffee machine.)
This is the final code:
using Spectre.Console; using System.Diagnostics; Process? proc = null; Console.CancelKeyPress += delegate { // call methods to clean up Kill(proc); }; bool shouldWait = false; while (true) { ProcessStartInfo startInfo = new ProcessStartInfo(); startInfo.FileName = "dotnet"; startInfo.Arguments = "watch " + string.Join(' ', args); //startInfo.LoadUserProfile = true; startInfo.WorkingDirectory = Environment.CurrentDirectory; startInfo.RedirectStandardOutput = true; startInfo.RedirectStandardInput = true; startInfo.RedirectStandardError = true; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; proc = new Process(); proc.StartInfo = startInfo; proc.OutputDataReceived += (sender, e) => { if (shouldWait) { shouldWait = false; Kill(proc); Thread.Sleep(15_000); return; } var line = e.Data; if (line == null) return; if (line.Contains("dotnet watch")) { if (line.Contains("Started")) { Console.Clear(); } if (line.Contains("Building")) { } } if (line.Contains(": error ")) { AnsiConsole.MarkupLineInterpolated($"->[bold red]:cross_mark: {e.Data}[/]"); return; } if (line.Contains("Waiting for a file to change before")) { Console.WriteLine("wain"); shouldWait = true; } Console.WriteLine("->" + e.Data); }; proc.ErrorDataReceived += (sender, e) => { if (e.Data != null) { AnsiConsole.MarkupLineInterpolated($"->[bold red]:cross_mark: {e.Data}[/]"); return; } }; AnsiConsole.MarkupLineInterpolated($"[bold green]Starting...[/]"); proc.Start(); Console.Clear(); proc.BeginOutputReadLine(); proc.BeginErrorReadLine(); await proc.WaitForExitAsync(); } static void Kill(Process? proc) { if (proc == null) return; if (proc.HasExited) return; proc.Kill(true); proc.Close(); proc = null; }
I frequently use .NET Watch for two main reasons:
Seriously, if you haven’t tried .NET Watch, you’re missing out. It’s like having a personal assistant who never sleeps compiling code . For more info, check out Microsoft’s documentation at https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-watch
But, like all good things, it comes with a couple of quirks:
Console.Clear
(or cls
command) would be a lifesaver to reset the console after each file change.
dotnet watch
for both the application and tests at the same time is like trying to juggle flaming torches—compilation conflicts galore! You’ll often find yourself stopping one of the watches to keep things from going up in flames.To tackle these issues, I whipped up a simple .NET tool, named Watch2. This little gem can clear the console and run the watch command with an optional delay. It’s built with .NET Core and can be installed as a global tool.
For more laughs and details, visit the NuGet package page at https://www.nuget.org/packages/watch2 .
RSCG – RazorSlices
name | RazorSlices |
nuget | https://www.nuget.org/packages/RazorSlices/ |
link | https://github.com/DamianEdwards/RazorSlices |
author | Damiam Edwards |
Generating html from razor templates. Attention: generates IHttpResult, not html string.
This is how you can use RazorSlices .
The code that you start with is
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.10" /> <PackageReference Include="RazorSlices" Version="0.8.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> </ItemGroup> <PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath> </PropertyGroup> </Project>
The code that you will use is
using RazorDemoSlices; var builder = WebApplication.CreateBuilder(args); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. //if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/hello", (string firstName,string lastName) => Results.Extensions.RazorSlice<RazorDemoSlices.Slices.PersonHTML, Person>( new Person { FirstName = firstName, LastName = lastName } )); app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast") .WithOpenApi(); app.Run(); internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
@inherits RazorSliceHttpResult<RazorDemoSlices.Person> <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello from Razor Slices!</title> </head> <body> <p> My name is @Model.FirstName @Model.LastName </p> </body> </html>
namespace RazorDemoSlices; public class Person { public string FirstName { get; set; }=string.Empty; public string LastName { get; set; }= string.Empty; }
The code that is generated is
#pragma checksum "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\PersonHTML.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "6b3710e80836b438a5d8935ea469d238fc095e46298456ca847e09519393bb5e" // <auto-generated/> #pragma warning disable 1591 [assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Slices_PersonHTML), @"mvc.1.0.view", @"/Slices/PersonHTML.cshtml")] namespace AspNetCoreGeneratedDocument { #line default using global::System; using global::System.Collections.Generic; using global::System.Linq; using global::System.Threading.Tasks; using global::Microsoft.AspNetCore.Mvc; using global::Microsoft.AspNetCore.Mvc.Rendering; using global::Microsoft.AspNetCore.Mvc.ViewFeatures; #nullable restore #line (3,2)-(4,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using System.Globalization; #nullable disable #nullable restore #line (4,2)-(5,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using Microsoft.AspNetCore.Razor; #nullable disable #nullable restore #line (5,2)-(6,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using Microsoft.AspNetCore.Http.HttpResults; #line default #line hidden #nullable disable [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Slices/PersonHTML.cshtml")] [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] #nullable restore internal sealed class Slices_PersonHTML : RazorSliceHttpResult<RazorDemoSlices.Person> #nullable disable { #pragma warning disable 1998 public async override global::System.Threading.Tasks.Task ExecuteAsync() { WriteLiteral("<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"utf-8\">\r\n <title>Hello from Razor Slices!</title>\r\n</head>\r\n<body>\r\n <p>\r\n My name is "); Write( #nullable restore #line (10,21)-(10,36) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\PersonHTML.cshtml" Model.FirstName #line default #line hidden #nullable disable ); WriteLiteral(" "); Write( #nullable restore #line (10,38)-(10,52) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\PersonHTML.cshtml" Model.LastName #line default #line hidden #nullable disable ); WriteLiteral("\r\n </p>\r\n</body>\r\n</html>"); } #pragma warning restore 1998 #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<dynamic> Html { get; private set; } = default!; #nullable disable } } #pragma warning restore 1591
#pragma checksum "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "95493514af34e5705fffb1e5c7121f0e7abde13ee7a1cff8fbafa2085da18fff" // <auto-generated/> #pragma warning disable 1591 [assembly: global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemAttribute(typeof(AspNetCoreGeneratedDocument.Slices__ViewImports), @"mvc.1.0.view", @"/Slices/_ViewImports.cshtml")] namespace AspNetCoreGeneratedDocument { #line default using global::System; using global::System.Collections.Generic; using global::System.Linq; using global::System.Threading.Tasks; using global::Microsoft.AspNetCore.Mvc; using global::Microsoft.AspNetCore.Mvc.Rendering; using global::Microsoft.AspNetCore.Mvc.ViewFeatures; #nullable restore #line (3,2)-(4,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using System.Globalization; #nullable disable #nullable restore #line (4,2)-(5,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using Microsoft.AspNetCore.Razor; #nullable disable #nullable restore #line (5,2)-(6,1) "D:\gth\RSCG_Examples\v2\rscg_examples\EmbeddedResourcePropertyGenerator\src\RazorDemoSlices\RazorDemoSlices\Slices\_ViewImports.cshtml" using Microsoft.AspNetCore.Http.HttpResults; #line default #line hidden #nullable disable [global::Microsoft.AspNetCore.Razor.Hosting.RazorCompiledItemMetadataAttribute("Identifier", "/Slices/_ViewImports.cshtml")] [global::System.Runtime.CompilerServices.CreateNewOnMetadataUpdateAttribute] #nullable restore internal sealed class Slices__ViewImports : RazorSliceHttpResult #nullable disable { #pragma warning disable 1998 public async override global::System.Threading.Tasks.Task ExecuteAsync() { WriteLiteral("\r\n"); WriteLiteral("\r\n"); } #pragma warning restore 1998 #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; } = default!; #nullable disable #nullable restore [global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute] public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<dynamic> Html { get; private set; } = default!; #nullable disable } } #pragma warning restore 1591
// <auto-generated/> using global::System.Diagnostics.CodeAnalysis; using global::RazorSlices; #nullable enable namespace RazorDemoSlices { /// <summary> /// All calls to create Razor Slices instances via the generated <see cref="global::RazorSlices.IRazorSliceProxy"/> classes /// go through this factory to ensure that the generated types' Create methods are always invoked via the static abstract /// methods defined in the <see cref="global::RazorSlices.IRazorSliceProxy"/> interface. This ensures that the interface /// implementation is never trimmed from the generated types. /// </summary> /// <remarks> /// Workaround for https://github.com/dotnet/runtime/issues/102796 /// </remarks> [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] // Hide from IntelliSense. internal static class RazorSlicesGenericFactory { public static RazorSlice CreateSlice<TProxy>() where TProxy : IRazorSliceProxy => TProxy.CreateSlice(); public static RazorSlice<TModel> CreateSlice<TProxy, TModel>(TModel model) where TProxy : IRazorSliceProxy => TProxy.CreateSlice(model); } } namespace RazorDemoSlices.Slices { /// <summary> /// Static proxy for the Razor Slice defined in <c>Slices\PersonHTML.cshtml</c>. /// </summary> public sealed class PersonHTML : global::RazorSlices.IRazorSliceProxy { [global::System.Diagnostics.CodeAnalysis.DynamicDependency(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, TypeName, "RazorDemoSlices")] private const string TypeName = "AspNetCoreGeneratedDocument.Slices_PersonHTML, RazorDemoSlices"; [global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] private static readonly global::System.Type _sliceType = global::System.Type.GetType(TypeName) ?? throw new global::System.InvalidOperationException($"Razor view type '{TypeName}' was not found. This is likely a bug in the RazorSlices source generator."); private static readonly global::RazorSlices.SliceDefinition _sliceDefinition = new(_sliceType); /// <summary> /// Creates a new instance of the Razor Slice defined in <c>Slices\PersonHTML.cshtml</c> . /// </summary> public static global::RazorSlices.RazorSlice Create() => global::RazorDemoSlices.RazorSlicesGenericFactory.CreateSlice<global::RazorDemoSlices.Slices.PersonHTML>(); /// <summary> /// Creates a new instance of the Razor Slice defined in <c>Slices\PersonHTML.cshtml</c> with the given model. /// </summary> public static global::RazorSlices.RazorSlice<TModel> Create<TModel>(TModel model) => global::RazorDemoSlices.RazorSlicesGenericFactory.CreateSlice<global::RazorDemoSlices.Slices.PersonHTML, TModel>(model); // Explicit interface implementation static global::RazorSlices.RazorSlice global::RazorSlices.IRazorSliceProxy.CreateSlice() => _sliceDefinition.CreateSlice(); // Explicit interface implementation static global::RazorSlices.RazorSlice<TModel> global::RazorSlices.IRazorSliceProxy.CreateSlice<TModel>(TModel model) => _sliceDefinition.CreateSlice(model); } }
Code and pdf at
https://ignatandrei.github.io/RSCG_Examples/v2/docs/RazorSlices