{"id":18603092,"url":"https://github.com/devlooped/cloudactors","last_synced_at":"2026-04-16T23:04:02.690Z","repository":{"id":185242465,"uuid":"673219104","full_name":"devlooped/CloudActors","owner":"devlooped","description":"An opinionated, simplified and uniform Cloud Native actors' library that integrates with Microsoft Orleans.","archived":false,"fork":false,"pushed_at":"2025-06-27T00:29:34.000Z","size":417,"stargazers_count":39,"open_issues_count":6,"forks_count":4,"subscribers_count":5,"default_branch":"main","last_synced_at":"2025-06-28T21:54:14.409Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devlooped.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"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,"zenodo":null},"funding":{"github":"devlooped"}},"created_at":"2023-08-01T06:25:59.000Z","updated_at":"2025-04-30T15:00:49.000Z","dependencies_parsed_at":"2023-08-01T07:39:48.541Z","dependency_job_id":"f955035c-dd8a-4b6c-93ca-e0e42311b540","html_url":"https://github.com/devlooped/CloudActors","commit_stats":null,"previous_names":["devlooped/cloudactors"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/devlooped/CloudActors","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FCloudActors","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FCloudActors/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FCloudActors/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FCloudActors/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devlooped","download_url":"https://codeload.github.com/devlooped/CloudActors/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FCloudActors/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262503536,"owners_count":23321204,"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":"2024-11-07T02:13:29.488Z","updated_at":"2026-04-16T23:04:02.676Z","avatar_url":"https://github.com/devlooped.png","language":"C#","funding_links":["https://github.com/sponsors/devlooped","https://github.com/sponsors"],"categories":[],"sub_categories":[],"readme":"# Cloud Native Actors\n\n\u003cp align=\"center\"\u003e\n  \u003cimage src=\"https://raw.githubusercontent.com/devlooped/CloudActors/main/assets/img/banner.png\" alt=\"Orleans logo\" width=\"320px\"\u003e\n\u003c/p\u003e\n\nAn opinionated, simplified and uniform Cloud Native actors' library that integrates with Microsoft Orleans.\n\n[![Version](https://img.shields.io/nuget/v/Devlooped.CloudActors.svg?color=royalblue)](https://www.nuget.org/packages/Devlooped.CloudActors) \n[![Downloads](https://img.shields.io/nuget/dt/Devlooped.CloudActors.svg?color=darkmagenta)](https://www.nuget.org/packages/Devlooped.CloudActors) \n[![EULA](https://img.shields.io/badge/EULA-OSMF-blue?labelColor=black\u0026color=C9FF30)](osmfeula.txt)\n[![OSS](https://img.shields.io/github/license/devlooped/oss.svg?color=blue)](license.txt) \n\n## Motivation\n\nWatch the [Orleans Virtual Meetup 7](https://www.youtube.com/watch?v=FKL-PS8Q9ac) where Yevhen \n(of [Streamstone](https://github.com/yevhen/Streamstone) fame) makes the case for using message\npassing style with actors instead of the more common RPC style offered by Orleans.\n\n\u003c!-- include https://github.com/devlooped/.github/raw/main/osmf.md --\u003e\n## Open Source Maintenance Fee\n\nTo ensure the long-term sustainability of this project, users of this package who generate \nrevenue must pay an [Open Source Maintenance Fee](https://opensourcemaintenancefee.org). \nWhile the source code is freely available under the terms of the [License](license.txt), \nthis package and other aspects of the project require [adherence to the Maintenance Fee](osmfeula.txt).\n\nTo pay the Maintenance Fee, [become a Sponsor](https://github.com/sponsors/devlooped) at the proper \nOSMF tier. A single fee covers all of [Devlooped packages](https://www.nuget.org/profiles/Devlooped).\n\n\u003c!-- https://github.com/devlooped/.github/raw/main/osmf.md --\u003e\n\n\u003c!-- #content --\u003e\n## Overview\n\nRather than the RPC-style programming offered (and encouraged) out of the \nbox by Orleans, Cloud Native Actors offers a message-passing style of programming \nwith a uniform API to access actors: Execute and Query. \n\nThese uniform operations receive a message (a.k.a. command or query) and \noptionally return a result. Consumers always use the same API to invoke \noperations on actors, and the combination of the actor id and the message \nconstitute enough information to route the message to the right actor.\n\nActors can be implemented as plain CLR objects, with no need to inherit \nfrom any base class or implement any interface. The Orleans plumbing of \ngrains and their activation is completely hidden from the developer.\n\n## Features\n\nRather than relying on `dynamic` dispatch, this implementation relies heavily on source generators \nto provide strong-typed routing of messages, while preserving a flexible mechanism for implementors.\n\nIn addition, this library makes the grains completely transparent to the developer. They don't even \nneed to take a dependency on Orleans. In other words: the developer writes his business logic as \na plain CLR object (POCO).\n\nThe central abstraction of the library is the actor bus:\n\n```csharp\npublic interface IActorBus\n{\n    Task ExecuteAsync(string id, IActorCommand command);\n    Task\u003cTResult\u003e ExecuteAsync\u003cTResult\u003e(string id, IActorCommand\u003cTResult\u003e command);\n    Task\u003cTResult\u003e QueryAsync\u003cTResult\u003e(string id, IActorQuery\u003cTResult\u003e query);\n}\n```\n\nActors receive messages to process, which are typically plain records such as:\n\n```csharp\npublic partial record Deposit(decimal Amount) : IActorCommand;  // 👈 marker interface for void commands\n\npublic partial record Withdraw(decimal Amount) : IActorCommand;\n\npublic partial record Close(CloseReason Reason = CloseReason.Customer) : IActorCommand\u003cdecimal\u003e; // 👈 marker interface for value-returning commands\n\npublic enum CloseReason\n{\n    Customer,\n    Fraud,\n    Other\n}\n\npublic partial record GetBalance() : IActorQuery\u003cdecimal\u003e; // 👈 marker interface for queries (a.k.a. readonly methods)\n```\n\nWe can see that the only thing that distinguishes a regular Orleans parameter \nfrom an actor message, is that it implements the `IActorCommand` or `IActorQuery` \ninterface. You can see the three types of messages supported by the library:\n\n* `IActorCommand` - a message that is sent to an actor to be processed, but does not return a result.\n* `IActorCommand\u003cTResult\u003e` - a message that is sent to an actor to be processed, and returns a result.\n* `IActorQuery\u003cTResult\u003e` - a message that is sent to an actor to be processed, and returns a result. \n  It differs from the previous type in that it is a read-only operation, meaning it does not mutate \n  the state of the actor. This causes a [Readonly method](https://learn.microsoft.com/en-us/dotnet/orleans/grains/request-scheduling#readonly-methods) \n  invocation on the grain.\n\nThe actor, for its part, only needs the `[Actor]` attribute to be recognized as such:\n\n```csharp\n[Actor]\npublic partial class Account(string id)    // 👈 no need for parameterless constructor or inheriting anything by default\n{\n    public string Id { get; } = id;\n    public decimal Balance { get; private set; }\n    public bool IsClosed { get; private set; }\n    public CloseReason Reason { get; private set; }\n\n    //public void Execute(Deposit command)      // 👈 methods can be overloads of message types\n    //{\n    //    // validate command\n    //    // decrease balance\n    //}\n\n    // Showcases that operations can have a name that's not Execute\n    public Task DepositAsync(Deposit command)   // 👈 but can also use any name you like\n    {\n        // validate command\n        Balance += command.Amount;\n        return Task.CompletedTask;\n    }\n\n    // Showcases that operations don't have to be async\n    public void Execute(Withdraw command)       // 👈 methods can be sync too\n    {\n        // validate command\n        Balance -= command.Amount;\n    }\n\n    // Showcases value-returning operation with custom name.\n    // In this case, closing the account returns the final balance.\n    // As above, this can be async or not.\n    public decimal Close(Close command)\n    {\n        var balance = Balance;\n        Balance = 0;\n        IsClosed = true;\n        Reason = command.Reason;\n        return balance;\n    }\n\n    // Showcases a query that doesn't change state\n    public decimal Query(GetBalance _) =\u003e Balance;  // 👈 becomes [ReadOnly] grain operation\n}\n```\n\n\u003e NOTE: no attributes are needed anywhere for state persistence — only the `[Actor]` attribute \n\u003e on the class itself is required. The source generator automatically includes in persisted state:\n\u003e all properties that have a setter (regardless of accessibility — `public`, `private`, etc.), and \n\u003e all non-`const`, non-`static`, non-`readonly` instance fields. Get-only properties and `readonly` \n\u003e fields are excluded, as they are expected to be initialized via the constructor or derived from \n\u003e other state.\n\nOn the hosting side, an `AddCloudActors` extension method is provided to register the \nautomatically generated grains to route invocations to the actors:\n\n```csharp\nvar builder = WebApplication.CreateSlimBuilder(args);\n\nbuilder.Host.UseOrleans(silo =\u003e\n{\n    silo.UseLocalhostClustering();\n});\n\n// 👇 registers generated grains, actor bus and activation features\nbuilder.Services.AddCloudActors(); \n```\n\n## State Deserialization\n\nThe above `Account` class only provides a single constructor receiving the account \nidentifier. After various operations are performed on it, however, the state will \nbe changed via private property setters (or direct field mutation). When you annotate \na class with the `[Actor]` attribute, a source generator will create an inner class to \nhold all state properties (and fields), and implement (explicitly) an `IActor\u003cTState\u003e` \ninterface to allow getting/setting the instance state.\n\nThis provides seamless integration with Orleans' recommended `IPersistentState\u003cT\u003e` \ninjection mechanism used by the generated grain.\n\nThe generator produces a nested `ActorState` record for the above `Account` actor, \ncapturing its mutable state as an Orleans-serializable snapshot:\n\n```csharp\n[GeneratedCode(\"Devlooped.CloudActors\")]\n[GenerateSerializer]\npublic partial class ActorState : IActorState\u003cAccount\u003e\n{\n    [Id(0)] public decimal Balance;\n    [Id(1)] public bool IsClosed;\n    [Id(2)] public CloseReason Reason;\n}\n```\n\nThis is a sort of typed [Memento pattern](https://grokipedia.com/page/Memento_pattern) which allows \nthe Orleans state persistence mechanisms to read and write the actor state without requiring \nany additional code from the developer.\n\n\u003e [!NOTE]\n\u003e The generated `ActorState` is always in sync with the actor's mutable members because it is \n\u003e regenerated at compile time. State is read from storage on activation and written after each \n\u003e successful command — with an automatic rollback (re-read) if the write fails.\n\n## Event Sourcing\n\nQuite a natural extension of the message-passing style of programming for these actors, \nis to go full event sourcing. The library provides an interface `IEventSourced` for that:\n\n```csharp\npublic interface IEventSourced\n{\n    IReadOnlyList\u003cobject\u003e Events { get; }\n    void AcceptEvents();\n    void LoadEvents(IEnumerable\u003cobject\u003e history);\n}\n```\n\nThe sample [Streamstone](https://github.com/yevhen/Streamstone)-based grain storage will \ninvoke `LoadEvents` with the events from the stream (if found), and `AcceptEvents` will be \ninvoked after the grain is saved, so it can clear the events list.\n\nOptimistic concurrency is implemented by exposing the stream version as the `IGrainState.ETag` \nand parsing it when persisting to ensure consistency.\n\nUsers are free to implement this interface in any way they deem fit, but the library provides \na default implementation if the interface is inherited but not implemented. The generated \nimplementation provides a `Raise\u003cT\u003e(@event)` method for the actor's methods to raise events, \nand invokes provided `Apply(@event)` methods to apply the events to mutate state. The generator \nassumes this convention, using the single parameter to every `Apply` method on the actor as \nthe switch to route events (either when raised or loaded from storage).\n\nFor example, if the above `Account` actor was converted to an event-sourced actor, it would \nlook like this:\n\n```csharp\n[Actor]\npublic partial class Account : IEventSourced  // 👈 interface is *not* implemented by user!\n{\n    public Account(string id) =\u003e Id = id;\n\n    public string Id { get; }\n    public decimal Balance { get; private set; }\n    public bool IsClosed { get; private set; }\n\n    public void Execute(Deposit command)\n    {\n        if (IsClosed)\n            throw new InvalidOperationException(\"Account is closed\");\n\n        // 👇 Raise\u003cT\u003e is generated when IEventSourced is inherited\n        Raise(new Deposited(command.Amount));\n    }\n\n    public void Execute(Withdraw command)\n    {\n        if (IsClosed)\n            throw new InvalidOperationException(\"Account is closed\");\n        if (command.Amount \u003e Balance)\n            throw new InvalidOperationException(\"Insufficient funds.\");\n\n        Raise(new Withdrawn(command.Amount));\n    }\n\n    public decimal Execute(Close command)\n    {\n        if (IsClosed)\n            throw new InvalidOperationException(\"Account is closed\");\n\n        var balance = Balance;\n        Raise(new Closed(Balance, command.Reason));\n        return balance;\n    }\n\n    public decimal Query(GetBalance _) =\u003e Balance;\n\n    // 👇 generated generic Apply(object) dispatches to each based on event type with no reflection\n\n    partial void Apply(Deposited @event) =\u003e Balance += @event.Amount;\n\n    partial void Apply(Withdrawn @event) =\u003e Balance -= @event.Amount;\n\n    partial void Apply(Closed @event)\n    {\n        Balance = 0;\n        IsClosed = true;\n        Reason = @event.Reason;\n    }\n}\n```\n\n\u003e [!TIP]\n\u003e By generating the partial `Apply` methods, the generator allows users to implement only the \n\u003e event types they care about, without needing to provide an empty implementation for the rest.\n\nWhen `IEventSourced` is inherited without being implemented, the generator provides the full\nwiring: `Raise\u003cT\u003e(event)` / `Raise\u003cT\u003e()` methods that apply the event and record it in the \npending events list, a type-switched `Apply(object)` dispatcher, and `partial void` declarations\nfor each event type raised. There is also an optional hook for post-raise callbacks:\n\n```csharp\n// Invoked after every Raise\u003cT\u003e(event) call — implement to react to raised events.\npartial void OnRaised\u003cT\u003e(T @event) where T : notnull;\n```\n\n\u003e [!NOTE]\n\u003e Note how there's no dynamic dispatch here 💯.\n\nAn important corollary of this project is that the design of a library and particularly \nits implementation details, will vary greatly if it can assume source generators will \nplay a role in its consumption. In this particular case, many design decisions \nwere different initially before I had the generators in place, and the result afterwards \nwas a simplification in many aspects, with less base types in the main library/interfaces \nproject, and more incremental behavior added as users opt-in to certain features.\n\n## Typed Actor IDs\n\nInstead of identifying actors with plain strings, you can use strongly-typed IDs.\n\n### Primitive IDs\n\nWhen the first constructor parameter is a primitive BCL value type (e.g. `long`, `Guid`), \nthe generator produces a nested `{Actor}Id` wrapper struct and a `NewId` factory method:\n\n```csharp\n[Actor]\npublic partial class Order(long id)\n{\n    public long Id =\u003e id;\n    // ...\n}\n\n// Generated:\n//   public readonly record struct OrderId(long Id);\n//   public static OrderId NewId(long id) =\u003e new(id);\n```\n\nFor `Guid` IDs a parameterless `NewId()` is also generated, using `Guid.CreateVersion7()` \non .NET 9+ or `Guid.NewGuid()` on earlier runtimes.\n\n### Structured IDs\n\nAny strongly-typed ID library that generates `IParsable\u003cTSelf\u003e` and `IFormattable` on the ID \ntype works out of the box. The most popular choices include:\n\n- [StructId](https://www.nuget.org/packages/StructId) — single-line `IStructId\u003cT\u003e` declaration, source-generated\n- [StronglyTypedId](https://www.nuget.org/packages/StronglyTypedId) — attribute-driven, supports multiple backing types\n- [Vogen](https://www.nuget.org/packages/Vogen) — value-object generator with validation support\n\nCloud Native Actors detects these types automatically: if the actor's first constructor parameter \nimplements `IParsable\u003cT\u003e`, it is treated as the typed ID and the generator produces typed \n`IActorBus` overloads for it — no extra configuration required.\n\nHere is an example using [StructId](https://www.nuget.org/packages/StructId):\n\n```csharp\n// Just declare the struct — StructId generates all the boilerplate\npublic readonly partial record struct ProductId : IStructId\u003cGuid\u003e;\n\n[Actor]\npublic partial class Product(ProductId id)\n{\n    public ProductId Id =\u003e id;\n\n    public decimal Price { get; private set; }\n\n    public void Execute(SetPrice command) =\u003e Price = command.Price;\n\n    public decimal Query(GetPrice _) =\u003e Price;\n}\n```\n\n```csharp\nvar id = new ProductId(Guid.CreateVersion7()); // or ProductId.New()\n\nawait bus.ExecuteAsync(id, new SetPrice(9.99m));\nvar price = await bus.QueryAsync(id, new GetPrice());\n```\n\n### Typed Bus Overloads\n\nFor any actor with a typed ID (primitive or structured), the generator produces typed \n`IActorBus` extension overloads so you never have to format the ID string manually:\n\n```csharp\n// Instead of:\nawait bus.ExecuteAsync(\"order/42\", new PlaceOrder(...));\n\n// You can use the typed overload:\nawait bus.ExecuteAsync(new OrderId(42), new PlaceOrder(...));\n```\n\nThe ID is always stored and routed as `\"{actortype}/{id}\"` (e.g. `\"order/42\"`, `\"product/...\"`).\n\n## Telemetry and Monitoring\n\nThe core implementation of the `IActorBus` is instrumented with `ActivitySource` and \n`Metric`, providing out of the box support for [Open Telemetry](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs)-based monitoring, as well \nas via [dotnet trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace) \nand [dotnet counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters).\n\nTo export telemetry using [Open Telemetry](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs), \nfor example:\n\n```csharp\nusing var tracer = Sdk\n    .CreateTracerProviderBuilder()\n    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(\"ConsoleApp\"))\n    .AddSource(source.Name) // other app sources\n    .AddSource(\"CloudActors\")\n    .AddConsoleExporter()\n    .AddZipkinExporter()\n    .AddAzureMonitorTraceExporter(o =\u003e o.ConnectionString = config[\"AppInsights\"])\n    .Build();\n```\n\nCollecting traces via [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace):\n\n```shell\ndotnet trace collect --name [PROCESS_NAME] --providers=\"Microsoft-Diagnostics-DiagnosticSource:::FilterAndPayloadSpecs=[AS]CloudActors,System.Diagnostics.Metrics:::Metrics=CloudActors\"\n```\n\nMonitoring metrics via [dotnet-counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters):\n\n```shell\ndotnet counters monitor --process-id [PROCESS_ID] --counters CloudActors\n```\n\n## How it works\n\nThe library uses source generators to generate the grain classes. It's easy to inspect the \ngenerated code by setting the `EmitCompilerGeneratedFiles` property to `true` in the project \nand inspecting the `obj` folder.\n\nFor each `[Actor]` class the generator produces a partial `{Name}Grain : Grain` class that:\n\n- Injects `IPersistentState\u003cActorState\u003e` and reads it on activation\n- Routes incoming `IActorCommand` / `IActorCommand\u003cTResult\u003e` / `IActorQuery\u003cTResult\u003e` to the \n  exact method you defined on your actor — preserving your method name and sync/async signature\n- After every command writes the new state to storage; on failure, rolls back by re-reading\n- Marks `QueryAsync` with `[ReadOnly]` so Orleans handles it as a concurrent read\n\nBecause the grain is `partial`, you can extend it with your own members in a separate file \nif needed.\n\n\u003e [!NOTE]\n\u003e Note how there's no dynamic dispatch 💯. Message routing is a compile-time `switch` on the \n\u003e concrete message type, generated by the source generator directly from your actor's methods.\n\nSince source generators [can't depend on other generated code](https://github.com/dotnet/roslyn/issues/57239),\ngrain types are registered with Orleans through assembly-level attributes (`ApplicationPartAttribute` \nand `GenerateCodeForDeclaringAssembly`) emitted by the generator into the silo/host project — \nno manual grain type registration is needed.\n\nThe `services.AddCloudActors()` call (generated as an extension on `IServiceCollection`) registers:\n\n- `IActorBus` → `OrleansActorBus`  \n- `IActorIdFactory` → a compile-time factory that parses typed actor IDs without reflection  \n- Replaces `IPersistentStateFactory` with a wrapper that passes the typed ID to the actor constructor  \n\nFinally, in order to improve discoverability for consumers of the `IActorBus` interface,\nextension method overloads are generated that surface the available actor messages \nas non-generic overloads, such as:\n\n![execute overloads](https://raw.githubusercontent.com/devlooped/CloudActors/main/assets/img/command-overloads.png?raw=true)\n\n![query overloads](https://raw.githubusercontent.com/devlooped/CloudActors/main/assets/img/query-overloads.png?raw=true)\n\n\u003c!-- #content --\u003e\n\n\u003c!-- #sponsors --\u003e\n\u003c!-- include https://github.com/devlooped/sponsors/raw/main/footer.md --\u003e\n# Sponsors \n\n\u003c!-- sponsors.md --\u003e\n[![Clarius Org](https://avatars.githubusercontent.com/u/71888636?v=4\u0026s=39 \"Clarius Org\")](https://github.com/clarius)\n[![MFB Technologies, Inc.](https://avatars.githubusercontent.com/u/87181630?v=4\u0026s=39 \"MFB Technologies, Inc.\")](https://github.com/MFB-Technologies-Inc)\n[![Khamza Davletov](https://avatars.githubusercontent.com/u/13615108?u=11b0038e255cdf9d1940fbb9ae9d1d57115697ab\u0026v=4\u0026s=39 \"Khamza Davletov\")](https://github.com/khamza85)\n[![SandRock](https://avatars.githubusercontent.com/u/321868?u=99e50a714276c43ae820632f1da88cb71632ec97\u0026v=4\u0026s=39 \"SandRock\")](https://github.com/sandrock)\n[![DRIVE.NET, Inc.](https://avatars.githubusercontent.com/u/15047123?v=4\u0026s=39 \"DRIVE.NET, Inc.\")](https://github.com/drivenet)\n[![Keith Pickford](https://avatars.githubusercontent.com/u/16598898?u=64416b80caf7092a885f60bb31612270bffc9598\u0026v=4\u0026s=39 \"Keith Pickford\")](https://github.com/Keflon)\n[![Thomas Bolon](https://avatars.githubusercontent.com/u/127185?u=7f50babfc888675e37feb80851a4e9708f573386\u0026v=4\u0026s=39 \"Thomas Bolon\")](https://github.com/tbolon)\n[![Kori Francis](https://avatars.githubusercontent.com/u/67574?u=3991fb983e1c399edf39aebc00a9f9cd425703bd\u0026v=4\u0026s=39 \"Kori Francis\")](https://github.com/kfrancis)\n[![Reuben Swartz](https://avatars.githubusercontent.com/u/724704?u=2076fe336f9f6ad678009f1595cbea434b0c5a41\u0026v=4\u0026s=39 \"Reuben Swartz\")](https://github.com/rbnswartz)\n[![Jacob Foshee](https://avatars.githubusercontent.com/u/480334?v=4\u0026s=39 \"Jacob Foshee\")](https://github.com/jfoshee)\n[![](https://avatars.githubusercontent.com/u/33566379?u=bf62e2b46435a267fa246a64537870fd2449410f\u0026v=4\u0026s=39 \"\")](https://github.com/Mrxx99)\n[![Eric Johnson](https://avatars.githubusercontent.com/u/26369281?u=41b560c2bc493149b32d384b960e0948c78767ab\u0026v=4\u0026s=39 \"Eric Johnson\")](https://github.com/eajhnsn1)\n[![Jonathan ](https://avatars.githubusercontent.com/u/5510103?u=98dcfbef3f32de629d30f1f418a095bf09e14891\u0026v=4\u0026s=39 \"Jonathan \")](https://github.com/Jonathan-Hickey)\n[![Ken Bonny](https://avatars.githubusercontent.com/u/6417376?u=569af445b6f387917029ffb5129e9cf9f6f68421\u0026v=4\u0026s=39 \"Ken Bonny\")](https://github.com/KenBonny)\n[![Simon Cropp](https://avatars.githubusercontent.com/u/122666?v=4\u0026s=39 \"Simon Cropp\")](https://github.com/SimonCropp)\n[![agileworks-eu](https://avatars.githubusercontent.com/u/5989304?v=4\u0026s=39 \"agileworks-eu\")](https://github.com/agileworks-eu)\n[![Zheyu Shen](https://avatars.githubusercontent.com/u/4067473?v=4\u0026s=39 \"Zheyu Shen\")](https://github.com/arsdragonfly)\n[![Vezel](https://avatars.githubusercontent.com/u/87844133?v=4\u0026s=39 \"Vezel\")](https://github.com/vezel-dev)\n[![ChilliCream](https://avatars.githubusercontent.com/u/16239022?v=4\u0026s=39 \"ChilliCream\")](https://github.com/ChilliCream)\n[![4OTC](https://avatars.githubusercontent.com/u/68428092?v=4\u0026s=39 \"4OTC\")](https://github.com/4OTC)\n[![domischell](https://avatars.githubusercontent.com/u/66068846?u=0a5c5e2e7d90f15ea657bc660f175605935c5bea\u0026v=4\u0026s=39 \"domischell\")](https://github.com/DominicSchell)\n[![Adrian Alonso](https://avatars.githubusercontent.com/u/2027083?u=129cf516d99f5cb2fd0f4a0787a069f3446b7522\u0026v=4\u0026s=39 \"Adrian Alonso\")](https://github.com/adalon)\n[![torutek](https://avatars.githubusercontent.com/u/33917059?v=4\u0026s=39 \"torutek\")](https://github.com/torutek)\n[![mccaffers](https://avatars.githubusercontent.com/u/16667079?u=110034edf51097a5ee82cb6a94ae5483568e3469\u0026v=4\u0026s=39 \"mccaffers\")](https://github.com/mccaffers)\n[![Seika Logiciel](https://avatars.githubusercontent.com/u/2564602?v=4\u0026s=39 \"Seika Logiciel\")](https://github.com/SeikaLogiciel)\n[![Andrew Grant](https://avatars.githubusercontent.com/devlooped-user?s=39 \"Andrew Grant\")](https://github.com/wizardness)\n[![Lars](https://avatars.githubusercontent.com/u/1727124?v=4\u0026s=39 \"Lars\")](https://github.com/latonz)\n[![prime167](https://avatars.githubusercontent.com/u/3722845?v=4\u0026s=39 \"prime167\")](https://github.com/prime167)\n\n\n\u003c!-- sponsors.md --\u003e\n[![Sponsor this project](https://avatars.githubusercontent.com/devlooped-sponsor?s=118 \"Sponsor this project\")](https://github.com/sponsors/devlooped)\n\n[Learn more about GitHub Sponsors](https://github.com/sponsors)\n\n\u003c!-- https://github.com/devlooped/sponsors/raw/main/footer.md --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlooped%2Fcloudactors","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevlooped%2Fcloudactors","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlooped%2Fcloudactors/lists"}