I am trying to write my own "Application_Error" for my site.
I need to catch 404's (to see what mistakes users are making) & 500's to catch my programming errors. I did this succesfully in my site before I started using Umbraco in the global.asax.
I have tried the following method.
1: I created a class MyGlobal (file MyGlobal.vb in the App_Code directory:
Public Class MyGlobal
Inherits umbraco.Web.UmbracoApplication
Protected Overloads Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
Dim ctx As HttpContext = HttpContext.Current
ctx.Response.Redirect("http://wwww.google.com")
End Sub
End Class
Obivously, this is only to test.
In addition, I editeted my Global.asax file and now it looks like this:
<%# Application Language="VB" Inherits="MyGlobal" %>
<script runat="server">
</script>
I already have:
<customErrors mode="Off" />
&
<httpErrors existingResponse="PassThrough"/>
in my web.config.
1: When I try to enter a non-existent page, i get the Umbraco 404 error page.
2: When I purposely create a 500 error, the system shows me the error page and does not redirect to Google.
Please help as I can not go live without this.
Thanks.
Yoni
Edit:
As I am working in VB, I have used this:
Imports Microsoft.VisualBasic
Imports Umbraco.Core
Public Class MyCustomEvent1
Inherits ApplicationEventHandler
Protected Overrides Sub ApplicationStarted(umbracoApplication As UmbracoApplicationBase, applicationContext As ApplicationContext)
AddHandler umbracoApplication.[Error], AddressOf umbracoApplication_Error
End Sub
Private Sub umbracoApplication_Error(sender As Object, e As EventArgs)
Dim x As HttpContext = HttpContext.Current
x.Response.Redirect("http://www.espn.com")
' Do your stuff
End Sub
End Class
It is not firing when I try either a 404 or a 500 error.
(If I put a redirect in the upper section, i get an error that "System.Web.HttpException: Response is not available in this context.")
Can you help?
Thanks
Concerning 404's, you should look at the package 301 URL Tracker. This is an amazing free package tracking all 404's. While doing that it also tracks all renames of nodes, when a node has been renamed, it also creates a redirect to the new url. Of cource you can also create your own redirects using urls from a previous site or using regex.
Writing your own global asax is not best practice when you use Umbraco, because umbraco uses a lot of things at startup itself. Instead you can hook into the ApplicationEventHandler events to register your custom code at startup. The only thing to activate the handlers is implementing the ApplicationEventHandler class. All classes derived from this class will be called at the umbraco boot-up.
The UmbracoApplicationBase class is derived from the HttpApplication. You can hook into the Error event of this class to implement your own error handler logic.
public class MyCustomEvent1 : ApplicationEventHandler
{
protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext)
{
umbracoApplication.Error += umbracoApplication_Error;
// very bad example
// Response.Redirect("http://google.com")
}
void umbracoApplication_Error(object sender, EventArgs e)
{
// Do your stuff
}
}
EDIT
If you need to create your own "404 not found" pipeline you should create an IContentFinder (see documentation). The example below comes from this PDF: http://www.zpqrtbnk.net/CoreInternalsForWebsiteDevelopment.pdf and this session from Stéphane Gay presented at Codegarden.
public class My404ContentFinder : IContentFinder
{
public bool TryFindContent(PublishedContentRequest contentRequest)
{
if (!contentRequest.HasDomain)
return false;
var contentCache = contentRequest.RoutingContext.UmbracoContext.ContentCache;
var domainRoot = contentCache.GetById(contentRequest.Domain.RootNodeId);
var firstSegment = contentRequest.Uri.AbsolutePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries).First();
var root = domainRoot.Children.FirstOrDefault(x => x.UrlName == firstSegment);
root = root ?? domainRoot.Children.First();
var page = root.Descendants().FirstOrDefault(x => x.Name == "404");
if (page == null) return false;
contentRequest.PublishedContent = page;
var wcd = Domain.GetDomainsById(root.Id, true).SingleOrDefault(x => x.IsWildcard);
if (wcd != null) contentRequest.Culture = new CultureInfo(wcd.Language.CultureAlias);
return true;
}
}
Related
I need to dynamically creates controllers in a ASP.NET Core 6 MVC application.
I found some way to somewhat achieve this but not quite.
I'm able to dynamically add my controller but somehow it reflects only on the second request.
So here is what I do: first I initialize my console app as follows:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace DynamicControllerServer
{
internal class Program
{
static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
ApplicationPartManager partManager = builder.Services.AddMvc().PartManager;
// Store thePartManager in my Middleware to be able to add controlelr after initialization is done
MyMiddleware._partManager = partManager;
// Register controller change event
builder.Services.AddSingleton<IActionDescriptorChangeProvider>(MyActionDescriptorChangeProvider.Instance);
builder.Services.AddSingleton(MyActionDescriptorChangeProvider.Instance);
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();
// Add Middleware which is responsible to cactn the request and dynamically add the missing controller
app.UseMiddleware<MyMiddleware>();
app.RunAsync();
Console.WriteLine("Server has been started successfully ...");
Console.ReadLine();
}
}
}
Then my middleware looks like this: it basically detects that there is the "dynamic" keyword in the url. If so, it will load the assembly containing the DynamicController:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using System;
using System.Reflection;
namespace DynamicControllerServer
{
public class MyMiddleware
{
public RequestDelegate _next { get; }
private string dllName = "DynamicController1.dll";
static public ApplicationPartManager _partManager = null;
public MyMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
if (httpContext.Request.Path.HasValue)
{
var queryParams = httpContext.Request.Path.Value;
if(httpContext.Request.Path.Value.Contains("api/dynamic"))
{
// Dynamically load assembly
Assembly assembly = assembly = Assembly.LoadFrom(#"C:\Temp\" + dllName);
// Add controller to the application
AssemblyPart _part = new AssemblyPart(assembly);
_partManager.ApplicationParts.Add(_part);
// Notify change
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
}
}
await _next(httpContext); // calling next middleware
}
}
}
The ActionDescriptorChange provider looks like this:
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Primitives;
namespace DynamicControllerServer
{
public class MyActionDescriptorChangeProvider : IActionDescriptorChangeProvider
{
public static MyActionDescriptorChangeProvider Instance { get; } = new MyActionDescriptorChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
}
Dynamic controller is in separate dll and is very simple:
using Microsoft.AspNetCore.Mvc;
namespace DotNotSelfHostedOwin
{
[Route("api/[controller]")]
[ApiController]
public class DynamicController : ControllerBase
{
public string[] Get()
{
return new string[] { "dynamic1", "dynamic1", DateTime.Now.ToString() };
}
}
}
Here are the packages used in that project:
<PackageReference Include="Microsoft.AspNetCore" Version="2.2.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
This works "almost" fine ... when first request is made to:
https://localhost:5001/api/dynamic
then it goes in the middleware and load the assembly, but returns a 404 error.
Then second request will actually work as expected:
Second request returns the expected result:
I must doing it wrong and probably my middleware is executed too late in the flow to reflect the dynamic controller right away.
Question is: what should be the proper way to achieve this?
Second question I have is say now the external dll holding our dynamic controller is updated.
How can I reload that controller to get the new definition?
Any help would be appreciated
Thanks in advance
Nick
Here is the answer to my own question in case it can help somebody out there.
It seems building and loading the controller from the middleware will always end up with failure on the first call.
This makes sense since we are already in the http pipeline.
I end up doing same thing from outside the middleware.
Basically my application detect a change in the controller assembly, unload the original assembly and load the new one.
You cannot use the Default context since it will not allow reloading different dll for same assembly:
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); // Produce an exception on updates
To be able to reload new dll for same assembly, I’m loading each controller in its own assembly context. To do that you need to create your own class deriving from AssemblyLoadContext and managing assembly load:
public class MyOwnContext: AssemblyLoadContext
{
// You can find lots of example in the net
}
When you want to unload the assembly, you just unload the context:
MyOwnContextObj.Unload();
Now to add or remove the controller on the fly, you need to keep reference of the PartManager and the ApplicationPart.
To add controller
ApplicationPart part = new AssemblyPart(assembly);
_PartManager.ApplicationParts.Add(part);
To remove:
_PartManager.ApplicationParts.Remove(part);
On course once done, still use following piece of code to acknowledge the change:
MyActionDescriptorChangeProvider.Instance.HasChanged = true;
MyActionDescriptorChangeProvider.Instance.TokenSource.Cancel();
That allow updating controller on the fly with no interruption of service.
Hope this helps people out there.
I have done a similar solution (used for managing a web app plugins) with some differences that may help you:
List all the external assemblies in a config file or appsettings.json so all the dll names and/or addresses are known at startup
Instead of registering controllers when they are called, register them at program.cs/start up :
//Foreah dllName from settings file
var assembly = Assembly.LoadFrom(#"Base address" + dllNameLoadedFromSettings);
var part = new AssemblyPart(assembly);
services.AddControllersWithViews()
.ConfigureApplicationPartManager(apm => apm.ApplicationParts.Add(part));
// Any other configuration based on the usage you want
Second: I usually keep plugin dlls in the bin folder so when using IIS as soon as a dll file in bin is changed the upper-level app is automatically reset. So your second question would be solved too.
I have a very simple test in a test project in a solution using ASP MVC V5 and attribute routing. Attribute routing and the MapMvcAttributeRoutes method are part of ASP MVC 5.
[Test]
public void HasRoutesInTable()
{
var routes = new RouteCollection();
routes.MapMvcAttributeRoutes();
Assert.That(routes.Count, Is.GreaterThan(0));
}
This results in:
System.InvalidOperationException :
This method cannot be called during the applications pre-start initialization phase.
Most of the answers to this error message involve configuring membership providers in the web.config file. This project has neither membership providers or a web.config file so the error seems be be occurring for some other reason. How do I move the code out of this "pre-start" state so that the tests can run?
The equivalent code for attributes on ApiController works fine after HttpConfiguration.EnsureInitialized() is called.
I recently upgraded my project to ASP.NET MVC 5 and experienced the exact same issue. When using dotPeek to investigate it, I discovered that there is an internal MapMvcAttributeRoutes extension method that has a IEnumerable<Type> as a parameter which expects a list of controller types. I created a new extension method that uses reflection and allows me to test my attribute-based routes:
public static class RouteCollectionExtensions
{
public static void MapMvcAttributeRoutesForTesting(this RouteCollection routes)
{
var controllers = (from t in typeof(HomeController).Assembly.GetExportedTypes()
where
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t)
select t).ToList();
var mapMvcAttributeRoutesMethod = typeof(RouteCollectionAttributeRoutingExtensions)
.GetMethod(
"MapMvcAttributeRoutes",
BindingFlags.NonPublic | BindingFlags.Static,
null,
new Type[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapMvcAttributeRoutesMethod.Invoke(null, new object[] { routes, controllers });
}
}
And here is how I use it:
public class HomeControllerRouteTests
{
[Fact]
public void RequestTo_Root_ShouldMapTo_HomeIndex()
{
// Arrange
var routes = new RouteCollection();
// Act - registers traditional routes and the new attribute-defined routes
RouteConfig.RegisterRoutes(routes);
routes.MapMvcAttributeRoutesForTesting();
// Assert - uses MvcRouteTester to test specific routes
routes.ShouldMap("~/").To<HomeController>(x => x.Index());
}
}
One problem now is that inside RouteConfig.RegisterRoutes(route) I cannot call routes.MapMvcAttributeRoutes() so I moved that call to my Global.asax file instead.
Another concern is that this solution is potentially fragile since the above method in RouteCollectionAttributeRoutingExtensions is internal and could be removed at any time. A proactive approach would be to check to see if the mapMvcAttributeRoutesMethod variable is null and provide an appropriate error/exceptionmessage if it is.
NOTE: This only works with ASP.NET MVC 5.0. There were significant changes to attribute routing in ASP.NET MVC 5.1 and the mapMvcAttributeRoutesMethod method was moved to an internal class.
In ASP.NET MVC 5.1 this functionality was moved into its own class called AttributeRoutingMapper.
(This is why one shouldn't rely on code hacking around in internal classes)
But this is the workaround for 5.1 (and up?):
public static void MapMvcAttributeRoutes(this RouteCollection routeCollection, Assembly controllerAssembly)
{
var controllerTypes = (from type in controllerAssembly.GetExportedTypes()
where
type != null && type.IsPublic
&& type.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase)
&& !type.IsAbstract && typeof(IController).IsAssignableFrom(type)
select type).ToList();
var attributeRoutingAssembly = typeof(RouteCollectionAttributeRoutingExtensions).Assembly;
var attributeRoutingMapperType =
attributeRoutingAssembly.GetType("System.Web.Mvc.Routing.AttributeRoutingMapper");
var mapAttributeRoutesMethod = attributeRoutingMapperType.GetMethod(
"MapAttributeRoutes",
BindingFlags.Public | BindingFlags.Static,
null,
new[] { typeof(RouteCollection), typeof(IEnumerable<Type>) },
null);
mapAttributeRoutesMethod.Invoke(null, new object[] { routeCollection, controllerTypes });
}
Well, it's really ugly and I'm not sure if it'll be worth the test complexity, but here's how you can do it without modifying your RouteConfig.Register code:
[TestClass]
public class MyTestClass
{
[TestMethod]
public void MyTestMethod()
{
// Move all files needed for this test into a subdirectory named bin.
Directory.CreateDirectory("bin");
foreach (var file in Directory.EnumerateFiles("."))
{
File.Copy(file, "bin\\" + file, overwrite: true);
}
// Create a new ASP.NET host for this directory (with all the binaries under the bin subdirectory); get a Remoting proxy to that app domain.
RouteProxy proxy = (RouteProxy)ApplicationHost.CreateApplicationHost(typeof(RouteProxy), "/", Environment.CurrentDirectory);
// Call into the other app domain to run route registration and get back the route count.
int count = proxy.RegisterRoutesAndGetCount();
Assert.IsTrue(count > 0);
}
private class RouteProxy : MarshalByRefObject
{
public int RegisterRoutesAndGetCount()
{
RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes); // or just call routes.MapMvcAttributeRoutes() if that's what you want, though I'm not sure why you'd re-test the framework code.
return routes.Count;
}
}
}
Mapping attribute routes needs to find all the controllers you're using to get their attributes, which requires accessing the build manager, which only apparently works in app domains created for ASP.NET.
What are you testing here? Looks like you are testing a 3rd party extension method. You shouldn't be using your unit tests to test 3rd party code.
When I serve a javascript file to a user from the /Content Directory I want to replace a string token in that file with a value, so that when the user requests a given file, it has all the customizations they expect.
I think that means I need to somehow proxy requests to the /Content directory, perform the dynamic insertion, and give the file to the user.
I'm interested in performing this insertion as a stream or as a in -memory file. I'd prefer to use a stream just because it's probably more efficient memory wise.
How do I get ASP.NET to proxy this directory?
I've attempted
Using routes to point to a controller
WCF to proxy a URL
But they all seem "ugly" to me and I'd like to make this insertion/replacement as transparent as possible int he project.
Is there a cleaner way?
The easiest way is to create an action on a controller.
public class JavascriptController : Controller
{
public ActionResult Load(string file)
{
var content = System.IO.File.ReadAllText(Server.MapPath(string.Format("~/Content/{0}", file)));
//make replacements io content here
return this.Content(content, "application/javascript");
}
}
You can then access the javascript like this (assuming you have the default routing):
http://localhost:53287/Javascript/Load?file=file.js
where file.js is the name of the file you are requesting.
Don't worry about the url, you can customise this by creating another route if necessary
Here is alternative answer to the answer I posted above, taking into account your comment regarding dynamic javascript.
Firstly, I don't know of a way to do this specifically using either mvc or wcf.. the only way I know how to do this is with a lower-level HttpModule
Take a look at the following code:
public class JavascriptReplacementModule : IHttpModule
{
public class ResponseFilter : MemoryStream
{
private Stream outputStream = null;
public ResponseFilter(Stream output)
{
outputStream = output;
}
public override void Flush()
{
base.Flush();
this.Seek(0, SeekOrigin.Begin);
var sr = new StreamReader(this);
string contentInBuffer = sr.ReadToEnd();
//Do replacements here
outputStream.Write(UTF8Encoding.UTF8.GetBytes(contentInBuffer), 0, UTF8Encoding.UTF8.GetByteCount(contentInBuffer));
outputStream.Flush();
}
protected override void Dispose(bool disposing)
{
outputStream.Dispose();
base.Dispose(disposing);
}
}
public void Dispose() { }
public void Init(HttpApplication context)
{
context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute);
}
void context_PostRequestHandlerExecute(object sender, EventArgs e)
{
var context = (HttpApplication)sender;
if (context.Request.Url.AbsolutePath.StartsWith("/Content") && context.Request.Url.AbsolutePath.EndsWith(".js"))
{
HttpContext.Current.Response.Filter = new ResponseFilter(HttpContext.Current.Response.Filter);
}
}
}
And register the module like this (make sure you put the full type in the type attribute):
<system.webServer>
<modules>
<add name="JavascriptReplacementModule" type="JavascriptReplacementModule"/>
</modules>
</system.webServer>
This allows you to modify the output stream before it gets to the client
Is there any way to gain access to the controller that is currently executing from within Global.asax?
I'd like to design an API with similar syntax to:
MyClass.RegisterComponents().When(IController => /* Some condition */)
Although I could move this code to a place where the controller is in context, I'd like to keep it centralised and portable.
So far, I have been unable to obtain the controller. Any ideas?
I have considered creating a base controller and extending all of my controllers from this base class, however, I'd like to make this library portable with the ability to be installed via NuGet. For this reason I am unable to take this approach.
You can do following in your global.asmx file.
private void Application_BeginRequest(object sender, EventArgs e)
{
string controllerName = Request.RequestContext.RouteData.Values.Where(p => p.Key =="controller").FirstOrDefault(p => p.Key);
}
I found a solution to this. Not a very good one but it solves my problem.
Register a global IActionFilter using an assembly start up method I found on David Ebbo's blog (http://blog.davidebbo.com/2011/02/register-your-http-modules-at-runtime.html).
The global action filter simply stores the action context in the current HttpContext.Items[] collection which is a per request collection.
public class GlobalActionFilter : System.Web.Mvc.IActionFilter {
internal static readonly object ActionExecutedFilterKey =
"__MvcResourceLoaderActionExecutedContext";
internal static readonly object ActionExecutingFilterKey =
"__MvcResourceLoaderActionExecutingContext";
static MvcResourceLoaderGlobalFilter __instance =
new MvcResourceLoaderGlobalFilter();
MvcResourceLoaderGlobalFilter() { }
public void OnActionExecuted(System.Web.Mvc.ActionExecutedContext filterContext) {
System.Web.HttpContext.Current.Items[ActionExecutedFilterKey] =
filterContext;
}
public void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext) {
System.Web.HttpContext.Current.Items[ActionExecutingFilterKey] =
filterContext;
}
public static void RegisterGlobalFilter() {
if (!System.Web.Mvc.GlobalFilters.Filters.Contains(__instance))
System.Web.Mvc.GlobalFilters.Filters.Add(__instance);
}
}
I can then access the context anywhere.
I'm looking at using ELMAH for the first time but have a requirement that needs to be met that I'm not sure how to go about achieving...
Basically, I am going to configure ELMAH to work under asp.net MVC and get it to log errors to the database when they occur. On top of this I be using customErrors to direct the user to a friendly message page when an error occurs. Fairly standard stuff...
The requirement is that on this custom error page I have a form which enables to user to provide extra information if they wish. Now the problem arises due to the fact that at this point the error is already logged and I need to associate the loged error with the users feedback.
Normally, if I was using my own custom implementation, after I log the error I would pass through the ID of the error to the custom error page so that an association can be made. But because of the way that ELMAH works, I don't think the same is quite possible.
Hence I was wondering how people thought that one might go about doing this....
Cheers
UPDATE:
My solution to the problem is as follows:
public class UserCurrentConextUsingWebContext : IUserCurrentConext
{
private const string _StoredExceptionName = "System.StoredException.";
private const string _StoredExceptionIdName = "System.StoredExceptionId.";
public virtual string UniqueAddress
{
get { return HttpContext.Current.Request.UserHostAddress; }
}
public Exception StoredException
{
get { return HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] as Exception; }
set { HttpContext.Current.Application[_StoredExceptionName + this.UniqueAddress] = value; }
}
public string StoredExceptionId
{
get { return HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] as string; }
set { HttpContext.Current.Application[_StoredExceptionIdName + this.UniqueAddress] = value; }
}
}
Then when the error occurs, I have something like this in my Global.asax:
public void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var item = new UserCurrentConextUsingWebContext();
item.StoredException = args.Entry.Error.Exception;
item.StoredExceptionId = args.Entry.Id;
}
Then where ever you are later you can pull out the details by
var item = new UserCurrentConextUsingWebContext();
var error = item.StoredException;
var errorId = item.StoredExceptionId;
item.StoredException = null;
item.StoredExceptionId = null;
Note this isn't 100% perfect as its possible for the same IP to have multiple requests to have errors at the same time. But the likely hood of that happening is remote. And this solution is independent of the session, which in our case is important, also some errors can cause sessions to be terminated, etc. Hence why this approach has worked nicely for us.
The ErrorLogModule in ELMAH (version 1.1 as of this writing) provides a Logged event that you can handle in Global.asax and which you can use to communicate details, say via HttpContext.Items collection, to your custom error page. If you registered the ErrorLogModule under the name ErrorLog in web.config then your event handler in Global.asax will look like this:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
var id = args.Entry.Id
// ...
}