94 votes

Utilisation de Razor en dehors de MVC dans .NET Core

J'aimerais utiliser Razor comme moteur de modèles dans une application console .NET que j'écris en .NET Core.

Les moteurs Razor autonomes que j'ai rencontrés (RazorEngine, RazorTemplates) nécessitent tous l'utilisation de .NET. Je suis à la recherche d'une solution qui fonctionne avec .NET Core.

2 votes

github.com/aspnet/Razor ne nécessite que le runtime de base (utilisant la bibliothèque standard .NET)

55voto

Simon Mourier Points 49585

Voici un exemple de code qui dépend uniquement de Razor (pour l'analyse syntaxique et la génération de code C#) et de Roslyn (pour la compilation du code C#, mais vous pouvez également utiliser l'ancien CodeDom).

Il n'y a pas de MVC dans ce morceau de code, donc pas de vue, pas de fichiers .cshtml, pas de contrôleur, juste l'analyse des sources par Razor et l'exécution compilée. Cependant, la notion de modèle est toujours présente.

Vous n'aurez besoin d'ajouter que les paquets nuget suivants : Microsoft.AspNetCore.Razor.Language (testé avec la v5.0.5), Microsoft.AspNetCore.Razor.Runtime (testé avec la v2.2.0) et Microsoft.CodeAnalysis.CSharp (testé avec la v3.9.0) nugets.

Ce code source C# est compatible avec .NET 5, NETCore 3.1 (pour les versions plus anciennes, consultez l'historique de cette réponse), NETStandard 2 et .NET Framework. Pour le tester, il suffit de créer une application console .NET framework ou .NET core, de la coller, d'ajouter les nugets et de créer le fichier hello.txt à la main (il doit être situé à côté des exécutables).

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; // needed or not depends on .NET version
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...
        }
    }
}

2 votes

Beau travail, merci ! Pour le faire fonctionner dans la bibliothèque de classes netstandard 2.0 dans l'application netcore2 sur mac et linux, j'ai dû ajouter une référence supplémentaire à la dll netstandard : MetadataReference.CreateFromFile(Path.Combine(Path.GetDirect‌​oryName(typeof(objec‌​t).Assembly.Location‌​),"netstandard.dll")‌​),

2 votes

@Isantipov - ok, merci de le signaler, je n'avais pas testé cela sur d'autres plateformes que Windows. J'ai mis à jour la réponse.

0 votes

Le moteur Razor trouve-t-il automatiquement le fichier "_ViewImports.cshtml" ou dois-je localiser moi-même les fichiers de mise en page, d'importation de vues, etc. en suivant cette voie ? De même, qu'en est-il du 'ViewContext' et des autres propriétés connexes dans les vues - comment le moteur du rasoir sait-il comment les définir ? Ou sont-elles nulles ?

50voto

Toddams Points 874

Récemment, j'ai créé une bibliothèque appelée RazorLight .

Il n'a pas de dépendances redondantes, comme les parties ASP.NET MVC et peut être utilisé dans des applications en console. Pour l'instant, il ne prend en charge que .NET Core (NetStandard1.6), mais c'est exactement ce dont vous avez besoin.

Voici un petit exemple :

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" });

3 votes

C'était assez facile à mettre en œuvre, mais les performances sont plutôt mauvaises. J'ai créé une boucle qui génère environ 1000 lignes de html. Cela prenait environ 12 secondes à chaque fois. La création d'une seule page de 200 lignes prenait environ 1-2 secondes. Dans un projet MVC, une page prend environ 20 millisecondes. Donc, si vous n'êtes pas préoccupé par les performances, c'est une option viable.

0 votes

Si vous utilisez ParseString, les modèles ne sont pas mis en cache, ce qui explique les problèmes de performances. Utilisez plutôt Parse avec un gestionnaire de modèles approprié (pour les fichiers ou les ressources intégrées) - de cette façon, le modèle ne sera compilé qu'une seule fois et la fois suivante, il sera pris dans le cache. Et vous verrez les mêmes chiffres que dans le projet MVC

9 votes

Mise à jour : la version 2.0 met en cache les modèles construits à partir de chaînes de caractères.

32voto

ADOConnection Points 228

Pour tous ceux qui sont en 2021+ ici : J'ai commencé https://github.com/adoconnection/RazorEngineCore

Il dispose des dernières ASP.NET Core 5 Razor et ses caractéristiques syntaxiques.

L'utilisation est tout à fait la même que celle de RazorEngine :

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

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

Console.WriteLine(result);

Sauvegarde et chargement rapides

// 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);

20voto

Nate Barbettini Points 26922

Il existe un exemple fonctionnel pour .NET Core 1.0 à l'adresse suivante aspnet/Entropy/samples/Mvc.RenderViewToString . Comme cela pourrait changer ou disparaître, je vais détailler ici l'approche que j'utilise dans mes propres applications.

Tl;dr - Razor fonctionne très bien en dehors de MVC ! Cette approche permet de gérer des scénarios de rendu plus complexes, comme les vues partielles et l'injection d'objets dans les vues, mais je vais me contenter d'un exemple simple ci-dessous.


Le service de base ressemble à ceci :

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());
        }
    }
}

Une simple application de console de test doit simplement initialiser le service (et certains services de soutien), et l'appeler :

Programme.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>();
        }
    }
}

Cela suppose que vous avez une classe de modèle de vue :

EmailViewModel.cs

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

        public string SenderName { get; set; }
    }
}

Et les fichiers de mise en page et de visualisation :

Vues/_Layout.cshtml

<!DOCTYPE html>

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

Views/EmailTemplate.cshtml

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

Hello @Model.UserName,

<p>
    This is a generic email about something.<br />
    <br />
</p>

0 votes

Salut Nate, petite question, je pensais que view.RenderAsync(viewContext).GetAwaiter().GetResult(); devrait être évitée et que l'on devrait utiliser async/await ou y a-t-il une raison pour laquelle vous avez écrit cet exemple de manière synchrone ?

0 votes

@dustinmoris Merci de le signaler, c'était un oubli de ma part. Vous avez tout à fait raison. J'ai mis à jour le code. La démo en console appelle toujours GetAwaiter().GetResult() à cause de l'absence d'un support asynchrone approprié pour la console :)

0 votes

Bonjour @nate-barbettini, j'ai une autre petite question, est-ce que le RazorViewEngine fait des optimisations pour que la même vue ne soit pas compilée à chaque fois à partir de zéro ou est-ce que je devrais créer ma propre couche de cache ou autre pour accélérer le rendu des pages razor dans une application web non MVC ?

8voto

ArcadeRenegade Points 31

Voici une classe qui permet de faire fonctionner la réponse de Nate comme un service scopé dans un projet ASP.NET Core 2.0.

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);
    }
}

Dans Startup.cs

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

Dans un contrôleur

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();
    }
}

0 votes

Je n'arrive pas à le faire fonctionner. J'obtiens l'erreur suivante : Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.Razor.IRazorViewEngine' while attempting to activate 'Mvc.RenderViewToString.RazorViewToStringRenderer'.".

1 votes

Il s'agit d'une réponse très agréable et simple, qui fonctionne surtout avec des images Docker soutenues par Linux. Beaucoup d'autres solutions ne fonctionnent pas à cause de certains problèmes spécifiques à Linux..... Celle-ci fonctionne. Merci ! Ceci devrait être la réponse pour ASPNET CORE 2+.

0 votes

Est-ce que cela utilise la mise en cache ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X