Mostly for Nathan:
Trying to build upon their work - your best buddy is going to be the CLRProfiler. Once you figure out how to use it, it's an awesome tool that tells you what's allocating the most garbage, when and where.
Once you've found trouble spots, there's a variety of things you can do. Using structs instead of classes is often what you want - structs are put on the stack; and an array of structs is just one object.
If there are moments in your game where pauses make sense, (when the user hits the pause button, for example - or in a turn-based game, the beginning of every turn, maybe?) - that's a good time to force a garbage collection.
Earlier I said we're using fixed pools. Not anymore. What we've done is, we've written our own SpecialNew and SpecialDelete functions for some of our problem classes. Delete doesn't delete; it just puts the object at the front of a linked list. Thus, the object never gets collected. New first checks that linked list to see if an old object is available, clears it out and hands it back, only actually calling the C# new if no object is available. Voila, no garbage collection, and no fixed-pool restrictions on number of objects allocated. It seems so simple once you've seen it, I don't know why I didn't think of it; it was one of our other programmers who showed me the light.
One problem with it (and fixed-pools) is suddenly you lose the advantage of a constructor that is guaranteed to set your object to a known initialized state. Just like with C++, if you forget to initialize a variable in your SpecialNew...then that object's behavior is undefined. We get around this with reflection. We don't actually reflect through every object when it's SpecialNewed to initialize everything...that would probably be too slow - what we do, is in our debug builds, we reflect through the object to make sure it matches a canonical initialized object that was newed the regular way, and fire an assertion if it doesn't. This has caught a handful of omissions.
One might ask: why use C# to make our game if one of the most useful features of C#, the garbage collection, shouldn't be used on the Xbox? Well, in C++, if you forget to delete an object when it goes out of scope, you've got a memory leak that will eventually crash your game. With C# on the Xbox, you do the same thing, you've got a memory "leak" that will eventually cause a garbage collection and make your game drop a frame and maybe have a noticeable hitch. I'll take the latter. It allows you to fix on an as-needed basis: you can play fast-and-loose with the heap for your early product development, and then fix your problem classes as needed.
As for delegates and closures: I love, love, love delegates and it's the main reason I like working with C#. If I found out I shouldn't use them on the Xbox I would go into a Black Hole of Despair. That said, you do have to massage them sometimes. A closure, obviously, will put an object on the heap every time it's created. So if you, say, create a closure and pass it to a function for every entity every frame, that's probably going to create too much garbage. Sigh. A not-so-obvious case is if you pass a member function to a delegate parameter, that member function gets...wrapped...somehow...(I really need to look deeper into it, there are some websites that discuss it in high detail)...and so it puts garbage on the heap also. Here you can protect yourself by creating a static delegate variable and assigning the member function to it...that way the wrap only happens once and never gets collected. Still, I'm happy to play fast-and-loose with delegates and closures and then fix them once I discover the problem cases.
Brief plug - I'll be speaking at the next GameFest about this stuff and more!