Decorate a plain C# method with an attribute. Betta generates a stable GUID, inputs, outputs and an icon — a real component you can use on the canvas.
Subclass GH_Component, register each port, invent a GUID, marshal types through SolveInstance. Betta generates all of it automatically and loads it when Grasshopper starts.
[GrasshopperCollection("Curve", "Analysis")]
public class CurveTools : IBettaCollection
{
[GrasshopperMethod("Divide")]
public List<Point3d> Divide(Curve curve, int count)
{
curve.DivideByCount(count, true, out Point3d[] pts);
return pts.ToList();
}
}
A ribbon tab, a component, and every port — done. No GH_Component, no RegisterInputParams, no SolveInstance, no GUID to babysit. The method body is the component.
public class DivideComponent : GH_Component
{
public DivideComponent() : base("Divide","Divide","Divide a curve into points","Curve","Analysis") { }
protected override void RegisterInputParams(GH_InputParamManager pm)
{
pm.AddCurveParameter("Curve","C","curve to divide",GH_ParamAccess.item);
pm.AddIntegerParameter("Count","N","segment count",GH_ParamAccess.item,10);
}
protected override void RegisterOutputParams(GH_OutputParamManager pm)
=> pm.AddPointParameter("Points","P","division points",GH_ParamAccess.list);
protected override void SolveInstance(IGH_DataAccess DA)
{
Curve curve = null; int count = 0;
if (!DA.GetData(0, ref curve)) return;
if (!DA.GetData(1, ref count)) return;
curve.DivideByCount(count, true, out Point3d[] pts);
DA.SetDataList(0, pts);
}
public override Guid ComponentGuid => new Guid("d3b07384-…-0002");
protected override Bitmap Icon => null;
}
One component, the usual way. Everything wrapped around the single highlighted line is what Betta generates for you — for every method, every time.
Your services stay plain and framework-agnostic — no Grasshopper types leak in, so the same code unit-tests with no Rhino in sight.
IBettaCollectionA plain interface or class carrying [GrasshopperMethod]. Inheriting the marker is the opt-in — attribute-only types in unrelated DLLs are ignored.
ComponentDescriptorReflection at startup builds one descriptor per [GrasshopperMethod]: ports from the signature, a GUID from MD5(signature), an icon from the type's GUID.
IGH_ObjectProxyEach descriptor becomes a canvas proxy registered with Grasshopper's ComponentServer — so it appears as a first-class node in the toolbar.
Method.InvokeOn the canvas every proxy shares one generic GH_Component CLR type. Per solve, the injector maps inputs → your method → outputs. Rename the display freely; the GUID is pinned to the signature, so saved .gh files survive rebuilds and machine moves.
That's the contract to learn. Everything else — ports, GUID, icon, DI, caching — Betta derives.
[GrasshopperCollection]
Default Category + SubCategory for every method on the type. One tab, one group.
[GrasshopperMethod]
Publishes the method as a component. Optional NickName, Description, IconResource, Guid, Enabled.
[GrasshopperParameter]
Input name / nickname / description. List<T> → list input automatically. DefaultValue seeds unwired sockets.
| Return type | Example | Becomes |
|---|---|---|
primitive / Rhino geometry | double Area(...) | one output |
List<T> | List<Point3d> Grid(...) | one list output |
ValueTuple | (double Sum, double Avg) Stats(...) | one per element — named tuple → named outputs |
plain custom class | Report Analyze(...) | one output per public property |
[GrasshopperOpaque] class | [GrasshopperOpaque] Mesh Build(...) | one typed wire — auto Param_BettaGoo<T> |
Task<T> / ValueTask<T> | Task<Brep> LoadAsync(...) | async — cached by input hash, re-solves on completion |
IObservable<T> | IObservable<double> Ticker(...) | live output — pushes every emission through the port |
The one-liner is the front door. Behind it: pipelines, cloud-API ergonomics, live data, and the boring correctness that keeps saved files working.
Mark a type [GrasshopperOpaque] and it flows as a single typed wire — Load → Deconstruct pipelines without exploding into properties.
Task<T> solves off the UI thread; a CancellationToken quits stale work on re-solve; IProgress<int> drives a free status tag.
Return IObservable<T> and the component goes live — emissions push through, coalesced onto the UI thread so a chatty source can't flood the canvas.
[GrasshopperSecret] reads from Credential Manager, [GrasshopperTrigger] adds a Run gate, [GrasshopperValueList] auto-drops a wired dropdown.
Component GUIDs are MD5 of the signature and typed wires hash typeof(T).FullName — so .gh files round-trip across rebuilds and machines.
Opaque types opt into IBettaPreview / IBettaBakeable — detected by name, forwarded by reflection. Add the package only if you need it.
[GrasshopperCollection("Web", "Live")]
public class Feeds : IBettaCollection
{
// async · cancellation · secret — all inferred
[GrasshopperMethod("Fetch")]
public async Task<string> Fetch(string url,
[GrasshopperSecret("api.key")] string key,
CancellationToken ct) => await Http.GetAsync(url, key, ct);
// return IObservable and the component goes live
[GrasshopperMethod("Poll")]
public IObservable<string> Poll(string url, int everyMs)
=> Observable.Interval(everyMs).Select(_ => Http.Get(url));
}
The repo ships a Claude Code skill and a CLAUDE.md that hand a coding agent the contract — attributes, return-type mapping, deploy path, GUID gotchas. The boilerplate is generated at runtime, so refactors don't re-spend tokens on it and there's far less code in the diff to review.
[GrasshopperCollection("MyPack", "Maths")]
public class MyService : IBettaCollection
{
[GrasshopperMethod("Cube")]
public double Cube(
[GrasshopperParameter("Value")] double x) => x * x * x;
}
Betta is a normal Grasshopper plugin. Any of these drops it under a Betta tab after a Rhino restart — no .NET, no build.
Run _PackageManager, search Betta, click Install, restart Rhino.
From a Rhino command prompt: yak install betta.
Download the release .zip, right-click → Unblock, then unzip into %AppData%\Grasshopper\Libraries\ and restart.
Target net7.0-windows (or any TFM that references netstandard2.0).
dotnet new classlib -n MyPack -f net7.0-windowsBetta.Abstractions
The SDK contract — not the .gha — with ExcludeAssets="runtime".
<PackageReference Include="Betta.Abstractions" Version="0.7.0"
ExcludeAssets="runtime" />Class-direct or interface + impl; both opt in by inheriting IBettaCollection.
[GrasshopperCollection("Curve", "Analysis")]
public class CurveTools : IBettaCollection
{
[GrasshopperMethod("Divide")]
public List<Point3d> Divide(Curve c, int n)
{
c.DivideByCount(n, true, out Point3d[] p);
return p.ToList();
}
}Betta watches %AppData%\Grasshopper\Libraries\Betta\ and hot-adds it — no restart.
dotnet build -c Release
# copy MyPack.dll → %AppData%\Grasshopper\Libraries\Betta\