Created as simplified and lightweight alternative to other ASP.NET frameworks like ASP.NET MVC
- simple and straightforward in development and maintenance
- MVC-like
- code, data, templates are split
- code consists of: controllers, models, framework core and optional 3rd party libs
- uses ParsePage template engine (detailed docs)
- data stored by default in SQL Server database using db.net (detailed docs)
- RESTful with some practical enhancements
- flexible CRUD flows with Hashtables or typed DTOs (guide)
- integrated auth - simple flat access levels auth
- UI based on Bootstrap 5 with minimal custom CSS and themes support - it's easy to customize or apply your own theme (see README_THEMES)
- use of well-known 3rd party libraries: jQuery, jQuery Form, jGrowl, markdown libs, etc...
- extensible dashboard panels with charts, table and progress templates (dashboard.md)
- dynamic controllers with JSON configuration (
FwDynamicController) and Vue.js powered UI (FwVueController) (detailed docs) - base API controller (
FwApiController) for building REST APIs - attachments handling with optional Amazon S3 storage
- auto database updates and environment self-tests (
FwUpdates,FwSelfTest) - in-memory caching via
FwCache - optional Entity Builder for quick scaffolding
- per-user date/time and timezone handling (see "Per-user Date/Time and Timezones" below)
http://demo.engineeredit.com/ - this is how it looks in action right after installation before customizations
- CRUD workflows with
FwModel– compare Hashtable and typed DTO approaches for standard operations.
- clone this git repository
- delete files in
/osafw-app/App_Data/sql/updates- these are framework updates, not need for your fresh app
- in Visual Studio open
osafw-asp.net-core.sln(you may "save as" solution with your project name) - press Ctrl+F5 to run (or F5 if you really need debugger)
- open in browser https://localhost:PORT/Dev/Configure and check configuration. If database not configured:
- create
demodatabase (or other name you have in appsettings.json) - click "Initialize DB"
- review debug log in
/osafw-app/App_Data/logs/main.log - edit or create new controllers and models in
/osafw-app/App_Code/controllersand/osafw-app/App_Code/models - modify templates in
/osafw-app/App_Data/template
All the details can be found in the Microsoft docs Short summary how to deploy without VS publish (from clone git repo):
- on the server - install IIS and .NET Core Hosting Bundle
- install latest .NET 8 SDK to have
dotnetCLI - create website directory
- make
git clonefrom repo to website directory - make
dotnet publish --configuration Releasein the directory
- create website in IIS with root directory to
/bin/Release/net8.0/publish - set website's app pool to "No managed code" (so pool works like a proxy to app core)
- open your website address in browser
- create database and apply sql files in order:
- fwdatabase.sql - core fw tables
- database.sql - your app specific tables
- lookups.sql - fill lookup tables
- views.sql - (re-)create views
- roles.sql (optional, only if RBAC used)
- demo.sql (optional, demo tables)
/osafw-tests - application tests
/osafw-app - application
/App_Code - all the C# code is here
/controllers - your controllers
/fw - framework core libs
/models - your models
/App_Data - non-public directory
/sql - initial database.sql script and update sql scripts
/template - all the html templates
/logs/main.log - application log (ensure to enable write rights to /logs dir for IIS)
/upload - upload dir for private files
/wwwroot - website public root folder
/assets - your web frontend assets
/css
/fonts
/img
/js
/lib - assets built from libman.json
/upload - upload dir for public files
/favicon.ico - change to your favicon!
/robots.txt - default robots.txt (empty)
/appsettings.json - settings for db connection, mail, logging and for IIS/.NET stuff too
Controllers automatically directly mapped to URLs, so developer doesn't need to write routing rules:
GET /Controller- list viewIndexAction()GET /Controller/ID- one record viewShowAction()GET /Controller/new- one record new formShowFormAction()GET /Controller/ID/edit- one record edit formShowFormAction()GET /Controller/ID/delete- one record delete confirmation formShowDeleteAction()POST /Controller- insert new recordSaveAction()PUT /Controller- update multiple recordsSaveMultiAction()POST/PUT /Controller/ID- update recordSaveAction()DELETE /Controller/ID- delete recordDeleteAction()GET/POST /Controller/(Something)[/ID]- call for arbitrary action from the controllerSomethingAction()
For example GET /Products will call ProductsController.IndexAction()
And this will cause rendering templates from /App_Data/templates/products/index
highlighted as bold is where you could place your code.
FW.run()FwHooks.initRequest()- place code here which need to be run on request start
fw.dispatch()- performs REST urls matching and calls controller/action, if no controller found callsHomeController.NotFoundAction(), if no requested action found in controller - calls controller action defined in contoller'sroute_default_action(either "index" or "show")fw._auth()- check if user can access requested controller/action, also performs basic CSRF validationfw.call_controller()SomeController.init()- place code here which need to be run every time request comes to this controllerSomeController.SomeAction()- your code for particular actionSomeModel.someMethod()- controllers may call model's methods, place most of your business logic in models
fw.Finalize()
-
GET /Admin/Users
FwHooks.initRequest()AdminUsers.init()AdminUsers.IndexAction()- then ParsePage parses templates from
/template/admin/users/index/
-
GET /Admin/Users/123/edit
FwHooks.initRequest()AdminUsers.init()AdminUsers.ShowFormAction(123)Users.one(123)
- then ParsePage parses templates from
/template/admin/users/showform/
-
POST /Admin/Users/123
FwHooks.initRequest()AdminUsers.init()AdminUsers.SaveAction(123)Users.update(123)
fw.redirect("/Admin/Users/123/edit")//redirect back to edit screen after db updated
-
GET /Admin/Users/(Custom)/123?param1=1¶m2=ABC - controller's custom action (non-standard REST)
FwHooks.initRequest()AdminUsers.init()AdminUsers.CustomAction(123)- here you can get params usingreqi("param1") -> 1andreqs("params") -> "ABC"- then ParsePage parses templates from
/template/admin/users/custom/unless you redirect somewhere else
-
POST /Admin/Users/(Custom)/123 with posted params
param1=1andparam2=ABCFwHooks.initRequest()AdminUsers.init()AdminUsers.CustomAction(123)- here you can still get params usingreqi("param1") -> 1andreqs("params") -> "ABC"- then ParsePage parses templates from
/template/admin/users/custom/unless you redirect somewhere else
Frequently asked details about flow for the IndexAction() (in controllers inherited from FwAdminController and FwDynamicController):
initFilter()- initializesMe.list_filterfrom query string filter params&f[xxx]=..., note, filters remembered in sessionsetListSorting()- initializesMe.list_orderbybased onlist_filter("sortby")andlist_filter("sortdir"), also usesMe.list_sortdefandMe.list_sortmapwhich can be set in controller'sinit()or inconfig.jsonsetListSearch()- initializesMe.list_wherebased onlist_filter("s")andMe.search_fieldssetListSearchStatus()- add toMe.list_wherefiltering bystatusfield if such field defined in the controller's modelgetListRows()- query database and save rows toMe.list_rows(only current page based onMe.list_filter("pagenum")andMe.list_filter("pagesize")). Also setsMe.list_countto total rows matched by filters andMe.list_pagerfor pagination if there are more than one page. UsesMe.list_view,Me.list_where,Me.list_orderby
You could either override these particular methods or whole IndexAction() in your specific controller.
The following controller fields used above can be defined in controller's init() or in config.json:
Me.list_sortdef- default list sorting in format: "sort_name[ asc|desc]"Me.list_sortmap- mapping for sort names (fromlist_filter["sortby"]) to actual db fields, Hashtablesort_name => db_field_nameMe.search_fields- search fields, space-separatedMe.list_view- table/view to use ingetListRows(), if empty model'stable_nameused
- FwCache – simple wrapper around
IMemoryCachefor application and request caching. Accessible in controllers and models viafw.cache - FwUpdates – applies SQL scripts from
/App_Data/sql/updatesautomatically in development - FwSelfTest – runs configuration and controller tests to verify environment
- FwActivityLogs – unified activity and change logging model. Can be used directly or via
fw.logActivityhelper - FwApiController – base class for building authenticated REST APIs
- Entity Builder – text based definition to generate SQL and CRUD scaffolding
The framework supports per-user formatting and timezone conversion:
- Defaults come from
appsettings.json(appSettings.date_format,time_format,timezone) - For each user can be overridden - see
userstable fieldsdate_format,time_format,timezone(e.g. on login/profile save). - Rendering in templates uses these values automatically via ParsePage. Inputs are interpreted using the user’s format; output can be converted from database timezone to the user’s timezone.
See the detailed guide with examples and constants in datetime.md.
Application configuration available via fw.config([SettingName]).
Most of the global settings are defined in appsettings.json appSettings section. But there are several calculated settings:
| SettingName | Description | Example |
|---|---|---|
| hostname | set from server variable HTTP_HOST | osalabs.com |
| ROOT_DOMAIN | protocol+hostname | https://osalabs.com |
| ROOT_URL | part of the url if Application installed under sub-url | /suburl if App installed under osalabs.com/suburl |
| site_root | physical application path to the root of public directory | C:\inetpub\somesite\www |
| template | physical path to the root of templates directory | C:\inetpub\somesite\www\App_Data\template |
| log | physical path to application log file | C:\inetpub\somesite\www\App_Data\logs\main.log |
| tmp | physical path to the system tmp directory | C:\Windows\Temp |
Main and recommended approach - use fw.logger() function, which is available in controllers and models (so no prefix required).
Examples: logger("some string to log", var_to_dump), logger(LogLevel.WARN, "warning message")
All logged messages and var content (complex objects will be dumped wit structure when possible) written on debug console as well as to log file (default /App_Data/logs/main.log)
You can configure log level in appsettings.json - search for log_level in appSettings
Another debug function that might be helpful is fw.rw() - but it output it's parameter directly into response output (i.e. you will see output right in the browser)
- naming conventions:
- table name:
user_lists(lowercase, underscore delimiters is optional) - model name:
UserLists(UpperCamelCase) - controller name:
UserListsControllerorAdminUserListsController(UpperCamelCase with "Controller" suffix) - template path:
/template/userlists
- table name:
- keep all paths without trailing slash, use beginning slash where necessary
- db updates:
- first, make changes in
/App_Data/sql/database.sql- this file is used to create db from scratch - then create a file
/App_Data/sql/updates/updYYYY-MM-DD[-123].sqlwith all the CREATE, ALTER, UPDATE... - this will allow to apply just this update to existing database instances
- first, make changes in
- use
fw.routeRedirect()if you got request to one Controller.Action, but need to continue processing in another Controller.Action- for example, if for a logged user you need to show detailed data and always skip list view - in the
IndexAction()just usefw.routeRedirect("ShowForm")
- for example, if for a logged user you need to show detailed data and always skip list view - in the
- uploads
- save all public-readable uploads under
/wwwroot/upload(default, seeUPLOAD_DIRinappsettings.json) - for non-public uploads use
/upload - or
S3model and upload to the cloud
- save all public-readable uploads under
- put all validation code into controller's
Validate(). See usage example inAdminDemosController - use
logger()and review/App_Data/logs/main.logif you stuck- make sure you have
log_levelset toDEBUGinappsettings.json
- make sure you have
How to quickly create a Report
- all reports accessed via
AdminReportsControllerIndexAction- shows a list of all available reports (basically renders static html template with a link to specific reports)ShowAction- based on passed report code calls related Report model
- base report model is
FwReports, major methods (you may override in the specific report):getReportFilters()- set data for the report filtersgetReportData()- returns report data, usually based on some sql query (see Sample report)
ReportSamplemodel (in\App_Code\models\Reportsfolder) is a sample report implementation, that can be used as a template to build custom reports- basic steps to create a new report:
- copy
\App_Code\models\Reports\Sample.csto\App_Code\models\Reports\Cool.cs(to create Cool report) - edit
Cool.csand rename "Sample" to "Cool" - modify
getReportFilters()to match your report filters - modify
getReportData()to edit sql query and related post-processing - copy templates folder
\App_Data\template\reports\sampleto\App_Data\template\reports\cool - edit templates:
title.html- report titlelist_filter.html- for filtersreport_html.html- for report table/layout/appearance
- add link to a new report to
\App_Data\template\reports\index\main.html
- copy
PDF Export for reports setup PDF Reports done by generating report html and then converting it into pdf using Playwright (Chromium).
To enable PDF export using Playwright (Chromium):
- Configure PLAYWRIGHT_BROWSERS_PATH in
appsettings.json. By default it'sC:\Program Files\pw-browsers. (If you set it to empty it will use current userAppData\Localfolder) - Run scripts\install_playwright.bat to setup folder permissions before first PDF export.
- Lazy Initialization
- Playwright browser install will run automatically the first time a PDF report is generated.
- If there are issues with Playwright, normal app workflow is not affected.
- Manual Initialization
- Developers can manually trigger Playwright install from
/Dev/Manage(look for the "Init Playwright" link).
- Developers can manually trigger Playwright install from
- Troubleshooting
- If PDF export fails, check
/App_Data/logs/main.logfor errors. - Ensure the browser path is writable and accessible by the IIS user.
- If PDF export fails, check
For more technical details, see comments in ConvUtils.cs and FwReports.cs.
Framework includes a background service for scheduled tasks (like send emails, run reports, etc...). Uses Cronos nuget package. To enable:
- in Program.cs uncomment
builder.Services.AddHostedService<FwCronService>(); - add tasks like
insert into fwcron(icode, cron) values ('example_sleep', '* * * * *')- example task to run every minute - update
FwCron.runJobActionto call acutal code for tasks
