Verwenden von Razor außerhalb von MVC in .NET Core

71

Ich möchte Razor als Vorlagen-Engine in einer .NET-Konsolenanwendung verwenden, die ich in .NET Core schreibe.

Die eigenständigen Razor-Engines (RazorEngine, RazorTemplates) erfordern alle .NET. Ich suche nach einer Lösung, die mit .NET Core funktioniert.

Christof Jans
quelle
2
github.com/aspnet/Razor benötigt nur die
Kernlaufzeit

Antworten:

46

Kürzlich habe ich eine Bibliothek namens RazorLight erstellt .

Es hat keine redundanten Abhängigkeiten wie ASP.NET MVC-Teile und kann in Konsolenanwendungen verwendet werden. Im Moment unterstützt es nur .NET Core (NetStandard1.6) - aber genau das brauchen Sie.

Hier ist ein kurzes Beispiel:

IRazorLightEngine engine = EngineFactory.CreatePhysical("Path-to-your-views");

// Files and strong models
string resultFromFile = engine.Parse("Test.cshtml", new Model("SomeData")); 

// Strings and anonymous models
string stringResult = engine.ParseString("Hello @Model.Name", new { Name = "John" }); 
Toddams
quelle
3
Dies war ziemlich einfach zu implementieren, hat aber eine ziemlich schreckliche Leistung. Ich habe eine Schleife erstellt, die ungefähr 1000 HTML-Zeilen generiert. Es dauerte jedes Mal ungefähr 12 Sekunden. Das Erstellen einer einzelnen Seite mit 200 Zeilen dauerte ca. 1-2 Sekunden. In einem MVC-Projekt dauerte 1 Seite ungefähr 20 Millisekunden. Wenn Sie sich also keine Sorgen um die Leistung machen, ist dies eine praktikable Option.
DeadlyChambers
Wenn Sie ParseString verwenden - Vorlagen werden nicht zwischengespeichert, treten Leistungsprobleme auf. Verwenden Sie Parse stattdessen mit dem entsprechenden Vorlagenmanager (für Dateien oder eingebettete Ressourcen). Auf diese Weise wird die Vorlage nur einmal kompiliert und das nächste Mal aus dem Cache entnommen. Und Sie werden die gleichen Zahlen wie im MVC-Projekt sehen
Toddams
8
Update: 2.0-Version zwischenspeichert Vorlagen aus Strings
Toddams
2
@Toddams Dies funktioniert nicht in der Produktion, da Ansichten beim Veröffentlichen vorkompiliert werden. Können Sie bitte ein mvc-Projektbeispiel hinzufügen, das die Produktion (vorkompilierte Ansichten) und die Entwicklungsumgebung unterstützt? Ich bin nicht in der Lage, es für beide Umgebungen zusammen arbeiten zu lassen: ((
Freshblood
1
@Toddams Ich bin im selben Boot für die vorkompilierten Ansichten; RazorLight funktioniert überhaupt nicht, wenn wir dotnet publishdie App erstellen.
Bchhun
38

Hier ist ein Beispielcode, der nur von Razor (zum Parsen und Generieren von C # -Code) und Roslyn (zum Kompilieren von C # -Code, aber Sie können auch das alte CodeDom verwenden) abhängt.

In diesem Code befindet sich keine MVC, also keine Ansicht, keine CSHTM-Dateien, kein Controller, nur das Parsen von Razor-Quellen und die kompilierte Laufzeitausführung. Es gibt jedoch immer noch den Begriff Modell.

Sie müssen nur die folgenden Nuget-Pakete hinzufügen: Microsoft.AspNetCore.Razor.Language(getestet mit v3.1.7), Microsoft.AspNetCore.Razor.Runtime(getestet mit v2.2.0) und Microsoft.CodeAnalysis.CSharp(getestet mit v3.7.0) Nugets.

Dieser C # -Quellcode ist kompatibel mit NETCore 3.1 (für ältere Versionen überprüfen Sie den Verlauf dieser Antwort), NETStandard 2 und .NET Framework. Um es zu testen, erstellen Sie einfach ein .NET Framework oder eine .NET Core Console-App, fügen Sie es ein, fügen Sie die Nugets hinzu und erstellen Sie die Datei hello.txt von Hand.

using System;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Hosting;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace RazorTemplate
{
    class Program
    {
        static void Main(string[] args)
        {
            // points to the local path
            var fs = RazorProjectFileSystem.Create(".");

            // customize the default engine a little bit
            var engine = RazorProjectEngine.Create(RazorConfiguration.Default, fs, (builder) =>
            {
                // InheritsDirective.Register(builder); // in .NET core 3.1, compatibility has been broken (again), and this is not needed anymore...
                builder.SetNamespace("MyNamespace"); // define a namespace for the Template class
            });

            // get a razor-templated file. My "hello.txt" template file is defined like this:
            //
            // @inherits RazorTemplate.MyTemplate
            // Hello @Model.Name, welcome to Razor World!
            //

            var item = fs.GetItem("hello.txt", null);

            // parse and generate C# code
            var codeDocument = engine.Process(item);
            var cs = codeDocument.GetCSharpDocument();

            // outputs it on the console
            //Console.WriteLine(cs.GeneratedCode);

            // now, use roslyn, parse the C# code
            var tree = CSharpSyntaxTree.ParseText(cs.GeneratedCode);

            // define the dll
            const string dllName = "hello";
            var compilation = CSharpCompilation.Create(dllName, new[] { tree },
                new[]
                {
                    MetadataReference.CreateFromFile(typeof(object).Assembly.Location), // include corlib
                    MetadataReference.CreateFromFile(typeof(RazorCompiledItemAttribute).Assembly.Location), // include Microsoft.AspNetCore.Razor.Runtime
                    MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location), // this file (that contains the MyTemplate base class)

                    // for some reason on .NET core, I need to add this... this is not needed with .NET framework
                    MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "System.Runtime.dll")),

                    // as found out by @Isantipov, for some other reason on .NET Core for Mac and Linux, we need to add this... this is not needed with .NET framework
                    MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location), "netstandard.dll"))
                },
                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); // we want a dll


            // compile the dll
            string path = Path.Combine(Path.GetFullPath("."), dllName + ".dll");
            var result = compilation.Emit(path);
            if (!result.Success)
            {
                Console.WriteLine(string.Join(Environment.NewLine, result.Diagnostics));
                return;
            }

            // load the built dll
            Console.WriteLine(path);
            var asm = Assembly.LoadFile(path);

            // the generated type is defined in our custom namespace, as we asked. "Template" is the type name that razor uses by default.
            var template = (MyTemplate)Activator.CreateInstance(asm.GetType("MyNamespace.Template"));

            // run the code.
            // should display "Hello Killroy, welcome to Razor World!"
            template.ExecuteAsync().Wait();
        }
    }

    // the model class. this is 100% specific to your context
    public class MyModel
    {
        // this will map to @Model.Name
        public string Name => "Killroy";
    }

    // the sample base template class. It's not mandatory but I think it's much easier.
    public abstract class MyTemplate
    {
        // this will map to @Model (property name)
        public MyModel Model => new MyModel();

        public void WriteLiteral(string literal)
        {
            // replace that by a text writer for example
            Console.Write(literal);
        }

        public void Write(object obj)
        {
            // replace that by a text writer for example
            Console.Write(obj);
        }

        public async virtual Task ExecuteAsync()
        {
            await Task.Yield(); // whatever, we just need something that compiles...
        }
    }
}
Simon Mourier
quelle
2
Gute Arbeit, danke! Um es in der Netstandard 2.0-Klassenbibliothek zum Laufen zu bringen, die in der Netcore2-App auf Mac und Linux ausgeführt wird, musste ich einen zusätzlichen Verweis auf die Netstandard-DLL hinzufügen:MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(typeof(object).Assembly.Location),"netstandard.dll")),
Isantipov
2
@Isantipov - ok, danke für den Hinweis, dass ich dies nicht auf anderen Plattformen als Windows getestet habe. Ich habe die Antwort aktualisiert.
Simon Mourier
Würde die Razor Engine die Datei "_ViewImports.cshtml" automatisch finden, oder muss ich das Layout suchen, Importe anzeigen usw., Dateien selbst auf diesem Weg? Was ist auch mit dem 'ViewContext' und anderen verwandten Eigenschaften in den Ansichten - woher weiß die Rasiermaschine, wie diese festgelegt werden? Oder sind sie null?
James Wilkins
2
Wenn ich mich recht erinnere, befindet sich die eigentliche RazorViewEngine in MVC. Ich denke, das macht Razor zu nichts anderem als einem Parser und Compiler, denke ich. ;)
James Wilkins
1
@ Dave - Ich habe meine Antwort aktualisiert. Es sollte jetzt mit den neuesten Versionen funktionieren.
Simon Mourier
21

Für alle ab 2020 hier: Ich habe https://github.com/adoconnection/RazorEngineCore gestartet

Es verfügt über den neuesten ASP.NET Core 3.1.1 Razor und seine Syntaxfunktionen.

Die Verwendung entspricht der von RazorEngine:

RazorEngine razorEngine = new RazorEngine();
RazorEngineCompiledTemplate template = razorEngine.Compile("Hello @Model.Name");

string result = template.Run(new
{
    Name = "Alex"
});

Console.WriteLine(result);

Schnelles Speichern und Laden

// save to file
template.SaveToFile("myTemplate.dll");

//save to stream
MemoryStream memoryStream = new MemoryStream();
template.SaveToStream(memoryStream);
var template1 = RazorEngineCompiledTemplate.LoadFromFile("myTemplate.dll");
var template2 = RazorEngineCompiledTemplate.LoadFromStream(myStream);
ADOConnection
quelle
1
Dies scheint nicht zu funktionieren, wenn .NET Framework es aufruft (möglicherweise, weil sich der Razor-Renderer zwischen .net Core und Framework grundlegend unterscheidet?). Es gibt System.IO.FileNotFoundException: Could not load file or assembly 'Microsoft.CSharp' or one of its dependencies. The system cannot find the file specified.eine Chance , dieses Arbeits für .NET Framework?
Thalacker
3
Es ist nie in .NET Framework zu arbeiten sollte, gibt es ein wohlbekanntes github.com/Antaris/RazorEngine Paket für die
ADOConnection
Benötigt dies auch PreserveCompilationContextin den Projekteinstellungen? Ich würde es vorziehen, wenn möglich zu vermeiden.
Tyrrrz
@ Tyrrrz nein, es ist nicht erforderlich
ADOConnection
1
@thalacker v2020.9.1 hat NET 4.7.2 Unterstützung
ADOConnection
18

Es gibt ein funktionierendes Beispiel für .NET Core 1.0 unter aspnet / Entropy / samples / Mvc.RenderViewToString . Da sich dies ändern oder verschwinden könnte, werde ich hier den Ansatz, den ich in meinen eigenen Anwendungen verwende, detailliert beschreiben.

Tl; dr - Razor funktioniert wirklich gut außerhalb von MVC! Dieser Ansatz kann auch komplexere Rendering-Szenarien wie Teilansichten und das Einfügen von Objekten in Ansichten verarbeiten, obwohl ich im Folgenden nur ein einfaches Beispiel zeigen werde.


Der Kerndienst sieht folgendermaßen aus:

RazorViewToStringRenderer.cs

using System;
using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace RenderRazorToString
{
    public class RazorViewToStringRenderer
    {
        private readonly IRazorViewEngine _viewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;

        public RazorViewToStringRenderer(
            IRazorViewEngine viewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public async Task<string> RenderViewToString<TModel>(string name, TModel model)
        {
            var actionContext = GetActionContext();

            var viewEngineResult = _viewEngine.FindView(actionContext, name, false);

            if (!viewEngineResult.Success)
            {
                throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
            }

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    new ViewDataDictionary<TModel>(
                        metadataProvider: new EmptyModelMetadataProvider(),
                        modelState: new ModelStateDictionary())
                    {
                        Model = model
                    },
                    new TempDataDictionary(
                        actionContext.HttpContext,
                        _tempDataProvider),
                    output,
                    new HtmlHelperOptions());

                await view.RenderAsync(viewContext);

                return output.ToString();
            }
        }

        private ActionContext GetActionContext()
        {
            var httpContext = new DefaultHttpContext
            {
                RequestServices = _serviceProvider
            };

            return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }
    }
}

Eine einfache Testkonsolen-App muss lediglich den Dienst (und einige unterstützende Dienste) initialisieren und aufrufen:

Program.cs

using System;
using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.PlatformAbstractions;

namespace RenderRazorToString
{
    public class Program
    {
        public static void Main()
        {
            // Initialize the necessary services
            var services = new ServiceCollection();
            ConfigureDefaultServices(services);
            var provider = services.BuildServiceProvider();

            var renderer = provider.GetRequiredService<RazorViewToStringRenderer>();

            // Build a model and render a view
            var model = new EmailViewModel
            {
                UserName = "User",
                SenderName = "Sender"
            };
            var emailContent = renderer.RenderViewToString("EmailTemplate", model).GetAwaiter().GetResult();

            Console.WriteLine(emailContent);
            Console.ReadLine();
        }

        private static void ConfigureDefaultServices(IServiceCollection services)
        {
            var applicationEnvironment = PlatformServices.Default.Application;
            services.AddSingleton(applicationEnvironment);

            var appDirectory = Directory.GetCurrentDirectory();

            var environment = new HostingEnvironment
            {
                WebRootFileProvider = new PhysicalFileProvider(appDirectory),
                ApplicationName = "RenderRazorToString"
            };
            services.AddSingleton<IHostingEnvironment>(environment);

            services.Configure<RazorViewEngineOptions>(options =>
            {
                options.FileProviders.Clear();
                options.FileProviders.Add(new PhysicalFileProvider(appDirectory));
            });

            services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();

            var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
            services.AddSingleton<DiagnosticSource>(diagnosticSource);

            services.AddLogging();
            services.AddMvc();
            services.AddSingleton<RazorViewToStringRenderer>();
        }
    }
}

Dies setzt voraus, dass Sie eine Ansichtsmodellklasse haben:

EmailViewModel.cs

namespace RenderRazorToString
{
    public class EmailViewModel
    {
        public string UserName { get; set; }

        public string SenderName { get; set; }
    }
}

Und Layout- und Ansichtsdateien:

Ansichten / _Layout.cshtml

<!DOCTYPE html>

<html>
<body>
    <div>
        @RenderBody()
    </div>
    <footer>
Thanks,<br />
@Model.SenderName
    </footer>
</body>
</html>

Ansichten / EmailTemplate.cshtml

@model RenderRazorToString.EmailViewModel
@{ 
    Layout = "_EmailLayout";
}

Hello @Model.UserName,

<p>
    This is a generic email about something.<br />
    <br />
</p>
Nate Barbettini
quelle
2
@dustinmoris Wenn ich mich richtig erinnere, macht es etwas Caching für Sie. Ich habe es eine Weile nicht versucht.
Nate Barbettini
2
Genial! In .net Core 2.0 funktioniert dies jedoch nicht :( Es scheint, dass die Abhängigkeiten nicht geladen werden können: The type 'Attribute' is defined in an assembly that is not referenced. You must add a reference to assembly 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'- Ich bin nicht sicher, wie ich Rasierer anweisen soll, alle benötigten Abhängigkeiten zu laden - irgendwelche Ideen?
Matt Roberts
1
Dieser Code scheint Speicher in unserem Projekt zu verlieren, aber ich bin nicht sicher, was der genaue Schuldige ist.
Doug
2
Für alle, die den The type 'Attribute' is defined in an assembly that is not referencedFehler erhalten, wurde <PreserveCompilationContext>true</PreserveCompilationContext>das Problem durch Hinzufügen behoben.
Mark G
3
Beachten Sie, dass Sie zur Verwendung mit ASP.NET Core 3 IWebHostEnvironmentdem DI-Container eine Instanz hinzufügen und die MvcRazorRuntimeCompilationOptionsKlasse verwenden müssen, um Dateianbieter anstelle der RazorViewEngineOptionsKlasse bereitzustellen . Ein Beispiel für eine detaillierte RazorViewToStringRendererInstanziierung finden Sie hier: corstianboerman.com/2019-12-25/… .
Corstian Boerman
8

Hier ist eine Klasse, mit der Nates Antwort als Scoped Service in einem ASP.NET Core 2.0-Projekt funktioniert.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;

namespace YourNamespace.Services
{
    public class ViewRender : IViewRender
    {
        private readonly IRazorViewEngine _viewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IServiceProvider _serviceProvider;

        public ViewRender(
            IRazorViewEngine viewEngine,
            ITempDataProvider tempDataProvider,
            IServiceProvider serviceProvider)
        {
            _viewEngine = viewEngine;
            _tempDataProvider = tempDataProvider;
            _serviceProvider = serviceProvider;
        }

        public async Task<string> RenderAsync(string name)
        {
            return await RenderAsync<object>(name, null);
        }

        public async Task<string> RenderAsync<TModel>(string name, TModel model)
        {
            var actionContext = GetActionContext();

            var viewEngineResult = _viewEngine.FindView(actionContext, name, false);

            if (!viewEngineResult.Success)
            {
                throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
            }

            var view = viewEngineResult.View;

            using (var output = new StringWriter())
            {
                var viewContext = new ViewContext(
                    actionContext,
                    view,
                    new ViewDataDictionary<TModel>(
                        metadataProvider: new EmptyModelMetadataProvider(),
                        modelState: new ModelStateDictionary())
                    {
                        Model = model
                    },
                    new TempDataDictionary(
                        actionContext.HttpContext,
                        _tempDataProvider),
                    output,
                    new HtmlHelperOptions());

                await view.RenderAsync(viewContext);

                return output.ToString();
            }
        }

        private ActionContext GetActionContext()
        {
            var httpContext = new DefaultHttpContext {RequestServices = _serviceProvider};
            return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
        }
    }

    public interface IViewRender
    {
        Task<string> RenderAsync(string name);

        Task<string> RenderAsync<TModel>(string name, TModel model);
    }
}

In Startup.cs

public void ConfigureServices(IServiceCollection services)
{
     services.AddScoped<IViewRender, ViewRender>();
}

In einer Steuerung

public class VenuesController : Controller
{
    private readonly IViewRender _viewRender;

    public VenuesController(IViewRender viewRender)
    {
        _viewRender = viewRender;
    }

    public async Task<IActionResult> Edit()
    {
        string html = await _viewRender.RenderAsync("Emails/VenuePublished", venue.Name);
        return Ok();
    }
}
ArcadeRenegade
quelle
Ich bin nicht in der Lage, dies zum Laufen zu bringen. Ich erhalte die Fehlermeldung: Dienst für Typ 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' kann nicht aufgelöst werden, während versucht wird, 'Mvc.RenderViewToString.RazorViewToStringRenderer' zu aktivieren. '
Kjensen
1
Dies ist eine sehr schöne und einfache Antwort, die vor allem deshalb gut funktioniert, weil sie in Linux-gestützten Docker-Images funktioniert. Viele andere Lösungen funktionieren aufgrund einiger Linux-spezifischer Probleme nicht. Vielen Dank! Dies sollte die Antwort für ASPNET CORE 2+
Piotr Kula
Verwendet dies überhaupt Caching?
JJxtra
0

Ich habe mehrere Tage damit verbracht, mit Rasiermesserlicht herumzuspielen, aber es hat eine Reihe von Mängeln, wie zum Beispiel keine HTML-Helfer (@ Html. *) Oder URL-Helfer und andere Macken.

Hier ist eine Lösung, die für die Verwendung außerhalb einer MVC-App gekapselt ist. Es sind zwar Paketverweise auf Aspnet Core und MVC erforderlich, diese können jedoch problemlos zu einer Dienst- oder Konsolenanwendung hinzugefügt werden. Es werden keine Controller oder Webserver benötigt. RenderToStringAsync ist die aufzurufende Methode zum Rendern einer Ansicht für eine Zeichenfolge.

Der Vorteil ist, dass Sie Ihre Ansichten genauso schreiben können wie in einem .net Core-Webprojekt. Sie können dieselbe @ HTML- und andere Hilfsfunktionen und -methoden verwenden.

Sie können den physischen Dateianbieter in den Optionen für die Optionen für die Rasiermesseransicht durch einen eigenen benutzerdefinierten Anbieter ersetzen oder hinzufügen, um Ansichten aus Datenbanken, Webdienstaufrufen usw. zu laden. Getestet mit .net Core 2.2 unter Windows und Linux.

Bitte beachten Sie, dass Ihre .csproj-Datei dies als oberste Zeile haben muss:

<Project Sdk="Microsoft.NET.Sdk.Web">
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;

namespace RazorRendererNamespace
{
    /// <summary>
    /// Renders razor pages with the absolute minimum setup of MVC, easy to use in console application, does not require any other classes or setup.
    /// </summary>
    public class RazorRenderer : ILoggerFactory, ILogger
    {
        private class ViewRenderService : IDisposable, ITempDataProvider, IServiceProvider
        {
            private static readonly System.Net.IPAddress localIPAddress = System.Net.IPAddress.Parse("127.0.0.1");

            private readonly Dictionary<string, object> tempData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            private readonly IRazorViewEngine _viewEngine;
            private readonly ITempDataProvider _tempDataProvider;
            private readonly IServiceProvider _serviceProvider;
            private readonly IHttpContextAccessor _httpContextAccessor;

            public ViewRenderService(IRazorViewEngine viewEngine,
                IHttpContextAccessor httpContextAccessor,
                ITempDataProvider tempDataProvider,
                IServiceProvider serviceProvider)
            {
                _viewEngine = viewEngine;
                _httpContextAccessor = httpContextAccessor;
                _tempDataProvider = tempDataProvider ?? this;
                _serviceProvider = serviceProvider ?? this;
            }

            public void Dispose()
            {

            }

            public async Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
            {
                HttpContext httpContext;
                if (_httpContextAccessor?.HttpContext != null)
                {
                    httpContext = _httpContextAccessor.HttpContext;
                }
                else
                {
                    DefaultHttpContext defaultContext = new DefaultHttpContext { RequestServices = _serviceProvider };
                    defaultContext.Connection.RemoteIpAddress = localIPAddress;
                    httpContext = defaultContext;
                }
                var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
                using (var sw = new StringWriter())
                {
                    var viewResult = _viewEngine.FindView(actionContext, viewName, isMainPage);

                    if (viewResult.View == null)
                    {
                        viewResult = _viewEngine.GetView("~/", viewName, isMainPage);
                    }

                    if (viewResult.View == null)
                    {
                        return null;
                    }

                    var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
                    {
                        Model = model
                    };
                    if (viewBag != null)
                    {
                        foreach (KeyValuePair<string, object> kv in (viewBag as IDictionary<string, object>))
                        {
                            viewDictionary.Add(kv.Key, kv.Value);
                        }
                    }
                    var viewContext = new ViewContext(
                        actionContext,
                        viewResult.View,
                        viewDictionary,
                        new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
                        sw,
                        new HtmlHelperOptions()
                    );

                    await viewResult.View.RenderAsync(viewContext);
                    return sw.ToString();
                }
            }

            object IServiceProvider.GetService(Type serviceType)
            {
                return null;
            }

            IDictionary<string, object> ITempDataProvider.LoadTempData(HttpContext context)
            {
                return tempData;
            }

            void ITempDataProvider.SaveTempData(HttpContext context, IDictionary<string, object> values)
            {
            }
        }

        private readonly string rootPath;
        private readonly ServiceCollection services;
        private readonly ServiceProvider serviceProvider;
        private readonly ViewRenderService viewRenderer;

        public RazorRenderer(string rootPath)
        {
            this.rootPath = rootPath;
            services = new ServiceCollection();
            ConfigureDefaultServices(services);
            serviceProvider = services.BuildServiceProvider();
            viewRenderer = new ViewRenderService(serviceProvider.GetRequiredService<IRazorViewEngine>(), null, null, serviceProvider);
        }

        private void ConfigureDefaultServices(IServiceCollection services)
        {
            var environment = new HostingEnvironment
            {
                WebRootFileProvider = new PhysicalFileProvider(rootPath),
                ApplicationName = typeof(RazorRenderer).Assembly.GetName().Name,
                ContentRootPath = rootPath,
                WebRootPath = rootPath,
                EnvironmentName = "DEVELOPMENT",
                ContentRootFileProvider = new PhysicalFileProvider(rootPath)
            };
            services.AddSingleton<IHostingEnvironment>(environment);
            services.Configure<RazorViewEngineOptions>(options =>
            {
                options.FileProviders.Clear();
                options.FileProviders.Add(new PhysicalFileProvider(rootPath));
            });
            services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
            services.AddSingleton<ILoggerFactory>(this);
            var diagnosticSource = new DiagnosticListener(environment.ApplicationName);
            services.AddSingleton<DiagnosticSource>(diagnosticSource);
            services.AddMvc();
        }

        public void Dispose()
        {
        }

        public Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
        {
            return viewRenderer.RenderToStringAsync(viewName, model, viewBag, isMainPage);
        }

        void ILoggerFactory.AddProvider(ILoggerProvider provider)
        {

        }

        IDisposable ILogger.BeginScope<TState>(TState state)
        {
            throw new NotImplementedException();
        }

        ILogger ILoggerFactory.CreateLogger(string categoryName)
        {
            return this;
        }

        bool ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
        {
            return false;
        }

        void ILogger.Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
        }
    }
}
jjxtra
quelle
Verwenden Microsoft.NET.Sdk.Webbedeutet, dass das Projekt zur Ausführung ASP.NET Core-Laufzeit benötigt. Dies kann sehr schwer und für Konsolen- / Desktop-Apps unnötig sein.
Tyrrrz
Nach meiner Erfahrung ist das Aufblähen ziemlich gering. Eine in sich geschlossene EXE-Datei ab .net Core 3.1 mit aktivierter Laufzeit und aktiviertem Trimmen des gesamten asp.net-Kerns beträgt ca. 80 MB. Wenn Sie ein Installationsprogramm erstellen, werden diese Dateien auf ca. 30 MB komprimiert und wieder extrahiert. Wenn Sie einen Microservice haben, ist dies kein Problem. Für eine eigenständige Konsolen-App würde ich empfehlen, zu .net 5 zu wechseln, da Sie sonst viele zusätzliche DLL-Dateien und -Ordner haben.
jjxtra