ESP: Keeping Classic ASP.NET Alive
I'm a big fan of Classic ASP.NET for web development, and (among other things) our website is built 100% on ASP.NET with Elements. But, as you might have heard or found out, classic ASP.NET (by which I mean .aspx
files, <%
tags, and the like), is (has been?) going away with .NET Core.
I don't like that, and I expect many, many other .NET developers are even more locked into and used to working with ASP.NET than me, so a while back I started on a skunkworks side project: re-inventing Classic ASP.NET from scratch as part of the Elements toolchain.
The first fruits of this are starting to become tangible, and while a lot (a lot) still needs to be done, I thought I would share whats's there and how to use it with a few early adopters, to get feedback and – ideally – help implementing the rest.
My goal is for ESP (as I'm calling it for now) to support both compiling to a single self-contained server executable (that's the only mode right supported now) as well as the more loose "compile on-demand" project-less "website" model.
Right now, self-contained ESP servers ("Standalone" servers) host using our Internet Pack HTTP implementation, but the goal is that in the long run, they can also host in IIS & Co, and on ASP.NET Core's low-level HTTP stack.
ESP right now builds for Classic .NET and .NET Standard 2.0, which means it can be also used on .NET Core 3 and later. This means that while maintaining the classic ASP.NET code, your project could also benefit all the internal performance improvements added in the latest releases of .NET Core and .NET 5+. The plan is for it to also build for the other (applicable) Elements platforms, mainly native Windows, Linux, and macOS, in the long run. Yep, that's right. CPU-native ASP.NET on your Linux server.
Getting Started
Before you do, let me remind you again that this is still in the very, very early stages. A lot of things don't work yet. Many things will change. But the project is at a good place to get some additional eyes on it.
To start off, you will want Elements 2697 or later (shipped December 10). You will want to work in Fire or Water for the best experience because, while most of ESP lives in libraries and EBuild, I am making ease-of-use improvements to Fire/Water as well, and not bothering with VS for now.
You will also need to grab two libraries we don't currently ship, from GitHub: our established open-source network library Internet Pack (IP) and Elements.Web, which builds on IP to implement ESP functionality and the class libraries used by ASP.NET. You will want to manually build these two libraries and reference them from your ESP test project(s).
ESP Project Types
There are currently two types of projects you can use to work with ESP
- You can have a regular .NET (or .NET Core) Executable project that contains your code and your
.aspx
,.ascx
and similar files. Right now this kind of project would simply ignore the ASP.NET files but with a few easy steps, you can make it build as an ESP server. - You can use a "project-less" website format, much as you (probably) would use if you currently host an ASP.NET site in IIS and rely on IIS to build and serve the files as it finds them. Fire and Water have had limited support for working with these projects for a while now (mainly for my internal use to work on our website). That has been enhanced to support ESP and EBuild has been extended to build those "project-less" websites into a Standalone executable server (again, actually hosting a "project-less" website comes later).
Setting up an ESP Executable Project...
...is fairly simple. Let's assume you already have an .exe
project with ASP.NET
stuff. There's only a few changes you need to make to it to enable ESP;
- Add references to
Elements.Web.dll
andRemObjects.InternetPack.dll
, using "Add Reference" and then "Browse", or by dragging the .dlls onto the project. (use the right version, Classic or .NET Standard, depending on your Target Framework). - Add the following setting in the top section of your project file (manually for now, sorry):
<EnableASPX>True</EnableASPX>
. - Add something like the following to your startup code (the WaitFor is only needed to keep your console app alive; if you're using a WinForms or an NT Service project, you might now need it). This is all temporary.
var lServer := new RemObjects.Elements.Web.WebServer;
lServer.PageFactory := __WebPageFactory.Instance;
lServer.Start;
writeLn($"Server is running on Port {lServer.Port}.");
var fWait := new RemObjects.Elements.RTL.Event;fWait.WaitFor();
- Finally, if your project has a reference to
System.Web
, remove it, as otherwise, you'll get conflicts with types fromElement.Web.dll
. This mighty change later, but for now,Elements.Web
provides (or will provide ;) unique implementations for all needed classes and has compatibility aliases using theSystem.Web
names, so that existing ASP.NET code will hopefully build with as few changes as possible, if any.
That's it. Run it, and your .aspx pages will be served on http://localhost:8000.
Setting up a Project-Less Website...
...is a bit trickier, as the IDE only has limited support for using them, but not creating them. It might be easiest to start with the sample I'll link below, or if you have an existing site, create a new .sln
with Fire and Water and manually add this project entry to it:
Project("{D7F5FCA5-08BC-44E5-8A3A-60BF2B79FCCC}") = "Site", "Site", "{6334D709-84F9-4BFA-9156-20DD798F17B1}"
EndProject
There, the first GUID must match, as it is the special magic ID that tells Fire/Water (and EBuild) to treat this as a web project, and the second GUID should be new and unique to the project. "`Site`" should be the path to your website root folder, relative from the .sln.
Next, add references to Elements.Web.dll
and RemObjects.InternetPack.dll
by dropping the .dll
s (or .fx
and .a
😱) into the ./Bin
folder of your site.
You should now be able to build and run the project, and again your .aspx pages
will be served on http://localhost:8000.
Project-Less Websites, Behind the Scenes
What happens behind the scenes is that EBuild, on compile,. will create an ad-hoc in-memory Elements project for you. It will use all binaries found in ./Bin
as references (you will also see that reflected in the IDE), and add all the code files and .as?x
files to the project to be processed and compiled.Iit also adds all images and other files to be added as resources (this will be configurable later).
It will also look at your Web.config
for mere details. It will migrate the appSettings
section to end up in your .exe.config
, and it will use any references listed in <system.web>
.
You can also add two types of custom ESP-specific configuration settings, to add project settings, references, and NuGet package references to your projects:
<configSections>
<section name="esp.projectSettings" type="RemObjects.Elements.Web.ProjectSettings"/>
<section name="esp.projectReferences" type="RemObjects.Elements.Web.ProjectReferences"/>
</configSections>
<esp.projectSettings>
<TargetFramework>.NETCore6.0</TargetFramework>
<OutputFolder>../Bin</OutputFolder>
<RemapErrors>True</RemapErrors>
</esp.projectSettings>
<esp.projectReferences>
<packages>
<add package="System.Configuration.ConfigurationManager:*"/>
<add package="System.ServiceModel.Syndication:*"/>
</packages>
<assemblies>
<add assembly="Mercury"/>
</assemblies>
</esp.projectReferences>
The above shows how you'd switch the project to .NET Core (the default, for now, is Classic .NET 4.x), set an output folder, and add a few references. I will get back to the "RemapErrors
" setting later.
How Does it Work?
Firstly, it probably won't, unless your project is super simple and only uses the parts that are implemented 😅. That's where you come in, I need your feedback on what's missing and I didn't think of ;). On top of the many things that I already know are still missing, see below...
Let's look at ESP on three levels: EBuild, Library, and at runtime. For the remainder of this text, it should not matter much if you have an Executable project or project-less website.
EBuild
EBuild does most of the heavy lifting for making ESP work. It will process each .aspx
, .ascx
, .master
, .ashx
and .asmx
file and generate a code-behind file for it that will be injected into the compile. You can (and will want to) see these files in Fire/Water via the "Generated Source Files" node in the jump bar at the top. Also, new in .2697, you can right-click a file in the Solution Tree and select "Show Generated Code-Behind File" to see it, and you can sue the same to get back from a generated file to the original. In Fire, you can also press ^⌥G (sorry, Water is running out of available keyboard shortcuts, because Windows ;).
EBuild also generates a file called EspSupport.g.pas
that defines the __WebPageFactoryclass
you saw earlier in the entry-point code. This class knows about each .aspx
file that was processed, and the path(s) it should serve them under. It also has a list of known needed redirects (e.g. to add "/
" to folder URLs, or optionally, but for now the default, to hide the ".aspx
" extension and make prettier user-visible URLs), and resources.
For project-less websites, EBuild also generates EspEntryPoint.g.pas
, which contains the same startup code seen earlier.
The processing code for .aspx
(and .ascx
and .master
) is fairly tricky and complex, as you can imagine, and I expect/hope that we will together find many corner-cases that arent covered yet.
By default, EBuild does emit {$LINE
/#line
directives to map errors in the generated files back to the .aspx
files, where possible (just as Classic ASP.NET does). Because I expect this to be flaky and not 100% reliable for now, you can disable this via the RemapErrors
setting we saw before (this works for Executable projects too, you just have to set it manually in the .elements project file), byu setting it to False
.
What should work?
Basic pages, Controls, Master Pages should work, and do work in my testing. Advanced control features such as ParseChildren
, etc. won't yet (and some might never).
One difference/constraint of ESP, which I'm not planning to change, is the following: Classic ASP.NET builds each control separately, and then uses Reflection/IL metadata to know things such as the type on a control's property, its attributes, etc. EBuild processes each file standalone, and will then compile them all together (this part will change for a future "loosely hosted" mode). We introduced a new literal() helper function to get around not knowing property types on controls at conversion time, but one difference you will see is that while Classic ASP.NET simply ignored unknown properties, with ESP you will get an error. I'm not sure how/if ESP will handle ParseChildren & Co, yet.
The Library: Elements.Web
The next part is the Elements.Web
library, which provides or will provide all the classes needed to replicate ASP.NET.
It purposely does not use System.Web
to remain platform-agnostic (first for Core, later for Island as well), and in fact, it cannot be used in the same project with System.Web
(for now), as while ESP uses unique names of its classes, the library provides convenient aliases for many types and namespaces to make sure that existing ASP/NET code can compile in ESP (and ideally dual-maintained in ESP and Classic ASP.NET for a while) with few or no changes at all (This might later get extracted/refactored into an optional separate library, to allow referencing System.Web
in your projects again, if needed).
Elements.Web
provides many of the classes you expect, from the base Control
, UserControl
, Page
, MasterPage
classes, to infrastructure such as Request
, Response
, Context
, Browser
, Session
, etc.
Many of these are still very incomplete. For example, support for accessing Forms, QueryStrings, Session data, Cookies, is there to make code compile, but they are still just empty stubs.
Elements.Web
also provides the HTTP server functionality itself. Right now that is very simple and built on InternetPack. The idea is that this would later be abstracted so that the higher-level "ASP.NET" classes can run on top of different HTTP infrastructures, mainly planned IP, ASP.NET Core's low-level web technology, and directly on IIS and other "real" web servers.
Help on fleshing out Elements.Web
would be greatly appreciated.
At Runtime
The runtime logic (for now) is very simple. The HTTP server, implemented in Elements.Web/EspServer.pas
, basically replies on the PageFactory class to know what to do with a given URL path. It can pass execution on to the generated code for an .aspx
, it can do redirects, and serve known embedded resources (also listed in the probably now misnamed PageFactory). Or it can serve a generic 404.
Of course, this will get fleshed out over time.
Most of the execution of actual pages (or, later .ashx
) is simply deferred to the code generated by EBuild at compile-time, which renders the page, its master page(s), controls, and the like.
Give it a Try
I think that's all that is to say, for now. Please give it a try, let me/us know what works and what doesn't (and which missing features are most crucial to you), and – if you can – help us flesh out the Elements.Web
library.
I will update this topic below, as things change/evolve.