/SourceGeneration

Descriptor language transpiler for .NET datalayers and reactive UI

Primary LanguageC#

SourceGeneration

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#:

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.

Datalayer Generation

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.

Brief Example

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 UI Components

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.

State

Each component has a set of fields which it remembers between requests to render.

state {
    int count,
    string message = {"Default C# value expression"}
}

Interface

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.

HTML Nodes

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.