C4/: The main library. When there's a question of what concrete implementation to use,Det/: The main driver for C4 that sets up R0's backend and the like, as well as kicking off the scheduler.Clients/: Contains all (other than Det) executable things that need to interact with C4
UI/
CommonUI/: Put any easily re-usable Blazor components here.MappingEditor: Contains the electron-based Blazor app for a standalone mappings editor (WIP)Web: Contains the Blazor app for a standalone mappings editor (WIP)SatelliteExec/: If needed, this holds a basic project that kicks off Satellite's executable.
API/: Contains implementations of all (non-default-CRUD) endpoints that C4 exposes via Ctrl.Utils/: Contains projects for utilities that don't directly depend on C4's code
Testbed/: Contains tests for both C4 and (ideally) all supported ERPs.
Modules/:
Satellite: Contains a git submodule that points to Satellite's main repoTypes/: Contains projects for each Type ModuleScripts/
ESM/: Contains projects for each ESM's scriptsCSM/: Contains projects for each CSM's scriptsC4 is composed of a few critical modules and concepts:
<System>ESM<Client Acronym>CSMstatic class R0 contains static methods that redirect toR0.Backend, which is an instance of IR0Backend. The backend's type is decided in the program's main.LayeredBackend : IR0Backend, which allows redirecting different typesIR0Backends.TypeResolver: Keeps a global registry of C# types and maintains mappings Type<->Names
Ctrl so it can automatically register CRUD endpoints for new types.ScriptResolver: Maintains a global registry of IScript types and handles compiling Pipeline -> IPipeline.
Pipeline instances, when resolving pipelines by name.Ctrl: Exposes (by way of ICtrl) a basic singleton interface for registering endpoints that C4 will handle.
Note: Due to the current implementation of R0, this exact method may not be possible yet. You may need to use
pickers as nodes instead.
R0Pick<T1> -> Map<T1,T2> -> R0Put<T2>C4 makes heavy use of singletons with a swappable backend and a static class that the programmer interacts with
(as in the case of R0, Ctrl, TypeResolver, and ScriptResolver). These are composed of a static class frontend
(Ctrl, TypeResolver, etc), an interface (ICtrl, ITypeResolver, etc), an implementation that lives in Det
(WatsonCtrl, R0LiteDB, etc), and a property on the static class that points to the active instance
(usually named Global). The static class should always contain wrapper methods that forward to the interface's
methods, so as a normal C4 programmer, you'll typically only need to know things like R0.Put(...), and won't need to
bother with the backend details on that.
Foo into R0 puts it into a table namedFoo
Foo contains a member of type Bar, it will not try to separate that member into another table.Primary Keys:
[Primary] and will use them to generateR0.PKOf<T>(ent) to get the primary key of an entity.
[AutoInc] attribute which can be applied to an int? to get SQL-likePKOf for an entity with null [AutoInc] field(s) will be different than for one non-nulls.An important detail to note about the type and script resolvers is that while the type resolver is a global,
append-only cache, the instance the script resolver users can be overridden for particular scopes. This works
off of an AsyncLocal<IScriptResolver>, which essentially acts like a stack that automatically resets
after leaving an async function.
General notes:
ScriptResolver.PushNew(instance)
instance, either new up another resolver or use the existing instance's ForWorkingSet to make a newPushNew returns a type which implements IDisposable - if you do using var _ = PushNew(...), you can applyFor general information on how an
AsyncLocal<T>works, see
Microsoft's docs.
When creating a new instance of a script resolver via ForWorkingSet, you must pass a GUID. Although not required,
the instance of the resolver you're passing it to may choose to re-use an existing script resolver if that instance
was also created with the same GUID. This is primarily useful in the web API, where we may want to refer to the same
resolver multiple times in a row.
Note: The current resolver doesn't yet implement this re-use.
For both the type and script resolvers, C4 makes use of a class called ProperType:
`record ProperType(Name, Generics[], Version)`
When calling a method such as ResolveType or ResolveScript based on a string, C4 will:
ProperType ptpt based on any aliases registered with the given system
GP2018Customer may be aliased as GPCustomerpt as an internal indexC4 supports both generic scripts and generic types. When the type and script resolvers register a new type, they will
first check to see if the type is a generic type definition (Foo<T> as opposed to Foo<int>). If it is, they'll
strip the type parameters out of the ProperType before putting it in their caches.
Generics are subject to the following rules:
Foo<T, U> and Foo<T> will attempt to overwrite the same generic type.
Foo<_, _>.XX.
CEFProductX's types, just use those and register a TypeResolverHook that registers them[Primary] fields.XPick<T>, XPush<T>, as well as helper nodes like XRequestRefund or similar.X (if needed) from pipelinesNote: Currently, the runset style of having pickers and pushers synchronizing R0 with the outside world may
not be ready for primetime. Thus, it's best to make the pickers/pushers also able to act as nodes that can pick
based on inputs, avoiding syncing into R0 first.
C4 implements a basic HTTP REST API.
By default, C4 listens on port 9000. This will likely be changed in the future.
Most web APIs will return a message in this format:
{
"Code": 200, // Usually just the HTTP code mirrored
"Msg": "", // Description of what came back
"Val": ... // The actual value you requested
}
Additionally, Val on CRUD endpoints that list will either be T[] or ValWrapper<T>[], depending on what
was requested.
By default, the type resolver will call out to Ctrl to register a full set of CRUD (Put, List, Where, etc)
every time it auto-registers a type marked [Record], unless it's also [VisibleOnAPI(false)].
Note also that when using Ctrl.AddCRUD, any type marked Sensitivity.Sensitive will have their list/get/delete
endpoints replaced with error messages (although PUT still work).
Ctrl's standard CRUD scheme is as follows, where TYPE is the name of the datatype:
/crud/TYPE - base path for all requests. For all paths below, prepend this.
PUT / - Upsert
[ (must be very first character) then it will upsert multipleValWrapper<TYPE> to show what R0 now sees.PUT /createonly - Same as above, but restricted to creation only (will never update)POST / - unfiltered listPOST /where/<filter> - for a given System.Dynamic.LinqR0.WhereLinq.ListParams):
Skip and Limit: For pagination.
0 and int.MaxValueOrdering: An array of OrderBy clauses. Default empty.WithPK: true to return ValWrapper<TYPE>[], false to return TYPE[] in the Val of the response.
trueGET /key/<KEY> - get by R0 primary keyDELETE /key/<KEY> - delete by R0 primary keyFor use in the frontend, it is preferred to use/maintain the R0Remote wrapper, which implements IR0Backend
over the web API, making networking transparent for the web app.
Note:
R0Remotewill translate list requests for aValWrapper<T>as a request to listTwith the
WithPKoption set. This may be made a core feature of R0 eventually.
Currently, authentication is done purely via username and password. There was once a token implementation, but that
has been scrapped until we have a need for something more sophisticated.
C4 implements session handling:
/auth/login endpoint. It returnsThese aren't strictly required, but are how I've been doing things thus far:
<Nullable>enable</Nullable> in your .csproj files, and always do your best to fix thestring.IsNullOr* have helpers defined in Utils so you can do s.NullIfWhitespace() == null or similarUtils package for a helper. If you can't find one,s.NullIfWhitespace() is null or similar.s just fine, as well.new() up instances of your nodes to test, then have another test which makes a pipeline and tests that.