Skip to content

A simple web server framework based on ASP.NET core

License

Notifications You must be signed in to change notification settings

sysrpl/Codebot.Web

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Codebot.Web

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/

Directory and File Structure Explained

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.

Serving Pages from Your Class

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.

Using Templates

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; }

Formatting Templates

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!

Responding to Web Actions

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

Processing Web Action Arguments

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);

Other Examples

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

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.

About

A simple web server framework based on ASP.NET core

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages