[Solved] UmbracoForm Post Route Hijack?
c
V13.2.2, UF V13.1.1 I have an UmbracoForm used as a sign up form that needs to save some of it's data as a new node and some to an API end point and then save uploaded images to newly created Media folders under a particular parent. I can use work I've done before for most of this but it's what to point the UF at that's the issue. Previously I've done it with a form on a Surface Controller and processed it all in a ViewComponent, but this time it's different. Client wants to use UF. So.... I'm thinking if I create a form "Thank you" page and use that as the WorkFlow target for a "Send form to URL" workflow and then route hijack the "Thank you" page, I can do all the data manipulation in the controller there. Does this make any sense or am I rushing into a blind alley? Thoughts appreciated. Thanks.
j
There's no need for a surface controller, with Umbraco Forms you can (and should) use workflows for this. There's already a workflow for creating nodes (and mapping form values to property values). You can roll your own workflows for creating media and posting to APIs (though depending on the API you may find one exists already that works), it's pretty straightforward to roll your own, here are the docs - https://docs.umbraco.com/umbraco-forms/developer/extending/adding-a-workflowtype. The nice thing about splitting them up is that you can still give users control over other aspects of the workflow like which page users get redirected to etc. and make the individual workflow types re-useable.
c
Thanks, but I'm not using a Surface Controller for UFs. I only did that for a bespoke, hand-built form because UF couldn't handle a very niche requirement. This form is a bog-standard form but I have to do fancy stuff with it. So I was going to route-hijack the page I was going to point it to in the workflow. I'll have a look at the link you posted and see if it comes up to snuff 🙂
j
In a basic sense, a workflow is a controller for a form, it's what gets invoked on postback from your form - all that's really different is that you can line multiple workflows up to run one after the other.
c
Just looked at it now. I see it as a quick way to hook it all up. I think I can get it to work. The only thing I'm not sure about is the file uploads. There's a logo image single file upload and a multi-image file upload. If that's doable, then that'll be great. The images just have to go into the Media section, where as some of the info has to go into a new node AND down the wire to an API endpoint which is a legacy system that needs it.
Thanks for the heads up 🙂
j
You're welcome. I've handled file uploads from forms before just fine (though not recently, v8, so don't think I have code that will help). IIRC the files are uploaded as part of the forms submission and you can get at their storage location in the workflow to then do what you want with them. You can always decompile UF to take a look at the built-in workflows for guidance as well.
c
I've got code from the aforementioned ViewComponent and a couple of other projects importing stuff from API's, including images, so hopefully I should be ok. I'll give it a go in the morning 🙏
c
I nearly did! But, having crafted a beautiful WorkFlow that gets the data from the form, adds some other stuff too and creates a new page, code has no red squigglies, it doesn't get out of the starting blocks cos of this:-
InvalidOperationException: Cannot resolve 'Web.Core.Extensions.AdvertiserFormWorkflow' from root provider because it requires scoped service 'Umbraco.Cms.Core.IPublishedContentQuery'.
Seems to be something to do with middleware 😦 It's registered with the composer so it shouldn't be that. This is the constructor with DI:
public AdvertiserFormWorkflow(ILogger<AdvertiserFormWorkflow> logger, IContentService contentService, IMediaService mediaService, MiscHelpers miscHelpers)
Any clues?
The example has this using:
using Umbraco.Core.Composing;
which I've commented out because it doesn't like the "Core" bit. Maybe that's it. But how do I fix it?
j
What's in miscHelpers? My guess is something in there needs an IPublishedContentQuery but that won't exist here.
c
It's injected into miscHelpers. Actually, it's used to get the listing page ID for the new pages to be put under. So I guess I could just put that in the workflow.
I was trying to be smart 😉
n
At a quick glance, my guess would be that something is registered in DI in a different scope as the IPublishedContentQuery - for example if MiscHelpers is registered as a singleton, IPublishedContentQuery I don't think is and as a result you have a scoping conflict.
c
I just pulled the function that was using IPublishedContentQuery out of MiscHelpers and put it into the Workflow. Same issue 😦
How do you get around it then?
n
Can you share your workflow constructor definition, your MiscHelpers connstructor definition, and how both are registered with DI?
c
Workflow constructor: public AdvertiserFormWorkflow(ILogger logger, IContentService contentService, IMediaService mediaService, MiscHelpers miscHelpers, IPublishedContentQuery contentQuery) MiscHelpers constructor: public MiscHelpers(IConfiguration? configuration, ILogger _logger) Composer: //Adding custom UF Workflows builder.WithCollectionBuilder() .Add(); //Adding MiscHelpers builder.Services.AddTransient();
n
try injecting in the
IServiceScope
instead of the
IPublishedContentQuery
. Once you have a service scope you can get to the IPublisehdContentQuery by doing the following:
var publishedContentQuery = serviceScope.ServiceProvider.GetRequiredService<IPublishedContentQuery>();
It might allow you to bypass the scoping issue that appears to be occuring.
c
I'll give it a go, thanks
Didn't work, won't even build 😦 I put the function back in the MiscHelpers and injected IServiceScope there. I get this:- Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Web.Core.Helpers.MiscHelpers Lifetime: Transient ImplementationType: Web.Core.Helpers.MiscHelpers': Unable to resolve service for type 'Microsoft.Extensions.DependencyInjection.IServiceScope' while attempting to activate 'Web.Core.Helpers.MiscHelpers'.)
n
Hmm, that's strange.
c
Just tried putting it into the workflow again, same issue 😦
There's a workflow that creates nodes under other nodes, so it must be doable in a workflow.
I'm only using the ContentQueryService to get the ID of the advertisersList page. Is there any other way of doing it other than .... IPublishedContent? siteRoot = _contentQuery.ContentAtRoot().FirstOrDefault(); var advertiserListNode = siteRoot?.FirstChild(f => f.ContentType.Alias == "advertiserList"); ...when it's effectively in a controller?
n
Off the top of my head I'm not sure tbh. Is the error throwing at boot time or when you try and execute the workflow?
c
When I execute the workflow
I don't want to hard code the advertiserListNodeId, but I guess I might have to.
n
give me 10 min to try something 🙂
Right, so I have a very simple POC working
This is my workflow:
Copy code
cs
public class MyWorkflow : WorkflowType
{
    private readonly ILogger<MyWorkflow> logger;
    private readonly IPublishedContentQueryAccessor queryAccessor;

    public MyWorkflow(ILogger<MyWorkflow> logger, IPublishedContentQueryAccessor queryAccessor)
    {
        this.Id = new Guid("123eb0d5-adaa-4729-8b4c-4bb439dc0202");
        this.Name = "MyWorkflow";
        this.Description = "POC WORKFLOW";
        this.logger = logger;
        this.queryAccessor = queryAccessor;
    }
    public override Task<WorkflowExecutionStatus> ExecuteAsync(WorkflowExecutionContext context)
    {
        logger.LogInformation("Custom workflow hit");
        if(queryAccessor.TryGetValue(out var query))
        {
            var root = query.ContentAtRoot();
            logger.LogInformation("Items at root = {rootNodes}", string.Join("\n", root?.Select(x => string.Format("{0} - {1}", x.Id, x.Name)) ?? []));
        }
        else
        {
            logger.LogError("Unable to get IPublishedContentQuery");
        }
        return Task.FromResult(WorkflowExecutionStatus.Completed);
    }

    public override List<Exception> ValidateSettings()
    {
        return new List<Exception>();
    }
}
All it does is get all the root nodes and log them to the logger
This is a variant that uses the IScopedServiceProvider approach (sorry I didn't rename things just changed types and adjusted the code so it didn't error)
Copy code
cs
public class MyWorkflow : WorkflowType
{
    private readonly ILogger<MyWorkflow> logger;
    private readonly IScopedServiceProvider queryAccessor;

    public MyWorkflow(ILogger<MyWorkflow> logger, IScopedServiceProvider queryAccessor)
    {
        this.Id = new Guid("123eb0d5-adaa-4729-8b4c-4bb439dc0202");
        this.Name = "MyWorkflow";
        this.Description = "POC WORKFLOW";
        this.logger = logger;
        this.queryAccessor = queryAccessor;
    }
    public override Task<WorkflowExecutionStatus> ExecuteAsync(WorkflowExecutionContext context)
    {
        logger.LogInformation("Custom workflow hit");

        var query = queryAccessor.ServiceProvider?.GetRequiredService<IPublishedContentQuery>();
            var root = query?.ContentAtRoot();
            logger.LogInformation("Items at root = {rootNodes}", string.Join("\n", root?.Select(x => string.Format("{0} - {1}", x.Id, x.Name)) ?? []));
        
        return Task.FromResult(WorkflowExecutionStatus.Completed);
    }

    public override List<Exception> ValidateSettings()
    {
        return new List<Exception>();
    }
}
c
I'll look and digest, thanks. I hard coded the ID in just to be able to check out all the other code in the workflow. But I'll return to this very shortly and let you know. I don't want to hard code ID's 🙂
That's worked great. Thanks very much. So it was the use of IPublishedContentQueryAccessor that clinched it. I've put that in the MiscHelpers now and it's all working. Phew! 🙏
6 Views