The ideal solution would be to track my patch using Git, but at the top-level
  (e.g. directly on main, not on foo). Theoretically, it would be possible to
  add a blob on the Git tree that points into the submodule location:
Even if this is doable, personally I find it very convoluted.  Unless this was
implemented natively and included user-facing commands that make it easy to
understand and manage, I'd stay away from it.
Alternative 1: Patch
Is there are more elegant/streamlined way of doing a patch in a git
  submodule, other than the accepted answer?
If you would like to avoid messing with the submodule at all, I would suggest
copying over/checking out the worktree somewhere else and using only that
during the build.  That way, the submodule is always "clean" (and perhaps
"immutable", from main's point of view) and you only have to worry about it
in the build directory.
Simplified build process example:
cd main
mkdir -p build
cp -R foo/ build/
cp myconfig.patch build/
cd build
patch <myconfig.patch
make
Note that this builds only foo, and that main's build process does not need
to be altered besides having to point to build/ instead of foo/.
If you do not intend on modifying foo itself/would rather keep it "pristine",
you could also turn it into a bare repository and use
GIT_WORK_TREE="$PWD/build" git checkout HEAD instead of cp, so that it is
only checked out during the build.  This is similar to how
makepkg(8) does it (at least in my experience with the AUR) in order
to avoid modifying the original sources ($source array vs $srcdir).  It
also splits source retrieval from the build itself (prepare() vs build()).
See also PKGBUILD(5) and Creating packages.
In your case, development and an IDE are also involved, so it might be trickier
if you want to inspect both the original and the build files at once.
Pros:
- Sources are separated from the build files
- maindoes not affect- foo
- Does not depend on git/makes it merely a build automation issue
- Only requires a patch file
Cons:
- Need to keep the patch file updated (vs rebasing changes)
- Need to change the build process
I'd go with this one if your patches are small and/or very specific to main.
P.S.: It is possible to go one step further and track foo's version
directly in the build process instead of using submodules if you wanted to:
Move foo one directory up, then in the build process:
cd build
GIT_DIR='../../foo/.git' git checkout "$myrev"
patch <myconfig.patch
make
Alternative 2: Separate Branch
Also, when I update foo to the latest version, I will have to cherry-pick the
  patch too which introduces a lot of noise in foo's history.
You don't really have to cherry-pick it, you could just keep the changes in
your branch instead and merge master every once in a while.
Personally, I would avoid this unless your changes are much more significant
than the noise caused by keeping it in sync (i.e.: the merges and conflicts).
I find merge commits to be very opaque, especially when conflicts are involved,
as unrelated/accidental changes are harder to detect.
Rebasing your commits onto master is also an option.
Pros:
- No need for a separate repository
- Keeps the worktree in the same place (no need to mess with your IDE)
Cons:
- Pollutes foo's repository with unrelated commits (when merging)
- Pollutes foo's repository with unrelated commit objects (when rebasing)
- Murky history of the evolution of your changes to config.h(when rebasing)
Alternative 3: Soft Fork
Also, when I update foo to the latest version, I will have to cherry-pick the
  patch too which introduces a lot of noise in foo's history.
Unfortunately, this is a very bad approach because I am making changes to foo
  that only matters to main
If you want to change foo to suit main, but not mess with foo upstream,
why not create a soft-fork of foo?  If you do not care too much about
foo-fork's history, you could just commit your changes on the main-project
branch and keep it in sync with foo's master through rebase:
Creating the fork:
cd foo
git remote add foo-fork 'https://foo-fork.com'
git branch main-project master
git push -u foo-fork main-project
Keeping it in sync:
git checkout main-project
git pull --rebase foo/master
# (resolve the conflicts, if any)
git push foo-fork
Pros:
- Easy to sync with upstream (e.g.: with pull --rebase)
- Keeps the worktree in the same place (no need to mess with your IDE)
Cons:
- Murky history of the evolution of your changes to config.h(because of
rebasing)
The added benefit of using a patch instead of rebasing is that you keep the
history of the patch.  But if you want to keep things really simple sync-wise,
I suppose that this is the way.
Alternative 4: Hard Fork
If you find that foo changes too much/too often and/or you need to patch too
many things, your best bet is probably creating a full fork and cherry-picking
their changes instead.