I am new .asp.net core. I am testing a controller that renders a view to a string and then utilises evo pdf to render the view.
All is working perfectly and I am also able to successfully test using postman.
However my test app errors when I use vs 2017 test explorer to debug my test (Xunit).
Searched Locations within the razor engine
The error occurs within my RenderViewToString method as my razor view engine is unable to locate the view to render. The paths searched to locate the views are as expected. Any guidance is appreciated.
//Unit Test Code
[Fact]
public async void GetPdf()
{
var response = await _client.PostAsJsonAsync<Common.DTO.Invoice>("/api/values/1", GetDummyData());
using (var file = System.IO.File.Create(#"c:\\Test" + DateTime.Now.ToString("yyyyyMMddHHmmss") + ".pdf"))
{
//create a new file to write to
await response.Content.CopyToAsync(file);
await file.FlushAsync(); // flush back to disk before disposing
}
}
//Render view to string service
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, ViewDataDictionary viewData);
}
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, ViewDataDictionary viewData)
{
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 viewContext = new ViewContext(
actionContext,
viewResult.View,
viewData,
new TempDataDictionary(actionContext.HttpContext, _tempDataProvider),
sw,
new HtmlHelperOptions()
);
await viewResult.View.RenderAsync(viewContext);
return sw.ToString();
}
}
}
I was getting the same error with core 2.0. The problem is RazorViewEngine is not working as expected with empty RouteData object;
So i injected IHttpContextAccessor and got HttpContext and RouteData from it;
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IViewRenderService, ViewRenderService>();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddMvc();
}
RazorToStringHelper.cs:
public interface IViewRenderService
{
Task<string> RenderToStringAsync(string viewName, object model);
}
public class ViewRenderService : IViewRenderService
{
private readonly IRazorViewEngine _razorViewEngine;
private readonly ITempDataProvider _tempDataProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
public ViewRenderService(
IRazorViewEngine razorViewEngine,
IHttpContextAccessor httpContextAccessor,
ITempDataProvider tempDataProvider)
{
_razorViewEngine = razorViewEngine;
_tempDataProvider = tempDataProvider;
_httpContextAccessor = httpContextAccessor;
}
public async Task<string> RenderToStringAsync(string viewName, object model)
{
var httpContext = _httpContextAccessor.HttpContext;
var actionContext = new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor());
var viewResult = _razorViewEngine.FindView(actionContext, viewName, false);
if (viewResult.View == null)
{
throw new ArgumentNullException($"{viewName} does not match any available view");
}
using (var sw = new StringWriter())
{
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();
}
}
}
Due to time constraints I abandoned the XUnit approach, wrote a test app and also
utilised postman as this was an api requirement to render a pdf from a razor view.
Related
I tried a lot of examples but I do not get the good response.
The ReportUpload method creates some Report entity based on the list from ExcelManager list
and adds them to the Reports DbSet.
My goal would be to read out the added entities from the mocked DbSet and assert them.
How can I pass the mocked DbContext and Dbset into UnitOfWork, please?
GeneralRepository.cs
public class GeneralRepository<TContext, TEntity> : IGeneralRepository<TEntity>
where TEntity : class
where TContext : DbContext
{
protected readonly TContext _context;
protected readonly DbSet<TEntity> dbSet;
public GeneralRepository(TContext context)
{
_context = context;
dbSet = _context.Set<TEntity>();
}
public async Task AddAsync(TEntity entity)
{
await _context.Set<TEntity>().AddAsync(entity);
}
...
}
IUnitOfWork.cs
public interface IUnitOfWork : IDisposable
{
IGeneralRepository<Report> Reports{ get; }
...
}
UnitOfWork.cs
public sealed class UnitOfWork : IUnitOfWork
{
private readonly MyDbContext _context;
public UnitOfWork(MyDbContext context)
{
_context = context;
}
private IGeneralRepository<Report>_report;
public IGeneralRepository<Report> Reports => _report ??= new GeneralRepository<MyDbContext, Report>(_context);
...
}
ReportService.cs
public class ReportService : GeneralService, IReportService
{
private readonly IExcelManager _excelManager;
public ReportService(IUnitOfWork unitOfWork, IExcelManager excelManager)
{
UnitOfWork = unitOfWork;
_excelManager = excelManager;
}
public async Task<string> ReportUpload(MemoryStream ms)
{
var workingList = _excelManager.ReadExcel(ms);
var i = 0;
while (i < workingList.Count)
{
var report = new Report { ... }
await UnitOfWork.Reports.AddAsync(report);
}
....
}
ReportServiceTest.cs
public class ReportServiceTests
{
[Fact()]
public async Task ReportUploadTest()
{
//Arrange
....
var mockSet = new Mock<DbSet<Report>>();
var mockContext = new Mock<MyDbContext>();
mockContext.Setup(x => x.Reports).Returns(mockSet.Object);
var reportRepositoryMock = new Mock<IGeneralRepository<Report>>();
reportRepositoryMock.Setup(m => m.AddAsync(It.IsAny<Report>()));
var unitOfWorkMock = new Mock<IUnitOfWork>();
unitOfWorkMock.Setup(p => p.Reports)
.Returns(reportRepositoryMock.Object);
...
//Act
var reportService = new ReportService(unitOfWorkMock.Object,exelManagerMock.Object);
await reportService.ReportUpload(new MemoryStream());
//Assert
???
}
DbContext cannot be passed to the UnitOfWork object because its context field is private.
I had to use SQLite in memory to test the GeneralRepository method.
var exelManagerMock = new Mock<IExcelManager>();
exelManagerMock.Setup(p => p.ReadExcel(It.IsAny<MemoryStream>()))
.Returns(listOfExcelReadResult);
var dbFixture = new DatabaseFixture();
var context = dbFixture.CreateContext();
var unitOfWork = new UnitOfWork(context);
//I need some plus data
await unitOfWork.Providers.AddRangeAsync(providers);
await context.SaveChangesAsync();
var reportService = new ReportService(unitOfWork, exelManagerMock.Object);
await reportService.ReportUpload(new MemoryStream(), 2021);
var allReports = await reportService.UnitOfWork.Reports.GetAsync();
Assert.Equal(3, allReports.Count);
I have a method to get header value using IHttpContextAccessor
public class HeaderConfiguration : IHeaderConfiguration
{
public HeaderConfiguration()
{
}
public string GetTenantId(IHttpContextAccessor httpContextAccessor)
{
return httpContextAccessor.HttpContext.Request.Headers["Tenant-ID"].ToString();
}
}
I am testing GetBookByBookId method
Let's say the method looks like this:
public class Book
{
private readonly IHttpContextAccessor _httpContextAccessor;
private IHeaderConfiguration _headerConfiguration;
private string _tenantID;
public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor){
var headerConfig = new HeaderConfiguration();
_httpContextAccessor = httpContextAccessor;
_tenantID = headerConfig.GetTenantId(_httpContextAccessor);
}
public Task<List<BookModel>> GetBookByBookId(string id){
//do something with the _tenantId
//...
}
}
Here's my unit test for GetBookByBookId method
[Fact]
public void test_GetBookByBookId()
{
//Arrange
//Mock IHttpContextAccessor
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());
//Mock HeaderConfiguration
var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
mockHeaderConfiguration.Setup(x => x.GetTenantId(mockHttpContextAccessor.Object)).Returns(It.IsAny<string>());
var book = new Book( mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);
var bookId = "100";
//Act
var result = book.GetBookByBookId(bookId);
//Assert
result.Result.Should().NotBeNull().And.
BeOfType<List<BookModel>>();
}
But for this line:
mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());
It says
System.NotSupportedException: 'Type to mock must be an interface or an abstract or non-sealed class. '
I was wondering what's the proper way to mock IHttpContextAccessor with header value?
You can use the DefaultHttpContext as a backing for the IHttpContextAccessor.HttpContext. Saves you having to set-up too many things
Next you cannot use It.IsAny<string>() as a Returns result. They were meant to be used in the set up expressions alone.
Check the refactor
[Fact]
public async Task test_GetBookByBookId() {
//Arrange
//Mock IHttpContextAccessor
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
var context = new DefaultHttpContext();
var fakeTenantId = "abcd";
context.Request.Headers["Tenant-ID"] = fakeTenantId;
mockHttpContextAccessor.Setup(_ => _.HttpContext).Returns(context);
//Mock HeaderConfiguration
var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
mockHeaderConfiguration
.Setup(_ => _.GetTenantId(It.IsAny<IHttpContextAccessor>()))
.Returns(fakeTenantId);
var book = new Book(mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);
var bookId = "100";
//Act
var result = await book.GetBookByBookId(bookId);
//Assert
result.Should().NotBeNull().And.
BeOfType<List<BookModel>>();
}
There may also be an issue with the Class Under Test as it is manually initializing the HeaderConfiguration when it should actually be explicitly injected.
public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor) {
_httpContextAccessor = httpContextAccessor;
_tenantID = headerConfiguration.GetTenantId(_httpContextAccessor);
}
In my scenario I had to mock IHttpContextAccessor and access the inner request url bits.
I'm sharing it here because I spent a decent amount of time figuring this out and hopefully it'll help someone.
readonly Mock<IHttpContextAccessor> _HttpContextAccessor =
new Mock<IHttpContextAccessor>(MockBehavior.Strict);
void SetupHttpContextAccessorWithUrl(string currentUrl)
{
var httpContext = new DefaultHttpContext();
setRequestUrl(httpContext.Request, currentUrl);
_HttpContextAccessor
.SetupGet(accessor => accessor.HttpContext)
.Returns(httpContext);
static void setRequestUrl(HttpRequest httpRequest, string url)
{
UriHelper
.FromAbsolute(url, out var scheme, out var host, out var path, out var query,
fragment: out var _);
httpRequest.Scheme = scheme;
httpRequest.Host = host;
httpRequest.Path = path;
httpRequest.QueryString = query;
}
}
If you are making use of the wonderful NSubstitute package for NUnit, you can do this...
var mockHttpAccessor = Substitute.For<IHttpContextAccessor>();
var context = new DefaultHttpContext
{
Connection =
{
Id = Guid.NewGuid().ToString()
}
};
mockHttpAccessor.HttpContext.Returns(context);
// usage...
I have an application that uses Markdown in Razor Pages (compiled using a custom TagHelper) to display static content. There are about 300 pages worth.
I'm trying to render these Razor pages to html so that I can index the content using Lucene.net, in order to provide full text search.
I've managed to create some code (see below) that works, but prepends previously rendered content whenever subsequent pages are rendered. That is, when I render the first page it works perfectly but, when I render another page the first page's content is also included and, when I render a third page the first two pages content are included.
I have tried creating the dependencies manually within the method, and I have tried creating a new instance of this class for each page, but always with the same result.
What am I missing?
public class RazorPageRenderer
{
private readonly IRazorViewEngine razorViewEngine;
private readonly ITempDataProvider tempDataProvider;
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IActionContextAccessor actionContextAccessor;
private readonly IRazorPageActivator razorPageActivator;
private readonly ILogger logger;
public RazorPageRenderer(
IRazorViewEngine razorViewEngine,
ITempDataProvider tempDataProvider,
IHttpContextAccessor httpContextAccessor,
IActionContextAccessor actionContextAccessor,
IRazorPageActivator razorPageActivator,
ILogger<RazorPageRenderer> logger)
{
this.razorViewEngine = razorViewEngine;
this.tempDataProvider = tempDataProvider;
this.httpContextAccessor = httpContextAccessor;
this.actionContextAccessor = actionContextAccessor;
this.razorPageActivator = razorPageActivator;
this.logger = logger;
}
public async Task<string> RenderAsync(IRazorPage razorPage) => await RenderAsync<object>(razorPage, null);
public async Task<string> RenderAsync<T>(IRazorPage razorPage, T model)
{
try
{
var viewDataDictionary = CreateViewDataDictionary(razorPage.GetType(), model);
await using var writer = new StringWriter();
var view = new RazorView(
razorViewEngine,
razorPageActivator,
Array.Empty<IRazorPage>(),
razorPage,
HtmlEncoder.Default,
new DiagnosticListener(nameof(RazorPageRenderer)));
var viewContext = new ViewContext(
actionContextAccessor.ActionContext,
view,
viewDataDictionary,
new TempDataDictionary(
httpContextAccessor.HttpContext,
tempDataProvider),
writer,
new HtmlHelperOptions());
if (razorPage is Page page)
{
page.PageContext = new PageContext(actionContextAccessor.ActionContext)
{
ViewData = viewContext.ViewData
};
}
razorPage.ViewContext = viewContext;
razorPageActivator.Activate(razorPage, viewContext);
await razorPage.ExecuteAsync();
return writer.ToString();
}
catch (Exception exception)
{
logger.LogError(
"An exception occured while rendering page: {Page}. Exception: {Exception}",
razorPage.Path,
exception);
}
return null;
}
private static ViewDataDictionary CreateViewDataDictionary(Type pageType, object model)
{
var dictionaryType = typeof(ViewDataDictionary<>)
.MakeGenericType(pageType);
var ctor = dictionaryType.GetConstructor(new[]
{typeof(IModelMetadataProvider), typeof(ModelStateDictionary)});
var viewDataDictionary = (ViewDataDictionary)ctor?.Invoke(
new object[] {new EmptyModelMetadataProvider(), new ModelStateDictionary()});
if (model != null && viewDataDictionary != null)
{
viewDataDictionary.Model = model;
}
return viewDataDictionary;
}
}
I thought I would have a go at using Blazor server-side, and so far I've managed to overcome most headaches one way or another and enjoyed it, until now.
I'm trying to write a validator for Google Recaptcha v3, which requires a users IP address. Usually I would just get the IHttpContextAccessor with:
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetService(typeof(IHttpContextAccessor));
But that now returns null! I also found that trying to get IConfiguration in the same way failed, but for that, I could just make a static property in Startup.cs.
This is the last hurdle in a days work, and it's got me baffled.
Any ideas on how to get that IP address into a validator?
Thanks!
Edit:
I just found the error making httpContextAccessor null!
((System.RuntimeType)validationContext.ObjectType).DeclaringMethodthrew an exception of type 'System.InvalidOperationException'
this is the validator:
public class GoogleReCaptchaValidationAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
Lazy<ValidationResult> errorResult = new Lazy<ValidationResult>(() => new ValidationResult("Google reCAPTCHA validation failed", new String[] { validationContext.MemberName }));
if (value == null || String.IsNullOrWhiteSpace(value.ToString()))
{
return errorResult.Value;
}
var configuration = Startup.Configuration;
string reCaptchResponse = value.ToString();
string reCaptchaSecret = configuration["GoogleReCaptcha:SecretKey"];
IHttpContextAccessor httpContextAccessor = validationContext.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("secret", reCaptchaSecret),
new KeyValuePair<string, string>("response", reCaptchResponse),
new KeyValuePair<string, string>("remoteip", httpContextAccessor.HttpContext.Connection.RemoteIpAddress.ToString())
});
HttpClient httpClient = new HttpClient();
var httpResponse = httpClient.PostAsync("https://www.google.com/recaptcha/api/siteverify", content).Result;
if (httpResponse.StatusCode != HttpStatusCode.OK)
{
return errorResult.Value;
}
String jsonResponse = httpResponse.Content.ReadAsStringAsync().Result;
dynamic jsonData = JObject.Parse(jsonResponse);
if (jsonData.success != true.ToString().ToLower())
{
return errorResult.Value;
}
return ValidationResult.Success;
}
}
For this issue, it is caused by that when DataAnnotationsValidator call AddDataAnnotationsValidation, it did not pass IServiceProvider to ValidationContext.
For this issue, you could check Make dependency resolution available for EditContext form validation so that custom validators can access services. #11397
private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validationContext = new ValidationContext(editContext.Model);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
For a workaround, you could implement your own DataAnnotationsValidator and AddDataAnnotationsValidation .
Follow steps below:
Custom DataAnnotationsValidator
public class DIDataAnnotationsValidator: DataAnnotationsValidator
{
[CascadingParameter] EditContext DICurrentEditContext { get; set; }
[Inject]
protected IServiceProvider ServiceProvider { get; set; }
protected override void OnInitialized()
{
if (DICurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(DataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DataAnnotationsValidator)} " +
$"inside an EditForm.");
}
DICurrentEditContext.AddDataAnnotationsValidationWithDI(ServiceProvider);
}
}
Custom EditContextDataAnnotationsExtensions
public static class EditContextDataAnnotationsExtensions
{
private static ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> _propertyInfoCache
= new ConcurrentDictionary<(Type, string), PropertyInfo>();
public static EditContext AddDataAnnotationsValidationWithDI(this EditContext editContext, IServiceProvider serviceProvider)
{
if (editContext == null)
{
throw new ArgumentNullException(nameof(editContext));
}
var messages = new ValidationMessageStore(editContext);
// Perform object-level validation on request
editContext.OnValidationRequested +=
(sender, eventArgs) => ValidateModel((EditContext)sender, serviceProvider, messages);
// Perform per-field validation on each field edit
editContext.OnFieldChanged +=
(sender, eventArgs) => ValidateField(editContext, serviceProvider, messages, eventArgs.FieldIdentifier);
return editContext;
}
private static void ValidateModel(EditContext editContext, IServiceProvider serviceProvider,ValidationMessageStore messages)
{
var validationContext = new ValidationContext(editContext.Model, serviceProvider, null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
private static void ValidateField(EditContext editContext, IServiceProvider serviceProvider, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
{
if (TryGetValidatableProperty(fieldIdentifier, out var propertyInfo))
{
var propertyValue = propertyInfo.GetValue(fieldIdentifier.Model);
var validationContext = new ValidationContext(fieldIdentifier.Model, serviceProvider, null)
{
MemberName = propertyInfo.Name
};
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
messages.Clear(fieldIdentifier);
messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage));
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
editContext.NotifyValidationStateChanged();
}
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (!_propertyInfoCache.TryGetValue(cacheKey, out propertyInfo))
{
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
_propertyInfoCache[cacheKey] = propertyInfo;
}
return propertyInfo != null;
}
}
Replace DataAnnotationsValidator with DIDataAnnotationsValidator
<EditForm Model="#starship" OnValidSubmit="#HandleValidSubmit">
#*<DataAnnotationsValidator />*#
<DIDataAnnotationsValidator />
<ValidationSummary />
</EditForm>
For IHttpContextAccessor, you need to register in Startup.cs like
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddHttpContextAccessor();
}
I want to have a MiddleWare module run in ASP.NET Core after the MVC Razor View Engine has processed data. I can get it to run but it seems to not have collected all the data. I have a Tag Helper that that updates an DI object's collection but when the Middleware runs, the DI object's collection is empty. My startup.cs looks like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseMiddleware<MyMiddleware>();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
And My MiddleWare is this:
public class MyMiddleware
{
private readonly RequestDelegate nextMiddleware;
private readonly IScriptManager _scriptManager;
public MyMiddleware(RequestDelegate next, IScriptManager scriptManager)
{
this.nextMiddleware = next;
_scriptManager = scriptManager;
}
public async Task Invoke(HttpContext context)
{
var cnt = _scriptManager.ScriptTexts.Count;
.. get HTML
Stream originalStream = context.Response.Body;
...
.. update HTML
await context.Response.WriteAsync(htmlData);
I do get the HTML I want but it seems that the collection in my DI does not get updated.
*** Notes - Possible but not working Result Filter
services.AddMvc(options =>
{
options.Filters.Add(new AppendToHtmlBodyFilter());
});
public class AppendToHtmlBodyFilter : TypeFilterAttribute
{
private readonly IScriptManager _scriptManager;
public AppendToHtmlBodyFilter():base(typeof(SampleActionFilterImpl))
{
}
private class SampleActionFilterImpl : IResultFilter
{
private readonly IScriptManager _scriptManager;
public SampleActionFilterImpl(IScriptManager scriptManager)
{
_scriptManager = scriptManager;
//_logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
}
public void OnResultExecuted(ResultExecutedContext context)
{
var cnt = _scriptManager.ScriptTexts.Count;
Stream originalStream = context.HttpContext.Response.Body;
using (MemoryStream newStream = new MemoryStream())
{
context.HttpContext.Response.Body = newStream;
context.HttpContext.Response.Body = originalStream;
newStream.Seek(0, SeekOrigin.Begin);
StreamReader reader = new StreamReader(newStream);
var htmlData = reader.ReadToEnd();
As far as i know there is no way to run a middleware after mvc in request pipeline. If you want to manipulate razor output, you can use filters. Result filter seems suitable for your case.
Result filters are ideal for any logic that needs to directly surround
view execution or formatter execution. Result filters can replace or
modify the action result that's responsible for producing the
response.
See official docs https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#result-filters
Also see how to use dependency injection with a filter https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters#dependency-injection
Update
I couldn't get it work with result filter(it worked json result but didn't work viewresult).
However i found a good example with middleware: http://www.mikesdotnetting.com/article/269/asp-net-5-middleware-or-where-has-my-httpmodule-gone
public class MyMiddleware
{
private readonly RequestDelegate nextMiddleware;
private readonly IScriptManager _scriptManager;
public MyMiddleware(RequestDelegate next, IScriptManager scriptManager)
{
this.nextMiddleware = next;
_scriptManager = scriptManager;
}
public async Task Invoke(HttpContext context)
{
var cnt = _scriptManager.ScriptTexts.Count;
using (var memoryStream = new MemoryStream())
{
var bodyStream = context.Response.Body;
context.Response.Body = memoryStream;
await _next(context);
var isHtml = context.Response.ContentType?.ToLower().Contains("text/html");
if (context.Response.StatusCode == 200 && isHtml.GetValueOrDefault())
{
memoryStream.Seek(0, SeekOrigin.Begin);
using (var streamReader = new StreamReader(memoryStream))
{
var responseBody = await streamReader.ReadToEndAsync();
// update html
using (var amendedBody = new MemoryStream())
using (var streamWriter = new StreamWriter(amendedBody))
{
streamWriter.Write(responseBody);
amendedBody.Seek(0, SeekOrigin.Begin);
await amendedBody.CopyToAsync(bodyStream);
}
}
}
}
}
}