Search code examples
c#.net.net-coreashx

Can I use .ashx files inside .NET Core project?


I couldn't find a satisfying answer to that question.
I'm trying to migrate my code from .NET Framework into .NET Core, and I ran into problems with my .ashx files.
Basically there are compile errors (Because I need to update my used NuGet Packages).
but even before that, I'm trying to figure out - Can I even use .ashx files inside .NET Core proejct?
Are there any changes I need to know about? Or should it be identical to .NET Framework (Besides packages).

Thanks!


Solution

  • Simply put, no.

    Long answer:
    All .ashx files have a background class that inherits either from System.Web.IHttpHandler or System.Web.IHttpAsyncHandler.

    Now, if your handlers are programmed well, most of what you have to do is declare those interfaces in .NET Core:

    namespace System.Web
    {
    
        [System.Runtime.InteropServices.ComVisible(true)]
        public interface IAsyncResult
        {
            bool IsCompleted { get; }
    
            System.Threading.WaitHandle AsyncWaitHandle { get; }
    
            // Return value:
            //     A user-defined object that qualifies or contains information about an asynchronous
            //     operation.
            object AsyncState { get; }
            
            // Return value:
            //     true if the asynchronous operation completed synchronously; otherwise, false.
            bool CompletedSynchronously { get; }
        }
    
    
        public interface IHttpAsyncHandler : IHttpHandler
        {
            
            // Return value:
            //     An System.IAsyncResult that contains information about the status of the process.
            IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
            //     An System.IAsyncResult that contains information about the status of the process.
            void EndProcessRequest(IAsyncResult result);
        }
    
        public interface IHttpHandler
        {
    
            // true if the System.Web.IHttpHandler instance is reusable; otherwise, false.
            bool IsReusable { get; }
    
            void ProcessRequest(Microsoft.AspNetCore.Http.HttpContext context);
        }
    }
    

    And automatically rename System.Web.HttpContext into Microsoft.AspNetCore.Http.HttpContext.

    Then you need to remove all references to System.Web.HttpContext.Current (which you wouldn't need to do if you programmed your handlers properly).

    If that is a lot of work, my answer here https://stackoverflow.com/a/40029302/155077 suggest a dirty workaround (basically, spoofing System.Web.HttpContext.Current).

    Then, all you need to do is search to root path recursively for the ashx-file-paths, parse those files with regex for the corresponding classes (or do it manually if you have few) and add them as endpoint in .NET Core. Either you can just instanciate the class (singleton IsReusable = true), or you can write a handler that creates a new instance of class, and then calls ProcessRequest in said class.

    You'll need to fix all compilation problems, though.
    You can also parse the paths in the source project once, and generate the middleware/endpoint-injection code programmatically once.
    That's probably the best option you have for large projects.

    Though, you are in luck if you have ashx-handlers.
    But don't just start just yet.
    If you have aspx-pages or ascx-controls, you are definitely out of luck, and don't need to start with the .ashx-handlers in the first place - unless your aspx/ascx usage is so tiny, you can replace them fairly quickly.

    Still, if you just need to port a lot of plain raw .ashx handlers producing json/xml, this method can work excellently.

    Oh, also note that you need to change context.Response.OutputStream with context.Response.Body.

    You might also have to change everything to async (if it isn't already), or you need to allow synchronous methods in the .NET-Core application startup.

    Here's how to extract data:

    namespace FirstTestApp
    {
    
    
        public class EndpointData
        {
            public string Path;
            public string Class;
            public string Language;
        }
    
    
        public static class HandlerFinder
        {
    
            private static System.Collections.Generic.List<EndpointData> FindAllHandlers()
            {
                string searchPath = @"D:\username\Documents\Visual Studio 2017\TFS\COR-Basic-V4\Portal\Portal";
                searchPath = @"D:\username\Documents\Visual Studio 2017\TFS\COR-Basic\COR-Basic\Basic\Basic";
    
                string[] ashxFiles = System.IO.Directory.GetFiles(searchPath, "*.ashx", System.IO.SearchOption.AllDirectories);
    
                int searchPathLength = searchPath.Length;
    
    
                System.Collections.Generic.List<EndpointData> ls = new System.Collections.Generic.List<EndpointData>();
    
                foreach (string ashxFile in ashxFiles)
                {
                    string input = @"<%@ WebHandler Language=""VB"" Class=""Portal.Web.Data"" %>";
                    input = System.IO.File.ReadAllText(ashxFile, System.Text.Encoding.UTF8);
    
                    // http://regexstorm.net/tester
                    // <%@ WebHandler Language="VB" Class="AmCharts.JSON" %>
                    System.Text.RegularExpressions.Match mClass = System.Text.RegularExpressions.Regex.Match(input, @"\<\%\@\s*WebHandler.*Class=""(?<class>.*?)"".*?\%\>", System.Text.RegularExpressions.RegexOptions.Multiline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
                    System.Text.RegularExpressions.Match mLanguage = System.Text.RegularExpressions.Regex.Match(input, @"\<\%\@\s*WebHandler.*Language=""(?<language>.*?)"".*?\%\>", System.Text.RegularExpressions.RegexOptions.Multiline | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
    
                    string endpointPath = ashxFile.Substring(searchPathLength).Replace(System.IO.Path.DirectorySeparatorChar, '/');
                    string classname = mClass.Groups["class"].Value;
                    string languagename = mLanguage.Groups["language"].Value;
                    ls.Add(new EndpointData() { Path = endpointPath, Class = classname, Language = languagename });
                }
    
                System.Console.WriteLine("Finished " + ls.Count.ToString());
    
                string json = System.Text.Json.JsonSerializer.Serialize(ls, ls.GetType(),
                    new System.Text.Json.JsonSerializerOptions
                    {
                        IncludeFields = true,
                        WriteIndented = true
                    });
    
                // System.IO.File.WriteAllText(@"D:\PortalHandlers.json", json);
                System.IO.File.WriteAllText(@"D:\BasicHandlers.json", json);
    
                return ls; // 137 Portal + 578 Basic
            }
    
    
            public static int Test(string[] args)
            {
                FindAllHandlers(); 
                return 0;
            } // End Sub Test 
    
    
        } // End Class HandlerFinder
    
    
    } // End Namespace FirstTestApp
    

    And here's the Middleware with example handler "Ping.ashx".

    namespace FirstTestApp
    {
    
        using Microsoft.AspNetCore.Http;
    
    
        public class AshxOptions
        { }
    
    
        public interface IHttpHandler
        {
    
            // true if the System.Web.IHttpHandler instance is reusable; otherwise, false.
            bool IsReusable { get; }
    
            void ProcessRequest(Microsoft.AspNetCore.Http.HttpContext context);
        }
    
    
        public class Ping
        : IHttpHandler
        {
            public void ProcessRequest(Microsoft.AspNetCore.Http.HttpContext context)
            {
                context.Response.StatusCode = 200;
                context.Response.ContentType = "text/plain; charset=utf-8;";
                context.Response.WriteAsync(System.Environment.MachineName).Wait();
            }
    
    
            public bool IsReusable
            {
                get
                {
                    return false;
                }
            } // End Property IsReusable 
        }
    
    
    
    
    
        /// <summary>
        /// Some Middleware that exposes something 
        /// </summary>
        public class AshxMiddleware<T>
            where T:IHttpHandler, new()
        {
            private readonly Microsoft.AspNetCore.Http.RequestDelegate m_next;
            private readonly AshxOptions m_options;
            private readonly ConnectionFactory m_factory;
            
    
    
            /// <summary>
            /// Creates a new instance of <see cref="AshxMiddleware"/>.
            /// </summary>
            public AshxMiddleware(
                  Microsoft.AspNetCore.Http.RequestDelegate next
                , Microsoft.Extensions.Options.IOptions<object> someOptions
                , ConnectionFactory db_factory
                )
            {
                if (next == null)
                {
                    throw new System.ArgumentNullException(nameof(next));
                }
    
                if (someOptions == null)
                {
                    throw new System.ArgumentNullException(nameof(someOptions));
                }
    
                if (db_factory == null)
                {
                    throw new System.ArgumentNullException(nameof(db_factory));
                }
    
                m_next = next;
                m_options = (AshxOptions)someOptions.Value;
                m_factory = db_factory;
            }
    
    
            /// <summary>
            /// Processes a request.
            /// </summary>
            /// <param name="httpContext"></param>
            /// <returns></returns>
            public async System.Threading.Tasks.Task InvokeAsync(Microsoft.AspNetCore.Http.HttpContext context)
            {
                if (context == null)
                {
                    throw new System.ArgumentNullException(nameof(context));
                }
    
                // string foo = XML.NameValueCollectionHelper.ToXml(httpContext.Request);
                // System.Console.WriteLine(foo);
    
                // httpContext.Response.StatusCode = 200;
                // The Cache-Control is per the HTTP 1.1 spec for clients and proxies
                // If you don't care about IE6, then you could omit Cache-Control: no-cache.
                // (some browsers observe no-store and some observe must-revalidate)
                context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate, max-age=0";
                // Other Cache-Control parameters such as max-age are irrelevant 
                // if the abovementioned Cache-Control parameters (no-cache,no-store,must-revalidate) are specified.
    
    
    
                // Expires is per the HTTP 1.0 and 1.1 specs for clients and proxies. 
                // In HTTP 1.1, the Cache-Control takes precedence over Expires, so it's after all for HTTP 1.0 proxies only.
                // If you don't care about HTTP 1.0 proxies, then you could omit Expires.
                context.Response.Headers["Expires"] = "-1, 0, Tue, 01 Jan 1980 1:00:00 GMT";
    
                // The Pragma is per the HTTP 1.0 spec for prehistoric clients, such as Java WebClient
                // If you don't care about IE6 nor HTTP 1.0 clients 
                // (HTTP 1.1 was introduced 1997), then you could omit Pragma.
                context.Response.Headers["pragma"] = "no-cache";
    
    
                // On the other hand, if the server auto-includes a valid Date header, 
                // then you could theoretically omit Cache-Control too and rely on Expires only.
    
                // Date: Wed, 24 Aug 2016 18:32:02 GMT
                // Expires: 0
    
                context.Response.StatusCode = 200;
    
                T foo = new T();
                foo.ProcessRequest(context);
            } // End Task AshxMiddleware 
    
    
        } // End Class AshxMiddleware 
    
    
    } // End Namespace 
    

    Then you still need a generic Endpoint Middleware, here:

    namespace FirstTestApp
    {
        using FirstTestApp;
        using Microsoft.AspNetCore.Builder;
    
        // using Microsoft.AspNetCore.Diagnostics.HealthChecks;
        // using Microsoft.Extensions.Diagnostics.HealthChecks;
        // using Microsoft.AspNetCore.Routing;
        // using Microsoft.Extensions.DependencyInjection;
    
    
        /// <summary>
        /// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add health checks.
        /// </summary>
        public static class GenericEndpointRouteBuilderExtensions
        {
    
    
            /// <summary>
            /// Adds a health checks endpoint to the <see cref="IEndpointRouteBuilder"/> with the specified template.
            /// </summary>
            /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the health checks endpoint to.</param>
            /// <param name="pattern">The URL pattern of the health checks endpoint.</param>
            /// <returns>A convention routes for the health checks endpoint.</returns>
            public static IEndpointConventionBuilder MapEndpoint<T>(
               this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
               string pattern)
            {
                if (endpoints == null)
                {
                    throw new System.ArgumentNullException(nameof(endpoints));
                }
    
                return MapEndpointCore<T>(endpoints, pattern, null);
            }
    
            /// <summary>
            /// Adds a health checks endpoint to the <see cref="IEndpointRouteBuilder"/> with the specified template and options.
            /// </summary>
            /// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/> to add the health checks endpoint to.</param>
            /// <param name="pattern">The URL pattern of the health checks endpoint.</param>
            /// <param name="options">A <see cref="SomeOptions"/> used to configure the health checks.</param>
            /// <returns>A convention routes for the health checks endpoint.</returns>
            public static IEndpointConventionBuilder MapEndpoint<T>(
               this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints,
               string pattern,
               object options)
            {
                if (endpoints == null)
                {
                    throw new System.ArgumentNullException(nameof(endpoints));
                }
    
                if (options == null)
                {
                    throw new System.ArgumentNullException(nameof(options));
                }
    
                return MapEndpointCore<T>(endpoints, pattern, options);
            }
    
    
            private static IEndpointConventionBuilder MapEndpointCore<T>(
                  Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints
                , string pattern
                , object? options)
            {
                if (ReferenceEquals(typeof(T), typeof(SQLMiddleware)))
                {
                    if (endpoints.ServiceProvider.GetService(typeof(ConnectionFactory)) == null)
                    {
    
                        throw new System.InvalidOperationException("Dependency-check failed. Unable to find service " +
                            nameof(ConnectionFactory) + " in " +
                            nameof(GenericEndpointRouteBuilderExtensions.MapEndpointCore)
                            + ", ConfigureServices(...)"
                        );
                    }
                }
    
                object[] args = options == null ? System.Array.Empty<object>() :
                    new object[]
                    {
                        Microsoft.Extensions.Options.Options.Create(options)
                    }
                ;
    
                Microsoft.AspNetCore.Http.RequestDelegate pipeline = endpoints.CreateApplicationBuilder()
                   .UseMiddleware<T>(args)
                   .Build();
    
                return endpoints.Map(pattern, pipeline).WithDisplayName(typeof(T).AssemblyQualifiedName!);
            }
    
    
        } // End Class 
    
    
    }
    

    And then you can set up the Endpoint with:

    app.MapEndpoint<AshxMiddleware<Ping>>("/ping.ashx", new AshxOptions() { });
    

    Keep in mind, middleware is async, and your ASHX-Handlers probably aren't.
    That's gonna be a source of work-intensive problems (or deadlocks, if you just use .Wait() like in my example).