diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo.Abp.AspNetCore.MultiTenancy.csproj b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo.Abp.AspNetCore.MultiTenancy.csproj
index 22a2cb4247..fe6f4f498a 100644
--- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo.Abp.AspNetCore.MultiTenancy.csproj
+++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo.Abp.AspNetCore.MultiTenancy.csproj
@@ -23,4 +23,9 @@
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs
index 7ac6f41176..080427cf6b 100644
--- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs
+++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/AbpAspNetCoreMultiTenancyOptions.cs
@@ -16,6 +16,8 @@ using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
+using Volo.Abp.AspNetCore.MultiTenancy.Views;
+using Volo.Abp.AspNetCore.RazorViews;
using Volo.Abp.Http;
using Volo.Abp.Json;
using Volo.Abp.MultiTenancy;
@@ -68,7 +70,7 @@ public class AbpAspNetCoreMultiTenancyOptions
}
}
- context.Response.Headers.Add("Abp-Tenant-Resolve-Error", HtmlEncoder.Default.Encode(exception.Message));
+ context.Response.Headers.Append("Abp-Tenant-Resolve-Error", HtmlEncoder.Default.Encode(exception.Message));
if (isCookieAuthentication && context.Request.Method.Equals("Get", StringComparison.OrdinalIgnoreCase) && !context.Request.IsAjax())
{
context.Response.Redirect(context.Request.GetEncodedUrl());
@@ -133,18 +135,11 @@ public class AbpAspNetCoreMultiTenancyOptions
}
else
{
- context.Response.StatusCode = (int)HttpStatusCode.NotFound;
- context.Response.ContentType = "text/html";
-
var message = exception.Message;
var details = exception is BusinessException businessException ? businessException.Details : string.Empty;
- await context.Response.WriteAsync($"
\r\n");
- await context.Response.WriteAsync($"{HtmlEncoder.Default.Encode(message)}
{HtmlEncoder.Default.Encode(details!)}
\r\n");
- await context.Response.WriteAsync("\r\n");
-
- // Note the 500 spaces are to work around an IE 'feature'
- await context.Response.WriteAsync(new string(' ', 500));
+ var errorPage = new MultiTenancyMiddlewareErrorPage(new MultiTenancyMiddlewareErrorPageModel(message, details!));
+ await errorPage.ExecuteAsync(context);
}
return true;
diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs
new file mode 100644
index 0000000000..751eec21bb
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs
@@ -0,0 +1,96 @@
+//
+#pragma warning disable 1591
+namespace Volo.Abp.AspNetCore.RazorViews
+{
+ #line hidden
+ using System;
+ using System.Threading.Tasks;
+#nullable restore
+#line 1 "MultiTenancyMiddlewareErrorPage.cshtml"
+using System.Globalization;
+
+#line default
+#line hidden
+#nullable disable
+#nullable restore
+#line 2 "MultiTenancyMiddlewareErrorPage.cshtml"
+using Volo.Abp.AspNetCore.MultiTenancy.Views;
+
+#line default
+#line hidden
+#nullable disable
+#nullable restore
+#line 3 "MultiTenancyMiddlewareErrorPage.cshtml"
+using Volo.Abp.AspNetCore.RazorViews;
+
+#line default
+#line hidden
+#nullable disable
+ internal class MultiTenancyMiddlewareErrorPage : AbpCompilationRazorPageBase
+ {
+ #pragma warning disable 1998
+ public async override global::System.Threading.Tasks.Task ExecuteAsync()
+ {
+#nullable restore
+#line 5 "MultiTenancyMiddlewareErrorPage.cshtml"
+
+ Response.ContentType = "text/html; charset=utf-8";
+ Response.StatusCode = 404;
+
+#line default
+#line hidden
+#nullable disable
+ WriteLiteral("\n");
+ WriteLiteral("\n\n \n \n ");
+#nullable restore
+#line 22 "MultiTenancyMiddlewareErrorPage.cshtml"
+ Write(HtmlEncoder.Encode(Model.Message));
+
+#line default
+#line hidden
+#nullable disable
+ WriteLiteral("\n \n \n ");
+#nullable restore
+#line 25 "MultiTenancyMiddlewareErrorPage.cshtml"
+ Write(HtmlEncoder.Encode(Model.Message));
+
+#line default
+#line hidden
+#nullable disable
+ WriteLiteral("
\n ");
+#nullable restore
+#line 26 "MultiTenancyMiddlewareErrorPage.cshtml"
+ Write(HtmlEncoder.Encode(Model.Details));
+
+#line default
+#line hidden
+#nullable disable
+ WriteLiteral("
\n \n\n");
+ }
+ #pragma warning restore 1998
+#nullable restore
+#line 10 "MultiTenancyMiddlewareErrorPage.cshtml"
+
+ public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model)
+ {
+ Model = model;
+ }
+
+ public MultiTenancyMiddlewareErrorPageModel Model { get; set; }
+
+#line default
+#line hidden
+#nullable disable
+ }
+}
+#pragma warning restore 1591
diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.cshtml b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.cshtml
new file mode 100644
index 0000000000..847eb5884d
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.cshtml
@@ -0,0 +1,28 @@
+@using System.Globalization
+@using Volo.Abp.AspNetCore.MultiTenancy.Views
+@using Volo.Abp.AspNetCore.RazorViews
+@inherits AbpCompilationRazorPageBase
+@{
+ Response.ContentType = "text/html; charset=utf-8";
+ Response.StatusCode = 404;
+}
+
+@functions{
+ public MultiTenancyMiddlewareErrorPage(MultiTenancyMiddlewareErrorPageModel model)
+ {
+ Model = model;
+ }
+
+ public MultiTenancyMiddlewareErrorPageModel Model { get; set; }
+}
+
+
+
+
+ @HtmlEncoder.Encode(Model.Message)
+
+
+ @HtmlEncoder.Encode(Model.Message)
+ @HtmlEncoder.Encode(Model.Details)
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPageModel.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPageModel.cs
new file mode 100644
index 0000000000..bf314eb495
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPageModel.cs
@@ -0,0 +1,14 @@
+namespace Volo.Abp.AspNetCore.MultiTenancy.Views;
+
+public class MultiTenancyMiddlewareErrorPageModel
+{
+ public string Message { get; set; }
+
+ public string Details { get; set; }
+
+ public MultiTenancyMiddlewareErrorPageModel(string message, string details)
+ {
+ Message = message;
+ Details = details;
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj
index 1e27cbac3b..cdfd58b219 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo.Abp.AspNetCore.Mvc.csproj
@@ -35,4 +35,9 @@
+
+
+
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs
new file mode 100644
index 0000000000..9d30ea018c
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.Designer.cs
@@ -0,0 +1,50 @@
+//
+#pragma warning disable 1591
+namespace Volo.Abp.AspNetCore.RazorViews
+{
+ #line hidden
+ using System;
+ using System.Threading.Tasks;
+#nullable restore
+#line 1 "AbpMvcLibsErrorPage.cshtml"
+using Volo.Abp.AspNetCore.RazorViews;
+
+#line default
+#line hidden
+#nullable disable
+ internal class AbpMvcLibsErrorPage : AbpCompilationRazorPageBase
+ {
+ #pragma warning disable 1998
+ public async override global::System.Threading.Tasks.Task ExecuteAsync()
+ {
+#nullable restore
+#line 3 "AbpMvcLibsErrorPage.cshtml"
+
+ Response.ContentType = "text/html; charset=utf-8";
+ Response.StatusCode = 500;
+
+#line default
+#line hidden
+#nullable disable
+ WriteLiteral(@"
+
+
+
+ Error - The Libs folder is missing!
+
+
+ ⚠️ The Libs folder under the wwwroot/libs directory is empty!
+
+ The Libs folder contains mandatory NPM Packages for running the project.
+
+ Make sure you run the abp install-libs CLI tool command.
+
+ For more information, check out the ABP CLI documentation
+
+
+");
+ }
+ #pragma warning restore 1998
+ }
+}
+#pragma warning restore 1591
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml
new file mode 100644
index 0000000000..5a5d6c8d19
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsErrorPage.cshtml
@@ -0,0 +1,22 @@
+@using Volo.Abp.AspNetCore.RazorViews
+@inherits AbpCompilationRazorPageBase
+@{
+ Response.ContentType = "text/html; charset=utf-8";
+ Response.StatusCode = 500;
+}
+
+
+
+
+ Error - The Libs folder is missing!
+
+
+ ⚠️ The Libs folder under the wwwroot/libs directory is empty!
+
+ The Libs folder contains mandatory NPM Packages for running the project.
+
+ Make sure you run the abp install-libs CLI tool command.
+
+ For more information, check out the ABP CLI documentation
+
+
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs
index b59cbefa45..e806996ebb 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs
+++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Libs/AbpMvcLibsService.cs
@@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.FileProviders;
+using Volo.Abp.AspNetCore.RazorViews;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AspNetCore.Mvc.Libs;
@@ -35,22 +36,8 @@ public class AbpMvcLibsService : IAbpMvcLibsService, ITransientDependency
{
if (!await CheckLibsAsyncOnceAsync(httpContext))
{
- httpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
- httpContext.Response.ContentType = "text/html";
- await httpContext.Response.WriteAsync(
- "" +
- " " +
- " Error - The Libs folder is missing!" +
- " " +
- " " +
- " ⚠️ The Libs folder under the wwwroot/libs directory is empty!
" +
- " The Libs folder contains mandatory NPM Packages for running the project.
" +
- " Make sure you run the abp install-libs CLI tool command.
" +
- " For more information, check out the ABP CLI documentation
" +
- " " +
- "",
- Encoding.UTF8
- );
+ var errorPage = new AbpMvcLibsErrorPage();
+ await errorPage.ExecuteAsync(httpContext);
return;
}
diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AbpCompilationRazorPageBase.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AbpCompilationRazorPageBase.cs
new file mode 100644
index 0000000000..3308f06270
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AbpCompilationRazorPageBase.cs
@@ -0,0 +1,283 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace Volo.Abp.AspNetCore.RazorViews;
+
+public abstract class AbpCompilationRazorPageBase
+{
+ private readonly static Encoding UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+ private readonly static char[] NewLineChars = new[] { '\r', '\n' };
+ private readonly Stack _textWriterStack = new Stack();
+
+ ///
+ /// The request context
+ ///
+ protected HttpContext Context { get; private set; } = default!;
+
+ ///
+ /// The request
+ ///
+ protected HttpRequest Request { get; private set; } = default!;
+
+ ///
+ /// The response
+ ///
+ protected HttpResponse Response { get; private set; } = default!;
+
+ ///
+ /// The output stream
+ ///
+ protected TextWriter Output { get; private set; } = default!;
+
+ ///
+ /// Html encoder used to encode content.
+ ///
+ protected HtmlEncoder HtmlEncoder { get; set; } = HtmlEncoder.Default;
+
+ ///
+ /// Url encoder used to encode content.
+ ///
+ protected UrlEncoder UrlEncoder { get; set; } = UrlEncoder.Default;
+
+ ///
+ /// JavaScript encoder used to encode content.
+ ///
+ protected JavaScriptEncoder JavaScriptEncoder { get; set; } = JavaScriptEncoder.Default;
+
+ ///
+ /// Execute an individual request
+ ///
+ /// The stream to write to
+ public async Task ExecuteAsync(Stream stream)
+ {
+ // We technically don't need this intermediate buffer if this method accepts a memory stream.
+ var buffer = new MemoryStream();
+ Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true);
+ await ExecuteAsync();
+ await Output.FlushAsync();
+ await Output.DisposeAsync();
+ buffer.Seek(0, SeekOrigin.Begin);
+ await buffer.CopyToAsync(stream);
+ }
+
+ ///
+ /// Execute an individual request
+ ///
+ ///
+ public async Task ExecuteAsync(HttpContext context)
+ {
+ Context = context;
+ Request = Context.Request;
+ Response = Context.Response;
+ var buffer = new MemoryStream();
+ Output = new StreamWriter(buffer, UTF8NoBOM, 4096, leaveOpen: true);
+ await ExecuteAsync();
+ await Output.FlushAsync();
+ await Output.DisposeAsync();
+ buffer.Seek(0, SeekOrigin.Begin);
+ await buffer.CopyToAsync(Response.Body);
+ }
+
+ ///
+ /// Execute an individual request
+ ///
+ public abstract Task ExecuteAsync();
+
+ protected virtual void PushWriter(TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(writer);
+
+ _textWriterStack.Push(Output);
+ Output = writer;
+ }
+
+ protected virtual TextWriter PopWriter()
+ {
+ Output = _textWriterStack.Pop();
+ return Output;
+ }
+
+ ///
+ /// Write the given value without HTML encoding directly to .
+ ///
+ /// The to write.
+ protected void WriteLiteral(object value)
+ {
+ WriteLiteral(Convert.ToString(value, CultureInfo.InvariantCulture));
+ }
+
+ ///
+ /// Write the given value without HTML encoding directly to .
+ ///
+ /// The to write.
+ protected void WriteLiteral(string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ Output.Write(value);
+ }
+ }
+
+ private List? AttributeValues { get; set; }
+
+ protected void WriteAttributeValue(string thingy, int startPostion, object value, int endValue, int dealyo, bool yesno)
+ {
+ if (AttributeValues == null)
+ {
+ AttributeValues = new List();
+ }
+
+ AttributeValues.Add(value.ToString()!);
+ }
+
+ private string? AttributeEnding { get; set; }
+
+ protected void BeginWriteAttribute(string name, string beginning, int startPosition, string ending, int endPosition, int thingy)
+ {
+ Debug.Assert(string.IsNullOrEmpty(AttributeEnding));
+
+ Output.Write(beginning);
+ AttributeEnding = ending;
+ }
+
+ protected void EndWriteAttribute()
+ {
+ Debug.Assert(AttributeValues != null);
+ Debug.Assert(!string.IsNullOrEmpty(AttributeEnding));
+
+ var attributes = string.Join(" ", AttributeValues);
+ Output.Write(attributes);
+ AttributeValues = null;
+
+ Output.Write(AttributeEnding);
+ AttributeEnding = null;
+ }
+
+ ///
+ /// Writes the given attribute to the given writer
+ ///
+ /// The name of the attribute to write
+ /// The value of the prefix
+ /// The value of the suffix
+ /// The s to write.
+ protected void WriteAttribute(
+ string name,
+ string leader,
+ string trailer,
+ params AttributeValue[] values)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ ArgumentNullException.ThrowIfNull(leader);
+ ArgumentNullException.ThrowIfNull(trailer);
+
+ WriteLiteral(leader);
+ foreach (var value in values)
+ {
+ WriteLiteral(value.Prefix);
+
+ // The special cases here are that the value we're writing might already be a string, or that the
+ // value might be a bool. If the value is the bool 'true' we want to write the attribute name
+ // instead of the string 'true'. If the value is the bool 'false' we don't want to write anything.
+ // Otherwise the value is another object (perhaps an HtmlString) and we'll ask it to format itself.
+ string? stringValue;
+ if (value.Value is bool)
+ {
+ if ((bool)value.Value)
+ {
+ stringValue = name;
+ }
+ else
+ {
+ continue;
+ }
+ }
+ else
+ {
+ stringValue = value.Value as string;
+ }
+
+ // Call the WriteTo(string) overload when possible
+ if (value.Literal && stringValue != null)
+ {
+ WriteLiteral(stringValue);
+ }
+ else if (value.Literal)
+ {
+ WriteLiteral(value.Value);
+ }
+ else if (stringValue != null)
+ {
+ Write(stringValue);
+ }
+ else
+ {
+ Write(value.Value);
+ }
+ }
+ WriteLiteral(trailer);
+ }
+
+ ///
+ /// is invoked
+ ///
+ /// The to invoke
+ protected void Write(HelperResult result)
+ {
+ result.WriteTo(Output);
+ }
+
+ ///
+ /// Writes the specified to .
+ ///
+ /// The to write.
+ ///
+ /// is invoked for types.
+ /// For all other types, the encoded result of is written to
+ /// .
+ ///
+ protected void Write(object value)
+ {
+ if (value is HelperResult helperResult)
+ {
+ helperResult.WriteTo(Output);
+ }
+ else
+ {
+ Write(Convert.ToString(value, CultureInfo.InvariantCulture));
+ }
+ }
+
+ ///
+ /// Writes the specified with HTML encoding to .
+ ///
+ /// The to write.
+ protected void Write(string? value)
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ WriteLiteral(HtmlEncoder.Encode(value));
+ }
+ }
+
+ protected string HtmlEncodeAndReplaceLineBreaks(string input)
+ {
+ if (string.IsNullOrEmpty(input))
+ {
+ return string.Empty;
+ }
+
+ // Split on line breaks before passing it through the encoder.
+ return string.Join("
" + Environment.NewLine,
+ input.Split("\r\n", StringSplitOptions.None)
+ .SelectMany(s => s.Split(NewLineChars, StringSplitOptions.None))
+ .Select(HtmlEncoder.Encode));
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AttributeValue.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AttributeValue.cs
new file mode 100644
index 0000000000..0608e1a3fd
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/AttributeValue.cs
@@ -0,0 +1,34 @@
+using System;
+
+namespace Volo.Abp.AspNetCore.RazorViews;
+
+public class AttributeValue
+{
+ public AttributeValue(string prefix, object value, bool literal)
+ {
+ Prefix = prefix;
+ Value = value;
+ Literal = literal;
+ }
+
+ public string Prefix { get; }
+
+ public object Value { get; }
+
+ public bool Literal { get; }
+
+ public static AttributeValue FromTuple(Tuple value)
+ {
+ return new AttributeValue(value.Item1, value.Item2, value.Item3);
+ }
+
+ public static AttributeValue FromTuple(Tuple value)
+ {
+ return new AttributeValue(value.Item1, value.Item2, value.Item3);
+ }
+
+ public static implicit operator AttributeValue(Tuple value)
+ {
+ return FromTuple(value);
+ }
+}
diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/HelperResult.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/HelperResult.cs
new file mode 100644
index 0000000000..90a7288e78
--- /dev/null
+++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/RazorViews/HelperResult.cs
@@ -0,0 +1,19 @@
+using System;
+using System.IO;
+
+namespace Volo.Abp.AspNetCore.RazorViews;
+
+public class HelperResult
+{
+ public HelperResult(Action action)
+ {
+ WriteAction = action;
+ }
+
+ public Action WriteAction { get; }
+
+ public void WriteTo(TextWriter writer)
+ {
+ WriteAction(writer);
+ }
+}
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
index 6bd7405de0..48d756465f 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
+++ b/framework/src/Volo.Abp.Cli.Core/Volo.Abp.Cli.Core.csproj
@@ -24,6 +24,7 @@
+
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
index aae91511b3..7b249f0517 100644
--- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/AbpCliCoreModule.cs
@@ -70,6 +70,7 @@ public class AbpCliCoreModule : AbpModule
options.Commands[CliCommand.Name] = typeof(CliCommand);
options.Commands[ClearDownloadCacheCommand.Name] = typeof(ClearDownloadCacheCommand);
options.Commands[RecreateInitialMigrationCommand.Name] = typeof(RecreateInitialMigrationCommand);
+ options.Commands[GenerateRazorPage.Name] = typeof(GenerateRazorPage);
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Pro");
options.DisabledModulesToAddToSolution.Add("Volo.Abp.LeptonXTheme.Lite");
diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateRazorPage.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateRazorPage.cs
new file mode 100644
index 0000000000..7adcf0bc12
--- /dev/null
+++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/GenerateRazorPage.cs
@@ -0,0 +1,227 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Razor.Language;
+using Microsoft.AspNetCore.Razor.Language.Extensions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Volo.Abp.Cli.Args;
+using Volo.Abp.DependencyInjection;
+
+namespace Volo.Abp.Cli.Commands;
+
+public class GenerateRazorPage : IConsoleCommand, ITransientDependency
+{
+ public const string Name = "generate-razor-page";
+
+ public ILogger Logger { get; set; }
+
+ public GenerateRazorPage()
+ {
+ Logger = NullLogger.Instance;
+ }
+
+ public Task ExecuteAsync(CommandLineArgs commandLineArgs)
+ {
+ var targetProjectDirectory = Directory.GetCurrentDirectory();
+ var projectEngine = CreateProjectEngine(targetProjectDirectory);
+
+ var results = MainCore(projectEngine, targetProjectDirectory);
+
+ foreach (var result in results)
+ {
+ File.WriteAllText(result.FilePath, result.GeneratedCode);
+ }
+
+ Logger.LogInformation($"{results.Count} files successfully generated.");
+
+ return Task.CompletedTask;
+ }
+
+ public string GetUsageInfo()
+ {
+ var sb = new StringBuilder();
+
+ sb.AppendLine("");
+ sb.AppendLine("Usage:");
+ sb.AppendLine("abp generate-razor-page");
+ sb.AppendLine("");
+ sb.AppendLine("See the documentation for more info: https://abp.io/docs/latest/cli");
+
+ return sb.ToString();
+ }
+
+ public string GetShortDescription()
+ {
+ return "Generates code files for Razor page.";
+ }
+
+ private RazorProjectEngine CreateProjectEngine(string targetProjectDirectory, Action? configure = null)
+ {
+ var fileSystem = RazorProjectFileSystem.Create(targetProjectDirectory);
+ var projectEngine = RazorProjectEngine.Create(RazorConfiguration.Default, fileSystem, builder =>
+ {
+ builder
+ .SetNamespace("Volo.Abp.AspNetCore.RazorViews")
+ .ConfigureClass((document, @class) =>
+ {
+ @class.ClassName = Path.GetFileNameWithoutExtension(document.Source.FilePath);
+ @class.Modifiers.Clear();
+ @class.Modifiers.Add("internal");
+ });
+
+ SectionDirective.Register(builder);
+
+ builder.Features.Add(new SuppressChecksumOptionsFeature());
+ builder.Features.Add(new SuppressMetadataAttributesFeature());
+
+ if (configure != null)
+ {
+ configure(builder);
+ }
+
+ builder.AddDefaultImports(@"
+@using System
+@using System.Threading.Tasks");
+ });
+
+ return projectEngine;
+ }
+
+ private List MainCore(RazorProjectEngine projectEngine, string targetProjectDirectory)
+ {
+ var results = new List();
+ Logger.LogInformation("Generating code files for pages in {0}", targetProjectDirectory);
+
+ var cshtmlFiles = projectEngine.FileSystem.EnumerateItems(targetProjectDirectory)
+ .Where(x => File.ReadAllText(x.PhysicalPath).Contains("@inherits AbpCompilationRazorPageBase"))
+ .ToList();
+
+ if (!cshtmlFiles.Any())
+ {
+ Logger.LogInformation("No .cshtml or .razor files were found.");
+ return results;
+ }
+
+ foreach (var item in cshtmlFiles)
+ {
+ Logger.LogInformation(" Generating code file for page {0} ...", item.FileName);
+
+ results.Add(GenerateCodeFile(projectEngine, item));
+
+ Logger.LogInformation(" Done!");
+ }
+
+ return results;
+ }
+
+ private RazorPageGeneratorResult GenerateCodeFile(RazorProjectEngine projectEngine, RazorProjectItem projectItem)
+ {
+ var projectItemWrapper = new FileSystemRazorProjectItemWrapper(Logger, projectItem);
+ var codeDocument = projectEngine.Process(projectItemWrapper);
+ var cSharpDocument = codeDocument.GetCSharpDocument();
+ if (cSharpDocument.Diagnostics.Any())
+ {
+ var diagnostics = string.Join(Environment.NewLine, cSharpDocument.Diagnostics);
+ Logger.LogInformation($"One or more parse errors encountered. This will not prevent the generator from continuing: {Environment.NewLine}{diagnostics}.");
+ }
+
+ var generatedCodeFilePath = Path.ChangeExtension(projectItem.PhysicalPath, ".Designer.cs");
+ return new RazorPageGeneratorResult
+ {
+ FilePath = generatedCodeFilePath,
+ GeneratedCode = cSharpDocument.GeneratedCode,
+ };
+ }
+
+ private class SuppressChecksumOptionsFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
+ {
+ public int Order { get; set; }
+
+ public void Configure(RazorCodeGenerationOptionsBuilder options)
+ {
+ Check.NotNull(options, nameof(options));
+
+ options.SuppressChecksum = true;
+ }
+ }
+
+ private class SuppressMetadataAttributesFeature : RazorEngineFeatureBase, IConfigureRazorCodeGenerationOptionsFeature
+ {
+ public int Order { get; set; }
+
+ public void Configure(RazorCodeGenerationOptionsBuilder options)
+ {
+ Check.NotNull(options, nameof(options));
+ options.SuppressMetadataAttributes = true;
+ }
+ }
+
+ private class FileSystemRazorProjectItemWrapper : RazorProjectItem
+ {
+ private readonly ILogger _logger;
+ private readonly RazorProjectItem _source;
+
+ public FileSystemRazorProjectItemWrapper(ILogger logger, RazorProjectItem item)
+ {
+ _logger = logger;
+ _source = item;
+
+ // Mask the full name since we don't want a developer's local file paths to be committed.
+ PhysicalPath = $"{_source.FileName}";
+ }
+
+ public override string BasePath => _source.BasePath;
+
+ public override string FilePath => _source.FilePath;
+
+ public override string PhysicalPath { get; }
+
+ public override bool Exists => _source.Exists;
+
+ public override Stream Read()
+ {
+ var processedContent = ProcessFileIncludes();
+ return new MemoryStream(Encoding.UTF8.GetBytes(processedContent));
+ }
+
+ private string ProcessFileIncludes()
+ {
+ var basePath = Path.GetDirectoryName(_source.PhysicalPath);
+ var cshtmlContent = File.ReadAllText(_source.PhysicalPath);
+
+ var startMatch = "<%$ include: ";
+ var endMatch = " %>";
+ var startIndex = 0;
+ while (startIndex < cshtmlContent.Length)
+ {
+ startIndex = cshtmlContent.IndexOf(startMatch, startIndex, StringComparison.Ordinal);
+ if (startIndex == -1)
+ {
+ break;
+ }
+ var endIndex = cshtmlContent.IndexOf(endMatch, startIndex, StringComparison.Ordinal);
+ if (endIndex == -1)
+ {
+ throw new InvalidOperationException($"Invalid include file format in {_source.PhysicalPath}. Usage example: <%$ include: ErrorPage.js %>");
+ }
+ var includeFileName = cshtmlContent.Substring(startIndex + startMatch.Length, endIndex - (startIndex + startMatch.Length));
+ _logger.LogInformation(" Inlining file {0}", includeFileName);
+ var includeFileContent = File.ReadAllText(Path.Combine(basePath, includeFileName));
+ cshtmlContent = string.Concat(cshtmlContent.Substring(0, startIndex), includeFileContent, cshtmlContent.Substring(endIndex + endMatch.Length));
+ startIndex += includeFileContent.Length;
+ }
+ return cshtmlContent;
+ }
+ }
+
+ private class RazorPageGeneratorResult
+ {
+ public string FilePath { get; set; }
+
+ public string GeneratedCode { get; set; }
+ }
+}