Unity Runtime UI – Crib Notes

Initial Setup

Every UI Document component references a UI Document asset (.uxml file) that defines the UI and a Panel Settings asset that renders it. You can connect more than one UI Document asset to a single Panel Settings asset.

Right-click Project window, and then select Create > UI Toolkit > UI Document

  1. In the Hierarchy right click the Scene, select GameObject > UI Toolkit > UI Document. This creates the following:
    • A UI Toolkit folder with a Panel Settings asset and a default runtime theme.
    • A GameObject with a UI Document component which in turn the is connected to the Panel Settings asset.
  2. In the Hierarchy select the UIDocument  then drag SimpleRuntimeUI.uxml from your Project window to the inspector (Source Asset)
  3. Create a companion script that derives from MonoBehaviour.
  4. Add SimpleRuntimeUI.cs as a component of the UIDocument GameObject.
  5. Click on the PanelSettings object Project > Assets > UI Toolkit then drag the theme style sheet (.tss) to the Panel.
  1. Use the Package Manager to install the Input System package.
  2. Select Edit > Project Settings > Player.
  3. Set Active Input Handling to Input System Package (New).
  4. Select GameObject > UI > Event System. This adds an Event System GameObjectThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
    See in Glossary that includes a Standalone Input Module in the Scene. The module shows an error message along with a button in the Inspector window.

Custom USS

  • Right click Project Create UI ToolkitStyle Sheet
  • Add style sheet to umxl view
    • Add a Style tag with src attribute pointing to the style sheet
    • In the UI Builder upper left corner (+▼) → Add Existing USS

Using UXML instances as templates

Any UXML file in the project is available as a library object in the UI Builder.

Editing Templates

Open in UI Builder

Library→ProjectComponent (RMB)→Open UI Builder

Close the current context and open the component in the UI Builder.

Open as Sub-Document

Hierarchy→Component (RMB)→Open Instance in Isolation

Edit the component in the UI Builder, maintaining a breadcrumb to the previous context.

Open Sub-Document in Place

Hierarchy→Component (RMB)→Open Instance in Context

Edit the component while keeping the view on the current component. Maintains a breadcrumb to the previous context.

Common Attributes

  • name
  • tooltip
  • tabindex

Accessing UXML Elements in C#

Typically a controller for a UI Element will is added to the UI Document as a component. Alternately the the UI Document can be set as a field on a MonoBehaviour instance.

Extending the MonoBehaviour class is most expedient way to control a UI component when it has only one instance. Public accessors permit access from other controllers, and should be used sparingly.

Select UI Document, in the Inspector→Add Component→Scripts.

using UnityEngine;
using UnityEngine.UIElements;

public class GameListController : MonoBehaviour {
    public string GameListID = "GameList";

    private VisualElement Root {
        get { return GetComponent<UIDocument>().rootVisualElement; }

    private ListView GameList{
        get { return Root.Q<ListView>(GameListID); }

Add event handlers to the MonoBehaviour Start method.

public void Start() {
public UIDocument uiDocument1;
[SerializeReference] private UIDocument uiDocument2;
var uiDocument = GetComponent<UIDocument>();
public VisualElement Root {
    get { return GetComponent<UIDocument>().rootVisualElement; }

Custom Control Class for UXML Elements [source]

Create a C# class derived from the VisualElement class or a subclass.

You can initialize the control in the constructor, or when it’s added to the UI.

Event Handling


You can register an event handler on an existing class, to handle events such as a mouse click.

Dynamically Adding Elements

ScrollView scrollView = Root.Q<ScrollView>(GameListID);
scrollView.contentContainer.Add(new Label("I done got clicked"));

External Links

UXML Elements Reference

Override VisualElement.ContainsPoint() to assign custom intersection logic. [source]

You can also create USS custom properties to style a custom control. [source]







NetCat (nc) is used to read and write to TCP and UDP network connections. It is an excellent tool for diagnosing network connectivity issues.


sudo apt-get install netcat


To listen to a specific port.

nc -l -p 8000

To write to a specific port

nc 8000
nc -v 8000

MySQL in C#

Package Introduction

Database access is a core aspect of many applications. C# can interface with many different databases such as Oracle, Microsoft SQL, and MongoDB. In this case, we will be working with MySQL. The first step is to add the MySQL package to your project.

dotnet add package MySql.Data

Saving Your Credentials

When you run a client program to connect to the MySQL server, it is inadvisable to specify your password in a way that exposes it to discovery by other users. We will store our credentials in a .env file and load them into the environment variables. The code to load .env files is trivial, so we will just add it directly to the project. Add the .env file to your .gitignore so that you don’t upload your credentials to your code repository.

class DotEnv {
    public static void Load() {
        string filepath = Path.Combine(


    public static void Load(string filePath) {
        if (!File.Exists(filePath)) return;

        foreach (var line in File.ReadAllLines(filePath)) {
            var parts = line.Split('=',StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length != 2) continue;
            if (Environment.GetEnvironmentVariable(parts[0]) != null) continue;
            Environment.SetEnvironmentVariable(parts[0], parts[1]);

Example .env File


Creating a Connection

Before you can perform database operations, you need to establish a connection. This is achieved with a MySqlConnection object. We will add the variables from the .env file with string interpolation.

using MySql.Data.MySqlClient;

var env = Environment.GetEnvironmentVariables();

string cs = @$"

var con = new MySqlConnection(cs);
Console.WriteLine($"MySQL version : {con.ServerVersion}");

Prepared Statements

After the connection has been established you will typically perform one or more operations, this is enabled by the MySqlCommand object. There are three primary methods that you will usually use:

  • ExecuteReader: Query the database.
  • ExecuteNonQuery: Insert, Update, Delete data.
  • ExecuteScalar: Retrieve a single value.

Although possible to build SQL queries using string interpolation, it is not advisable to put query values directly in the query string. Doing so leaves your database open to injection attacks. Instead of placing the value into the query directly, we will use placeholders, prefixed with ‘@’.


string sql = $"SELECT Name FROM Country WHERE Continent = {continent};


string sql = "SELECT Name FROM Country WHERE Continent = @continent;

When using parameterized strings the values are added using the Parameters field of the MySqlCommand object.

Update the Database

To insert a new record into the database we use the ExecuteNonQuery method.


Executes a SQL statement returning the affected row count. [*]

        string sql = "INSERT INTO users(username, password, email) values (@username, @password, @email)";
        using var cmd = new MySqlCommand(sql, con);
        cmd.Parameters.AddWithValue("@username", username);
        cmd.Parameters.AddWithValue("@password", password);
        cmd.Parameters.AddWithValue("@email", email);

Query the Database

Sends the CommandText value to MySqlConnection and builds a MySqlDataReader object.

The Reader object contains the rows that result from the SQL command execution. The Read method advances the reader to the next row, returning true if a row exists. The using keyword ensures that classes that implement the IDisposable interface call their dispose method.

using (MySqlDataReader reader = cmd.ExecuteReader()){
    while (reader.Read()) {

You retrieve values from the current row with the class indexer which can retrieve values by column index or name. There are also several methods to retrieve a value by type, for example, GetInt32. The following statements perform the same action.

var name = reader[0];
var name = reader["name"];
var name = reader.GetString(0);
var name = reader.GetString("name");

External Reading

Official MySQL/Net Tutorial


Nuget MySQL Package




Express – Attaching Data to a Proxy Router


Forward an HTTP request from one server to another while attaching data to the request. In the example the user will select a file from the Portal-Server and attach the file data on a request to the API-Server. We will also attach the user identification that was returned from the Authentication-Server.

Encoding Type

When you make a POST request, you have to encode the data that forms the body of the request in some way [1]. This encoding type is communicated in the header field enctype, which has one of three values.

  • application/x-www-form-urlencoded – A URL encoded form, this is the default.
  • multipart/form-data – Multipart form with fields separated by a unique value. Used for uploading files.
  • text/plain – Sends the data without any encoding.

We will only concern ourselves with the first two encoding types.

Encoding: application/x-www-form-urlencoded

Submit the POST with the parameters encoded in the body as a string of key-value pairs. Each pair is separated by an ‘&’ and the pair is seperated by ‘=’.


The key value pairs are inserted into the body and can be extracted by the server with the bodyParser package. The key-value pairs can also be directly attached to the URL request, in which case they are retrieved from the req.query field. See implementation.

Encoding: multipart/form-data

Generally used for submitting file, or binary data. We will highlight the differences by manipulating the first example. We will keep everything else the same except for the encoding type. To retrieve the data on the server we will use the multer package.

The only difference in the multipart request as opposed to the urlencoded request is the in the body.

Content-Disposition: form-data; name="jobid"

Content-Disposition: form-data; name="jobname"


Instead of the ampersand delimited string of parameters, the parameters are now delimited by the unique boundary string “——WebKitFormBoundaryB2AGWn1yYcOCi0T9“. The boundary string is chosen in such a way as it does not appear anywhere else in the body data. Normally the programmer does not need to worry themselves about it, it is generated automatically. The boundary string is send in the HTTP content-type request header, appended to the type by a semicolon. See implementation.

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryB2AGWn1yYcOCi0T9

Attaching a file to a form

The simplest way to add a file is to include the file-input HTML tag on the form.

<input id="upload-dialog" type="file" name="fileupload">

The raw file is added to the body preceded by some file information. This is similar to how data fields get appended to the body.

Content-Disposition: form-data; name="fileupload"; filename="iama_text_file.txt"
Content-Type: text/plain

In here is some simple text.
Two lines to fancy it up a bit.


To retrieve the file on the server we will again use the multer package, except this time we’ll use the ‘single‘ method. This will place the file in the ‘uploads‘ subdirectory. Note, the parameter we pass matches the name we gave to the upload-dialog above.

multer({ dest: 'uploads/' }).single('fileupload')

If you now examine the uploads/ subdirectory, you’ll find a file with a hash file for a name. This is because do not want to save raw filenames to your file-system. The uploaded file information is stored in the req.file field (see below). You can file the saved filename as well as the original filename here. You can sanitize the filename and move or copy the file as you see fit.

fieldname: 'fileupload',
originalname: 'iama_text_file.txt',
encoding: '7bit',
mimetype: 'text/plain',
destination: 'uploads/',
filename: '5f66c866e68790a2c494275829acd0b8',
path: 'uploads/5f66c866e68790a2c494275829acd0b8',
size: 61

Attaching File Data to a Proxy

Now that we understand the formatting of a file-upload, we want to manipulate a HTTP request to dynamically attach file data. The situation is that a file was previously uploaded and stored on the Portal-Server. Now we want to send that file to an API-Server with whatever other information is required. Doing this allows us to test the API-Server without special consideration, simply by using a web form (as we’ve been doing). Then when we deploy the API-Server we firewall it so that only the Portal-Server sees it. But, we proxy the call from the Portal-Server to the API-Server.

To facilitate proxies we are going to use the http-proxy-middleware package. To view the full code visit the github repository for this project.

Proxy Request

The basic proxy requires a options object. The one we’ll be using is presented below. There are many more options available in the documentation.

  • target – Target host name.
  • pathRewrite – Regex rewrite of the target’s url path.
  • onProxyReq – The function to manipulate the request.
    target: "http://localhost:8000",
    pathRewrite: { 'forward': 'raw' },

    onProxyReq: appendFile

We will only be attaching the proxy to a single url. We call ‘multer’ first to extract existing parameters. Then we call the proxy, passing in the options object. This is all that is required for a simple proxy. What remains is attaching the file and data information to the request.


Attach Data to Request Body

The onProxyReq function accepts three parameters: proxyReq, req, and res. We will only be concerned with the first two.

function appendFile(proxyReq, req, res) {
    const args = {
        userid: "user@id"

    const filepath = Path.join(".", "uploads", req.body.filename);
    const formData = new FormData();

    Object.keys(args).map(key => formData.append(key, args[key]));
    formData.append('fileupload', FS.readFileSync(filepath), 'file.txt');

    proxyReq.setHeader('content-type', `multipart/form-data; boundary=${formData.getBoundary()}`);
    proxyReq.setHeader('content-length', formData.getBuffer().length);


(8) The FormData class is a library to create readable "multipart/form-data" streams. Can be used to submit forms and file uploads to other web applications.

(10) We add all values to the FormData object. We preserve old values (3) and append new values (4). This is optional and can be omitted if you want to remove the previous parameters.

(11) The file data is added to the FormData object using the readFileSync function which returns a Buffer object.

(13) We manually set the ready on the proxy request particularly to specify the boundary string.

(14) The FormData object will also provide us with the buffer length.

(16) The buffered FormData object is written out to proxyReq.

External References

[1] https://www.w3.org/html/wg/spec/association-of-controls-and-forms.html#attr-fs-enctype

[2] https://nodejs.org/api/http.html#http_class_http_clientrequest






Browser HTML Form

        <form action="/multipart-data?id=x" method="post" enctype="multipart/form-data">
            <input type="number" name="jobid" value="0"><br>
            <input type="text" name="jobname" value=""><br>
            <input type="submit"><br>

Server Express Router

import express from "express";
import bodyParser from "body-parser";

const router = express.Router();

    multer({ dest: 'uploads/' }).none(),

function log(req, res, next) {
    console.log(req.method + ` ` + req.originalUrl);

function printBody(req, res, next){

export default router;

Terminal Output

POST /justdata
{ jobid: '0', jobname: 'asdf' }
{ id: 'x' }

Request Headers

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-CA,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 234
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryB2AGWn1yYcOCi0T9

Request Body

Content-Disposition: form-data; name="jobid"

Content-Disposition: form-data; name="jobname"



Browser HTML Form

        <form action="/justdata?id=x" method="post" enctype="application/x-www-form-urlencoded">
            <input type="number" name="jobid" value="0"><br>
            <input type="text" name="jobname" value=""><br>
            <input type="submit"><br>

Server Express Router

import express from "express";
import bodyParser from "body-parser";

const router = express.Router();

    bodyParser.urlencoded({ extended: true }),

function log(req, res, next) {
    console.log(req.method + ` ` + req.originalUrl);

function printBody(req, res, next){

export default router;

Terminal Output

POST /justdata
{ jobid: '0', jobname: 'asdf' }
{ id: 'x' }

Request Headers

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: en-CA,en-GB;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 20
Content-Type: application/x-www-form-urlencoded

Request Body


MySQL Server on Ubuntu 22.04 (WSL)

Install and Setup

sudo apt update
sudo apt upgrade
sudo apt install mysql-server
sudo service mysql start
sudo service mysql status
sudo mysql
sql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password by 'password';
sudo mysql_secure_installation

Add User

mysql -u root -p
sql> CREATE USER 'MyUsername'@'localhost' IDENTIFIED BY 'password';
sql> CREATE DATABASE datebase_name;
sql> quit

Access Database

mysql -u MyUsername -p
sql> show databases;
sql> use database_name;
GRANT ALL ON demo.* TO 'demo'@'localhost';
mysql --login-path=demo


C# JSON Client-Server Implementation


A Socket is one end of a two way communication link. The major protocols for socket communication are TCP and UDP. The primary difference is that TCP guarantees data delivery and the order of data packets. UDP does not make such guarantees, but as a consequence it is faster. Developers typically default to TCP.

The C# socket library is found inthe System.Net package.

using System.Net;
using System.Net.Sockets;

IP Endpoint

An IPEndpoint is the pairing of an IPAddress and a port number. You can use DNS lookup to obtain an IP address.

IPHostEntry ipHostInfo = await Dns.GetHostEntryAsync("google.com");
IPAddress ipAddress = ipHostInfo.AddressList[0];
IPEndPoint ipEndPoint = new(ipAddress, 7000);

When creating the server endpoint, you can specify ‘any’ for the ip address.

IPEndPoint ipEndPoint = new(IPAddress.Any, 7000);

Terminate a Socket

Shutdown disables sends and receives on a Socket.

Close will terminate the Socket connection and releases all associated resources.



A server must first listen for and accept connections. Then, when a connection is made, listen for data on a seperate Socket.

Creating a Socket

Bind associates a socket with an endpoint.

Listen causes a connection-oriented (server) Socket to listen for incoming connection attempts.

Socket socket = new Socket(ipEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Accepting Connections

Accept blocks until an incoming connection attempt is queued then it extracts the first pending request from a queue. It then creates and returns a new Socket. You can call the RemoteEndPoint method of the returned Socket to identify the remote host’s network address and port number.

See AcceptAsync for the asynchronous accept call.

Socket handler = this.listener.Accept();


You create the client side socket in the same manner as the server. The difference being, instead of Bind and Listen, the client uses Connect.

IPAddress ipAdd = IPAddress.Parse("");
IPEndPoint ipEndPt = new IPEndPoint(ipAdd, port);
Socket socket = new Socket(ipAdd.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Reading & Writing

Reading and writing date on a socket is the same for both the server and client side socket. Once the actual connection is made, the difference between the two is arbitrary. Both the read and operations require a byte array to act as a buffer. So your data will need to be converted to and from an array of bytes.


The Send method is used to write to a socket. While there are a number of different method flavours, the most common is to send the entire contents of a byte array.

byte[] msg = Encoding.ASCII.GetBytes(aString);

If you are using a connection-oriented protocol, Send will block until the requested number of bytes are sent, unless a time-out was set by using Socket.SendTimeout. If the time-out value was exceeded, the Send call will throw a SocketException

If the receiving socket (server) has not been started a SocketException will be thrown. If the server has been started but Accept has not been called reading and writing will hang. This can be remedied by setting a timeout (in ms).

Set Read/Write Timeouts

socket.ReceiveTimeout = 1000;
scoket.SentTimeout = 1000;


If the remote host shuts down the Socket connection with the Shutdown method, and all available data has been received, the Receive method will complete immediately and return zero bytes. This allows you to detect a clean shutdown.

There are many flavours of the Receive method, but we will only concern ourselves with two of them. The first reads all bytes from a socket. It returns the number of bytes read. You will need to provide your own EOF indicator or wait until 0 bytes are read which means the socket has finished writing and closed. This is useful if you are only connecting the sockets for a single read-write operation.

byte[] bytes = new byte[BUFFER_SIZE];

The second, and the one we will be using, is to read a specific number of bytes from the socket.

int count = socket.Receive(bytes, nextReadSize, SocketFlags.None);

We will use this to first read the size of the data, then read the body of the data.

// read size
byte[] bytes = new byte[INT_BUFFER_SIZE];
socket.Receive(bytes, INT_BUFFER_SIZE, SocketFlags.None);
int size = BitConverter.ToInt32(bytes, 0);

// read message
byte[] bytes = new byte[size];
socket.Receive(bytes, size, SocketFlags.None)
string data = Encoding.ASCII.GetString(bytes, 0, count);



The json encoding and decoding will be handled by the Newtonsoft Json.Net library.

dotnet add package Newtonsoft.Json --version 13.0.1

The include statements for this library are.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;


There are 3 source files in the project:

  • Server.cs: Manages new client connecting to the server and emits Connection objects.
  • Connection.cs: Wraps a socket providing the read-write capabilities.
  • Client.cs: A connection object that connects to its own socket.


Most of the details for this class are provided in the above background section. The main detail here is the way new connections are managed. I have put the loop inside a IEnumerable method. This should be looped to hand off new connection objects. The following example does this in a new thread.

Thread thread = new Thread(new ThreadStart(()=>{    
    foreach(Connection connection in server.Connections()){


The connection object reads and writes JSON objects or Strings in two parts. First it writes four bytes representing the integer size of the data. Next it writes the data it’s self.

Client client = new Client().Connect("", 7000);
client.socket.ReceiveTimeout = 3000;
Console.WriteLine("> " + client.ReadString());

C# Creating a Class Library

This instruction post is an abbreviation of Microsoft tutorial found HERE.


  • Create the Library
  • Using the Library
  • Unit Testing
  • Including the .dss Directly
  • Setup

    Create an SLN (project solution file)

    An SLN is a Visual Studio solution file that keeps information about the organization of projects in a file.

    dotnet new sln

    Create the class library project

    The -o (–output) flag specifies the directory to place the project. It will create the directory.

    dotnet new classlib -o StringLibrary

    Add the project to the solution

    dotnet sln add StringLibrary/StringLibrary.csproj

    Add code to the library

    Add the following code to the StringLibrary/Class1.cs file

    namespace UtilityLibraries;
    public static class StringLibrary
        public static bool StartsWithUpper(this string? str)
            if (string.IsNullOrWhiteSpace(str))
                return false;
            char ch = str[0];
            return char.IsUpper(ch);

    Build the solution

    dotnet build

    Using the Library

    Create a console app

    dotnet new console -o ShowCase

    Add the console app to the solution

    dotnet sln add ShowCase/ShowCase.csproj

    Modify the ShowCase/Program.cs file

    using UtilityLibraries;
    class Program {
        static void Main(string[] args) {
            foreach(string s in args){
                var uc = s.StartsWithUpper();
                Console.WriteLine($"String {s} starts with uppser case {uc}");

    Add a ‘StringLibrary’ reference to ‘ShowCase’

    dotnet add ShowCase/ShowCase.csproj reference StringLibrary/StringLibrary.csproj

    Run ‘ShowCase’

    dotnet run --project ShowCase/ShowCase.csproj AB ab

    Unit Testing

    Create the unit test project and add a reference

    dotnet new mstest -o StringLibraryTest
    dotnet add StringLibraryTest/StringLibraryTest.csproj reference StringLibrary/StringLibrary.csproj

    Run the unit test

    dotnet test StringLibraryTest/StringLibraryTest.csproj

    Including the .dll directly

    Add the following to your project’s .csproj file. Change the StringLibrary project name as necessary.

        <Reference Include="StringLibrary">

    Installing C# on Ubuntu Linux

  • Background
  • Install the SDK
  • Install the Runtime
  • Create an Application
  • External Reading
  • Background

    The .NET framework was main implementation of the open specification, Common Language Infrastructure (CLI) which is platform independent target for high level languages.

    The Common Language Runtime (CLR) is synonymous to Java’s JRE. Compilers target the CLR and emit managed code. This code permits cross-language integration and exception handling.

    The .NET framework was cross-platform by design only, not on implementation. Until about 2012, the .NET Runtime (CLR) was for Windows, and the .Net Framework was Windows-centric. There was limited cross-platform support from the Mono project, being unsupported, it lagged behind the official releases.

    In June of 2016, Microsoft released .NET Core. This was a port of the CLR to Linux with a rewriting of the framework from the ground up. Version 2.0 was released in 2017, and version 3.0 in 2019. This numbering system conflicted with the Windows proprietary .NET framework numbering. As such, version 4 was skipped and .NET 5.0 was released in 2020 followed by .NET 6.0 in 2021.

    As of version 5.0, the old .NET Framework was given “legacy support” status, and .NET Core was renamed to just .NET. This is why the confusion really gets bad.

    In summary, .NET Framework had versions 1.0, 1.1, 2.0, 3.0, 3.5, and 4.0-4.8. It will never have a 5.0 version. While .NET Core had versions 1.0, 2.0, 2.1, 3.0, and 3.1. Then it was renamed, to just .NET 5.

    Future versions of .NET will provide LTS for even-numbered versions. New versions release every November.

    This image is licensed under the Creative Commons Attribution-Share Alike 4.0 International license.

    Source https://en.wikipedia.org/wiki/File:Overview_of_the_Common_Language_Infrastructure_2015.svg

    Install the SDK


    The .NET SDK allows you to develop apps with .NET. If you install the .NET SDK, you don’t need to install the runtime.

    sudo apt-get update
    sudo apt-get install -y dotnet6

    Install the runtime (optional)

    The ASP.NET Core Runtime allows you to run apps that were made with .NET that didn’t provide the runtime.

    sudo apt-get update
    sudo apt-get install -y aspnetcore-runtime-6.0

    You can install the .NET Runtime, which doesn’t include ASP.NET Core.

    sudo apt-get update
    sudo apt-get install -y dotnet-runtime-6.0

    Create an application

    Create a new .NET application called “HelloWorld”

    1. Open your terminal
    2. Create and enter a new directory called “HelloWorld”
    3. Use the “dotnet” command to create a new application.
    dotnet new console --framework net6.0

    In your IDE, replace the contents of Program.cs with the following code:

    namespace HelloWorld {
        class Program {
            static void Main(string[] args) {
                Console.WriteLine("Hello World!");

    Alternatively you can create a new console app with the directory structure by including the output flag (-o).

    dotnet new console -o HelloWorld

    External Reading

    Create a Class Library