Is there a way to generate Urls with WebAPI? - c#
POST EDITED BELOW
We can't figure out why UrlHelper is returning null strings when used from the WebApi controller's context.
We've done the neccesary debugging but we can't find out why this happens, the RouteData has the routes in it but it doesn't seem to be working.
For the most part, we use a RenderViewToString function, which loads views that consist of calls to Url.RouteUrl(routeName).
Something that's been tried is creating a custom UrlHelper (but to no avail) and debugging with either UrlHelper's (MVC / HTTP).
Attribute routing is used everywhere with route names.
Example usage code:
public class WebApiController : BaseApiController
{
[HttpPost]
[ResponseType(typeof(string))]
[Route("cart/get/checkout", Name = "api.cart.get.checkout")]
public IHttpActionResult GetCheckOutShoppingCart([FromBody] string data)
{
return Ok(RenderViewToString("CartController", "_CheckOutCartPartial", new ShoppingCartModel(Auth.IsAuthenticated ? Auth.GetCustomer().DefaultShippingInfo.CountryId : 148)
{
AddInsurance = false,
InsuredShipping = insuredShipping,
CurrentDeliveryMethodId = deliveryMethodId,
CurrentPaymentMethodId = paymentMethodId
}));
}
}
BaseApiController class:
public class BaseApiController : ApiController
{
public static string RenderViewToString(string controllerName, string viewName)
{
return RenderViewToString(controllerName, viewName, new Dictionary<string, object>());
}
public static string RenderViewToString(string controllerName, string viewName, object model)
{
using (var writer = new StringWriter())
{
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext =
new ControllerContext(
new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null),
new HttpResponse(null))), routeData, new FakeController());
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View,
new ViewDataDictionary(model), new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
public static string RenderViewToString(string controllerName, string viewName, Dictionary<string, Object> data)
{
using (var writer = new StringWriter())
{
var viewData = new ViewDataDictionary();
foreach (var kv in data)
{
viewData[kv.Key] = kv.Value;
}
var routeData = new RouteData();
routeData.Values.Add("controller", controllerName);
var fakeControllerContext =
new ControllerContext(
new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null),
new HttpResponse(null))), routeData, new FakeController());
var razorViewEngine = new RazorViewEngine();
var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false);
var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, viewData,
new TempDataDictionary(), writer);
razorViewResult.View.Render(viewContext, writer);
return writer.ToString();
}
}
private class FakeController : ControllerBase
{
protected override void ExecuteCore()
{
}
}
}
EDIT
We've put together a class that should in theory work, but it doesn't.
The RouteData has both the MVC and API routes in the collection.
public static class Url
{
public static bool IsWebApiRequest()
{
return
HttpContext.Current.Request.RequestContext.HttpContext.CurrentHandler is
System.Web.Http.WebHost.HttpControllerHandler;
}
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
if (IsWebApiRequest())
{
var helper = new System.Web.Http.Routing.UrlHelper();
url = helper.Link(routeName, routeValues);
}
else
{
var helper = new System.Web.Mvc.UrlHelper();
url = helper.RouteUrl(routeName, routeValues);
}
return url;
}
catch
{
return url;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
if (IsWebApiRequest())
{
var helper = new System.Web.Http.Routing.UrlHelper();
url = helper.Link(routeName, routeValues);
}
else
{
var helper = new System.Web.Mvc.UrlHelper();
url = helper.HttpRouteUrl(routeName, routeValues);
}
return url;
}
catch
{
return url;
}
}
}
This issue has been resolved by building a custom UrlHelper class, which looks into the RouteTable and then returns the url with the patterns replaced.
public static class Link
{
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var route = (Route)RouteTable.Routes[routeName];
if (route == null)
return url;
url = "~/".AbsoluteUrl() + route.Url;
url = url.Replace("{culture}", System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.ToLower());
if (routeValues == null)
return url;
var values = routeValues.GetType().GetProperties();
Array.ForEach(values, pi => url = Regex.Replace(url, "{" + pi.Name + "}", pi.GetValue(routeValues, null).ToString()));
return url;
}
catch
{
var newUrl = RouteUrl("403");
if(newUrl == String.Empty)
throw;
return newUrl;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
return RouteUrl(routeName, routeValues);
}
}
Try Uri.Link(routeName, object)
var uri = Url.Link("api.cart.get.checkout", new { id = 1 });
This will inject the given object's properties into the route if the property name matches a route parameter.
WebApi is not MVC. Even though it looks very similar, it's a completely different system.
UrlHelper (which is MVC) will be looking at the MVC route table, and ignore any WebApi routes.
Try creating the route the hard way, essentially hard-coding the controller and action names into the views. :-(
OK, based on your comment, it seems that you're trying to call your Url.RouteUrl(string routeName, object routeValues = null) or Url.HttpRouteUrl(string routeName, object routeValues = null) from your MVC layout.cshtml file.
If that's the case, the Url.IsWebApiRequest() would return false because the layout.cshtml file is only processed as part of handling MVC request and not WebAPI. That will cause the url generation methods to use MVC route collection instead of the WebAPI collection.
Change your Url class so that RouteUrl always builds WebAPI url and HttpRouteUrl only builds MVC url then in your layout file use proper method based on which kind of url you need in the particular context.
public static class Url
{
public static bool IsWebApiRequest()
{
return
HttpContext.Current.Request.RequestContext.HttpContext.CurrentHandler is
System.Web.Http.WebHost.HttpControllerHandler;
}
public static string RouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var helper = new System.Web.Http.Routing.UrlHelper();
return helper.Link(routeName, routeValues);
}
catch
{
return url;
}
}
public static string HttpRouteUrl(string routeName, object routeValues = null)
{
var url = String.Empty;
try
{
var helper = new System.Web.Mvc.UrlHelper();
return helper.HttpRouteUrl(routeName, routeValues);
}
catch
{
return url;
}
}
}
Related
Getting MVC5 Views as string, with nested hosting
I have the following code that is executed by Hangfire (there is no HttpContext) and works perfectly when I run it locally: class FakeController : ControllerBase { protected override void ExecuteCore() { } public static string RenderViewToString(string controllerName, string viewName, object viewData) { using (var writer = new StringWriter()) { var routeData = new RouteData(); routeData.Values.Add("controller", controllerName); var fakeControllerContext = new ControllerContext( new HttpContextWrapper( new HttpContext(new HttpRequest(null, "http://nopage.no", null) , new HttpResponse(null)) ), routeData, new FakeController() ); var razorViewEngine = new RazorViewEngine(); var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false); var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer); razorViewResult.View.Render(viewContext, writer); return writer.ToString(); } } } The way we have our application set up however is as follows: https://application.net <- One application https://application.net/admin <- The other application that this code runs on. When I run the code on https://application.net/admin, I get the following exception: System.ArgumentException: The virtual path '/' maps to another application, which is not allowed. It occurs at this line: razorViewEngine.FindView(fakeControllerContext, viewName, "", false). I tried creating my own RazorViewEngine and overrode some of the methods to find the views, to no avail. class MyViewEngine : RazorViewEngine { protected override bool FileExists(ControllerContext controllerContext, string virtualPath) { if (!base.FileExists(controllerContext, virtualPath)) return base.FileExists(controllerContext, "~/../admin" + virtualPath.TrimStart('~')); return true; } protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) { var newViewPath = viewPath; if (!base.FileExists(controllerContext, viewPath)) newViewPath = "~/../admin/" + viewPath.TrimStart('~'); return base.CreateView(controllerContext, newViewPath, masterPath); } } This failed because it did not want to let me out of the "upmost part of the directory tree". Is there an easy fix for this, or an alternate way of creating strings from razor views? - The purpose is to create email templates. There will be many emails created, and I don't want to use a HttpClient to ask my own endpoint to create it.
I was able to replicated issue. httpContext was not getting set correctly. Change code this way and no need to override RazorViewEngine: static string GetUrlRoot() { var httpContext = HttpContext.Current; if (httpContext == null) { return "http://localhost"; } return httpContext.Request.Url.GetLeftPart(UriPartial.Authority) + httpContext.Request.ApplicationPath; } public static string RenderViewToString(string controllerName, string viewName, object viewData) { using (var writer = new StringWriter()) { var routeData = new RouteData(); routeData.Values.Add("controller", controllerName); var fakeControllerContext = new ControllerContext( new HttpContextWrapper( new HttpContext(new HttpRequest(null, GetUrlRoot(), null) , new HttpResponse(null)) ), routeData, new FakeController() ); var razorViewEngine = new RazorViewEngine(); var razorViewResult = razorViewEngine.FindView(fakeControllerContext, viewName, "", false); var viewContext = new ViewContext(fakeControllerContext, razorViewResult.View, new ViewDataDictionary(viewData), new TempDataDictionary(), writer); razorViewResult.View.Render(viewContext, writer); return writer.ToString(); } }
After a lot of trying and failing I ended up abandoning my approach, using Rick Strahl's WestWind.RazorHosting library instead. Here's the most important part of the documentation I used. And here is how my code ended up looking at the end. class FakeController : IDisposable { private readonly RazorFolderHostContainer _host; public FakeController() { _host = new RazorFolderHostContainer { TemplatePath = $#"{HostingEnvironment.ApplicationPhysicalPath}Views\EmailTemplate\" }; _host.AddAssemblyFromType(typeof(Controller)); _host.AddAssemblyFromType(typeof(EmailTemplateController.TestViewModel)); _host.Start(); } public string RenderViewToString(string viewName, object viewData) { return _host.RenderTemplate($#"~/{viewName}.cshtml", viewData); } public void Dispose() { _host.Stop(); } } This looks for views in my EmailTemplate folder that's placed in Views, finding *.cshtml files that I want to use. Since it gets them from the Views folder, I'm still able to use the same views with the normal controllers, and hence it will work when I use the views in my endpoints too. Nifty.
Unit Test a HtmlHelper in ASP.net MVC
I have a HtmlHelper that returns the bootstrap selected class. I use this helper to apply the active state to my menu items. Helper Code public static string IsSelected(this HtmlHelper html, string controllers = "", string actions = "", string ccsClass = "selected") { var viewContext = html.ViewContext; var isChildAction = viewContext.Controller.ControllerContext.IsChildAction; if (isChildAction) { viewContext = html.ViewContext.ParentActionViewContext; } var routeValues = viewContext.RouteData.Values; var currentController = routeValues["controller"].ToString(); var currentAction = routeValues["action"].ToString(); if (string.IsNullOrEmpty(controllers)) { controllers = currentController; } if (string.IsNullOrEmpty(actions)) { actions = currentAction; } var acceptedActions = actions.Trim().Split(',').Distinct().ToArray(); var acceptedControllers = controllers.Trim().Split(',').Distinct().ToArray(); return acceptedControllers.Contains(currentController) && acceptedActions.Contains(currentAction) ? ccsClass : string.Empty; } Testing Code [Test] public void WhenPassedControllerAndActionMatchContextReturnSelectedClass() { var htmlHelper = CreateHtmlHelper(new ViewDataDictionary()); var result = htmlHelper.IsSelected("performance", "search"); Assert.AreEqual("selected", result); } public static HtmlHelper CreateHtmlHelper(ViewDataDictionary viewData) { var mocks = new MockRepository(); var controllerContext = mocks.DynamicMock<ControllerContext>( mocks.DynamicMock<HttpContextBase>(), new RouteData(), mocks.DynamicMock<ControllerBase>()); var viewContext = new ViewContext(controllerContext, mocks.StrictMock<IView>(), viewData, new TempDataDictionary(), mocks.StrictMock<TextWriter>()); //var mockViewContext = MockRepository.GenerateMock<ViewContext>(); var mockViewDataContainer = mocks.DynamicMock<IViewDataContainer>(); mockViewDataContainer.Expect(v => v.ViewData).Return(viewData); return new HtmlHelper(viewContext, mockViewDataContainer); } This is giving me an error. When I debug, I see that it is because ControllerContext is null on Line 5 of Helper code. What would be the flexible, correct way of testing that code?
I've noticed that your are using rhino-mocks but it possible (and really easy) to solve your problem using Typemock Isolater by faking the dependencies of HtmlHelper as shown in the following example: [TestMethod, Isolated] public void WhenPassedControllerAndActionMatchContextReturnSelectedClass() { var fakeHtmlHalper = Isolate.Fake.Dependencies<HtmlHelper>(); var fakeViewContext = Isolate.GetFake<ViewContext>(fakeHtmlHalper); Isolate.WhenCalled(() => fakeViewContext.RouteData.Values["controller"]).WillReturn("performance"); Isolate.WhenCalled(() => fakeViewContext.RouteData.Values["action"]).WillReturn("search"); var result = fakeHtmlHalper.IsSelected("performance", "search"); Assert.AreEqual("selected", result); }
Retrieve Instance of ActionExecutingContext
I would like to retrieve the instance of ActionExecutingContext inside of public ActionResult Contact2(string one, string two) and not in the class albumAttribute. Is it possible to do it? Thanks! [HttpPost] [album] public ActionResult Contact2(string one, string two) { ViewBag.Message = "Your contact page."; var ss = Response.Status; var genres = new List<Genre> { new Genre { Name = "Disco"}, new Genre { Name = "Jazz"}, new Genre { Name = "Rock"} }; //return View(genres); //return View("contact2", genres); return View("contact22", genres); } public class albumAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { HttpRequestBase req = filterContext.HttpContext.Request; HttpResponseBase res = filterContext.HttpContext.Response; UriBuilder uriBuilder = new UriBuilder("http://" + req.Url.Authority + req.Url.LocalPath); NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); query.Add("album", "first"); uriBuilder.Query = query.ToString(); string url = req.Url.AbsolutePath.ToString(); res.Redirect(uriBuilder.Uri.OriginalString); base.OnActionExecuting(filterContext); /* UriBuilder uriBuilder = new UriBuilder("http://" + req.Url.Authority + "/Home/About"); res.Redirect(uriBuilder.Uri.OriginalString); base.OnActionExecuting(filterContext); */ } }
Based on your comments: Action filters execute prior to Actions so inside an Action you won't be able to use base.OnActionExecuting(filterContext). Other than that all the code that's attached in the image could be executed without ActionExecutingContext object, just add it to your Action and for getting a Request and Response objects use Response and Request controller properties. You can also use return this.Redirect(yourUrl); instead of res.Redirect(...) [HttpPost] [album] public ActionResult Contact2(string one, string two) { var req = this.Request; var res = this.Response; UriBuilder uriBuilder = new UriBuilder("http://" + req.Url.Authority + req.Url.LocalPath); NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); query.Add("album", "first"); uriBuilder.Query = query.ToString(); string url = req.Url.AbsolutePath.ToString(); return this.Redirect(uriBuilder.Uri.OriginalString); }
Output buffering in ASP.NET MVC [duplicate]
I want to output two different views (one as a string that will be sent as an email), and the other the page displayed to a user. Is this possible in ASP.NET MVC beta? I've tried multiple examples: 1. RenderPartial to String in ASP.NET MVC Beta If I use this example, I receive the "Cannot redirect after HTTP headers have been sent.". 2. MVC Framework: Capturing the output of a view If I use this, I seem to be unable to do a redirectToAction, as it tries to render a view that may not exist. If I do return the view, it is completely messed up and doesn't look right at all. Does anyone have any ideas/solutions to these issues i have, or have any suggestions for better ones? Many thanks! Below is an example. What I'm trying to do is create the GetViewForEmail method: public ActionResult OrderResult(string ref) { //Get the order Order order = OrderService.GetOrder(ref); //The email helper would do the meat and veg by getting the view as a string //Pass the control name (OrderResultEmail) and the model (order) string emailView = GetViewForEmail("OrderResultEmail", order); //Email the order out EmailHelper(order, emailView); return View("OrderResult", order); } Accepted answer from Tim Scott (changed and formatted a little by me): public virtual string RenderViewToString( ControllerContext controllerContext, string viewPath, string masterPath, ViewDataDictionary viewData, TempDataDictionary tempData) { Stream filter = null; ViewPage viewPage = new ViewPage(); //Right, create our view viewPage.ViewContext = new ViewContext(controllerContext, new WebFormView(viewPath, masterPath), viewData, tempData); //Get the response context, flush it and get the response filter. var response = viewPage.ViewContext.HttpContext.Response; response.Flush(); var oldFilter = response.Filter; try { //Put a new filter into the response filter = new MemoryStream(); response.Filter = filter; //Now render the view into the memorystream and flush the response viewPage.ViewContext.View.Render(viewPage.ViewContext, viewPage.ViewContext.HttpContext.Response.Output); response.Flush(); //Now read the rendered view. filter.Position = 0; var reader = new StreamReader(filter, response.ContentEncoding); return reader.ReadToEnd(); } finally { //Clean up. if (filter != null) { filter.Dispose(); } //Now replace the response filter response.Filter = oldFilter; } } Example usage Assuming a call from the controller to get the order confirmation email, passing the Site.Master location. string myString = RenderViewToString(this.ControllerContext, "~/Views/Order/OrderResultEmail.aspx", "~/Views/Shared/Site.Master", this.ViewData, this.TempData);
Here's what I came up with, and it's working for me. I added the following method(s) to my controller base class. (You can always make these static methods somewhere else that accept a controller as a parameter I suppose) MVC2 .ascx style protected string RenderViewToString<T>(string viewPath, T model) { ViewData.Model = model; using (var writer = new StringWriter()) { var view = new WebFormView(ControllerContext, viewPath); var vdd = new ViewDataDictionary<T>(model); var viewCxt = new ViewContext(ControllerContext, view, vdd, new TempDataDictionary(), writer); viewCxt.View.Render(viewCxt, writer); return writer.ToString(); } } Razor .cshtml style public string RenderRazorViewToString(string viewName, object model) { ViewData.Model = model; using (var sw = new StringWriter()) { var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName); var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw); viewResult.View.Render(viewContext, sw); viewResult.ViewEngine.ReleaseView(ControllerContext, viewResult.View); return sw.GetStringBuilder().ToString(); } } Edit: added Razor code.
This answer is not on my way . This is originally from https://stackoverflow.com/a/2759898/2318354 but here I have show the way to use it with "Static" Keyword to make it common for all Controllers . For that you have to make static class in class file . (Suppose your Class File Name is Utils.cs ) This example is For Razor. Utils.cs public static class RazorViewToString { public static string RenderRazorViewToString(this Controller controller, string viewName, object model) { controller.ViewData.Model = model; using (var sw = new StringWriter()) { var viewResult = ViewEngines.Engines.FindPartialView(controller.ControllerContext, viewName); var viewContext = new ViewContext(controller.ControllerContext, viewResult.View, controller.ViewData, controller.TempData, sw); viewResult.View.Render(viewContext, sw); viewResult.ViewEngine.ReleaseView(controller.ControllerContext, viewResult.View); return sw.GetStringBuilder().ToString(); } } } Now you can call this class from your controller by adding NameSpace in your Controller File as following way by passing "this" as parameter to Controller. string result = RazorViewToString.RenderRazorViewToString(this ,"ViewName", model); As suggestion given by #Sergey this extension method can also call from cotroller as given below string result = this.RenderRazorViewToString("ViewName", model); I hope this will be useful to you make code clean and neat.
This works for me: public virtual string RenderView(ViewContext viewContext) { var response = viewContext.HttpContext.Response; response.Flush(); var oldFilter = response.Filter; Stream filter = null; try { filter = new MemoryStream(); response.Filter = filter; viewContext.View.Render(viewContext, viewContext.HttpContext.Response.Output); response.Flush(); filter.Position = 0; var reader = new StreamReader(filter, response.ContentEncoding); return reader.ReadToEnd(); } finally { if (filter != null) { filter.Dispose(); } response.Filter = oldFilter; } }
I found a new solution that renders a view to string without having to mess with the Response stream of the current HttpContext (which doesn't allow you to change the response's ContentType or other headers). Basically, all you do is create a fake HttpContext for the view to render itself: /// <summary>Renders a view to string.</summary> public static string RenderViewToString(this Controller controller, string viewName, object viewData) { //Create memory writer var sb = new StringBuilder(); var memWriter = new StringWriter(sb); //Create fake http context to render the view var fakeResponse = new HttpResponse(memWriter); var fakeContext = new HttpContext(HttpContext.Current.Request, fakeResponse); var fakeControllerContext = new ControllerContext( new HttpContextWrapper(fakeContext), controller.ControllerContext.RouteData, controller.ControllerContext.Controller); var oldContext = HttpContext.Current; HttpContext.Current = fakeContext; //Use HtmlHelper to render partial view to fake context var html = new HtmlHelper(new ViewContext(fakeControllerContext, new FakeView(), new ViewDataDictionary(), new TempDataDictionary()), new ViewPage()); html.RenderPartial(viewName, viewData); //Restore context HttpContext.Current = oldContext; //Flush memory and return output memWriter.Flush(); return sb.ToString(); } /// <summary>Fake IView implementation used to instantiate an HtmlHelper.</summary> public class FakeView : IView { #region IView Members public void Render(ViewContext viewContext, System.IO.TextWriter writer) { throw new NotImplementedException(); } #endregion } This works on ASP.NET MVC 1.0, together with ContentResult, JsonResult, etc. (changing Headers on the original HttpResponse doesn't throw the "Server cannot set content type after HTTP headers have been sent" exception). Update: in ASP.NET MVC 2.0 RC, the code changes a bit because we have to pass in the StringWriter used to write the view into the ViewContext: //... //Use HtmlHelper to render partial view to fake context var html = new HtmlHelper( new ViewContext(fakeControllerContext, new FakeView(), new ViewDataDictionary(), new TempDataDictionary(), memWriter), new ViewPage()); html.RenderPartial(viewName, viewData); //...
This article describes how to render a View to a string in different scenarios: MVC Controller calling another of its own ActionMethods MVC Controller calling an ActionMethod of another MVC Controller WebAPI Controller calling an ActionMethod of an MVC Controller The solution/code is provided as a class called ViewRenderer. It is part of Rick Stahl's WestwindToolkit at GitHub. Usage (3. - WebAPI example): string html = ViewRenderer.RenderView("~/Areas/ReportDetail/Views/ReportDetail/Index.cshtml", ReportVM.Create(id));
If you want to forgo MVC entirely, thereby avoiding all the HttpContext mess... using RazorEngine; using RazorEngine.Templating; // For extension methods. string razorText = System.IO.File.ReadAllText(razorTemplateFileLocation); string emailBody = Engine.Razor.RunCompile(razorText, "templateKey", typeof(Model), model); This uses the awesome open source Razor Engine here: https://github.com/Antaris/RazorEngine
Additional tip for ASP NET CORE: Interface: public interface IViewRenderer { Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model); } Implementation: public class ViewRenderer : IViewRenderer { private readonly IRazorViewEngine viewEngine; public ViewRenderer(IRazorViewEngine viewEngine) => this.viewEngine = viewEngine; public async Task<string> RenderAsync<TModel>(Controller controller, string name, TModel model) { ViewEngineResult viewEngineResult = this.viewEngine.FindView(controller.ControllerContext, name, false); if (!viewEngineResult.Success) { throw new InvalidOperationException(string.Format("Could not find view: {0}", name)); } IView view = viewEngineResult.View; controller.ViewData.Model = model; await using var writer = new StringWriter(); var viewContext = new ViewContext( controller.ControllerContext, view, controller.ViewData, controller.TempData, writer, new HtmlHelperOptions()); await view.RenderAsync(viewContext); return writer.ToString(); } } Registration in Startup.cs ... services.AddSingleton<IViewRenderer, ViewRenderer>(); ... And usage in controller: public MyController: Controller { private readonly IViewRenderer renderer; public MyController(IViewRendere renderer) => this.renderer = renderer; public async Task<IActionResult> MyViewTest { var view = await this.renderer.RenderAsync(this, "MyView", model); return new OkObjectResult(view); } }
To render a view to a string in the Service Layer without having to pass ControllerContext around, there is a good Rick Strahl article here http://www.codemag.com/Article/1312081 that creates a generic controller. Code summary below: // Some Static Class public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false) { // first find the ViewEngine for this view ViewEngineResult viewEngineResult = null; if (partial) viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath); else viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); if (viewEngineResult == null) throw new FileNotFoundException("View cannot be found."); // get the view and attach the model to view data var view = viewEngineResult.View; context.Controller.ViewData.Model = model; string result = null; using (var sw = new StringWriter()) { var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); view.Render(ctx, sw); result = sw.ToString(); } return result; } // In the Service Class public class GenericController : Controller { } public static T CreateController<T>(RouteData routeData = null) where T : Controller, new() { // create a disconnected controller instance T controller = new T(); // get context wrapper from HttpContext if available HttpContextBase wrapper; if (System.Web.HttpContext.Current != null) wrapper = new HttpContextWrapper(System.Web.HttpContext.Current); else throw new InvalidOperationException("Cannot create Controller Context if no active HttpContext instance is available."); if (routeData == null) routeData = new RouteData(); // add the controller routing if not existing if (!routeData.Values.ContainsKey("controller") && !routeData.Values.ContainsKey("Controller")) routeData.Values.Add("controller", controller.GetType().Name.ToLower().Replace("controller", "")); controller.ControllerContext = new ControllerContext(wrapper, routeData, controller); return controller; } Then to render the View in the Service class: var stringView = RenderViewToString(CreateController<GenericController>().ControllerContext, "~/Path/To/View/Location/_viewName.cshtml", theViewModel, true);
you can get the view in string using this way protected string RenderPartialViewToString(string viewName, object model) { if (string.IsNullOrEmpty(viewName)) viewName = ControllerContext.RouteData.GetRequiredString("action"); if (model != null) ViewData.Model = model; using (StringWriter sw = new StringWriter()) { ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName); ViewContext viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw); viewResult.View.Render(viewContext, sw); return sw.GetStringBuilder().ToString(); } } We can call this method in two way string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", null) OR var model = new Person() string strView = RenderPartialViewToString("~/Views/Shared/_Header.cshtml", model)
I am using MVC 1.0 RTM and none of the above solutions worked for me. But this one did: Public Function RenderView(ByVal viewContext As ViewContext) As String Dim html As String = "" Dim response As HttpResponse = HttpContext.Current.Response Using tempWriter As New System.IO.StringWriter() Dim privateMethod As MethodInfo = response.GetType().GetMethod("SwitchWriter", BindingFlags.NonPublic Or BindingFlags.Instance) Dim currentWriter As Object = privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {tempWriter}, Nothing) Try viewContext.View.Render(viewContext, Nothing) html = tempWriter.ToString() Finally privateMethod.Invoke(response, BindingFlags.NonPublic Or BindingFlags.Instance Or BindingFlags.InvokeMethod, Nothing, New Object() {currentWriter}, Nothing) End Try End Using Return html End Function
I saw an implementation for MVC 3 and Razor from another website, it worked for me: public static string RazorRender(Controller context, string DefaultAction) { string Cache = string.Empty; System.Text.StringBuilder sb = new System.Text.StringBuilder(); System.IO.TextWriter tw = new System.IO.StringWriter(sb); RazorView view_ = new RazorView(context.ControllerContext, DefaultAction, null, false, null); view_.Render(new ViewContext(context.ControllerContext, view_, new ViewDataDictionary(), new TempDataDictionary(), tw), tw); Cache = sb.ToString(); return Cache; } public static string RenderRazorViewToString(string viewName, object model) { ViewData.Model = model; using (var sw = new StringWriter()) { var viewResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewName); var viewContext = new ViewContext(ControllerContext, viewResult.View, ViewData, TempData, sw); viewResult.View.Render(viewContext, sw); return sw.GetStringBuilder().ToString(); } } public static class HtmlHelperExtensions { public static string RenderPartialToString(ControllerContext context, string partialViewName, ViewDataDictionary viewData, TempDataDictionary tempData) { ViewEngineResult result = ViewEngines.Engines.FindPartialView(context, partialViewName); if (result.View != null) { StringBuilder sb = new StringBuilder(); using (StringWriter sw = new StringWriter(sb)) { using (HtmlTextWriter output = new HtmlTextWriter(sw)) { ViewContext viewContext = new ViewContext(context, result.View, viewData, tempData, output); result.View.Render(viewContext, output); } } return sb.ToString(); } return String.Empty; } } More on Razor render- MVC3 View Render to String
Quick tip For a strongly typed Model just add it to the ViewData.Model property before passing to RenderViewToString. e.g this.ViewData.Model = new OrderResultEmailViewModel(order); string myString = RenderViewToString(this.ControllerContext, "~/Views/Order/OrderResultEmail.aspx", "~/Views/Shared/Site.Master", this.ViewData, this.TempData);
To repeat from a more unknown question, take a look at MvcIntegrationTestFramework. It makes saves you writing your own helpers to stream result and is proven to work well enough. I'd assume this would be in a test project and as a bonus you would have the other testing capabilities once you've got this setup. Main bother would probably be sorting out the dependency chain. private static readonly string mvcAppPath = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + "\\..\\..\\..\\MyMvcApplication"); private readonly AppHost appHost = new AppHost(mvcAppPath); [Test] public void Root_Url_Renders_Index_View() { appHost.SimulateBrowsingSession(browsingSession => { RequestResult result = browsingSession.ProcessRequest(""); Assert.IsTrue(result.ResponseText.Contains("<!DOCTYPE html")); }); }
Here is a class I wrote to do this for ASP.NETCore RC2. I use it so I can generate html email using Razor. using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Routing; using System.IO; using System.Threading.Tasks; namespace cloudscribe.Web.Common.Razor { /// <summary> /// the goal of this class is to provide an easy way to produce an html string using /// Razor templates and models, for use in generating html email. /// </summary> public class ViewRenderer { public ViewRenderer( ICompositeViewEngine viewEngine, ITempDataProvider tempDataProvider, IHttpContextAccessor contextAccesor) { this.viewEngine = viewEngine; this.tempDataProvider = tempDataProvider; this.contextAccesor = contextAccesor; } private ICompositeViewEngine viewEngine; private ITempDataProvider tempDataProvider; private IHttpContextAccessor contextAccesor; public async Task<string> RenderViewAsString<TModel>(string viewName, TModel model) { var viewData = new ViewDataDictionary<TModel>( metadataProvider: new EmptyModelMetadataProvider(), modelState: new ModelStateDictionary()) { Model = model }; var actionContext = new ActionContext(contextAccesor.HttpContext, new RouteData(), new ActionDescriptor()); var tempData = new TempDataDictionary(contextAccesor.HttpContext, tempDataProvider); using (StringWriter output = new StringWriter()) { ViewEngineResult viewResult = viewEngine.FindView(actionContext, viewName, true); ViewContext viewContext = new ViewContext( actionContext, viewResult.View, viewData, tempData, output, new HtmlHelperOptions() ); await viewResult.View.RenderAsync(viewContext); return output.GetStringBuilder().ToString(); } } } }
I found a better way to render razor view page when I got error with the methods above, this solution for both web form environment and mvc environment. No controller is needed. Here is the code example, in this example I simulated a mvc action with an async http handler: /// <summary> /// Enables processing of HTTP Web requests asynchronously by a custom HttpHandler that implements the IHttpHandler interface. /// </summary> /// <param name="context">An HttpContext object that provides references to the intrinsic server objects.</param> /// <returns>The task to complete the http request.</returns> protected override async Task ProcessRequestAsync(HttpContext context) { if (this._view == null) { this.OnError(context, new FileNotFoundException("Can not find the mvc view file.".Localize())); return; } object model = await this.LoadModelAsync(context); WebPageBase page = WebPageBase.CreateInstanceFromVirtualPath(this._view.VirtualPath); using (StringWriter sw = new StringWriter()) { page.ExecutePageHierarchy(new WebPageContext(new HttpContextWrapper(context), page, model), sw); await context.Response.Output.WriteAsync(sw.GetStringBuilder().ToString()); } }
The easiest way for me was: public string GetFileAsString(string path) { var html = ""; FileStream fileStream = new FileStream(path, FileMode.Open); using (StreamReader reader = new StreamReader(fileStream)) { html += reader.ReadLine(); } return html; } I use this for emails and make sure that the file only contains CSS and HTML
Versioning ASP.NET Web API 2 with Media Types
I'm using ASP.NET Web API 2 with attribute routing but i can't seem to get the versioning using media types application/vnd.company[.version].param[+json] to work. I get the following error: The given key was not present in the dictionary. which originates from testing the key _actionParameterNames[descriptor] in FindActionMatchRequiredRouteAndQueryParameters() method. foreach (var candidate in candidatesFound) { HttpActionDescriptor descriptor = candidate.ActionDescriptor; if (IsSubset(_actionParameterNames[descriptor], candidate.CombinedParameterNames)) { matches.Add(candidate); } } Source: ApiControllerActionSelector.cs After further debugging I've realized that if you have two controllers [RoutePrefix("api/people")] public class PeopleController : BaseApiController { [Route("")] public HttpResponseMessage GetPeople() { } [Route("identifier/{id}")] public HttpResponseMessage GetPersonById() { } } [RoutePrefix("api/people")] public class PeopleV2Controller : BaseApiController { [Route("")] public HttpResponseMessage GetPeople() { } [Route("identifier/{id}")] public HttpResponseMessage GetPersonById() { } } you can't use your custom ApiVersioningSelector : DefaultHttpControllerSelector because it will test the keys,as stated above, from all controllers having the same [RoutePrefix("api/people")] and obviously an exception will be thrown. Just to be sure the right controller was selected I don't know if this is a bug, but using route [RoutePrefix("api/v1/people")] to version API makes me sad. NOTE: This works great without attribute routing. UPDATE public class ApiVersioningSelector : DefaultHttpControllerSelector { private HttpConfiguration _HttpConfiguration; public ApiVersioningSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration) { _HttpConfiguration = httpConfiguration; } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping(); var attributedRoutesData = request.GetRouteData().GetSubRoutes(); var subRouteData = attributedRoutesData.LastOrDefault(); //LastOrDefault() will get PeopleController, FirstOrDefault will get People{version}Controller which we don't want var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"]; var controllerName = actions[0].ControllerDescriptor.ControllerName; //For controller name without attribute routing //var controllerName = (string)routeData.Values["controller"]; HttpControllerDescriptor oldControllerDescriptor; if (controllers.TryGetValue(controllerName, out oldControllerDescriptor)) { var apiVersion = GetVersionFromMediaType(request); var newControllerName = String.Concat(controllerName, "V", apiVersion); HttpControllerDescriptor newControllerDescriptor; if (controllers.TryGetValue(newControllerName, out newControllerDescriptor)) { return newControllerDescriptor; } return oldControllerDescriptor; } return null; } private string GetVersionFromMediaType(HttpRequestMessage request) { var acceptHeader = request.Headers.Accept; var regularExpression = new Regex(#"application\/vnd\.mycompany\.([a-z]+)\.v([0-9]+)\+json", RegexOptions.IgnoreCase); foreach (var mime in acceptHeader) { var match = regularExpression.Match(mime.MediaType); if (match != null) { return match.Groups[2].Value; } } return "1"; } }
Thanks for the sharing your code. I have modified your version controller selector like below and tried some scenarios and it seems to work well. Can you try updating your controller selector like below and see if it works? public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { HttpControllerDescriptor controllerDescriptor = null; // get list of all controllers provided by the default selector IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping(); IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } //check if this route is actually an attribute route IEnumerable<IHttpRouteData> attributeSubRoutes = routeData.GetSubRoutes(); var apiVersion = GetVersionFromMediaType(request); if (attributeSubRoutes == null) { string controllerName = GetRouteVariable<string>(routeData, "controller"); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } string newControllerName = String.Concat(controllerName, "V", apiVersion); if (controllers.TryGetValue(newControllerName, out controllerDescriptor)) { return controllerDescriptor; } else { throw new HttpResponseException(HttpStatusCode.NotFound); } } else { // we want to find all controller descriptors whose controller type names end with // the following suffix(ex: CustomersV1) string newControllerNameSuffix = String.Concat("V", apiVersion); IEnumerable<IHttpRouteData> filteredSubRoutes = attributeSubRoutes.Where(attrRouteData => { HttpControllerDescriptor currentDescriptor = GetControllerDescriptor(attrRouteData); bool match = currentDescriptor.ControllerName.EndsWith(newControllerNameSuffix); if (match && (controllerDescriptor == null)) { controllerDescriptor = currentDescriptor; } return match; }); routeData.Values["MS_SubRoutes"] = filteredSubRoutes.ToArray(); } return controllerDescriptor; } private HttpControllerDescriptor GetControllerDescriptor(IHttpRouteData routeData) { return ((HttpActionDescriptor[])routeData.Route.DataTokens["actions"]).First().ControllerDescriptor; } // Get a value from the route data, if present. private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result = null; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); }