Skip to main content

How to resolve "InvalidOperationException: Cannot resolve 'ProjectName.SendEmailCustomUmbracoWorkflow' from root provider because it requires scoped service 'ProjectName.ICustomServiceName'"

When you access a custom service from within your Custom Umbraco Workflow or a similar environment, you may encounter an "Invalid Operation Exception." This error typically occurs because you are attempting to use a scoped service within a singleton context.

Solution

If you need to use a scoped service within a singleton, inject IServiceScopeFactory into the singleton. Then, create a scope to pull out your context when needed.

using ProjectName.Microsites.Cms.DataAccess.Constants;
using ProjectName.Web.Services.Interfaces;
using Newtonsoft.Json.Linq;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Attributes;
using Umbraco.Forms.Core.Enums;
using Umbraco.Forms.Core.Persistence.Dtos;

namespace ProjectName.Web.UI.Workflows
{
    public class CrmFieldMapper : WorkflowType
    {
        private readonly ILogger<CrmFieldMapper> _logger;
        private readonly IServiceScopeFactory _serviceScopeFactory;

        public CrmFieldMapper(ILogger<CrmFieldMapper> logger,
        IServiceScopeFactory serviceScopeFactory) //IFormDataService formDataService)
        {
            Id = new Guid("b8167f85-0ca4-4d63-80e1-069eef772dca");
            Name = "Crm Field Mapper";
            Description = "Custom workflow to map form fields to CRM fields";
            Icon = "icon-trafic";

            _logger = logger;
// Inject IServiceScopeFactory into the singleton
            _serviceScopeFactory = serviceScopeFactory;
            //_formDataService = formDataService;
        }

        [Setting("Field Mappings",
        Description = "Map fields from the form to CRM",
        View = $"~/App_Plugins/CrmFieldMapper/crmfieldmapper.html",
        PreValues = "")]
        public string MappingsField { get; set; } = string.Empty;

        public override async Task<WorkflowExecutionStatus> ExecuteAsync(
            WorkflowExecutionContext context)
        {
            try
            {
// Create a scope for your service and then use it
                using var scope = _serviceScopeFactory.CreateScope();
                var formDataService = scope.ServiceProvider
                .GetRequiredService<IFormDataService>();

                var mappableCRMFields = formDataService
                .GetAllActiveMappableCRMFieldsCached();

                if (mappableCRMFields != null)
                {
                    var record = context.Record;

                    var formGuid = record.Form;

                    var fieldMappings = new Dictionary<string, int>();

                    foreach (var mappableCRMField in mappableCRMFields)
                    {
                        var formAlias = GetFormAlias(record,
                        mappableCRMField.CRMFieldFriendlyName);
                        if (!string.IsNullOrEmpty(formAlias))
                        {
                            // Mapping is done for this form value,
// add it to the fieldMappings dictionary
                            fieldMappings.Add(formAlias,
                            mappableCRMField.Id);
                        }
                    }

                    if (fieldMappings.Any())
                    {
                        formDataService.SaveFormFieldMappings(formGuid,
                        fieldMappings,
                        FormMappingConstants.FieldMappingNotSetOptionId);
                    }
                }

                return WorkflowExecutionStatus.Completed;
            }
            catch (Exception ex)
            {
                _logger.LogError($"CrmFieldMapper Execute exception,
                ex.Message: {ex.Message},
                ex.StackTrace: {ex.StackTrace}");
            }
            return WorkflowExecutionStatus.Failed;
        }

        public override List<Exception> ValidateSettings()
        {
            List<Exception> exceptions = new List<Exception>();

            // Validation errors can be added
// to make sure certain mappings have been set
            //var settings = JArray.Parse(MappingsField)
            // as IEnumerable<dynamic>;
            //if (!settings.Any(x => x.alias.ToString().ToLower() == "name") ||
            // !settings.Any(x => x.alias.ToString().ToLower() == "email") ||
            // !settings.Any(x => x.alias.ToString().ToLower() == "message"))
            //{
            //    exceptions.Add(new Exception("Mappings for name,
            // email and message must be set."));
            //}

            return exceptions;
        }

        private string GetFormAlias(Record record, string alias)
        {
            // parse the mappings JSON string
            var settings = JArray.Parse(MappingsField) as IEnumerable<dynamic>;

            if (settings != null)
            {
                var mappedValue = settings
                .FirstOrDefault(x => x.alias.ToString().ToLower() ==
                alias.ToLower())?.value;

                if (mappedValue != null)
                {
                    // dynamic mappedValue is always a guid if it is not null
                    // get the field guid for the given alias and
// return the corresponding alias from the record
                    return record.GetRecordField((Guid)mappedValue).Alias;
                }
            }

            return string.Empty;
        }
    }
}

Error details



Comments