{"id":25501464,"url":"https://github.com/hughesjs/SuperFluid","last_synced_at":"2025-11-11T22:30:21.233Z","repository":{"id":172543717,"uuid":"649424985","full_name":"hughesjs/SuperFluid","owner":"hughesjs","description":"C# Library For Generating Fluent APIs","archived":false,"fork":false,"pushed_at":"2025-01-03T23:58:32.000Z","size":131,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-04T00:18:37.962Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hughesjs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-06-04T19:52:09.000Z","updated_at":"2025-01-03T23:57:54.000Z","dependencies_parsed_at":null,"dependency_job_id":"8f4ca0a3-1fae-4525-aabc-c88d33c6a31f","html_url":"https://github.com/hughesjs/SuperFluid","commit_stats":null,"previous_names":["hughesjs/superfluid"],"tags_count":9,"template":false,"template_full_name":"hughesjs/dotnet-8-ci-cd-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hughesjs%2FSuperFluid","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hughesjs%2FSuperFluid/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hughesjs%2FSuperFluid/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hughesjs%2FSuperFluid/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hughesjs","download_url":"https://codeload.github.com/hughesjs/SuperFluid/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239592970,"owners_count":19664856,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-19T04:02:22.809Z","updated_at":"2025-11-11T22:30:21.187Z","avatar_url":"https://github.com/hughesjs.png","language":"C#","funding_links":[],"categories":["Content","Source Generators"],"sub_categories":["199. [SuperFluid](https://ignatandrei.github.io/RSCG_Examples/v2/docs/SuperFluid) , in the [StateMachine](https://ignatandrei.github.io/RSCG_Examples/v2/docs/rscg-examples#statemachine) category","Patterns"],"readme":"[![GitHub Workflow Status CI](https://img.shields.io/github/actions/workflow/status/hughesjs/SuperFluid/dotnet-ci.yml?label=BUILD%20CI\u0026style=for-the-badge\u0026branch=master)](https://github.com/hughesjs/SuperFluid/actions)\n[![GitHub Workflow Status CD](https://img.shields.io/github/actions/workflow/status/hughesjs/SuperFluid/dotnet-cd.yml?label=BUILD%20CD\u0026style=for-the-badge\u0026branch=master)](https://github.com/hughesjs/SuperFluid/actions)\n![GitHub top language](https://img.shields.io/github/languages/top/hughesjs/SuperFluid?style=for-the-badge)\n[![GitHub](https://img.shields.io/github/license/hughesjs/SuperFluid?style=for-the-badge)](LICENSE)\n[![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/SuperFluid?style=for-the-badge)](https://nuget.org/packages/SuperFluid/)\n[![Nuget](https://img.shields.io/nuget/dt/SuperFluid?style=for-the-badge)](https://nuget.org/packages/SuperFluid/)\n![FTB](https://raw.githubusercontent.com/hughesjs/custom-badges/master/made-in/made-in-scotland.svg)\n\n---\n\n# SuperFluid\n\nA C# library for generating fluent APIs with grammar.\n\n# Introduction\n\nIt is often desirable to define an API that allows us to express our intentions as an easily readable method chain.\n\nThe most common example of this in C# would probably be LINQ:\n\n```cs\nvar result = myCollection\n    .Where(item =\u003e item.IsActive)\n    .OrderBy(item =\u003e item.Name)\n    .Select(item =\u003e new { item.Id, item.Name });\n```\n\nThe simple case of this is actually quite simple to implement, you just have each of your methods return the type of the declaring object and `this`.\n\n```cs\npublic class Car\n{\n    public Car Unlock()\n    {\n        // Do something\n        return this;\n    }\n    \n    public Car Enter()\n    {\n        // Do something\n        return this;\n    }\n    \n    public Car Start()\n    {\n        // Do something\n        return this;\n    }\n}\n\n// Which then lets us do\nvar car = new Car().Unlock().Enter().Start();\n```\n\nHowever, in this instance, there's nothing stopping us from starting the car before we've unlocked and entered it.\n\nClearly, in cases where we want to enforce a valid state, we have to define a grammar for our API.\n\nTypically, we accomplish this by designing a state machine for our API and then working out the set of all unique combinations of transitions, and creating interfaces for each of these states.\nWe can then make the return type for each method be the interface that represents the set of transitions that it allows.\n\n```csharp\npublic class Car: ICanEnter, ICanStart\n{\n    public ICanEnter Unlock()\n    {\n        // Do something\n        return this;\n    }\n    \n    public ICanStart Enter()\n    {\n        // Do something\n        return this;\n    }\n    \n    public void Start()\n    {\n        // Do something\n        return this;\n    }\n}\n\n// Which then lets us do\nvar car = new Car().Unlock().Enter().Start();\n\n// But we can't do\nvar car = new Car().Unlock().Start(); // Haven't entered yet\nvar otherCar = new Car().Enter().Start(); // Haven't unlocked yet\n```\n\n[This write up explains how tricky this can be to do by hand.](https://mitesh1612.github.io/blog/2021/08/11/how-to-design-fluent-api)\n\nThis is where SuperFluid comes in. It lets us define the grammar for your API in a YAML file and then generates the interfaces for you.\n\nAll you then need to do is implement the interfaces and you're good to go.\n\n# How to Use\n\n## Installation\n\nYou can install SuperFluid from Nuget:\n\n```\nInstall-Package SuperFluid\n```\n\n## Defining Your Grammar\n\n\u003e [!WARNING]\n\u003e Your grammar file needs to end with `.fluid.yml` to be picked up by SuperFluid.\n\nYour grammar is defined in a YAML file following this data structure. \n\n```cs\nrecord FluidApiDefinition\n{\n    public required string Name { get; init; }\n    public required string Namespace { get; init; }\n    public required FluidApiMethodDefinition InitialState { get; init; }\n    public required List\u003cFluidApiMethodDefinition\u003e Methods { get; init; }\n}\n\nrecord FluidApiMethodDefinition\n{\n\tpublic required string Name { get; init; }\n\tpublic string? ReturnType { get; init; }\n\tpublic List\u003cstring\u003e CanTransitionTo { get; init; };\n\tpublic List\u003cFluidApiArgumentDefinition\u003e Arguments { get; init; };\n\tpublic List\u003cFluidGenericArgumentDefinition\u003e GenericArguments { get; init; };\n}\n\nrecord FluidApiArgumentDefinition\n{\n    public required string Type { get; init; }\n    public required string Name { get; init; }\n    public string? DefaultValue { get; init; }\n}\n\nrecord FluidGenericArgumentDefinition\n{\n    public required List\u003cstring\u003e Constraints { get; init; }\n    public required string Name { get; init; }\n}\n```\n\nEssentially, you do the following:\n\n- Define the initial state of your API, the namespaces you want your interfaces to be in, and what you want the main interface to be called.\n- Define each of the methods that you want to be able to call on your API.\n- Define the arguments that each method takes.\n- Define the return type of each method.\n- Define the states that each method can transition to.\n\nThen Roslyn will generate the interfaces for you.\n\nA simple example of this would be:\n\n```yaml\nName: \"ICarActor\"\nNamespace: \"SuperFluid.Tests.Cars\"\nInitialState:\n  Name: \"Initialize\"\n  CanTransitionTo: \n    - \"Unlock\"\nMethods:\n  - Name: \"Unlock\"\n    CanTransitionTo:\n      - \"Lock\"\n      - \"Enter\"\n  - Name: \"Lock\"\n    CanTransitionTo:\n      - \"Unlock\"\n  - Name: \"Enter\"\n    CanTransitionTo:\n      - \"Start\"\n      - \"Exit\"\n  - Name: \"Exit\"\n    CanTransitionTo:\n      - \"Lock\"\n      - \"Enter\"\n  - Name: \"Start\"\n    Arguments:\n      # These are deliberately out of order to test that the parser sticks the defaults to the end of the argument list\n      - Name: \"direction\"\n        Type: \"string\"\n        DefaultValue: \"\\\"Forward\\\"\" # Note that we need the quotes here\n      - Name: \"speed\"\n        Type: \"int\"\n      - Name: \"hotwire\"\n        Type: \"bool\"\n        DefaultValue: \"false\"\n\n    # These constraints are pointless but are here to test the parser\n    GenericArguments:\n      - Name: \"T\"\n        Constraints:\n          - \"class\"\n          - \"INumber\"\n      - Name: \"X\"\n        Constraints:\n          - \"notnull\"\n      \n    CanTransitionTo:\n      - \"Stop\"\n      - \"Build\"\n  - Name: \"Stop\"\n    CanTransitionTo:\n      - \"Start\"\n      - \"Exit\"\n  - Name: \"Build\"\n    Arguments:\n      - Name: \"color\"\n        Type: \"string\"\n    CanTransitionTo: []\n    ReturnType: \"string\"\n```\n\nUnfortunately, Roslyn isn't great at giving you feedback for source generation errors. In Rider, you can find them under `Problems \u003e Toolset, Environment` if it's actually run.\n\nI plan to add an analyzer to the project that can give actual feedback to you but this might take a while.\n\n## Registering Your Grammar File with SuperFluid\n\nYou need to add your grammar file(s) as `AdditionalFiles` in your csproj file.\n\n```xml\n    \u003cItemGroup\u003e\n      \u003cAdditionalFiles Include=\"myGrammarFile.fluid.yml\" /\u003e\n    \u003c/ItemGroup\u003e\n```\n\nYou can have as many files as you want and they don't have to be in the root of your project.\n\n## Implementing Your API\n\nActually implementing the API is pretty simple. You just implement the root interface that has been generated. In the above example, that would be `ICarActor`.\n\nYou then just implement the methods on that interface, and you're good to go.\n\nOne note, if you use your IDE's feature to generate your method stubs, you might end up with multiple declarations of each method for each explicit interface that has it as a component. In this case, just delete the explicit implementations and implement the method once using the standard `public type name(args)` syntax. This is simply an artefact of the fact that you can arrive at the same method through multiple transitions.\n\n# Reference Project\n \nAnother one of my projects [PgfPlotsSdk](https://github.com/hughesjs/PgfPlotsSdk) uses SuperFluid to generate a complicated fluent API for working with LaTex PgfPlots.\n\nThe yaml file for this is [here](https://github.com/hughesjs/PgfPlotsSdk/blob/master/src/PgfPlotsSdk/SuperFluidDefinitions/PgfPlotsBuilder.fluid.yml).\n\nThe relevant class is [here](https://github.com/hughesjs/PgfPlotsSdk/blob/master/src/PgfPlotsSdk/Public/Builders/PgfPlotBuilder.cs).","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhughesjs%2FSuperFluid","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhughesjs%2FSuperFluid","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhughesjs%2FSuperFluid/lists"}