Aside from the semantics of the C# 3.0 code, there are a few key ideas to learn. These ideas are pretty easy.
Writing pure code and methods without side effects. A method implemented in a pure fashion doesn't affect any data outside of the method. If you pass a certain set of arguments to a pure method, you always get the same results.
One side note: there is a nomenclature issue when discussing functional programming in C#. When doing functional programming, you write lots of functions, hence the name. However, in C#, functions are called methods. Those terms are interchangeable in this tutorial.
Composability is one of the key concepts of functional transformations. If you write code that does not exhibit side effects, you gain composability. This means that anywhere in a pure functional transformation, you can refactor by writing a pure method, and calling it with impunity, knowing that your transformation will continue to operate properly.
Lazy evaluation, which is one of the features that allows LINQ to work in the real world. Lazy evaluation avoids the creation of large intermediate result sets. However, it has some pitfalls. For example, you can write a query that will eventually cause an InvalidCastException, but this exception isn't thrown when the code that contains the query is (apparently) executed. It is thrown when you iterate over the results of the query. In example programs, of course, this issue isn't too much of a problem, but when writing large systems, it could be problematic.
Use of lambda expressions to gain code readability. By now, probably, many developers are familiar with lambda expressions, which are inline anonymous methods written using a terse syntax.
Use of extension methods on generic interfaces. This allows us to write extension methods that operate on collections of a variety of constructed types. The prototypical example is the Where extension method that is implemented on IEnumerable<T>. It allows you to filter a collection, specifying the predicate as a lambda expression. This leads to very readable code.
Use of tuples to produce intermediate results and collections of intermediate results. I have a new favorite way to write code that produces collections of tupples, which is to use a named class defined using automatic properties instead of an anonymous type.
The differences between imperative code and declarative code. Imperative code is old-style procedural code that has observable side effects. Declarative code consists of a set of queries where the emphasis is on the actual transforms you are effecting, rather than an algorithm to implement the transform.
We'll explore each of these ideas in depth in this tutorial.
One of the most important areas of learning is the different approach to solving problems. When coding a complex transformation in a traditional imperative way, you often think in terms of algorithms. When coding a complex transformation in a declarative way, you think in terms of incremental transformations: collection X transforms to collection Y which then in turn transforms into collection Z. We'll explore this idea in detail in this tutorial. This idea could have gone into the list above, but it is very important, and I wanted to call it out specifically.
A question that some developers have posed is, "How does Language-Integrated Query (LINQ) figure into functional programming?" It is kind of a chicken and egg thing. LINQ is implemented using functional programming techniques, and to fully understand LINQ, you need to understand FP. In addition, when writing pure FP code, you often use queries, either in the form of Query Expressions, or using method syntax. This tutorial could be sub-titled, "Writing code in the pure functional programming style, using queries where appropriate." There is quite a bit more to FP than queries. It is a change in the basic approach to solving data transformation problems.
This tutorial is presented from a non-academic point of view. I want it to be accessible to both journeyman and experienced OOP developers. It is my intent to present the concepts in simple terms, and show their applicability in programs that you are writing right now.