Skip to content

Commit bcc7d63

Browse files
committed
Add support for rendering embedded razor views
1 parent d44dedb commit bcc7d63

File tree

2 files changed

+142
-5
lines changed

2 files changed

+142
-5
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace ServiceStack.Mvc
5+
{
6+
public class DictionaryDynamicObject : System.Dynamic.DynamicObject
7+
{
8+
Dictionary<string, object> Object { get; }
9+
public DictionaryDynamicObject(Dictionary<string, object> obj) => Object = obj;
10+
11+
public override bool TryGetMember(System.Dynamic.GetMemberBinder binder, out object result)
12+
{
13+
if (!Object.TryGetValue(binder.Name, out var ret))
14+
throw new InvalidOperationException(binder.Name);
15+
16+
var modelType = ret?.GetType();
17+
if (modelType != null && (!modelType.IsPublic
18+
&& modelType.BaseType == typeof(object)
19+
&& modelType.DeclaringType == null))
20+
{
21+
result = new DictionaryDynamicObject(ret.ToObjectDictionary());
22+
}
23+
else
24+
{
25+
result = ret;
26+
}
27+
28+
return true;
29+
}
30+
}
31+
}

src/ServiceStack.Mvc/RazorFormat.cs

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
using System.IO;
77
using System.Linq;
88
using System.Net;
9+
using System.Reflection;
910
using System.Threading.Tasks;
1011
using Microsoft.AspNetCore.Html;
1112
using Microsoft.AspNetCore.Http;
12-
using Microsoft.AspNetCore.Mvc;
1313
using Microsoft.AspNetCore.Mvc.Abstractions;
1414
using Microsoft.AspNetCore.Mvc.ModelBinding;
1515
using Microsoft.AspNetCore.Mvc.Razor;
@@ -23,6 +23,7 @@
2323
using ServiceStack.Auth;
2424
using ServiceStack.Caching;
2525
using ServiceStack.Configuration;
26+
using ServiceStack.Host;
2627
using ServiceStack.Host.Handlers;
2728
using ServiceStack.Html;
2829
using ServiceStack.IO;
@@ -34,6 +35,7 @@
3435
using ServiceStack.VirtualPath;
3536
using ServiceStack.Web;
3637
using ServiceStack.Text;
38+
using ActionContext = Microsoft.AspNetCore.Mvc.ActionContext;
3739

3840
namespace ServiceStack.Mvc
3941
{
@@ -345,7 +347,23 @@ public async Task<bool> ProcessRequestAsync(IRequest req, object dto, Stream out
345347
return true;
346348
}
347349

348-
private ViewEngineResult FindView(IEnumerable<string> viewNames, out Dictionary<string, object> routingArgs)
350+
public IView GetViewPage(string path)
351+
{
352+
var viewEngineResult = FindView(new[] {path}, out _);
353+
if (viewEngineResult?.Success == true)
354+
return viewEngineResult.View;
355+
return null;
356+
}
357+
358+
public IView GetContentPage(string path)
359+
{
360+
var viewEngineResult = GetRoutingPage(path, out _);
361+
if (viewEngineResult?.Success == true)
362+
return viewEngineResult.View;
363+
return null;
364+
}
365+
366+
public ViewEngineResult FindView(IEnumerable<string> viewNames, out Dictionary<string, object> routingArgs)
349367
{
350368
routingArgs = null;
351369
const string execPath = "";
@@ -378,10 +396,21 @@ private ViewEngineResult FindView(IEnumerable<string> viewNames, out Dictionary<
378396

379397
internal static ViewDataDictionary CreateViewData<T>(T model)
380398
{
399+
if (model is ViewDataDictionary viewData)
400+
return viewData;
401+
402+
if (model != null && model.GetType().IsAnonymousType())
403+
{
404+
return new ViewDataDictionary(
405+
metadataProvider: new EmptyModelMetadataProvider(),
406+
modelState: new ModelStateDictionary()) {
407+
Model = new DictionaryDynamicObject(model.ToObjectDictionary())
408+
};
409+
}
410+
381411
return new ViewDataDictionary<T>(
382412
metadataProvider: new EmptyModelMetadataProvider(),
383-
modelState: new ModelStateDictionary())
384-
{
413+
modelState: new ModelStateDictionary()) {
385414
Model = model
386415
};
387416
}
@@ -396,7 +425,7 @@ internal async Task RenderView(IRequest req, Stream stream, ViewDataDictionary v
396425
new RouteData(),
397426
new ActionDescriptor());
398427

399-
var sw = new StreamWriter(stream);
428+
var sw = new StreamWriter(stream); // don't dispose of stream so other middleware can re-read / filter it
400429
{
401430
if (viewData == null)
402431
viewData = CreateViewData((object)null);
@@ -445,6 +474,83 @@ internal async Task RenderView(IRequest req, Stream stream, ViewDataDictionary v
445474
await req.Response.WriteErrorBody(ex);
446475
}
447476
}
477+
478+
public async Task<ReadOnlyMemory<char>> RenderToHtmlAsync(IView view, object model = null, string layout = null)
479+
{
480+
using var ms = MemoryStreamFactory.GetStream();
481+
await WriteHtmlAsync(ms, view, model, layout);
482+
return MemoryProvider.Instance.FromUtf8(ms.GetBufferAsSpan());
483+
}
484+
485+
public async Task WriteHtmlAsync(Stream stream, IView view, object model = null, string layout = null, HttpContext ctx = null, IRequest req = null)
486+
{
487+
if (view == null)
488+
throw new ArgumentNullException(nameof(view));
489+
490+
var razorView = view as RazorView;
491+
492+
try
493+
{
494+
if (ctx == null)
495+
{
496+
ctx = new DefaultHttpContext {
497+
RequestServices = HostContext.Container
498+
};
499+
}
500+
501+
var actionContext = new ActionContext(
502+
ctx,
503+
new RouteData(),
504+
new ActionDescriptor());
505+
506+
var sw = new StreamWriter(stream);
507+
{
508+
var viewData = CreateViewData(model);
509+
510+
// Use "_Layout" if unspecified
511+
if (razorView != null)
512+
razorView.RazorPage.Layout = layout ?? DefaultLayout;
513+
514+
// Allows Layout from being overridden in page with: Layout = Html.ResolveLayout("LayoutUnlessOverridden")
515+
if (layout != null)
516+
viewData["Layout"] = layout;
517+
518+
viewData[Keywords.IRequest] = req ?? new BasicRequest { PathInfo = view.Path };
519+
520+
var viewContext = new ViewContext(
521+
actionContext,
522+
view,
523+
viewData,
524+
new TempDataDictionary(actionContext.HttpContext, tempDataProvider),
525+
sw,
526+
new HtmlHelperOptions());
527+
528+
await view.RenderAsync(viewContext);
529+
530+
await sw.FlushAsync();
531+
532+
try
533+
{
534+
using (razorView?.RazorPage as IDisposable) { }
535+
}
536+
catch (Exception ex)
537+
{
538+
log.Warn("Error trying to dispose Razor View: " + ex.Message, ex);
539+
}
540+
}
541+
}
542+
catch (StopExecutionException) { }
543+
catch (Exception origEx)
544+
{
545+
var ex = origEx.UnwrapIfSingleException();
546+
if (ex is StopExecutionException)
547+
return;
548+
if (ex == origEx)
549+
throw;
550+
throw ex;
551+
}
552+
}
553+
448554
}
449555

450556
public class RazorHandler : ServiceStackHandlerBase

0 commit comments

Comments
 (0)