Descriptor language transpiler for .NET datalayers and reactive UI
Several branches exist as submodules of other projects. This allows custom language features if a project calls for them, or for translation to languages other than C#:
- Visualizer - Source for the FSA creation/minimization demo found here (desktop recommended)
- "Rezacht Core" - a port for generating templated HTML on embedded systems in C
- Turtle Public - the branch used to generate the Minecraft Turtle monitoring portal, a low-stakes testbed for initially creating this project
Visit the wiki for toolchain setup and basic usage instructions.
Generated code allows developers to skip over tedium and keep focused on important design details.
My source generation platform aims to establish a definitive .NET project architecture of declarative descriptor languages. Several backend and frontend tasks follow patterns which can be boilerplated from a source descriptor document.
Portions of a datalayer are written to match the facilities of your database schema:
- database repository
- DTO model
- partial classes
- backend service interfaces
- frontend HTTP client
from a simple input syntax.
Individual tables or entities within an application are described in .model
files.
An example descriptor `Model/BlogPost.model`
schema {
int bpID,
int bpUserID,
string bpContent = {""}
}
partial WithComments {
List<string> user_comments = {new()}
}
repo {
BlogPost_GetByID(int bpID)
=> BlogPost,
BlogPost_GetByUserID(int suID)
=> List<BlogPost>,
BlogPost_GetWithComments(int bpID)
=> json BlogPost.WithComments,
BlogPost_Create(int bpSiteUserID, string bpContent)
=> BlogPost
}
service {
MakePost(string bpContent)
=> BlogPost,
GetByUser(int suID)
=> List<BlogPost.WithComments>
}
concisely describes a generated source
/* DO NOT EDIT THIS FILE */
// DFA RESTORED IN 44.1527ms
// GENERATED FROM 'D:\...\Models\BlogPost.model' AT 2023-08-17 21:52:12
#nullable disable
namespace Generated;
public class BlogPost
{
public int bpID { get; set; }
public int bpUserID { get; set; }
public string bpContent { get; set; }
= "";
public partial class WithComments : BlogPost
{
public List<string> user_comments { get; set; }
= new();
}
public class Repository
{
private readonly IModelDbAdapter db;
public Repository(IModelDbAdapter db)
{
this.db = db;
}
public BlogPost BlogPost_GetByID(int bpID)
{
return db.Execute<BlogPost>("BlogPost_GetByID", new
{
bpID
});
}
public List<BlogPost> BlogPost_GetByUserID(int suID)
{
return db.Execute<List<BlogPost>>("BlogPost_GetByUserID", new
{
suID
});
}
public BlogPost.WithComments BlogPost_GetWithComments(int bpID)
{
return db.ExecuteForJson<BlogPost.WithComments>("BlogPost_GetWithComments", new
{
bpID
});
}
public BlogPost BlogPost_Create(int bpSiteUserID,string bpContent)
{
return db.Execute<BlogPost>("BlogPost_Create", new
{
bpSiteUserID,
bpContent
});
}
}
public interface IService
{
BlogPost MakePost(string bpContent);
List<BlogPost.WithComments> GetByUser(int suID);
}
public interface IBackendService : IService
{
// Implement and inject this interface as a separate service
}
public class DbService : IService
{
private readonly IModelDbWrapper wrapper;
private readonly IBackendService impl;
public DbService(IModelDbWrapper wrapper, IBackendService impl)
{
this.wrapper = wrapper;
this.impl = impl;
}
public BlogPost MakePost(string bpContent)
{
return wrapper.Execute<BlogPost>(() => impl.MakePost(
bpContent
));
}
public List<BlogPost.WithComments> GetByUser(int suID)
{
return wrapper.Execute<List<BlogPost.WithComments>>(() => impl.GetByUser(
suID
));
}
}
public class ApiService : IService
{
private readonly IModelApiAdapter api;
public ApiService(IModelApiAdapter api)
{
this.api = api;
}
public BlogPost MakePost(string bpContent)
{
return api.Execute<BlogPost>("BlogPost/MakePost", new
{
bpContent
});
}
public List<BlogPost.WithComments> GetByUser(int suID)
{
return api.Execute<List<BlogPost.WithComments>>("BlogPost/GetByUser", new
{
suID
});
}
}
}
// GENERATED IN 29.7579ms
Several resources are generated which can be injected and invoked from C#; including services to call SQL parameterized statements, and API clients for a frontend.
DTO classes are also generated for the table schema and any other partial types defined.
Reactive components are defined in a custom declarative markup syntax. Components
are written as .view
files and tie into DI and routing to provide serverside
rendering of content. Endpoints are automatically generated according to the action
interface of the view component. The server performs actions and returns updated UI
content to reflect the latest state of the page.
Components can rely on each other (including recursively) and can be swapped out independently to update as little of the DOM as possible.
Each component has a set of fields which it remembers between requests to render.
state {
int count,
string message = {"Default C# value expression"}
}
Each component also specifies a set of actions which can be performed on the component. A controller endpoint is created for each which returns updated HTML after performing the specified action.
interface {
int IncrementCount()
=> int,
void SetMessage(string message)
}
The action can be invoked from clientside via a minimal JS function called
dispatch(el, action, pars)
. dispatch
will search for the nearest component
to the specified element el
and invoke the action
with optional parameters.
div(
p({message})
button(| onclick = {"dispatch(this, 'SetMessage', { message: 'New message' });"} |
<">Change message</">
)
)
Function return values are accessible when manipulating components serverside within controllers or services.
Document markup is written in fully parenthesized prefix notation. An HTML element is denoted as a lowercase identifier followed by a parenthesized list of parameters and children.
p(<">Hello, world!</">)
div(| class = {"bg-dark text-light"} |
p(<">Foo bar</">)
)
There are keywords and special tag types which indicate special generated constructs
or view logic (inject
, if
, for
, foreach
, etc.):
inject({Namespace.IService} {service})
if({await service.CheckBooleanCondition()}
h1(<">Success block</">)
h1(<">Failure block</">)
)
Multiple nodes can be grouped into one using <>
and </>
and multiline strings
surrounded with <">
and </">
.
if({await service.CheckBooleanCondition()}
<>
h1(<">Success block</">)
p(<">More content</">)
</>
p(
<">
Failure block
with more lines this time
</">
)
)
Nodes can inject services from the DI pipeline, accept child HTML content, and access state stored in parent components.
Visit the wiki for more indepth specifications of the HTML element grammar.