A simple framework to create websites using ASP.NET core. To create a new website simply write this code:
In Test.csproj place:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
</Project>
In Hello.cs place:
namespace Test;
using Codebot.Web;
[DefaultPage("home.html")]
public class Hello : PageHandler
{
public static void Main(string[] args) => App.Run(args);
}
In a console type:
mkdir wwwroot
echo "Test.Hello, Test" > wwwroot/home.dchc
echo "Hello World!" > wwwroot/home.html
dotnet add reference ../Codebot.Web/Codebot.Web.csproj
dotnet run --urls=http://0.0.0.0:5000/
Using the simple example above you would have the following directory and file structure:
+- Codebot.Web
| |
| + Codebot.Web.csproj
|
+- Test
|
+- Test.csproj
|
+- Hello.cs
|
+- wwwroot
|
+- home.dchc
|
+- home.html
In this arrangement Codebot.Web
folder is a sibling of the Test
folder. The Codebot.Web
folder contains a copy of this git repository and the Test
folder contains your website project.
The Test/wwwroot
folder contains the content of your website including any static files and sub folders might want to serve. When a client web browser requests a resource the web server will search for them beginning in the wwwroot
folder.
If a request is made to a folder the framework will search for a special file named home.dchc
(short for dotnet core handler class) then read its contents. The contents of home.dchc
should contain the name of the handler class which will be used to handle the incoming request. In our case, the name of the handler class is Test.Hello, Test
, where Test.Hello
is the namespace qualified name of the handler class and , Test
references the the assembly name where the handler class is located. This handler class should derived from BasicHandler or one of its descendants. An instance of that class handler type will be created by the framework and invoked with the current HttpContext
.
Using this pattern of folders containing a home.dchc
file its possible to design a website with one or more varying page handler types with each folder containing a home.dchc
file designating which handler class processes requests for that specific folder.
A simple way to serve a web page from your handler class is to decorate it with DefaultPage
attribute. This will cause the handler to look for a file resource starting in the wwwroot
folder matching the filename adorned to your attribute. In the code example at the top of this document that file resource is home.html
.
[DefaultPage("home.html")]
public class Hello : PageHandler
It should be noted that the DefaultPage
attribute is completely optional. If you wanted to generate the page yourself through code. You could override the EmptyPage
method and write a response manually using the following:
namespace Test;
using Codebot.Web;
public class Hello : PageHandler
{
protected override void EmptyPage() => Write("Hello World!");
public static void Main(string[] args) => App.Run(args);
}
This would result in the same response content being sent back to the client, but the Hello World!
would be output from your code rather than from a file resource.
Instead of using your DefaultPage
to serve a static file, it might be useful to use it as a template file. A template file can fill out a response using properties of your handler class. To use a template file simply add IsTemplate = true
to the DefaultPage
attribute decoration. Next add a property name, or multiple property names, to your default page file and it will act as a template.
[DefaultPage("home.html", IsTemplate = true)]
public class Hello : PageHandler
And in your home.html
default page file:
<html>
<body>The title of this page is {Title}</body>
</html>
The template engine will recognize the curly braces { }
and attempt to substitute its contents using a property of your object. In the example above The title of this page is {Title}
will be substituted with The title of this page is Blank
. This is because the base class of PageHandler defines a Title
property like so:
public virtual string Title { get => "Blank"; }
To alter the title property in your Hello
handler class you could add:
public override string Title { get => "My Home Page"; }
This would result in the response The title of this page is My Home Page
being generated by the template engine.
Different handler classes can use the same template resulting in different response results. Additionally, properties templated by curly braces { }
can be of any type. They are not required to be strings. For example if you had a User
class with properties like Name, Birthday, and Role, it could be templated in your file resource like so:
<html>
<body>
<h1>Welcome {CurrentUser.Name} of the {CurrentUser.Role} group!</h1>
<p>Your birthday is on {CurrentUser.Birthday}!</p>
</body>
</html>
To make this work your handler class would need to have a property named CurrentUser:
public User CurrentUser { get; private set; }
In addition to inserting templates into your page files, you can also use format specifiers to control how those properties are converted. For example if you have a property on your handler class called DonatedAmount
of type double
it could be formatted like so:
<p>We've received a total of {DonatedAmount:C2}!</p>
And if DonatedAmount was 10157.5
then the response would include We've received a total of $10,157.50!
In addition to using this framework to generate templated responses, it can also be used to respond to web action requests. To create a new web action request simply define a method and adorn it with the Action
attribute:
[Action("hello")]
public void HelloAction() => Write("Hello World!");
If the client then submits a request with an action named hello
it will receive back Hello World!
. Here is what a request to our action would look:
http://example.com/?action=hello
In the web action example above we simply returned some static text. A dynamic result can be produced using arguments from a GET
or POST
request, which typically might originate from a <form>
element on your page. To use those arguments you can use any number of Read
methods. Here is an example:
[Action("purchase")]
public void PurchaseAction()
{
string userId = ReadInt("userId");
string product = ReadString("item");
int count = ReadInt("qty");
DateTime deliveryDate = Read<DateTime>("deliveryDate");
var json = SubmitOrder(userId, product, count, deliveryDate);
ContentType = "text/json";
Write(json);
}
Note the various Read
methods at your disposal. Also note that a response in generated in json format using whatever backend technology you desire. In the example above SubmitOrder
supposedly does some work and returns json text. However you want to take action on a web action request is up to you. This framework just provides a simple way to accept those requests.
The invoker of our PurchaseAction
might come from a web page using a form element like so:
<form action="?action=purchase" method="POST">
<input type="text" name="userId">
<input type="text" name="item">
<input type="text" name="qty">
<input type="text" name="deliveryDate">
<input type="submit">
</form>
If you wanted to invoke our purchase action example without using a <form>
element but through JavaScript instead you might write the following:
let data = new FormData();
data.append("userId", 1);
data.append("item", "bananas");
data.append("qty", "12");
data.append("deliveryDate", "1/15/2020");
let request = new XMLHttpRequest();
request.open("POST", "?action=purchase");
request.send(data);
Here are a few other examples of tasks which can be accomplished using this framework.
Sending a file to the client based on some criteria:
[Action("download")]
public void DownloadAction()
{
string fileName = MapPath("/../private/" + Read("filename"));
if (FileExists(fileName) && UserAuthorized)
SendAttachment(fileName);
else
Redirect("/unauthorized");
}
Serving different templates based on some state of your website:
public override void EmptyPage()
{
if (StoreIsOpened)
// If we are opened include the storefront and format it as a template
Include("/templates/storefront.html", true);
else
// Otherwise send the static we're closed page
Include("/templates/wereclosed.html");
}
Handling json data assuming the entire request body is a json object:
[Action("search")]
public void SearchAction()
{
var criteria = JsonSerializer.Deserialize<SearchCriteria>(ReadBody());
var results = PerformSearch(criteria);
ContentType = 'text/json';
Write(JsonSerializer.Serialize(results));
}
Security in this framework can be handled through user authentication. You may implement your own security using the IUser
and IUserSecrity
interfaces. You are free to implement users and the security anyway you want, but this framework provides you with an basic version using xml storage of users in a private folder.
Here is an example of how to use this basic implemenation:
namespace Test.Web;
using Codebot.Web;
[LoginPage("/Templates/Login.html", IsTemplate = true)]
[DefaultPage("/Templates/Home.html", IsTemplate = true)]
public class HomePage : BasicUserPage
{
public static void Main(string[] args)
{
App.UseSecurity(new BasicUserSecurity());
App.Run(args);
}
}
This defines a home page type inheriting from BasicUserPage
and instructing App
to use BasicUserSecurity
as the security system. If you want to add to the user type using this basic system, you should derive from BasicUser
adding the information you need, for example email address and phone number. Next you would extend FileUserSecurity
taking care to override the CreateUser
, ReadUser
, WriteUser
, and GenerateDefaultUsers
methods.
You are free to implement your own user and security types using other storage options such as a database. If you want to do this follow FileUserSecurity
as a guide.