Pluggable Orleans Assemblies

I have…

I’m submitting a…

  • [ ] Regression (a behavior that stopped working in a new release)
  • [ ] Bug report
  • [ ] Performance issue
  • [x] Documentation issue or request

Question

Is there any way to dynamically add assemblies to Orleans for loading custom grains similar to the IPlugin method or do I just have to add the assembly in \backend\src\Squidex\Config\Orleans\OrleansServices.cs:48 ?

Thanks

You have to register the assembly, but if you dig in, you will find a way how to do register it from the plugin. Should not be a big problem. You just need to find the right extension method for IServiceCollection or IServiceProvider.

I’ve managed to register an assembly but now I’m getting an error when trying to call a method on my grain:
Cannot find generated GrainReference class for interface '<MyAssembly>.CustomId.ICustomIdGrain'

Full Stack Trace
Cannot find generated GrainReference class for interface \u0027<MyAssembly>.CustomId.ICustomIdGrain\u0027
   at Orleans.Runtime.TypeMetadataCache.GetGrainReferenceType(Type interfaceType)
   at Orleans.GrainFactory.MakeCaster(Type interfaceType)
   at System.Collections.Concurrent.ConcurrentDictionary\u00602.GetOrAdd(TKey key, Func\u00602 valueFactory)
   at Orleans.GrainFactory.Cast(IAddressable grain, Type interfaceType)
   at Orleans.GrainFactory.Cast[TGrainInterface](IAddressable grain)
   at <MyAssembly>.CustomId.CustomId.GetGrain(DomainId appId) in C:\Users\pains\source\repos\SQX\squidex\backend\src\<MyAssembly>\CustomId\CustomId.cs:line 28
   at <MyAssembly>.CustomId.CustomId.GetNextId(DomainId appId, String schemaName) in C:\Users\pains\source\repos\SQX\squidex\backend\src\<MyAssembly>\CustomId\CustomId.cs:line 23
   at <MyAssembly>.CustomId.CustomIdMiddleware.SetCustomId(CommandContext context, CreateContent command) in C:\Users\pains\source\repos\SQX\squidex\backend\src\<MyAssembly>\CustomId\CustomIdMiddleware.cs:line 59
   at <MyAssembly>.CustomId.CustomIdMiddleware.HandleAsync(CommandContext context, NextDelegate next) in C:\Users\pains\source\repos\SQX\squidex\backend\src\<MyAssembly>\CustomId\CustomIdMiddleware.cs:line 45
   at Squidex.Infrastructure.Commands.CustomCommandMiddlewareRunner.HandleAsync(CommandContext context, NextDelegate next) in C:\Users\pains\source\repos\SQX\squidex\backend\src\Squidex.Infrastructure\Commands\CustomCommandMiddlewareRunner.cs:line 32
   at Squidex.Web.CommandMiddlewares.ETagCommandMiddleware.HandleAsync(CommandContext context, NextDelegate next) in C:\Users\pains\source\repos\SQX\squidex\backend\src\Squidex.Web\CommandMiddlewares\ETagCommandMiddleware.cs:line 55
   at Squidex.Infrastructure.Commands.InMemoryCommandBus.PublishAsync(ICommand command) in C:\Users\pains\source\repos\SQX\squidex\backend\src\Squidex.Infrastructure\Commands\InMemoryCommandBus.cs:line 71
   at Squidex.Areas.Api.Controllers.Contents.ContentsController.InvokeCommandAsync(ICommand command) in C:\Users\pains\source\repos\SQX\squidex\backend\src\Squidex\Areas\Api\Controllers\Contents\ContentsController.cs:line 713
   at Squidex.Areas.Api.Controllers.Contents.ContentsController.PostContent(String app, String name, NamedContentData request, Boolean publish, Nullable\u00601 id) in C:\Users\pains\source\repos\SQX\squidex\backend\src\Squidex\Areas\Api\Controllers\Contents\ContentsController.cs:line 445
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.\u003CInvokeActionMethodAsync\u003Eg__Awaited|12_0(ControllerActionInvoker invoker, ValueTask\u00601 actionResultValueTask)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.\u003CInvokeNextActionFilterAsync\u003Eg__Awaited|10_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State\u0026 next, Scope\u0026 scope, Object\u0026 state, Boolean\u0026 isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.\u003CInvokeInnerFilterAsync\u003Eg__Awaited|13_0(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.\u003CInvokeNextExceptionFilterAsync\u003Eg__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

I’ve exhausted my Google-Fu and am hoping that you can suggest something simple that I’ve missed.

I’m pretty sure I’ve got everything set up right:

  • I’ve referenced ‘Microsoft.Orleans.Core’ and ‘Microsoft.Orleans.CodeGenerator.MSBuild’ v3.4.0 nuget packages in my project
  • I’ve added my project as a reference in the main Squidex web project.
  • I’ve registered my assembly in the Orleans Setup following the pattern that’s there:
    image
  • I’ve compared my project to both ‘Squidex.Domain.Apps.Entities’ and ‘Squidex.Infrastructure’
  • I’ve checked with ildasm that the GeneratedCode is created in the assembly:
  • I’m running this locally in Visual Studio 2019 for the backend and Visual Studio Code for the frontend.

Hopefully it’s something simple that I’ve missed.

Thanks

Can you show me the interface and grain class? Just the signature.

Btw: What do you build?

I’m trying to make a middleware that pre-assigns a block of sequential IDs to a schema and then automatically gets the next ID and sets it as the CustomID when a new record is created in the UI.

I tried doing it just in the middleware but it’s tough to avoid race conditions in a load-balanced environment that way. I thought that if I do it with a Grain then that’s hosted in just one place and I can avoid any race conditions.

Interface:
image

Grain:


(The UtcNow.Ticks is just Proof of Concept while I get the grain working)

Call:

There is already something for that :wink:

But your code also looks okay from what I see.

That’s close but doesn’t allow us to pre-allocate say 1-1000 for one schema, and 1001-2000 for the next schema.
If more than 1000 entities were stored in the first schema we’d have an ID clash.
With the middleware and grain it could then find out the top allocated range and assign the next range to that entity.

Yeah, I think the code is all right but I don’t see why Orleans won’t see its GrainReference class.
This feels like the sort of thing that gets fixed by deleting the whole folder, rebooting and then checking out from git again. :frowning:

In the obj folder there is usually a file with generated code. You can send it to me via PM if you want.

No, you just use two counters for that.

I’ve sent the file on Slack. Thanks :slight_smile:

Ok, for anyone else having a similar problem I think I’ve solved it.
It appears that there’s some interplay that means you can’t have an IPlugin and a Grain loaded from the same assembly.
I have no idea why that is the case but by moving my Grain into a separate assembly that isn’t added to the “plugins” configuration section it then works without issue.

Thank you very much. The plugins are loaded in a very speical way to avoid conflicts with nuget and so on. Perhaps this is the reason.

1 Like

Going back to the original issue about loading Grains from a plugin, I’m not sure if it will be possible.
Orleans is configured right up front from Program.cs before the plugins are even loaded.
If it is possible, I haven’t found the extension method that would do it yet.