LINQ (Language Integrated Query)
LINQ (Language Integrated Query) is uniform query syntax in C# to retrieve data from different sources and formats. It is integrated in C#, thereby eliminating the mismatch between programming languages and databases, as well as providing a single querying interface for different types of data sources. For example, SQL is a Structured Query Language used to save and retrieve data from a database. In the same way, LINQ is a structured query syntax built in C# to retrieve data from different types of data sources such as collections, ADO.Net DataSet, XML Docs, web service and MS SQL Server and other databases.
LINQ queries return results as objects. It enables you to uses object-oriented approach on the result set and not to worry about transforming different formats of results into objects.
The following example demonstrates a simple LINQ query that gets all strings from an array which contains a
.
1namespace ArgomentiAvanzati
2{
3 internal class Program
4 {
5 static void Main()
6 {
7 // Data source
8 string[] names = { "Bill", "Steve", "James", "Johan" };
9
10 // LINQ Query
11 var myLinqQuery = from name in names
12 where name.Contains('a')
13 select name;
14 // Query execution
15 foreach (var name in myLinqQuery)
16 Console.Write(name + " ");
17 }
18 }
19}
In the above example, string array names is a data source. The following is a LINQ query which is assigned to a variable myLinqQuery.
1from name in names
2where name.Contains('a')
3select name;
The above query uses query syntax of LINQ. You will learn more about it in the Query Syntax chapter. You will not get the result of a LINQ query until you execute it. LINQ query can be execute in multiple ways, here we used foreach loop to execute our query stored in myLinqQuery. The foreach loop executes the query on the data source and get the result and then iterates over the result set. Thus, every LINQ query must query to some kind of data sources whether it can be array, collections, XML or other databases. After writing LINQ query, it must be executed to get the result.
To understand why we should use LINQ, let’s look at some examples. Suppose you want to find list of teenage students from an array of Student objects. Before C# 2.0, we had to use a ‘foreach’ or a ‘for’ loop to traverse the collection to find a particular object. For example, we had to write the following code to find all Student objects from an array of Students where the age is between 12 and 20 (for teenage 13 to 19):
1namespace LINQDemo
2{
3 class Student
4 {
5 public int StudentID { get; set; }
6 public string? StudentName { get; set; }
7 public int Age { get; set; }
8
9 public override string ToString()
10 {
11 return string.Format($"[StudentID = {StudentID}, StudentName = {StudentName}, Age = {Age}]");
12 }
13 }
14 internal class Program
15 {
16 static void Main(string[] args)
17 {
18 Student[] studentArray =
19 {
20 new () { StudentID = 1, StudentName = "John", Age = 18},
21 new () { StudentID = 2, StudentName = "Steve", Age = 21},
22 new () { StudentID = 3, StudentName = "Bill", Age = 25},
23 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
24 new () { StudentID = 5, StudentName = "Ron" , Age = 31},
25 new () { StudentID = 6, StudentName = "Chris", Age = 17},
26 new () { StudentID = 7, StudentName = "Rob", Age = 19},
27 };
28 List<Student> students = [];
29 foreach (Student std in studentArray)
30 {
31 if (std.Age > 12 && std.Age < 20)
32 {
33 students.Add(std);
34 }
35 }
36 //write result
37 foreach (var studente in students)
38 {
39 Console.WriteLine(studente);
40 }
41 Console.ReadLine();
42 }
43
44 }
45}
Use of for loop is cumbersome, not maintainable and readable. C# 2.0 introduced delegate, which can be used to handle this kind of a scenario, as shown below.
1namespace LINQDemo2
2{
3 delegate bool FindStudent(Student std);
4 class StudentExtension
5 {
6 public static List<Student> Where(Student[] stdArray, FindStudent del)
7 {
8 List<Student> resultList = [];
9 foreach (Student std in stdArray)
10 if (del(std))
11 {
12 resultList.Add(std);
13 }
14 return resultList;
15 }
16 }
17 class Student
18 {
19 public int StudentID { get; set; }
20 public string? StudentName { get; set; }
21 public int Age { get; set; }
22
23 public override string ToString()
24 {
25 return string.Format($"[StudentID = {StudentID}, StudentName = {StudentName}, Age = {Age}]");
26 }
27 }
28
29 internal class Program
30 {
31 static void Main(string[] args)
32 {
33 Student[] studentArray =
34 [
35 new () { StudentID = 1, StudentName = "John", Age = 18},
36 new () { StudentID = 2, StudentName = "Steve", Age = 21},
37 new () { StudentID = 3, StudentName = "Bill", Age = 25},
38 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
39 new () { StudentID = 5, StudentName = "Ron" , Age = 31},
40 new () { StudentID = 6, StudentName = "Chris", Age = 17},
41 new () { StudentID = 7, StudentName = "Rob", Age = 19},
42 ];
43
44 //List<Student> students = StudentExtension.Where(studentArray, delegate (Student std)
45 //{
46 // return std.Age > 12 && std.Age < 20;
47 //});
48 //in alternativa al delegate si può usare una lambda
49 List<Student> students = StudentExtension.Where(studentArray,
50 std => std.Age > 12 && std.Age < 20);
51 //write result
52 foreach (var studente in students)
53 {
54 Console.WriteLine(studente);
55 }
56 Console.ReadLine();
57 }
58
59 }
60}
61
So, with C# 2.0, you got the advantage of delegate in finding students with any criteria. You don’t have to use a for loop to find students using different criteria. For example, you can use the same delegate function to find a student whose StudentId is 5 or whose name is Bill, as below:
1List<Student> students = StudentExtension.where(studentArray, delegate(Student std) {
2 return std.StudentID == 5;
3 });
4
5//Also, use another criteria using same delegate
6List<Student> students = StudentExtension.where(studentArray, delegate(Student std) {
7 return std.StudentName == "Bill";
8 });
The C# team felt that they still needed to make the code even more compact and readable. So they introduced the extension method, lambda expression, expression tree, anonymous type and query expression in C# 3.0. You can use these features of C# 3.0, which are building blocks of LINQ to query to the different types of collection and get the resulted element(s) in a single statement.
1
2namespace LINQDemo3
3{
4 class Student
5 {
6 public int StudentID { get; set; }
7 public string? StudentName { get; set; }
8 public int Age { get; set; }
9
10 public override string ToString()
11 {
12 return string.Format($"[StudentID = {StudentID}, StudentName = {StudentName}, Age = {Age}]");
13 }
14 }
15
16 class Program
17 {
18 static void Main(string[] args)
19 {
20 Student[] studentArray =
21 {
22 new () { StudentID = 1, StudentName = "John", Age = 18},
23 new () { StudentID = 2, StudentName = "Steve", Age = 21},
24 new () { StudentID = 3, StudentName = "Bill", Age = 25},
25 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
26 new () { StudentID = 5, StudentName = "Ron" , Age = 31},
27 new () { StudentID = 6, StudentName = "Chris", Age = 17},
28 new () { StudentID = 7, StudentName = "Rob", Age = 19},
29 };
30
31 // Use LINQ to find teenager students
32 Student[] teenAgerStudents = studentArray.Where(s => s.Age > 12 && s.Age < 20).ToArray();
33
34 // Use LINQ to find first student whose name is Bill
35 Student? bill = studentArray.Where(s => s.StudentName == "Bill").FirstOrDefault();
36 //if (bill != null)
37 //{
38 // Console.WriteLine(bill);
39 //}
40 // Use LINQ to find student whose StudentID is 5
41 Student? student5 = studentArray.Where(s => s.StudentID == 5).FirstOrDefault();
42
43 //write result
44 foreach (var studente in teenAgerStudents)
45 {
46 Console.WriteLine(studente);
47 }
48 Console.WriteLine("bill: " + bill);
49 Console.WriteLine("student5: " + student5);
50 }
51 }
52}
As you can see in the above example, we specify different criteria using LINQ operator and lambda expression in a single statement. Thus, LINQ makes code more compact and readable and it can also be used to query different data sources. For example, if you have a student table in a database instead of an array of student objects as above, you can still use the same query to find students using the Entity Framework.
• Familiar language: Developers don’t have to learn a new query language for each type of data source or data format.
• Less coding: It reduces the amount of code to be written as compared with a more traditional approach.
• Readable code: LINQ makes the code more readable so other developers can easily understand and maintain it.
• Standardized way of querying multiple data sources: The same LINQ syntax can be used to query multiple data sources.
• Compile time safety of queries: It provides type checking of objects at compile time.
• IntelliSense Support: LINQ provides IntelliSense for generic collections.
• Shaping data: You can retrieve data in different shapes.
We can write LINQ queries for the classes that implement IEnumerable<T>
or IQueryable<T>
interface.
The System.Linq.Enumerable class includes extension methods for the classes that implement IEnumerable<T>
interface, for example all the built-in collection classes implement IEnumerable<T>
interface and so we can write LINQ queries to retrieve data from the built-in collections.
The System.Linq.Queryable class includes extension methods for classes that implement IQueryable<t>
interface. The IQueryable<T>
interface is used to provide querying capabilities against a specific data source where the type of the data is known. For example, Entity Framework api implements IQueryable<T>
interface to support LINQ queries with underlying databases such as MS SQL Server.
Also, there are APIs available to access third party data; for example, LINQ to Amazon provides the ability to use LINQ with Amazon web services to search for books and other items. This can be achieved by implementing the IQueryable interface for Amazon.
The following figure shows the extension methods available in the Queryable class can be used with various native or third party data providers.
Ci sono due modi per usare LINQ: mediante l’uso delle “LINQ query” oppure mediante l’uso dei “LINQ methods”.
Query syntax is similar to SQL (Structured Query Language) for the database. It is defined within the C#.
LINQ Query Syntax:
1from <range variable> in <IEnumerable<T> or IQueryable<T> Collection>
2
3<Standard Query Operators> <lambda expression>
4
5<select or groupBy operator> <result formation>
The LINQ query syntax starts with from keyword and ends with select keyword. The following is a sample LINQ query that returns a collection of strings which contains a word “Tutorials”.
1namespace ArgomentiAvanzati
2{
3 internal class Program
4 {
5 static void Main(string[] args)
6 {
7 // string collection
8 IList<string> stringList = new List<string>()
9 {
10 "C# Tutorials",
11 "VB.NET Tutorials",
12 "Learn C++",
13 "MVC Tutorials" ,
14 "Java"
15 };
16 // LINQ Query Syntax
17 var result = from s in stringList
18 where s.Contains("Tutorials")
19 select s;
20
21 foreach (var str in result)
22 {
23 Console.WriteLine(str);
24 }
25 }
26 }
27}
Query syntax starts with a from
clause followed by a Range variable. The from
clause is structured like “From rangeVariableName in IEnumerableCollection”. In English, this means, from each object in the collection. It is similar to a foreach loop: foreach(Student s in studentList)
.
After the From clause, you can use different Standard Query Operators to filter, group, join elements of the collection. There are around 50 Standard Query Operators available in LINQ. In the above figure, we have used where
operator (aka clause) followed by a condition. This condition is generally expressed using lambda expression.
LINQ query syntax always ends with a select
or group
clause. The select
clause is used to shape the data. You can select the whole object as it is or only some properties of it. In the above example, we selected the each resulted string elements.
In the following example, we use LINQ query syntax to find out teenager students from the Student collection (sequence).
1namespace ArgomentiAvanzati
2{
3 public class Student
4 {
5 public int StudentID { get; set; }
6 public string? StudentName { get; set; }
7 public int Age { get; set; }
8 }
9 internal class Program
10 {
11 static void Main(string[] args)
12 {
13 // Student collection
14 IList<Student> studentList = new List<Student>()
15 {
16 new () { StudentID = 1, StudentName = "John", Age = 13},
17 new () { StudentID = 2, StudentName = "Mario", Age = 21},
18 new () { StudentID = 3, StudentName = "Bill", Age = 18},
19 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
20 new () { StudentID = 5, StudentName = "Ron" , Age = 15}
21 };
22
23 // LINQ Query Syntax to find out teenager students
24 var teenAgerStudent = from s in studentList
25 where s.Age > 12 && s.Age < 20
26 select s;
27 Console.WriteLine("Teen age Students:");
28
29 foreach (Student std in teenAgerStudent)
30 {
31 Console.WriteLine(std.StudentName);
32 }
33 }
34 }
35}
Method syntax (also known as fluent syntax) uses extension methods included in the Enumerable or Queryable static class, similar to how you would call the extension method of any class. The following is a sample LINQ method syntax query that returns a collection of strings which contains a word “Tutorials”.
1// string collection
2IList<string> stringList = new List<string>()
3{
4 "C# Tutorials",
5 "VB.NET Tutorials",
6 "Learn C++",
7 "MVC Tutorials" ,
8 "Java"
9};
10
11// LINQ Method Syntax
12var result = stringList.Where(s => s.Contains("Tutorials"));
The extension method Where() is defined in the Enumerable class. If you check the signature of the Where extension method, you will find the Where method accepts a predicate delegate as Func<Student, bool>
. This means you can pass any delegate function that accepts a Student object as an input parameter and returns a Boolean value as shown in the below figure. The lambda expression works as a delegate passed in the Where clause.
The following example shows how to use LINQ method syntax query with the IEnumerable<T>
collection.
1// Student collection
2IList<Student> studentList = new List<Student>()
3 {
4 new () { StudentID = 1, StudentName = "John", Age = 13},
5 new () { StudentID = 2, StudentName = "Mario", Age = 21},
6 new () { StudentID = 3, StudentName = "Bill", Age = 18},
7 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
8 new () { StudentID = 5, StudentName = "Ron" , Age = 15}
9 };
10
11// LINQ Method Syntax to find out teenager students
12var teenAgerStudents = studentList.Where(s => s.Age > 12 && s.Age < 20)
13 .ToList<Student>();
Altro esempio:
1namespace ArgomentiAvanzati
2{
3 public class Program
4 {
5 public static void Main()
6 {
7 // Student collection
8 IList<Student> studentList = new List<Student>()
9 {
10 new () { StudentID = 1, StudentName = "John", Age = 13},
11 new () { StudentID = 2, StudentName = "Mario", Age = 21},
12 new () { StudentID = 3, StudentName = "Bill", Age = 18},
13 new () { StudentID = 4, StudentName = "Ram" , Age = 20},
14 new () { StudentID = 5, StudentName = "Ron" , Age = 15}
15 };
16
17 Func<Student, bool> isStudentTeenAger = s => s.Age > 12 && s.Age < 20;
18 var teenAgerStudent = studentList.Where(isStudentTeenAger);
19
20 Console.WriteLine("Teen age Students:");
21
22 foreach (Student std in teenAgerStudent)
23 {
24 Console.WriteLine(std.StudentName);
25 }
26
27 //oppure
28 var teenAgerStudents = from s in studentList
29 where isStudentTeenAger(s)
30 select s;
31
32 Console.WriteLine("Teen age Students:");
33 foreach (Student std in teenAgerStudents)
34 {
35 Console.WriteLine(std.StudentName);
36 }
37 }
38 }
39
40 public class Student
41 {
42 public int StudentID { get; set; }
43 public string? StudentName { get; set; }
44 public int Age { get; set; }
45
46 }
47}
Standard Query Operators in LINQ are actually extension methods for the IEnumerable<T>
and IQueryable<T>
types. They are defined in the System.Linq.Enumerable
and System.Linq.Queryable
classes. There are over 50 standard query operators available in LINQ that provide different functionalities like filtering, sorting, grouping, aggregation, concatenation, etc.
Standard Query Operators in Query Syntax
:
1var teenAgerStudents = from s in studentList
2 where s.Age > 12 && s.Age < 20
3 select s;
Standard Query Operators in Method Syntax (Fluent)
:
1var teenAgerStudents = studentList.Where(s => s.Age > 12 && s.Age < 20)
2 .ToList<Student>();
Standard Query Operators can be classified based on the functionality they provide. The following table lists all the classification of Standard Query Operators:
Classification | Standard Query Operators |
---|---|
Filtering | Where, OfType |
Sorting | OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse |
Grouping | GroupBy, ToLookup |
Join | GroupJoin, Join |
Projection | Select, SelectMany |
Aggregation | Aggregate, Average, Count, LongCount, Max, Min, Sum |
Quantifiers | All, Any, Contains |
Elements | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault |
Set | Distinct, Except, Intersect, Union |
Partitioning | Skip, SkipWhile, Take, TakeWhile |
Concatenation | Concat |
Equality | SequenceEqual |
Generation | DefaultEmpty, Empty, Range, Repeat |
Conversion | AsEnumerable, AsQueryable, Cast, ToArray, ToDictionary, ToList |
Gli esempi di tutte le funzioni presenti nella tabella precedente sono reperibili qui
var
can be used to hold the result of the LINQ query.ID
; in altri termini il campo indicato come ID
, o Id
, oppure come NomeClasseID
, o NomeClasseId
, dovrà essere considerato univoco all’interno della collection. Questo campo è ciò che si chiama chiave primaria per la collection o base di dati considerata. 1using System.Collections;
2
3namespace LINQGym
4{
5 class Student
6 {
7 public int StudentID { get; set; }
8 public string? StudentName { get; set; }
9 public int Age { get; set; }
10 public double MediaVoti { get; set; }
11
12 public override string ToString()
13 {
14 return string.Format($"[StudentID = {StudentID}, StudentName = {StudentName}, Age = {Age}, MediaVoti = {MediaVoti}]");
15 }
16 }
17
18 class Assenza
19 {
20 public int ID { get; set; }
21 public DateTime Giorno { get; set; }
22 public int StudentID { get; set; }
23 }
24
25 class Persona
26 {
27 public string? Nome { get; set; }
28 public int Eta { get; set; }
29
30 public override string ToString()
31 {
32 return string.Format($"[Nome = {Nome}, Età = {Eta}]");
33 }
34 }
35 internal class Program
36 {
37 //stiamo definendo un tipo di puntatore a funzione
38 delegate bool CondizioneRicerca(Student s);
39
40 public static void AzioneSuElemento(Student s)
41 {
42 Console.WriteLine(s);
43 }
44
45 //metodo statico
46 public static bool VerificaCondizione(Student s)
47 {
48 return s.Age >= 18 && s.Age <= 25;
49 }
50 static void Main(string[] args)
51 {
52 //condizione: non devono esistere due studenti con lo stesso StudentID
53 //in questo caso si dice che StudentID è chiave primaria della collection
54 Student[] studentArray1 =
55 {
56 new () { StudentID = 1, StudentName = "John", Age = 18 , MediaVoti= 6.5},
57 new () { StudentID = 2, StudentName = "Steve", Age = 21 , MediaVoti= 8},
58 new () { StudentID = 3, StudentName = "Bill", Age = 25, MediaVoti= 7.4},
59 new () { StudentID = 4, StudentName = "Ram" , Age = 20, MediaVoti = 10},
60 new () { StudentID = 5, StudentName = "Ron" , Age = 31, MediaVoti = 9},
61 new () { StudentID = 6, StudentName = "Chris", Age = 17, MediaVoti = 8.4},
62 new () { StudentID = 7, StudentName = "Rob",Age = 19 , MediaVoti = 7.7},
63 new () { StudentID = 8, StudentName = "Robert",Age = 22, MediaVoti = 8.1},
64 new () { StudentID = 11, StudentName = "John", Age = 21 , MediaVoti = 8.5},
65 new () { StudentID = 12, StudentName = "Bill", Age = 25, MediaVoti = 7},
66 new () { StudentID = 13, StudentName = "Ram" , Age = 20, MediaVoti = 9 },
67 new () { StudentID = 14, StudentName = "Ron" , Age = 31, MediaVoti = 9.5},
68 new () { StudentID = 15, StudentName = "Chris", Age = 17, MediaVoti = 8},
69 new () { StudentID = 16, StudentName = "Rob2",Age = 19 , MediaVoti = 7},
70 new () { StudentID = 17, StudentName = "Robert2",Age = 22, MediaVoti = 8},
71 new () { StudentID = 18, StudentName = "Alexander2",Age = 18, MediaVoti = 9},
72 };
73
74 Student[] studentResultArray;
75 List<Student> studentResultList;
76
77 //definiamo delle condizioni di ricerca
78 //primo modo: uso di Func con lambda
79 Func<Student, bool> condizioneDiRicerca = s => s.Age >= 18 && s.Age <= 25;
80
81 //secondo modo: uso di un delegato implementato attraverso lambda
82 CondizioneRicerca condizioneDiRicerca2 = s => s.Age >= 18 && s.Age <= 25;
83 //terzo modo: uso di un delegato che punta a un metodo precedentemente definito
84 CondizioneRicerca condizioneDiRicerca3 = VerificaCondizione;
85 //quarto modo: usiamo direttamente la lambda - il più comodo
86
87 //creiamo una lista con gli stessi oggetti presenti nell'array
88 List<Student> studentList1 = [.. studentArray1];//equivalente a invocare studentArray1.ToList();
89 //studiamo la clausola Where
90 //trovare tutti gli studenti che hanno età compresa tra 18 e 25 anni, caso dell'array
91 studentResultArray = studentArray1.Where(s => s.Age >= 18 && s.Age <= 25).ToArray();
92 studentResultList = studentArray1.Where(s => s.Age >= 18 && s.Age <= 25).ToList();
93 //verifichiamo che il risultato sia corretto con una stampa
94 foreach (Student student in studentResultList)
95 {
96 Console.WriteLine(student);
97 }
98
99 //processing sull'array risultato
100 Console.WriteLine("\nprocessing sull'array risultato");
101 foreach (Student student in studentResultArray)
102 {
103 Console.WriteLine(student);
104 }
105 //processing degli elementi di un array usando il LINQ
106 Console.WriteLine("\nEsempio di Action su array");
107 Array.ForEach(studentResultArray, AzioneSuElemento);
108 //Stampa media voti
109 Console.WriteLine("\nStampa età e media voti");
110 Array.ForEach(studentResultArray, s =>
111 {
112 Console.Write(s.StudentName+ " ");
113 Console.Write(s.Age + " ");
114 Console.WriteLine(s.MediaVoti);
115 });
116
117 //processing degli elementi di una lista usando il LINQ
118 Console.WriteLine("\nProcessing degli elementi di una lista usando il LINQ");
119 studentResultList.ForEach(s =>
120 {
121 Console.Write(s.StudentName + " ");
122 Console.Write(s.Age + " ");
123 Console.WriteLine(s.MediaVoti);
124 });
125
126 //USO DELLA VERSIONE CON INDICE DELLA WHERE
127
128 //il metodo Where ha anche una versione con l'indice della collection
129 //in questo esempio prendiamo solo quelli che verificano la condizione sull'età e hanno indice pari
130 Console.WriteLine("selezioniamo solo quelli che verificano la condizione e hanno indice pari");
131
132 studentResultArray = studentArray1.Where(
133 (s, i) => (s.Age >= 18 && s.Age <= 25) && i % 2 == 0).ToArray();
134 Console.WriteLine("stampa su array");
135 Array.ForEach(studentResultArray, s => Console.WriteLine(s.StudentName + " age = " + s.Age));
136
137 studentResultList = studentList1.Where(
138 (s, i) => (s.Age >= 18 && s.Age <= 25) && i % 2 == 0).ToList();
139 Console.WriteLine("stampa su list");
140 studentResultList.
141 ForEach(s => Console.WriteLine(s.StudentName + " age = " + s.Age));
142
143 // E' possibile anche far applicare più volte la where per ottenere filtraggi multipli
144 Console.WriteLine("doppia where: quelli che verificano la condizione e che hanno ID>3");
145 studentResultList = studentList1.
146 Where(s => s.Age >= 18 && s.Age <= 25).
147 Where(s => s.StudentID > 3).ToList();
148 studentResultList.
149 ForEach(s => Console.WriteLine(s.StudentName + " age = " + s.Age + " ID = " + s.StudentID));
150
151 //Studiamo la clausola OfType
152 //nel caso di collection di tipo diverso è possibile trarre vantaggio dal metodo OfType
153 IList mixedList = new ArrayList
154 {
155 5,
156 "numero uno",
157 true,
158 "numero due",
159 new Student() { StudentID = 10, Age = 30, StudentName = "Roberto" }
160 };
161 List<string> mixedListResult = mixedList
162 .OfType<string>()
163 .ToList();
164 //IList mixedListResult2 =
165 // (from s in mixedList.OfType<Student>()
166 // where s.Age > 20
167 // select s).
168 // ToList();
169 List<Student> mixedListResult2 =mixedList
170 .OfType<Student>()
171 .Where(s => s.Age > 20)
172 .ToList();
173 Console.WriteLine("\nStampa del risultato con OfType method");
174 mixedListResult.ForEach(Console.WriteLine);
175 mixedListResult2.ForEach(Console.WriteLine);
176
177 //Studiamo la clausola OrderBy
178
179 //ordiniamo una lista di elementi
180 Console.WriteLine("\nOrdiniamo gli elementi di una lista con la clausola OrderBy");
181 Console.WriteLine("\nOrdiniamo in base all'età - LINQ method");
182 //per ordinare in ordine decrescente esiste OrderByDescending
183 //https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.orderbydescending
184 studentResultArray = [.. studentArray1.OrderBy(s => s.Age)];
185 Console.WriteLine("stampa su array");
186 Array.ForEach(studentResultArray, s => Console.WriteLine(s.StudentName + " age = " + s.Age));
187
188 studentResultList = [.. studentList1.OrderBy(s => s.Age)];
189 Console.WriteLine("\nstampa su list");
190 studentResultList.
191 ForEach(s => Console.WriteLine(s.StudentName + " age = " + s.Age));
192 //su una sola pipe
193 Console.WriteLine("Elaborazione di ordinamento e stampa su una sola pipe di codice");
194 studentList1
195 .OrderBy(s => s.Age)
196 .ToList()
197 .ForEach(s => Console.WriteLine(s.StudentName + " age = " + s.Age));
198
199 //ordinamenti multipli
200 Console.WriteLine("\nOrdinamenti multipli - LINQ method");
201 studentResultArray = [.. studentArray1
202 .OrderBy(s => s.Age)
203 .ThenBy(s => s.StudentName)];
204 Console.WriteLine("stampa su array");
205 Array.ForEach(studentResultArray, s => Console.WriteLine(s.StudentName + " age = " + s.Age));
206 //esiste anche la clausola ThenByDescending
207 //https://learn.microsoft.com/en-us/dotnet/api/system.linq.enumerable.thenbydescending
208 studentResultList = studentList1.OrderBy(s => s.Age).ThenBy(s => s.StudentName).ToList();
209 Console.WriteLine("\nstampa su list");
210 studentResultList.
211 ForEach(s => Console.WriteLine(s.StudentName + " age = " + s.Age));
212
213 //Studiamo la clausola select - proiettiamo gli elementi della sequenza in una nuova forma
214 Console.WriteLine("\nClausola select - LINQ method");
215 Console.WriteLine("\nUso di tipi anonimi");
216 //uso di tipi anonimi
217 //un tipo anonimo è un tipo che non ha una classe da cui discende, ma è creato direttamente a partire dalle properties
218 Array.ForEach(studentArray1
219 .Select(s => new { Nome = s.StudentName, Eta = s.Age })
220 .ToArray(),Console.WriteLine);
221 Console.WriteLine("\nUso di tipi anonimi - stampa solo il nome");
222 studentList1
223 .Select(s => new { Nome = s.StudentName, Eta = s.Age })
224 .ToList()
225 .ForEach(p => Console.WriteLine(p.Nome));
226
227 Console.WriteLine("\nUso di tipi non anonimi - stampa solo il nome");
228 studentList1
229 .Select(s => new Persona() { Nome = s.StudentName, Eta = s.Age })
230 .ToList()
231 .ForEach(p => Console.WriteLine(p.Nome));
232 Console.WriteLine("Primo esempio di raggruppamento - raggruppiamo in base all'età");
233 //Group By
234 //IEnumerable<IGrouping< int, Student>>?
235 var groupedResult = studentList1.GroupBy(s => s.Age);
236 foreach (var group in groupedResult)
237 {
238 Console.WriteLine("Group key(Age) = {0}", group.Key);
239 foreach (var student in group)
240 {
241 Console.WriteLine("Student = {0}", student);
242 }
243 //calcoliamo una funzione di gruppo: min, max, avg, count
244 // funzione count: quanti studenti con la stessa età
245 Console.WriteLine("Numero studenti con la stessa età nel gruppo = {0}", group.Count());
246 Console.WriteLine("Valore medio dei voti = {0}", group.Average(s => s.MediaVoti));
247 Console.WriteLine("Voto massimo nel gruppo = {0}", group.Max(s => s.MediaVoti));
248 Console.WriteLine("Voto minimo nel gruppo = {0}", group.Min(s => s.MediaVoti));
249
250 //C# implementa anche il metodo ToLookup che fa la stessa cosa di GroupBy ma la
251 //differenza sta nel fatto che con grandi basi di dati ToLookup carica tutto il risultato in memoria
252 //GroupBy carica il risultato associato a una chiave quando serve
253 //https://stackoverflow.com/questions/10215428/why-are-tolookup-and-groupby-different
254 //https://stackoverflow.com/a/10215531
255 }
256 Console.WriteLine("STAMPA RAGGRUPPAMENTO PER NOME");
257 var groupedResult2 = studentList1.GroupBy(s => s.StudentName);
258 foreach (var group in groupedResult2)
259 {
260 Console.WriteLine("Chiave di raggruppamento (Nome) = " + group.Key);
261 foreach (var student in group)
262 {
263 Console.WriteLine(student);
264 }
265 Console.WriteLine("Numero studenti omonimi: " + group.Count());
266 Console.WriteLine("Voto medio degli omonimi: " + group.Average(s => s.MediaVoti));
267 }
268 //intersezione tra due collection - Join
269 //creiamo un elenco di assenze di studenti
270 List<Assenza> assenzeList1 =
271 [
272 new (){ID = 1, Giorno = DateTime.Today, StudentID = 1 },
273 new (){ID = 2, Giorno = DateTime.Today.AddDays(-1) ,StudentID = 1 },
274 new (){ID = 3, Giorno = DateTime.Today.AddDays(-3), StudentID = 1 },
275 new (){ID = 4, Giorno = new DateTime(2020,11,30), StudentID = 2 },
276 new (){ID = 5, Giorno = new DateTime(2020,11,8), StudentID = 3 }
277 ];
278 //vogliamo riportare il nome dello studente e le date delle sue assenze
279 //facciamo una join tra la lista degli studenti e la lista delle assenze degli studenti e poi facciamo la proiezione del risultato su un nuovo oggetto
280 var innerJoinStudentiAssenze = studentList1
281 .Join(assenzeList1,
282 s => s.StudentID,
283 a => a.StudentID,
284 (s, a) => new { ID = s.StudentID, Nome = s.StudentName, GiornoAssenza = a.Giorno });
285 foreach (var obj in innerJoinStudentiAssenze)
286 {
287 Console.WriteLine($"ID = {obj.ID}, Nome = {obj.Nome}, GiornoAssenza = {obj.GiornoAssenza.ToShortDateString()}");
288 }
289 }
290 }
291}
Creare i seguenti POCO:
Artista (Id, Nome, Cognome, Nazionalità)
Opera (Id, Titolo, Quotazione, FkArtistaId)
Personaggio (Id, Nome, FkOperaId)
Creare per ciascun POCO una collection (lista) di tali oggetti:
artisti
è una lista di oggetti di tipo Artista
;opere
è una lista di oggetti di tipo Opera
;personaggi
è una lista di oggetti di tipo Personaggio
;Fk
indicano una foreign key
ossia una una property che “punta” ad una property di un altro oggetto. Ad esempio, i valori di FkOperaId
devono corrispondere a valori dell’Id
nella collection delle opere
. In altri termini una foreign key è una property i cui valori devono corrispondere a valori già presenti nella collection a cui puntano.Effettuare le seguenti query:
Classe Artista
1namespace LinqAlMuseo;
2
3public class Artista
4{
5 public int Id { get; set; }
6 public string Nome { get; set; } = null!;
7 public string Cognome { get; set; } = null!;
8 public string? Nazionalita { get; set; }
9
10 public override string ToString()
11 {
12 return string.Format($"[ID = {Id}, Nome = {Nome}, Cognome = {Cognome}, Nazionalità = {Nazionalita}]"); ;
13 }
14
15}
Classe Opera
1namespace LinqAlMuseo;
2
3public class Opera
4{
5 public int Id { get; set; }
6 public string Titolo { get; set; } = null!;
7 public decimal Quotazione { get; set; }
8 public int FkArtista { get; set; }
9 public override string ToString()
10 {
11 return String.Format($"[ID = {Id}, Titolo = {Titolo}, Quotazione = {Quotazione}, FkArtista = {FkArtista}]"); ;
12 }
13}
14
Classe Personaggio
1namespace LinqAlMuseo;
2
3public class Personaggio
4{
5 public int Id { get; set; }
6 public string Nome { get; set; } = null!;
7 public int FkOperaId { get; set; }
8 public override string ToString()
9 {
10 return string.Format($"[ID = {Id}, Nome = {Nome}, FkOperaId = {FkOperaId}]"); ;
11 }
12}
13
1//file Program.cs - utilizzo di Top Level Statements
2//Esercizio: LINQ al museo
3using LinqAlMuseo;
4using System.Globalization;
5using System.Text;
6
7//creazione delle collection
8//si parte da quelle che non puntano a nulla, ossia quelle che non hanno chiavi esterne
9IList<Artista> artisti =
10 [
11 new (){Id=1, Cognome="Picasso", Nome="Pablo", Nazionalita="Spagna"},
12 new (){Id=2, Cognome="Dalì", Nome="Salvador", Nazionalita="Spagna"},
13 new (){Id=3, Cognome="De Chirico", Nome="Giorgio", Nazionalita="Italia"},
14 new (){Id=4, Cognome="Guttuso", Nome="Renato", Nazionalita="Italia"}
15 ];
16//poi le collection che hanno Fk
17IList<Opera> opere =
18 [
19 new (){Id=1, Titolo="Guernica", Quotazione=50000000.00m , FkArtista=1},//opera di Picasso
20 new (){Id=2, Titolo="I tre musici", Quotazione=15000000.00m, FkArtista=1},//opera di Picasso
21 new (){Id=3, Titolo="Les demoiselles d’Avignon", Quotazione=12000000.00m, FkArtista=1},//opera di Picasso
22 new (){Id=4, Titolo="La persistenza della memoria", Quotazione=16000000.00m, FkArtista=2},//opera di Dalì
23 new (){Id=5, Titolo="Metamorfosi di Narciso", Quotazione=8000000.00m, FkArtista=2},//opera di Dalì
24 new (){Id=6, Titolo="Le Muse inquietanti", Quotazione=22000000.00m, FkArtista=3},//opera di De Chirico
25 ];
26IList<Personaggio> personaggi =
27 [
28 new (){Id=1, Nome="Uomo morente", FkOperaId=1},//un personaggio di Guernica
29 new (){Id=2, Nome="Un musicante", FkOperaId=2},
30 new (){Id=3, Nome="una ragazza di Avignone", FkOperaId=3},
31 new (){Id=4, Nome="una seconda ragazza di Avignone", FkOperaId=3},
32 new (){Id=5, Nome="Narciso", FkOperaId=5},
33 new (){Id=6, Nome="Una musa metafisica", FkOperaId=6},
34 ];
35
36//impostiamo la console in modo che stampi correttamente il carattere dell'euro e che utilizzi le impostazioni di cultura italiana
37Console.OutputEncoding = Encoding.UTF8;
38Thread.CurrentThread.CurrentCulture = new CultureInfo("it-IT");
39
40//Le query da sviluppare sono:
41//Effettuare le seguenti query:
42//1) Stampare le opere di un dato autore (ad esempio Picasso)
43//2) Riportare per ogni nazionalità (raggruppare per nazionalità) gli artisti
44//3) Contare quanti sono gli artisti per ogni nazionalità (raggruppare per nazionalità e contare)
45//4) Trovare la quotazione media, minima e massima delle opere di Picasso
46//5) Trovare la quotazione media, minima e massima di ogni artista
47//6) Raggruppare le opere in base alla nazionalità e in base al cognome dell’artista (Raggruppamento in base a più proprietà)
48//7) Trovare gli artisti di cui sono presenti almeno 2 opere
49//8) Trovare le opere che hanno personaggi
50//9) Trovare le opere che non hanno personaggi
51//10) Trovare l’opera con il maggior numero di personaggi
52
53
54//svolgimento delle query richieste
55//1) Stampare le opere di un dato autore (ad esempio Picasso)
56Console.WriteLine("**** 1) Stampare le opere di un dato autore (ad esempio Picasso)\n");
57//facciamo prima il filtraggio con la Where e poi la join
58var opereDiArtista = artisti
59 .Where(a => a.Cognome == "Picasso")
60 .Join(opere,
61 a => a.Id,
62 o => o.FkArtista,
63 (a, o) => o.Titolo);
64opereDiArtista.ToList().ForEach(Console.WriteLine);
65//altro metodo: facciamo prima la Join e poi il filtraggio con la Where sull'autore
66Console.WriteLine();
67var opereDiArtista2 = artisti
68 .Join(opere,
69 a => a.Id,
70 o => o.FkArtista,
71 (a, o) => new { a, o })
72 .Where(t => t.a.Cognome == "Picasso");
73opereDiArtista2.ToList().ForEach(t => Console.WriteLine(t.o.Titolo));
74Console.WriteLine();
75//altro modo:
76//step n.1: calcoliamo l'id di Picasso
77//step. n.2: calcoliamo le opere di quell'autore
78var autore = artisti.Where(a => a.Cognome == "Picasso").FirstOrDefault();
79if (autore != null)
80{
81 var opereDiArtista3 = opere.Where(o => o.FkArtista == autore.Id);
82 opereDiArtista3.ToList().ForEach(t => Console.WriteLine(t.Titolo));
83}
84
85//2) Riportare per ogni nazionalità (raggruppare per nazionalità) gli artisti
86Console.WriteLine("\n**** 2) Riportare per ogni nazionalità (raggruppare per nazionalità) gli artisti\n");
87//raggruppare gli artisti per nazionalità
88var artistiPerNazionalità = artisti.GroupBy(a => a.Nazionalita);
89foreach (var group in artistiPerNazionalità)
90{
91 Console.WriteLine($"Nazionalità: {group.Key}");
92 foreach (var artista in group)
93 {
94 Console.WriteLine($"\t{artista.Nome} {artista.Cognome}");
95 }
96}
97
98//3) Contare quanti sono gli artisti per ogni nazionalità (raggruppare per nazionalità e contare)
99Console.WriteLine("\n**** 3) Contare quanti sono gli artisti per ogni nazionalità (raggruppare per nazionalità e contare)\n");
100
101foreach (var group in artistiPerNazionalità)
102{
103 Console.WriteLine($"Nazionalità: {group.Key} Numero artisti: {group.Count()}");
104}
105
106//4) Trovare la quotazione media, minima e massima delle opere di Picasso
107Console.WriteLine("\n**** 4) Trovare la quotazione media, minima e massima delle opere di Picasso\n");
108//troviamo le opere di Picasso
109var opereDiPicasso = artisti
110 .Where(a => a.Cognome == "Picasso")
111 .Join(opere,
112 a => a.Id,
113 o => o.FkArtista,
114 (a, o) => o)
115 .ToList();
116//troviamo le quotazioni
117var quotazioneMinima = opereDiPicasso.Min(o => o.Quotazione);
118var quotazioneMedia = opereDiPicasso.Average(o => o.Quotazione);
119var quotazioneMassima = opereDiPicasso.Max(o => o.Quotazione);
120//stampiamo il risultato
121Console.WriteLine($"Quotazione minima = {quotazioneMinima}, " +
122 $"quotazione media = {quotazioneMedia:F2}, quotazione massima = {quotazioneMassima}");
123
124//5) Trovare la quotazione media, minima e massima di ogni artista
125Console.WriteLine("\n**** 5) Trovare la quotazione media, minima e massima di ogni artista\n");
126//raggruppiamo per artista (usando FkArtista) e poi su ogni gruppo di opere calcoliamo le funzioni di gruppo
127var operePerArtista = opere.GroupBy(o => o.FkArtista);
128foreach (var group in operePerArtista)
129{
130 Console.Write($"Id artista = {group.Key} ");
131 //vogliamo conoscere i dettagli dell'artista di cui conosciamo l'id
132 var artista = artisti.Where(a => a.Id == group.Key).FirstOrDefault();
133 if (artista != null)
134 {
135 Console.Write($"{artista.Nome} {artista.Cognome} ");
136 }
137 Console.WriteLine($"Quotazione minima = {group.Min(o => o.Quotazione):C2};" +
138 $" media = {group.Average(o => o.Quotazione):C2};" +
139 $" massima = {group.Max(o => o.Quotazione):C2}");
140}
141
142//stessa query - versione con inner join
143//effettuiamo prima la join tra opere e artisti e poi il raggruppamento
144var opereDiArtistaGroupBy = artisti
145 .Join(opere,
146 a => a.Id,
147 o => o.FkArtista,
148 (a, o) => new { a, o })
149 .GroupBy(t => t.a.Id);
150
151foreach (var group in opereDiArtistaGroupBy)
152{
153 Console.Write($"Id artista = {group.Key} |");
154 var artistaOpera = group.FirstOrDefault();
155 if (artistaOpera != null)
156 {
157 Console.Write($"Cognome = {artistaOpera.a.Cognome}");
158 }
159 Console.WriteLine($" | Quotazione media ={group.Average(t => t.o.Quotazione):C2} " +
160 $" | Quotazione minima = {group.Min(t => t.o.Quotazione):C2} " +
161 $" | Quotazione massima = {group.Max(t => t.o.Quotazione):C2} ");
162}
163
164//6) Raggruppare le opere in base alla nazionalità e in base al cognome dell’artista (Raggruppamento in base a più proprietà)
165Console.WriteLine("\n**** 6) Raggruppare le opere in base alla nazionalità e in base al cognome dell'artista (Raggruppamento in base a più proprietà)\n");
166var opereDiArtistaGroupByMultiplo = artisti
167 .Join(opere,
168 a => a.Id,
169 o => o.FkArtista,
170 (a, o) => new { a, o })
171 .GroupBy(t => new { t.a.Nazionalita, t.a.Cognome });
172
173foreach (var group in opereDiArtistaGroupByMultiplo)
174{
175 Console.WriteLine($"{group.Key.Nazionalita} {group.Key.Cognome} ");
176 foreach (var item in group)
177 {
178 Console.WriteLine($"\tOpera = {item.o.Titolo}");
179 }
180}
181
182//7)Trovare gli artisti di cui sono presenti almeno 2 opere
183Console.WriteLine("\n**** 7) Trovare gli artisti di cui sono presenti almeno 2 opere\n");
184//calcoliamo gli artisti di cui abbiamo almeno un'opera
185var artistiConAlmeno1Opera = artisti
186 .Join(opere,
187 a => a.Id,
188 o => o.FkArtista,
189 (a, o) => a);
190//per calcolare gli artisti di cui sono presenti almeno due opere procediamo così:
191//raggruppiamo gli artisti per FkArtista e successivamente filtriamo in base al conteggio degli
192//elementi in ogni gruppo
193Console.WriteLine("Artisti con almeno due opere");
194var artistiConAlmeno2Opere = opere
195 .GroupBy(o => o.FkArtista)
196 .Where(g => g.Count() >= 2)
197 .Join(artisti,
198 g => g.Key,
199 a => a.Id,
200 (g, a) => a);
201foreach (var artista in artistiConAlmeno2Opere)
202{
203 Console.WriteLine(artista);
204}
205
206//altra variante - riportiamo gli artisti con il relativo numero di opere
207opere
208 .GroupBy(o => o.FkArtista)
209 .Select(group => new { group.Key, NumeroOpere = group.Count() })
210 .Where(g => g.NumeroOpere >= 2)
211 .Join(artisti,
212 a2 => a2.Key,
213 a => a.Id,
214 (a2, a) => new { ID = a.Id, NomeArtista = a.Cognome, a2.NumeroOpere })
215 .ToList()
216 .ForEach(Console.WriteLine);
217
218//8)Trovare le opere che hanno personaggi
219Console.WriteLine("\n**** 8) Trovare le opere che hanno personaggi\n");
220//le opere con personaggi (è una semplice join)
221var opereConPersonaggi = opere
222 .Join(personaggi,
223 o => o.Id,
224 p => p.FkOperaId,
225 (o, p) => o)
226 //quando si effettua la Join tra due collection A e B, si ottiene il "Prodotto Cartesiano" AxB tra la prima collection A e
227 //la seconda collection B, seguito dalla condizione di selezione della Join, ossia l'uguaglianza dei keySelctor della Join.
228 //Nel caso in esame, la join produce le ennuple (o, p) tali che o.Id == p.FkOperaId, tuttavia prendendo solo le opere per
229 //ogni coppia (opera, personaggio), ossia facendo la proiezione (o,p) => o, si possono ottenere opere ripetute nel risultato finale.
230 //Ad esempio, un'opera con due personaggi comparirà due volte nel risultato finale e nella stampa.
231 //
232 //Per evitare inutili duplicazioni si può usare la clausola Distinct che restituisce una collection senza elementi ripetuti.
233 //👇👇👇
234 .Distinct();
235Console.WriteLine("Opere con personaggi");
236foreach (var opera in opereConPersonaggi)
237{
238 Console.WriteLine(opera);
239}
240
241//9)Trovare le opere che non hanno personaggi
242Console.WriteLine("\n**** 9) Trovare le opere che non hanno personaggi\n");
243//opere senza personaggi: dall'insieme delle opere prendo solo quelle che non sono contenute in opereConPersonaggi
244var opereSenzaPersonaggi = opere
245 .Where(o => !opereConPersonaggi.Contains(o));
246
247Console.WriteLine("Opere senza personaggi");
248foreach (var opera in opereSenzaPersonaggi)
249{
250 Console.WriteLine(opera);
251}
252
253//10)Trovare le opere con il maggior numero di personaggi
254Console.WriteLine("\n**** 10) Trovare le opere con il maggior numero di personaggi\n");
255//primo step: calcolo il numero di personaggi per opera
256var personaggiPerOpera = personaggi
257 .GroupBy(p => p.FkOperaId)
258 .Select(group => new { IdOpera = group.Key, NumeroPersonaggi = group.Count() });
259//secondo step: dobbiamo filtrare gli oggetti in modo da prendere solo quelli con il numero massimo di personaggi
260var numeroMassimoPersonaggi = personaggiPerOpera.Max(t => t.NumeroPersonaggi);
261//terzo step: filtro i dati in modo da prendere solo gli oggetti con il numero massimo di personaggi
262var opereConMaxNumeroPersonaggi = personaggiPerOpera
263 .Where(t => t.NumeroPersonaggi == numeroMassimoPersonaggi)
264 .Join(opere,
265 t => t.IdOpera,
266 o => o.Id,
267 (t, o) => new { id = o.Id, o.Titolo, Personaggi = t.NumeroPersonaggi });
268foreach (var item in opereConMaxNumeroPersonaggi)
269{
270 Console.WriteLine(item);
271}