Dependency Injection Issue with forms workflow

Have recently upgraded V13.6.0 to V13.7.1 and UF 13.4.1. A form workflow is now giving the following error when submitted:

“InvalidOperationException: Cannot resolve ‘Web.Core.Extensions.RegistrationFormWorkflow’ from root provider because it requires scoped service ‘Umbraco.Cms.Core.Security.IMemberManager’.”

The workflow creates an Umbraco Member and uses the IMemberService to do that. It was working fine, but not any more. I can’t even debug it because it fails before it gets to any breakpoint I can set.

I understand it’s something to do with DI, which I’m quite comfortable with, but I just don’t understand why it’s suddenly saying it can’t resolve a service it’s not even using.

If anyone has any clue as to what’s going on I’d love to hear it.

Thanks.

Did this happen exactly after the upgrade that you did? It might be that the IMemberService now uses the IMemberManager under the hood

It’s something that worked a week before upgrade and now I needed to make some alterations, it doesn’t. It builds fine but causes this error when the form is submitted.

ChatGPT suggests it’s to do with WorkFlows being singletons and IMemberService and IPasswordHasher are scoped and so the latter can’t be injected into the former.

I’ve just tried removing the DI’s for the scoped items and surrounding them with _serviceProvider so they get instantiated on demand, thus

using (var scope = _serviceProvider.CreateScope()) { 
                    
 var _memberService = scope.ServiceProvider.GetRequiredService<IMemberService>();
 var _passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher>();

.........}

But the error remains. :frowning:

Hmm, I would have expected that to work yeah… Do you perhaps have any notifications/events that could trigger when you do those changes?

Not sure what you mean. I just get a single Error in the Log Viewer with the same error, but more detailed of course.

The workflow also calls a helper that is injected. That helper also needs to access the member info. So I also removed the helper from the constructor and put it in it’s own scope section:-


 using (var scope = _serviceProvider.CreateScope()) {
       var _miscHelpers = scope.ServiceProvider.GetRequiredService<MiscHelpers>();
            
       // Because WorkFlows are singletons and so can't inject IMemberService and IPasswordHasher which are scoped services
      contact1Email = _miscHelpers.GetBuyersEmailFromName(Contact1Name);
      contact2Email = _miscHelpers.GetBuyersEmailFromName(Contact2Name);
      contact3Email = _miscHelpers.GetBuyersEmailFromName(Contact3Name);
 }

Error still remains :frowning:

Hi Craig,

Just to confirm, the workflow was working before the upgrade?
I didn’t think Workflows were singletons (I’m assuming this is an Umbraco Forms work flow?)

Could you share what your workflow definition looks like (along with using statements), and how are you registering it?

Workflows are transient, not singletons.

(Decompiled)

//
// Summary:
//     Builds a collection of type Umbraco.Forms.Core.WorkflowType.
public class WorkflowCollectionBuilder : LazyCollectionBuilderBase<WorkflowCollectionBuilder, WorkflowCollection, WorkflowType>
{
    protected override ServiceLifetime CollectionLifetime => ServiceLifetime.Transient;

    protected override WorkflowCollectionBuilder This => this;
}

In the post above you say the error is regarding the ‘Umbraco.Cms.Core.Security.IMemberManager.

Where are you injecting the IMemberManager?

Do you actually need the IMemberManager here? Normally that’s for getting at member information in views/controllers and I think that’s tied to a http context which I don’t think you can get at here anyway.

That’s the thing, I’m not using the IMemberManager. It’s a mystery why it’s complaining about it. (It’s ChatGPT that said workflows were singetons btw!)

Here’s the code Nik asked for without the ChatGPT-inspired changes:-

using System.Text;
using System.Text.Json;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Enums;
using Web.Core.Helpers;
using Web.Core.Models;
using Web.Core.Services;
using File = System.IO.File;

namespace Web.Core.Extensions;

public class RegistrationFormWorkflow : WorkflowType {

    private readonly ILogger<RegistrationFormWorkflow> _logger;
    private readonly IContentService _contentService;
    private readonly MiscHelpers _miscHelpers;
    private readonly IConfiguration _configuration;
    private readonly IWordService _wordService;
    private readonly IGenerateBookmarkForDocumentService _generateBookmarkForDocumentService;
    private readonly IWebHostEnvironment _webHostEnvironment;
    private readonly HttpClient _httpClient;
    private readonly IMemberService _memberService;
    private readonly IPasswordHasher _passwordHasher;


    public RegistrationFormWorkflow(ILogger<RegistrationFormWorkflow> logger, 
        IContentService contentService, 
        MiscHelpers miscHelpers, 
        IConfiguration configuration, 
        IWordService wordService,
        IGenerateBookmarkForDocumentService generateBookmarkForDocumentService,
        IWebHostEnvironment webHostEnvironment,
        HttpClient httpClient,
        IMemberService memberService,
        IPasswordHasher passwordHasher) {
        _logger = logger;
        _contentService = contentService;
        _miscHelpers = miscHelpers;
        _configuration = configuration;
        _wordService = wordService;
        _generateBookmarkForDocumentService = generateBookmarkForDocumentService;
        _webHostEnvironment = webHostEnvironment;
        _httpClient = httpClient;
        _memberService = memberService;
        _passwordHasher = passwordHasher;


        this.Id = new Guid("9833cd50-58be-4440-918c-f99a239f0424");
        this.Name = "Send to Signable";
        this.Description = "This workflow sends new member details to Signable";
        this.Icon = "icon-documents";
        this.Group = "Services";
    }
    

    public override async Task<WorkflowExecutionStatus> ExecuteAsync(WorkflowExecutionContext formContext) {
................
}

The form is an application form to send off a document to Signable and create an Umbraco member once Signable sends a successful receipt header.

The _miscHelper is just something that gets an email address from a list of representatives that are held as content items, by name.

The System.IO.File is just there to write the filled in docs to disc for testing.

Well, something wants an IMemberManager injected.

If you have a look through all your service dependencies - can you see anything that’s using IMemberManager?

The only thing I can see is the _miscHelper has a method that needs IMemberManager to get the current logged in Member. Not actually used in the WorkFlow:-


    public async Task<string?> GetMembersNotificationCategories() {
        MemberIdentityUser? currentMember = await _memberManager.GetCurrentMemberAsync();
        if (currentMember != null) {
            IMember? theMember = _memberService.GetByEmail(currentMember.Email);
            string? subscriptions = theMember.GetValue<string>("notificationsSubscribed");

            return subscriptions;
        }
        _logger.LogWarning("Member not found");
        return "Member not found";
    }

TBH, this is a recent addition. I tend to use MiscHelper as a dumping ground for little methods that can be used all over the place.

Maybe I should put this in it’s own class?

Well, that’ll be it then.

RegistrationFormWorkflow needs a MiscHelpers which needs an IMemberManager. The DI container can’t resolve that because IMemberManager is scoped.

This is a bad idea. It causes problems like the above (and I seem to remember it’s not the first time you’ve asked for help with this kind of issue :wink:).

Yes - Single Responsibility Principle FTW!

1 Like

I take your point (and the slap on the wrist :wink: ) I’ll rescind my bad practice and move on armed with the Single Responsibility Principle. However, I will report back here either way before I mark this as solved :wink:

Thanks Jason

1 Like

Haha, not a slap on the wrist - just a gentle shove in the right direction :grin:

This is one of those places where following best practice really does save time and effort, and not just in the long term or if it ever needs refactoring - it doesn’t take long to trip over dependency problems especially when working with Umbraco.

To provide a better explanation:

Generally, request based stuff is scoped because in ASP.NET that’s what scoped is for:

For web applications, a scoped lifetime indicates that services are created once per client request (connection).
ASP.NET Docs

So here the error is really surfacing an architectural problem - you’re trying do something that doesn’t need to be tied to a scope, but you’re accidentally consuming a scoped service to do it.

Now, sometimes you might need to consume a scoped service from another service that isn’t BUT that’s always good time to stop and ask if you really need to be doing that.

2 Likes

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.