Render View to String caching issue - c#

For my project I currently implemented a ViewRender for my asp.net core application. It generates Views without a controller to html, this works fine using the following code:
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
string viewgerendered = "";
try
{
using (var sw = new StringWriter())
{
var viewResult = _razorViewEngine.GetView(viewName, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
viewgerendered = sw.ToString();
return viewgerendered;
}
}
catch (Exception e)
{
object temp = e.Message + " - " + e.StackTrace;
return temp.ToString();
}
}
public Task RenderToStringAsync(string v)
{
throw new NotImplementedException();
}
}
Source: https://ppolyzos.com/2016/09/09/asp-net-core-render-view-to-string/
Changes which are made to views which use this renderer are not updated without restarting the application itself. Diving further into it, the views are cached. A comment within the source mentions using the _razorViewEngine.GetView method should get rid of my caching issue. However this doesn't work.
What I got, trying to figure out a way to register a new ViewRender, with a slight modification of the ViewRenderService.
//Seems not to be available on asp.net core 2.0...
services.AddMvc().Configure<MvcViewOptions>(options =>
{
options.ViewEngines.Clear();
options.ViewEngines.Add(typeof(CustomViewEngine));
});
And to overload the RazorViewEngine to expose the ViewLookupCache, where supposedly the view cache is located.
public class CustomViewEngine : RazorViewEngine
{
public CustomViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
Microsoft.AspNetCore.Razor.Language.RazorProject razorProject,
ILoggerFactory loggerFactory,
System.Diagnostics.DiagnosticSource diagnosticSource) :
base(pageFactory, pageActivator, htmlEncoder, optionsAccessor,razorProject,loggerFactory, diagnosticSource){ }
public void RemoveCachedView(string view)
{
this.ViewLookupCache.Remove(view);
}
}
There's not a lot to find on how caching is done within asp.net core 2.0 for views and clearing a particular view / set of. Basically I want to find a way how I can flush an entire selection of cached views as a command, for performance reasons.
Edit 13-04-2018
As suggested by K Finley, I tried emptying the ViewLookupCache as suggested. The code in short;
In my startup.cs ConfigureServices (not entirely sure if this is how a custom viewengine is registered).
services.AddSingleton<IRazorViewEngine, CustomViewEngine>();
services.AddSingleton<IViewRenderService, ViewRenderService>();
The custom view engine:
public class CustomViewEngine : RazorViewEngine
{
public CustomViewEngine(
IRazorPageFactoryProvider pageFactory,
IRazorPageActivator pageActivator,
HtmlEncoder htmlEncoder,
IOptions<RazorViewEngineOptions> optionsAccessor,
Microsoft.AspNetCore.Razor.Language.RazorProject razorProject,
ILoggerFactory loggerFactory,
System.Diagnostics.DiagnosticSource diagnosticSource) :
base(pageFactory, pageActivator, htmlEncoder, optionsAccessor, razorProject, loggerFactory, diagnosticSource)
{ }
public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null)
{
var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal));
base.ViewLookupCache.Remove(key);
}
public void RemoveViewFromCache(string viewName, bool isLayout)
{
//Code uses this one
var key = new ViewLocationCacheKey(viewName, isLayout);
base.ViewLookupCache.Remove(key);
}
}
And modified the original ViewRenderService...
public class ViewRenderService : IViewRenderService
{
private CustomViewEngine _razorViewEngine;
private ITempDataProvider _tempDataProvider;
private IServiceProvider _serviceProvider;
private IHostingEnvironment _hostingEnvironment;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider,
IHostingEnvironment hostingEnvironment)
{
_razorViewEngine = (CustomViewEngine)razorViewEngine;
...
try
{
using (var sw = new StringWriter())
{
_razorViewEngine.RemoveViewFromCache(viewName, false);
var viewResult = _razorViewEngine.GetView(viewName, viewName, false);
These modifications do delete the ViewLookupCache using the second method. However it still doesn't properly update my views. I do have to note the views don't have their own controller.

You need to enable file watcher:
Add the environment variable DOTNET_USE_POLLING_FILE_WATCHER=true or ENV DOTNET_USE_POLLING_FILE_WATCHER=true in the Dockerfile.

If you look at the RazorViewEngine source you can see how views are cached. I'll explain how to remove a view from the cache but you'll need some knowledge about the views in order to make it work.
In your example you're looking for the view in the cache based on just a name (I'm assuming the view name). This won't work because you need to look for the view in the cache using a ViewLocationCacheKey. There are 2 constructors on the ViewLocationCacheKey struct.
public ViewLocationCacheKey(
string viewName,
bool isMainPage)
public ViewLocationCacheKey(
string viewName,
string controllerName,
string areaName,
string pageName,
bool isMainPage,
IReadOnlyDictionary<string, string> values)
In the RazorViewEngine each of these are called depending on how the view is loaded (FromPath or FromViewLocations). With my situation I have a CMS that has full Razor support where any kind of content is just a a Razor View that gets rendered through the RazorViewEngine and therefore cached in Memory. When an update is made to a piece of content then I remove that view from the ViewLookupCache and allow it to get repopulated on the next load. Unfortunately the RazorViewEngine doesn't (currently) allow you to swap out the Cache to a distributed caching option (Redis, Memcached, etc.).
Here's how I'm handle it on my custom view engine.
public void RemoveViewFromCache(string viewName, string controller, bool isLayout, bool isPartial = false, string pageName = null, string areaName = null) {
var key = new ViewLocationCacheKey(viewName, controller, areaName, pageName, !isLayout | !isPartial, isLayout ? null : new Dictionary<string, string>(StringComparer.Ordinal))
base.ViewLookupCache.Remove(key);
}
I remove the view when an update is made and let the view get reloaded and cached naturally the next time it's requested.
If your app is doing anything with ViewLocationExpanderValues you'll have to make changes. Just inspect the ViewLookupCache collection while the app is running and you'll start to get a feel for what's going on here.

In ASP.NET Core version 2.2 and greater you can do,
services.Configure<RazorViewEngineOptions>(opts => opts.AllowRecompilingViewsOnFileChange = true);
In ConfigureServices to force view engine to recompile view when it changes.

Related

Change the JSON deserialization/serialization policy for single ASP.NET Core controller

I have a controller that I use for a third party API, which uses a snake case naming convention. The rest of my controllers are used for my own app, which uses a camelcase JSON convention. I'd like to automatically deserialize and serialize my models from/to snake case for the API in that one controller. This question explains how to use a snake case naming policy for JSON in the entire app, but is there a way that I can specify to use the naming policy only for that single controller?
I've seen Change the JSON serialization settings of a single ASP.NET Core controller which suggests using an ActionFilter, but that only helps for ensuring that outgoing JSON is serialized properly. How can I get the incoming JSON deserialized to my models properly as well? I know that I can use [JsonPropertyName] on the model property names but I'd prefer to be able to set something at the controller level, not at the model level.
The solution on the shared link in your question is OK for serialization (implemented by IOutputFormatter) although we may have another approach by extending it via other extensibility points.
Here I would like to focus on the missing direction (the deserializing direction which is implemented by IInputFormatter). You can implement a custom IModelBinder but it requires you to reimplement the BodyModelBinder and BodyModelBinderProvider which is not easy. Unless you accept to clone all the source code of them and modify the way you want. That's not very friendly to maintainability and getting up-to-date to what changed by the framework.
After researching through the source code, I've found that it's not easy to find a point where you can customize the deserializing behavior based on different controllers (or actions). Basically the default implementation uses a one-time init IInputFormatter for json (default by JsonInputFormatter for asp.net core < 3.0). That in chain will share one instance of JsonSerializerSettings. In your scenario, actually you need multiple instances of that settings (for each controller or action). The easiest point I think is to customize an IInputFormatter (extending the default JsonInputFormatter). It becomes more complicated when the default implementation uses ObjectPool for the instance of JsonSerializer (which is associated with a JsonSerializerSettings). To follow that style of pooling the objects (for better performance), you need a list of object pools (we will use a dictionary here) instead of just one object pool for the shared JsonSerializer as well as the associated JsonSerializerSettings (as implemented by the default JsonInputFormatter).
The point here is to based on the current InputFormatterContext, you need to build the corresponding JsonSerializerSettings as well as the JsonSerializer to be used. That sounds simple but once it comes to a full implementation (with fairly complete design), the code is not short at all. I've designed it into multiple classes. If you really want to see it working, just be patient to copy the code carefully (of course reading it through to understand is recommended). Here's all the code:
public abstract class ContextAwareSerializerJsonInputFormatter : JsonInputFormatter
{
public ContextAwareSerializerJsonInputFormatter(ILogger logger,
JsonSerializerSettings serializerSettings,
ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
PoolProvider = objectPoolProvider;
}
readonly AsyncLocal<InputFormatterContext> _currentContextAsyncLocal = new AsyncLocal<InputFormatterContext>();
readonly AsyncLocal<ActionContext> _currentActionAsyncLocal = new AsyncLocal<ActionContext>();
protected InputFormatterContext CurrentContext => _currentContextAsyncLocal.Value;
protected ActionContext CurrentAction => _currentActionAsyncLocal.Value;
protected ObjectPoolProvider PoolProvider { get; }
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context, encoding);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
_currentContextAsyncLocal.Value = context;
_currentActionAsyncLocal.Value = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>().ActionContext;
return base.ReadRequestBodyAsync(context);
}
protected virtual JsonSerializer CreateJsonSerializer(InputFormatterContext context) => null;
protected override JsonSerializer CreateJsonSerializer()
{
var context = CurrentContext;
return (context == null ? null : CreateJsonSerializer(context)) ?? base.CreateJsonSerializer();
}
}
public abstract class ContextAwareMultiPooledSerializerJsonInputFormatter : ContextAwareSerializerJsonInputFormatter
{
public ContextAwareMultiPooledSerializerJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions)
: base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, ObjectPool<JsonSerializer>> _serializerPools = new ConcurrentDictionary<object, ObjectPool<JsonSerializer>>();
readonly AsyncLocal<object> _currentPoolKeyAsyncLocal = new AsyncLocal<object>();
protected object CurrentPoolKey => _currentPoolKeyAsyncLocal.Value;
protected abstract object GetSerializerPoolKey(InputFormatterContext context);
protected override JsonSerializer CreateJsonSerializer(InputFormatterContext context)
{
object poolKey = GetSerializerPoolKey(context) ?? "";
if(!_serializerPools.TryGetValue(poolKey, out var pool))
{
//clone the settings
var serializerSettings = new JsonSerializerSettings();
foreach(var prop in typeof(JsonSerializerSettings).GetProperties().Where(e => e.CanWrite))
{
prop.SetValue(serializerSettings, prop.GetValue(SerializerSettings));
}
ConfigureSerializerSettings(serializerSettings, poolKey, context);
pool = PoolProvider.Create(new JsonSerializerPooledPolicy(serializerSettings));
_serializerPools[poolKey] = pool;
}
_currentPoolKeyAsyncLocal.Value = poolKey;
return pool.Get();
}
protected override void ReleaseJsonSerializer(JsonSerializer serializer)
{
if(_serializerPools.TryGetValue(CurrentPoolKey ?? "", out var pool))
{
pool.Return(serializer);
}
}
protected virtual void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context) { }
}
//there is a similar class like this implemented by the framework
//but it's a pity that it's internal
//So we define our own class here (which is exactly the same from the source code)
//It's quite simple like this
public class JsonSerializerPooledPolicy : IPooledObjectPolicy<JsonSerializer>
{
private readonly JsonSerializerSettings _serializerSettings;
public JsonSerializerPooledPolicy(JsonSerializerSettings serializerSettings)
{
_serializerSettings = serializerSettings;
}
public JsonSerializer Create() => JsonSerializer.Create(_serializerSettings);
public bool Return(JsonSerializer serializer) => true;
}
public class ControllerBasedJsonInputFormatter : ContextAwareMultiPooledSerializerJsonInputFormatter,
IControllerBasedJsonSerializerSettingsBuilder
{
public ControllerBasedJsonInputFormatter(ILogger logger, JsonSerializerSettings serializerSettings, ArrayPool<char> charPool, ObjectPoolProvider objectPoolProvider, MvcOptions options, MvcJsonOptions jsonOptions) : base(logger, serializerSettings, charPool, objectPoolProvider, options, jsonOptions)
{
}
readonly IDictionary<object, Action<JsonSerializerSettings>> _configureSerializerSettings
= new Dictionary<object, Action<JsonSerializerSettings>>();
readonly HashSet<object> _beingAppliedConfigurationKeys = new HashSet<object>();
protected override object GetSerializerPoolKey(InputFormatterContext context)
{
var routeValues = context.HttpContext.GetRouteData()?.Values;
var controllerName = routeValues == null ? null : routeValues["controller"]?.ToString();
if(controllerName != null && _configureSerializerSettings.ContainsKey(controllerName))
{
return controllerName;
}
var actionContext = CurrentAction;
if (actionContext != null && actionContext.ActionDescriptor is ControllerActionDescriptor actionDesc)
{
foreach (var attr in actionDesc.MethodInfo.GetCustomAttributes(true)
.Concat(actionDesc.ControllerTypeInfo.GetCustomAttributes(true)))
{
var key = attr.GetType();
if (_configureSerializerSettings.ContainsKey(key))
{
return key;
}
}
}
return null;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames)
{
foreach(var controllerName in controllerNames ?? Enumerable.Empty<string>())
{
_beingAppliedConfigurationKeys.Add((controllerName ?? "").ToLowerInvariant());
}
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
public IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>()
{
_beingAppliedConfigurationKeys.Add(typeof(T));
return this;
}
ControllerBasedJsonInputFormatter IControllerBasedJsonSerializerSettingsBuilder.WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer)
{
if (configurer == null) throw new ArgumentNullException(nameof(configurer));
foreach(var key in _beingAppliedConfigurationKeys)
{
_configureSerializerSettings[key] = configurer;
}
_beingAppliedConfigurationKeys.Clear();
return this;
}
protected override void ConfigureSerializerSettings(JsonSerializerSettings serializerSettings, object poolKey, InputFormatterContext context)
{
if (_configureSerializerSettings.TryGetValue(poolKey, out var configurer))
{
configurer.Invoke(serializerSettings);
}
}
}
public interface IControllerBasedJsonSerializerSettingsBuilder
{
ControllerBasedJsonInputFormatter WithSerializerSettingsConfigurer(Action<JsonSerializerSettings> configurer);
IControllerBasedJsonSerializerSettingsBuilder ForControllers(params string[] controllerNames);
IControllerBasedJsonSerializerSettingsBuilder ForControllersWithAttribute<T>();
IControllerBasedJsonSerializerSettingsBuilder ForActionsWithAttribute<T>();
}
To help conveniently configure the services to replace the default JsonInputFormatter, we have the following code:
public class ControllerBasedJsonInputFormatterMvcOptionsSetup : IConfigureOptions<MvcOptions>
{
private readonly ILoggerFactory _loggerFactory;
private readonly MvcJsonOptions _jsonOptions;
private readonly ArrayPool<char> _charPool;
private readonly ObjectPoolProvider _objectPoolProvider;
public ControllerBasedJsonInputFormatterMvcOptionsSetup(
ILoggerFactory loggerFactory,
IOptions<MvcJsonOptions> jsonOptions,
ArrayPool<char> charPool,
ObjectPoolProvider objectPoolProvider)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (jsonOptions == null)
{
throw new ArgumentNullException(nameof(jsonOptions));
}
if (charPool == null)
{
throw new ArgumentNullException(nameof(charPool));
}
if (objectPoolProvider == null)
{
throw new ArgumentNullException(nameof(objectPoolProvider));
}
_loggerFactory = loggerFactory;
_jsonOptions = jsonOptions.Value;
_charPool = charPool;
_objectPoolProvider = objectPoolProvider;
}
public void Configure(MvcOptions options)
{
//remove the default
options.InputFormatters.RemoveType<JsonInputFormatter>();
//add our own
var jsonInputLogger = _loggerFactory.CreateLogger<ControllerBasedJsonInputFormatter>();
options.InputFormatters.Add(new ControllerBasedJsonInputFormatter(
jsonInputLogger,
_jsonOptions.SerializerSettings,
_charPool,
_objectPoolProvider,
options,
_jsonOptions));
}
}
public static class ControllerBasedJsonInputFormatterServiceCollectionExtensions
{
public static IServiceCollection AddControllerBasedJsonInputFormatter(this IServiceCollection services,
Action<ControllerBasedJsonInputFormatter> configureFormatter)
{
if(configureFormatter == null)
{
throw new ArgumentNullException(nameof(configureFormatter));
}
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
return services.ConfigureOptions<ControllerBasedJsonInputFormatterMvcOptionsSetup>()
.PostConfigure<MvcOptions>(o => {
var jsonInputFormatter = o.InputFormatters.OfType<ControllerBasedJsonInputFormatter>().FirstOrDefault();
if(jsonInputFormatter != null)
{
configureFormatter(jsonInputFormatter);
}
});
}
}
//This attribute is used as a marker to decorate any controllers
//or actions that you want to apply your custom input formatter
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class UseSnakeCaseJsonInputFormatterAttribute : Attribute
{
}
Finally here's a sample configuration code:
//inside Startup.ConfigureServices
services.AddControllerBasedJsonInputFormatter(formatter => {
formatter.ForControllersWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.ForActionsWithAttribute<UseSnakeCaseJsonInputFormatterAttribute>()
.WithSerializerSettingsConfigurer(settings => {
var contractResolver = settings.ContractResolver as DefaultContractResolver ?? new DefaultContractResolver();
contractResolver.NamingStrategy = new SnakeCaseNamingStrategy();
settings.ContractResolver = contractResolver;
});
});
Now you can use the marker attribute UseSnakeCaseJsonInputFormatterAttribute on any controllers (or action methods) that you want to apply the snake-case json input formatter, like this:
[UseSnakeCaseJsonInputFormatter]
public class YourController : Controller {
//...
}
Note that the code above uses asp.net core 2.2, for asp.net core 3.0+, you can replace the JsonInputFormatter with NewtonsoftJsonInputFormatter and MvcJsonOptions with MvcNewtonsoftJsonOptions.
Warning: this doesn't work
I've been trying to create an attribute which would re-read the body of the request and return a bad result:
public class OnlyValidParametersAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (context.HttpContext.Request.ContentType.Contains("application/json"))
{
context.HttpContext.Request.EnableBuffering();
StreamReader stream = new StreamReader(context.HttpContext.Request.Body);
string body = await stream.ReadToEndAsync(); // Always set as "".
try
{
JsonConvert.DeserializeObject(
body,
context.ActionDescriptor.Parameters.FirstOrDefault()!.ParameterType,
new JsonSerializerSettings
{
MissingMemberHandling = MissingMemberHandling.Error,
}
);
}
catch (MissingMemberException e) // Unsure if this is the right exception to catch.
{
context.Result = new UnprocessableEntityResult();
}
base.OnActionExecuting(context);
}
await next();
}
}
However, in my .NET Core 3.1 application, body is always an empty string.
I'd return Content with json serialised with desired settings:
var serializeOptions = new JsonSerializerOptions
{
...
};
return Content(JsonSerializer.Serialize(data, options), "application/json");
For multiple methods I'd create a helper method.

How can I test a controller that uses identity with a readonly property?

I have the following code:
[Route("resources/avatar")]
[ApiController]
public class AvatarController : ControllerBase
{
private readonly ApplicationDbContext database;
private readonly IWebHostEnvironment environment;
private readonly IUserManagerWrapper userManagerWrapper;
public AvatarController(IUserManagerWrapper userManagerWrapper, IWebHostEnvironment environment,
ApplicationDbContext database)
{
this.userManagerWrapper = userManagerWrapper;
this.environment = environment;
this.database = database;
}
[HttpGet]
[Route("")]
public async Task<IActionResult> Index()
{
if (User == null) return DefaultImage();
var user = await this.userManagerWrapper.GetUserAsync(User);
if ((user?.Avatar?.Length ?? 0) == 0) return DefaultImage();
return File(user.Avatar, "image/jpeg");
}
}
I have an issue with testing this Index Page.
User is a property that comes from ControllerBase and is of type ClaimsPrincipal.
I used a wrapper where I would wrap the usermanager and then use an interface that I would mock.
The problem with this approach is I cannot set this ClaimsPrincipal to null because it is a read-only.
This was my test:
[TestFixture]
public class AvatarController_Should
{
[Test]
public async Task IndexReturnsDefaultImage()
{
var hostingEnvironmentMock = new Mock<IWebHostEnvironment>();
var dabataseName = nameof(IndexReturnsDefaultImage);
var options = AvatarTestUtil.GetOptions(dabataseName);
var userManagerWrapperMock = new Mock<IUserManagerWrapper>();
using (var actAndAssertContext = new ApplicationDbContext(options))
{
var sut = new AvatarController(userManagerWrapperMock.Object, hostingEnvironmentMock.Object, actAndAssertContext);
}
}
}
public class AvatarTestUtil
{
public static DbContextOptions<ApplicationDbContext> GetOptions(string databaseName)
{
var serviceCollection = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
return new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName)
.UseInternalServiceProvider(serviceCollection)
.Options;
}
}
}
I am open to using a completely new approach.
This was how I used to do test on the identity before, but I am stuck now.
Looking at the source code for ControllerBase, we can see User is defined as so
public ClaimsPrincipal User => HttpContext?.User;
So the User actually comes from HttpContext. But HttpContext is readonly as well. Digging into the source code deeper, though, we can see that HttpContext is derived from ControllerContext
public HttpContext HttpContext => ControllerContext.HttpContext;
Alas! ControllerContext actually has a setter in the concrete implementation!
public ControllerContext ControllerContext { get; set; }
We could set up a whole new ControllerContext here if we wanted. But we really just need ControllerContext.User. Luckily, that has a setter too. Since you only really need to set the User, we can do so directly here rather than newing up another ControllerContext.
using (var actAndAssertContext = new ApplicationDbContext(options))
{
var sut = new AvatarController(userManagerWrapperMock.Object, hostingEnvironmentMock.Object, actAndAssertContext);
sut.ControllerContext.HttpContext.User = null;
}

How to setup a controller manually [duplicate]

I would like to use Razor as a templating engine in a .NET console application that I'm writing in .NET Core.
The standalone Razor engines I've come across (RazorEngine, RazorTemplates) all require full .NET. I'm looking for a solution that works with .NET Core.
Here is a sample code that only depends on Razor (for parsing and C# code generation) and Roslyn (for C# code compilation, but you could use the old CodeDom as well).
There is no MVC in that piece of code, so, no View, no .cshtml files, no Controller, just Razor source parsing and compiled runtime execution. There is still the notion of Model though.
You will only need to add following nuget packages: Microsoft.AspNetCore.Razor.Language (tested with v5.0.5), Microsoft.AspNetCore.Razor.Runtime (tested with v2.2.0) and Microsoft.CodeAnalysis.CSharp (tested with v3.9.0) nugets.
This C# source code is compatible with .NET 5, NETCore 3.1 (for older versions check this answer's history), NETStandard 2 and .NET Framework. To test it just create a .NET framework or .NET core console app, paste it, add the nugets, and create the hello.txt file by hand (it must be located aside the executables).
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...
}
}
}
Recently I've created a library called RazorLight.
It has no redundant dependencies, like ASP.NET MVC parts and can be used in console applications. For now it only supports .NET Core (NetStandard1.6) - but that's exactly what you need.
Here is a short example:
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" });
For anyone in 2021+ here:
I've started https://github.com/adoconnection/RazorEngineCore
It has latest ASP.NET Core 5 Razor and it's syntax features.
Usage is quite the same as RazorEngine:
RazorEngine razorEngine = new RazorEngine();
RazorEngineCompiledTemplate template = razorEngine.Compile("Hello #Model.Name");
string result = template.Run(new
{
Name = "Alex"
});
Console.WriteLine(result);
Fast saving and loading
// 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);
There's a working example for .NET Core 1.0 at aspnet/Entropy/samples/Mvc.RenderViewToString. Since this might change or go away, I'll detail the approach I'm using in my own applications here.
Tl;dr - Razor works really well outside of MVC! This approach can handle more complex rendering scenarios like partial views and injecting objects into views as well, although I'll just demonstrate a simple example below.
The core service looks like this:
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());
}
}
}
A simple test console app just needs to initialize the service (and some supporting services), and call it:
Program.cs
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.PlatformAbstractions;
namespace RenderRazorToString
{
public class Program
{
public static void Main()
{
// Initialize the necessary services
var services = new ServiceCollection();
ConfigureDefaultServices(services);
var provider = services.BuildServiceProvider();
var renderer = provider.GetRequiredService<RazorViewToStringRenderer>();
// Build a model and render a view
var model = new EmailViewModel
{
UserName = "User",
SenderName = "Sender"
};
var emailContent = renderer.RenderViewToString("EmailTemplate", model).GetAwaiter().GetResult();
Console.WriteLine(emailContent);
Console.ReadLine();
}
private static void ConfigureDefaultServices(IServiceCollection services)
{
var applicationEnvironment = PlatformServices.Default.Application;
services.AddSingleton(applicationEnvironment);
var appDirectory = Directory.GetCurrentDirectory();
var environment = new HostingEnvironment
{
WebRootFileProvider = new PhysicalFileProvider(appDirectory),
ApplicationName = "RenderRazorToString"
};
services.AddSingleton<IHostingEnvironment>(environment);
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new PhysicalFileProvider(appDirectory));
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
var diagnosticSource = new DiagnosticListener("Microsoft.AspNetCore");
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddLogging();
services.AddMvc();
services.AddSingleton<RazorViewToStringRenderer>();
}
}
}
This assumes that you have a view model class:
EmailViewModel.cs
namespace RenderRazorToString
{
public class EmailViewModel
{
public string UserName { get; set; }
public string SenderName { get; set; }
}
}
And layout and view files:
Views/_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>
Here is a class to get Nate's answer working as a scoped service in an ASP.NET Core 2.0 project.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Routing;
namespace YourNamespace.Services
{
public class ViewRender : IViewRender
{
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRender(
IRazorViewEngine viewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderAsync(string name)
{
return await RenderAsync<object>(name, null);
}
public async Task<string> RenderAsync<TModel>(string name, TModel model)
{
var actionContext = GetActionContext();
var viewEngineResult = _viewEngine.FindView(actionContext, name, false);
if (!viewEngineResult.Success)
{
throw new InvalidOperationException(string.Format("Couldn't find view '{0}'", name));
}
var view = viewEngineResult.View;
using (var output = new StringWriter())
{
var viewContext = new ViewContext(
actionContext,
view,
new ViewDataDictionary<TModel>(
metadataProvider: new EmptyModelMetadataProvider(),
modelState: new ModelStateDictionary())
{
Model = model
},
new TempDataDictionary(
actionContext.HttpContext,
_tempDataProvider),
output,
new HtmlHelperOptions());
await view.RenderAsync(viewContext);
return output.ToString();
}
}
private ActionContext GetActionContext()
{
var httpContext = new DefaultHttpContext {RequestServices = _serviceProvider};
return new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
}
}
public interface IViewRender
{
Task<string> RenderAsync(string name);
Task<string> RenderAsync<TModel>(string name, TModel model);
}
}
In Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRender, ViewRender>();
}
In a controller
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();
}
}
If you are in 2022, there's an easy to use library called Razor.Templating.Core.
It works out of the box for MVC, API, Console and many other types of applications.
Supports .NET Core 3.1, .NET 5, .NET 6
Supports most of Razor features like ViewModel, ViewBag, ViewData, TagHelpers, Partial Views, ViewComponents and more
Supports Single File Publish, ReadyToRun
Usage is much simpler:
var htmlString = await RazorTemplateEngine.RenderAsync("/Views/ExampleView.cshtml", model, viewData);
Refer documentation here
P.S: I'm the author of this library.
I spent several days fiddling with razor light, but it has a number of deficiencies such as not having html helpers (#Html.*) or url helpers, and other quirks.
Here is a solution that is encapsulated for usage outside of an mvc app. It does require package references to aspnet core and mvc, but those are easy to add to a service or console application. No controllers or web server are needed. RenderToStringAsync is the method to call to render a view to a string.
The advantage is that you can write your views the same way you would in a .net core web project. You can use the same #Html and other helper functions and methods.
You can replace or add to the physical file provider in the razor view options setup with your own custom provider to load views from database, web service call, etc. Tested with .net core 2.2 on Windows and Linux.
Please note that your .csproj file must have this as the top line:
<Project Sdk="Microsoft.NET.Sdk.Web">
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ObjectPool;
namespace RazorRendererNamespace
{
/// <summary>
/// Renders razor pages with the absolute minimum setup of MVC, easy to use in console application, does not require any other classes or setup.
/// </summary>
public class RazorRenderer : ILoggerFactory, ILogger
{
private class ViewRenderService : IDisposable, ITempDataProvider, IServiceProvider
{
private static readonly System.Net.IPAddress localIPAddress = System.Net.IPAddress.Parse("127.0.0.1");
private readonly Dictionary<string, object> tempData = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
private readonly IRazorViewEngine _viewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ViewRenderService(IRazorViewEngine viewEngine,
IHttpContextAccessor httpContextAccessor,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_viewEngine = viewEngine;
_httpContextAccessor = httpContextAccessor;
_tempDataProvider = tempDataProvider ?? this;
_serviceProvider = serviceProvider ?? this;
}
public void Dispose()
{
}
public async Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
{
HttpContext httpContext;
if (_httpContextAccessor?.HttpContext != null)
{
httpContext = _httpContextAccessor.HttpContext;
}
else
{
DefaultHttpContext defaultContext = new DefaultHttpContext { RequestServices = _serviceProvider };
defaultContext.Connection.RemoteIpAddress = localIPAddress;
httpContext = defaultContext;
}
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using (var sw = new StringWriter())
{
var viewResult = _viewEngine.FindView(actionContext, viewName, isMainPage);
if (viewResult.View == null)
{
viewResult = _viewEngine.GetView("~/", viewName, isMainPage);
}
if (viewResult.View == null)
{
return null;
}
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
if (viewBag != null)
{
foreach (KeyValuePair<string, object> kv in (viewBag as IDictionary<string, object>))
{
viewDictionary.Add(kv.Key, kv.Value);
}
}
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
object IServiceProvider.GetService(Type serviceType)
{
return null;
}
IDictionary<string, object> ITempDataProvider.LoadTempData(HttpContext context)
{
return tempData;
}
void ITempDataProvider.SaveTempData(HttpContext context, IDictionary<string, object> values)
{
}
}
private readonly string rootPath;
private readonly ServiceCollection services;
private readonly ServiceProvider serviceProvider;
private readonly ViewRenderService viewRenderer;
public RazorRenderer(string rootPath)
{
this.rootPath = rootPath;
services = new ServiceCollection();
ConfigureDefaultServices(services);
serviceProvider = services.BuildServiceProvider();
viewRenderer = new ViewRenderService(serviceProvider.GetRequiredService<IRazorViewEngine>(), null, null, serviceProvider);
}
private void ConfigureDefaultServices(IServiceCollection services)
{
var environment = new HostingEnvironment
{
WebRootFileProvider = new PhysicalFileProvider(rootPath),
ApplicationName = typeof(RazorRenderer).Assembly.GetName().Name,
ContentRootPath = rootPath,
WebRootPath = rootPath,
EnvironmentName = "DEVELOPMENT",
ContentRootFileProvider = new PhysicalFileProvider(rootPath)
};
services.AddSingleton<IHostingEnvironment>(environment);
services.Configure<RazorViewEngineOptions>(options =>
{
options.FileProviders.Clear();
options.FileProviders.Add(new PhysicalFileProvider(rootPath));
});
services.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.AddSingleton<ILoggerFactory>(this);
var diagnosticSource = new DiagnosticListener(environment.ApplicationName);
services.AddSingleton<DiagnosticSource>(diagnosticSource);
services.AddMvc();
}
public void Dispose()
{
}
public Task<string> RenderToStringAsync<TModel>(string viewName, TModel model, ExpandoObject viewBag = null, bool isMainPage = false)
{
return viewRenderer.RenderToStringAsync(viewName, model, viewBag, isMainPage);
}
void ILoggerFactory.AddProvider(ILoggerProvider provider)
{
}
IDisposable ILogger.BeginScope<TState>(TState state)
{
throw new NotImplementedException();
}
ILogger ILoggerFactory.CreateLogger(string categoryName)
{
return this;
}
bool ILogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel)
{
return false;
}
void ILogger.Log<TState>(Microsoft.Extensions.Logging.LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
}
}
}

.NET Core 2.1 - Execute ControllerB.Action from ControllerA.Action returning a String

I have a system that generates emails. The content of parts of these emails is derived from ParialViews.
In my EmailController I want to be able to obtain the HTML snippet(s) that form areas of the email template by calling their relevant actions and inserting the View HTML.
I've seen the post(s) here (Return View as String in .NET Core) regarding it but it doesn't quite work...
The solution below does find and attempt to get the View HTML but it doesn't actually attempt to execute the View's controller action.
As the View is dynamic this results in an error.
So, can I execute ControllerB.Action retruning it's result as a string from within ControllerA.Action?
My current attempt:
The Service:
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, object model);
}
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IServiceProvider _serviceProvider;
public ViewRenderService(IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IServiceProvider serviceProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_serviceProvider = serviceProvider;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var httpContext = new DefaultHttpContext { RequestServices = _serviceProvider };
var actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
using (var sw = new StringWriter())
{
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
var viewDictionary = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary())
{
Model = model
};
var viewContext = new ViewContext(
actionContext,
viewResult.View,
viewDictionary,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
}
Starup.cs
services.AddScoped<IViewRenderService, ViewRenderService>();
Controller A
private readonly IViewRenderService _viewRenderService;
public ReportController(...
IViewRenderService viewRenderService)
{
...
_viewRenderService = viewRenderService;
}
The call from Controller A:
var theHtmlString = await _viewRenderService.RenderToStringAsync("ControllerB/Action1", new { parameters = paramsmodel });

Handle validation failure in exception filter, and re-render view

I'm using ASP.NET Core and FluentValidation.
When a POST action receives invalid input, it's customary to re-render the input form view, with validation errors:
if (!ModelState.IsValid)
return View("nameOfViewRenderedByGetAction", model);
But my validation is actually performed in a service, by FluentValidation, which throws ValidationException. I want to handle it in an exception filter:
public class ValidationFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// only handle ValidationException
var ex = context.Exception as ValidationException;
if (ex == null) return;
// re-render get action's view, or redirect to get action
// ??
}
}
I'm stuck at the "??" part, because Core has changed the signatures of many types, and ExceptionContext doesn't surface the data I need to make this work.
How do I do this?
It's a little late for an answer but I have a working solution for exactly the same application design. I use ASP.NET Core 3.0 and FluentValidation 8.x.
public class MvcValidationExceptionFilterAttribute : ExceptionFilterAttribute
{
private IModelMetadataProvider ModelMetadataProvider { get; }
public MvcValidationExceptionFilterAttribute(IModelMetadataProvider modelMetadataProvider)
{
ModelMetadataProvider = modelMetadataProvider;
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Framework calls without null")]
public override void OnException(ExceptionContext context)
{
if (context.Exception is ValidationException ex)
{
var validationResult = new ValidationResult(ex.Errors);
validationResult.AddToModelState(context.ModelState, null);
context.Result = new ViewResult { ViewData = new ViewDataDictionary(ModelMetadataProvider, context.ModelState) };
context.ExceptionHandled = true;
}
}
}
As this filter has a dependency we can't use the Attribute directly but register it with dependency injection in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MvcValidationExceptionFilterAttribute>();
To use the ExceptionFilter either apply it via the ServiceFilterAttribute:
[ServiceFilter(typeof(MvcValidationExceptionFilterAttribute))]
public class MyController : Controller
{
Or apply it globally in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add<MvcValidationExceptionFilterAttribute>();
})
From an exception filter, You can render a custom view by setting the context result.
public class ValidationFilterAttribute : ActionFilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext context)
{
// only handle ValidationException
var ex = context.Exception as ValidationException;
if (ex == null) return;
// re-render get action's view, or redirect to get action
var result = new ViewResult { ViewName = "GetView" }
context.HttpContext.Response.Clear();
context.Result = result;
}
}
Where GetView should be the name of your Get action's view.
Sample exception filter that uses a custom developer error view to display details about exceptions.
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IModelMetadataProvider _modelMetadataProvider;
public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
_modelMetadataProvider = modelMetadataProvider;
}
public override void OnException(ExceptionContext context)
{
if (!_hostingEnvironment.IsDevelopment())
{
// do nothing
return;
}
var result = new ViewResult {ViewName = "CustomError"};
result.ViewData = new ViewDataDictionary(_modelMetadataProvider,context.ModelState);
result.ViewData.Add("Exception", context.Exception);
// TODO: Pass additional detailed data via ViewData
context.Result = result;
}
}
Note that the above code is sending the context, model state and exception to the view.
In case all you need is custom error page refer to ASP.NET Core Error Handling
Generally, you should not be using an exception filter to turn an error into success. Consider using an action filter if you have a requirement like that.
Having said that, for some reason if you still need to redirect from an exception filter, this is how it can be done
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly IHostingEnvironment _hostingEnvironment;
public CustomExceptionFilterAttribute(
IHostingEnvironment hostingEnvironment,
IModelMetadataProvider modelMetadataProvider)
{
_hostingEnvironment = hostingEnvironment;
}
public override void OnException(ExceptionContext context)
{
if (!_hostingEnvironment.IsDevelopment())
{
// do nothing
return;
}
var result = new RedirectToRouteResult(
new RouteValueDictionary(new { controller = "Home", action = "Error" }));
context.Result = result;
}
}

Categories