Picture the scene. The Dataverse solution exports fine from DEV, sails through managed export, unpack and pack, and then face-plants the moment you try to import it on UAT with something like this:
Import of app module(s) failed: Invalid solution identified. There are more than one component with the same id added to the solution.
Component Name: AppSetting, Key:
parentappmoduleid:00000000-0000-0000-0000-000000000000
settingdefinitionid:7c1e5a92-6d38-4b0f-9a2e-1f8c4d6b93e0
More than one component with the same id. Great. Where did those come from? I certainly never added them. Off to the Solutions editor to find and remove the offenders — except they’re nowhere in the UI. Invisible. As far as the editor is concerned, they don’t exist, which is a bold position to take about something the import validator is very sure exists in triplicate.
After some digging, the culprit turned out to be the preferred solution setting. In my case the rogue components were a pair of AppSetting records with the same name, which Dataverse had silently created when I set the update recurrence interval on a Dynamics App. Because I’d marked one of my solutions as my preferred solution — a per-user setting — Power Platform quietly filed those new components into it. I never asked for them and never saw them added. Very considerate. Also the direct cause of the import blowing up.
It’s not a one-off, either. On a different project we configured an Azure Synapse Link for Dataverse on DEV, and later set it up by hand on UAT and Prod. That quietly seeded solution components too — a datalakefolder and a synapsedatabase — into the DEV solution. We only caught it because the solution gets exported and unpacked into source control, so the surprise components showed up in a diff before they could ruin a managed deployment to UAT. Lesson very much reinforced: unpack your solutions into version control.
The good news: there’s a perfectly civilised way to get these ghost components out that doesn’t involve unmanaged panic or rebuilding the whole solution. The bad news: it involves poking the Dataverse Web API by hand and pasting a fetch() into your browser console like it’s 2009 and we’re all still very excited about AJAX. Let’s go.
(A quick note: the URLs, GUIDs and solution names below are made-up examples. Substitute your own environment’s values. Also: this is a “know what you’re doing” operation — you’re removing something from a solution via the API, so make sure it’s the thing you actually mean to remove before you hit Enter.)
Step 1: Identify the solution
First we need two things about the solution: its GUID and its UniqueName.
- Go to make.powerapps.com, pick the correct environment — the one where the solution is still unmanaged, which is typically your DEV environment — and open Solutions. Click into the solution you care about, then look at the address bar. The bit right after
solutions/is your solution GUID, e.g.3f2a91c7-8b04-4e6d-a1f9-2c7d5e8b04a1. - Now go to your DEV environment’s actual application URL, something like
https://your-env.crm4.dynamics.com/main.aspx?appid=<some-app-guid>, and wait for it to fully load. This isn’t busywork — it’s how you make sure you’re logged in and have a live session cookie for this environment. Everything below rides on that cookie. - Keep the hostname, ditch the rest of the path, and append
/api/data/v9.2/solutions(<SOLUTION_GUID>). So you’d end up at:https://your-env.crm4.dynamics.com/api/data/v9.2/solutions(3f2a91c7-8b04-4e6d-a1f9-2c7d5e8b04a1)
Your browser will show you the raw solution record. Sanity-check that it’s the solution you think it is (turn on your browser’s “pretty print” for JSON if it looks like alphabet soup). - Copy the
uniquenamevalue out of that record.
You now have the solution GUID and UniqueName. Two down.
Step 2: Identify the component
Next we need the component’s own record GUID (its objectid) and its ComponentType (a magic number Dataverse uses to know what kind of thing it is).
- Figure out the component’s record GUID. Say we’re chasing an App Setting, which lives in the
appsettingstable — its record GUID isappsettingid. If the UI is being difficult (which is the whole reason we’re here), you can find the record via the Web API directly, or reach for a tool like XrmToolBox‘s FetchXML Builder. Copy that record GUID. - Now confirm the thing is actually a component of this solution. Query the relationship:
/api/data/v9.2/solutions(<SOLUTION_GUID>)/solution_solutioncomponent?$select=componenttype,objectid&$filter=objectid eq <OBJECT_GUID>
Tacked onto our example hostname:https://your-env.crm4.dynamics.com/api/data/v9.2/solutions(3f2a91c7-8b04-4e6d-a1f9-2c7d5e8b04a1)/solution_solutioncomponent?$select=componenttype,objectid&$filter=objectid eq b5d8e37a-4f21-4c9b-8e6d-9a3f7c2b1d54
If you get back a JSON array ([ ]) with exactly one object ({ }) inside it, you’ve got the right component in the right solution. Zero objects means it’s not part of this solution; more than one means you’ve filtered wrong. - Copy the
componenttypevalue. It’ll be a number.
You now have the object GUID and the ComponentType. Everything we need is on the table.
Step 3: Remove the component
Here’s the part that feels slightly illegal but isn’t. Dataverse exposes a RemoveSolutionComponent action, and we’re just going to call it.
- Make sure you’ve got the solution UniqueName (from Step 1) and the object GUID + ComponentType (from Step 2) handy.
- In the same browser window where you’re logged in — the one with the live session cookie, remember — open the Developer Tools (
F12) and switch to the Console tab. Reusing the tab where you already have an API call open is perfect. - Paste in the following, swapping in your three values. Note that
<OBJECT_COMPONENTTYPE>is a number, so it does not get quotes:
fetch("/api/data/v9.2/RemoveSolutionComponent", {
method: "POST",
body: JSON.stringify({
"SolutionComponent": {
"@odata.type": "Microsoft.Dynamics.CRM.solutioncomponent",
"solutioncomponentid": "<OBJECT_GUID>"
},
"ComponentType": <OBJECT_COMPONENTTYPE>,
"SolutionUniqueName": "<SOLUTION_UNIQUENAME>"
}),
headers: {
"Content-Type": "application/json"
}
});
- Hit
Enter. The command runs immediately — there’s no “are you sure?”, so this is your are-you-sure moment. - Pop over to the Network tab and check the request came back with a status code of
200 OK. That’s Dataverse’s way of telling you the component is gone.
And that’s it. The component is out of the solution, the UI can stop gaslighting you about it, and you can go back to whatever you were actually supposed to be doing today.
A word on the “preferred solution” thing
If you’d rather not do this dance again, it’s worth remembering why the components landed there in the first place. The preferred solution is a convenience feature — new components get auto-added to it — but “convenient” and “what I wanted” aren’t always the same sentence, and the auto-adding happens silently and invisibly in the UI, which is the worst possible combination.
Here’s the annoying bit: as far as I can tell there’s no way to actually unset the preferred solution once one is chosen. You can only point it at a different solution. So my workaround was to create a throwaway dummy solution on DEV and set that as the preferred one. Now when Dataverse feels the urge to helpfully stash a component somewhere, it dumps it into the sacrificial solution I never ship, and my real solutions stay clean. It’s a bit of a hack, but it’s a lot cheaper than debugging another mystery import failure at the worst possible moment.
Right. Off to add actual business value instead of hunting ghosts.