The RemoveFile element is for this. You use this to teach MSI to delete application data that it did not install. The advantage is rollback, files will be returned to the place.
You can also use the RemoveFolder element to remove the entire directory. Typically, the concept is to * delete the file and also indicates the folder. This is not recursive, so you need to do this for any subdirectories that could be created as well.
Writing custom actions is just reinventing the wheel and increasing installer fragility. It should be used only if the subdirectories cannot be known in advance. In this situation, the ideal story is to use temporary lines in MSI to dynamically emit lines in MSI during installation and let MSI handle the actual deletion. This allows you to work with rollback.
Here is a really simple version of how it will look. This can be improved by passing data from the user table instead of constant rows for ComponentID and DirectoryID.
public class RecursiveDeleteCustomAction { [CustomAction] public static ActionResult RecursiveDeleteCosting(Session session) { // SOMECOMPONENTID is the Id attribute of a component in your install that you want to use to trigger this happening const string ComponentID = "SOMECOMPONENTID"; // SOMEDIRECTORYID would likely be INSTALLDIR or INSTALLLOCATION depending on your MSI const string DirectoryID = "SOMEDIRECTORYID"; var result = ActionResult.Success; int index = 1; try { string installLocation = session[DirectoryID]; session.Log("Directory to clean is {0}", installLocation); // Author rows for root directory // * means all files // null means the directory itself var fields = new object[] { "CLEANROOTFILES", ComponentID, "*", DirectoryID, 3 }; InsertRecord(session, "RemoveFile", fields); fields = new object[] { "CLEANROOTDIRECTORY", ComponentID, "", DirectoryID, 3 }; InsertRecord(session, "RemoveFile", fields); if( Directory.Exists(installLocation)) { foreach (string directory in Directory.GetDirectories(installLocation, "*", SearchOption.AllDirectories)) { session.Log("Processing Subdirectory {0}", directory); string key = string.Format("CLEANSUBFILES{0}", index); string key2 = string.Format("CLEANSUBDIRECTORY{0}", index); session[key] = directory; fields = new object[] { key, ComponentID, "*", key, 3 }; InsertRecord(session, "RemoveFile", fields); fields = new object[] { key2, ComponentID, "", key, 3 }; InsertRecord(session, "RemoveFile", fields); index++; } } } catch (Exception ex) { session.Log(ex.Message); result = ActionResult.Failure; } return result; } private static void InsertRecord(Session session, string tableName, Object[] objects) { Database db = session.Database; string sqlInsertSring = db.Tables[tableName].SqlInsertString + " TEMPORARY"; session.Log("SqlInsertString is {0}", sqlInsertSring); View view = db.OpenView(sqlInsertSring); view.Execute(new Record(objects)); view.Close(); } }