Sharepoint - Get the all sub sites from a site collection along with site hierarchy using CSOM

Here's a approach based of the Search which should be way faster and more ressource friendly.

It currently only groups subwebs after rootwebs, but could easily modified to also group the subwebs.

Result:

[ 22.04.2016 11:41:02 ] Infrastructure found by Search:
https://rootSPDomain.de
       https://rootSPDomain.de
       https://rootSPDomain.de/SubWeb1
       https://rootSPDomain.de/SubWeb1/SubWeb1
       https://rootSPDomain.de/SubWeb2
       https://rootSPDomain.de/SubWeb2/SubWeb1

Code:

    private static Dictionary<string, List<string>> GetAllWebsBySearch(ref int errorCount)
    {
        var webUrls = new Dictionary<string, List<string>>();

        try
        {
            using (var ctx = new ClientContext(Settings.Default.SearchCenterUrl))
            {
                var sites = SearchWebs(ctx, Settings.Default.SearchQuery + " contentclass:STS_Site");
                var webs = SearchWebs(ctx, Settings.Default.SearchQuery + " contentclass:STS_Web");

                if (sites.Count == 0)
                    Log.ThrowCritical(string.Format("No sites found for searchstring '{0}'",
                        Settings.Default.SearchQuery));

                foreach (var site in sites)
                    webUrls.Add(site, new List<string> { site });

                foreach (var web in webs)
                {
                    var parentSite = sites.Where(site => web.StartsWith(site)).OrderByDescending(s => s.Length).FirstOrDefault();
                    if (parentSite != null)
                    {
                        if (!webUrls[parentSite].Contains(web))
                            webUrls[parentSite].Add(web);
                    }
                    else
                    {
                        Log.Error(string.Format("Could not find site for web {0}", web));
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Log.Error(string.Format("Error while searching webs at {0}",
                Settings.Default.SearchCenterUrl));
            Log.Error(string.Format(CultureInfo.InvariantCulture, "{0}", ex.Message));
            errorCount++;
        }
        return webUrls;
    }

    private static List<string> SearchWebs(ClientContext ctx, string query)
    {
        var returnValue = new List<string>();
        var rowLimit = 500;
        var startRow = 0;
        var allResultsFetched = false;
        var fieldPath = "Path";

        while (!allResultsFetched)
        {
            Log.Verbose(string.Format(CultureInfo.InvariantCulture, "Search with query - {0}", query));
            var keywordQuery = new KeywordQuery(ctx)
            {
                QueryText = query,
                RowLimit = rowLimit
            };
            keywordQuery.SelectProperties.Clear();
            keywordQuery.SelectProperties.Add(fieldPath);
            keywordQuery.StartRow = startRow;
            keywordQuery.TrimDuplicates = false;

            var searchExecutor = new SearchExecutor(ctx);
            var results = searchExecutor.ExecuteQuery(keywordQuery);
            ctx.ExecuteQuery();

            foreach (var resultRow in results.Value[0].ResultRows)
            {
                var path = resultRow.Keys.Contains(fieldPath) && (resultRow[fieldPath] != null)
                    ? resultRow[fieldPath].ToString()
                    : string.Empty;
                returnValue.Add(path);
            }

            if (results.Value[0].RowCount < rowLimit)
                allResultsFetched = true;
            else
                startRow = startRow + rowLimit + 1;
        }
        return returnValue;
    }

    private static void LogInfrastructure(Dictionary<string, List<string>> webInfrastructure)
    {
        var message = "Infrastructure found by Search:\n";
        foreach (var site in webInfrastructure)
        {
            message += site.Key + "\n";
            foreach (var web in site.Value)
                message += "\t" + web + "\n";
            message += "\n";
        }

        Log.Message(message);
    }

Usage:

//get all web urls from the search
var webInfrastructure = GetAllWebsBySearch(ref errorCount);
LogInfrastructure(webInfrastructure);

Change allWebs into a list containing level+Web

Add a int level parameter to LoadAllWebs

In the first call pass 0 as level.
In the recursive call inside LoadAllWebs pass level+1


Per Jakobsen solution worked for me. Please find the below code for reference

     using (var clientContext = new ClientContext(destinationSiteUrl))
       {
         clientContext.Credentials = new SharePointOnlineCredentials(UserName, secure);
         DataTable dtsubsites = new DataTable();
         Web oWebsite = clientContext.Web;
         clientContext.Load(oWebsite.Webs);
         clientContext.ExecuteQuery();
         var listclass = new List<webdetails>();
         var allWebs = new List<Web>();
         var objallWebs = LoadAllWebs(clientContext.Site.RootWeb, listclass, 0);
         GridView1.DataSource = objallWebs.ToList();
         GridView1.DataBind();
       }

 public class webdetails
  {
     public string csomweb { get; set; }
     public Int32 webhirarchy { get; set; }
  }

 public List<webdetails> LoadAllWebs(Web web, List<webdetails> allWebs,int level)
        {
            var ctx = web.Context;
            ctx.Load(web);
            ctx.Load(web.Webs);
            ctx.ExecuteQuery();
            // allWebs.Add(web);
            allWebs.Add(new webdetails { csomweb = web.Title, webhirarchy = level });
            foreach (var subWeb in web.Webs)
            {
                LoadAllWebs(subWeb, allWebs, level+1);
            }
            return allWebs;
        }