LINQ Joins

There are Different Types of SQL Joins which are used to query data from more than one tables. In this article, I would like to share how joins work in LINQ. LINQ has a JOIN query operator that provide SQL JOIN like behavior and syntax. Let's see how JOIN query operator works for joins. This article will explore the SQL Joins with C# LINQ.
  1. INNER JOIN
  2. LEFT OUTER JOIN
  3. CROSS JOIN
  4. GROUP JOIN
The JOIN query operator compares the specified properties/keys of two collections for equality by using the EQUALS keyword. By default, all joins queries written by the JOIN keyword are treated as equijoins.

LINQ PAD for running and debugging LINQ Query

I am a big fan of LINQ Pad since it allow us to run LINQ to SQL and LINQ to Entity Framework query and gives the query output. Whenever, I need to write LINQ to SQL and LINQ to Entity Framework query then, I prefer to write and run query on LINQ PAD. By using LINQ PAD, you can test and run your desired LINQ query and avoid the head-ache for testing LINQ query with in Visual Studio. You can download the LINQ Pad script used in this article by using this link.
In this article, I am using LINQ PAD for query data from database. It is simple and useful. For more help about LINQ PAD refer the link. You can download the database script used in this article by using this link. Suppose we following three tables and data in these three tables is shown in figure.

INNER JOIN

Inner join returns only those records or rows that match or exists in both the tables.

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID orderby od.OrderID select new { od.OrderID,
  2. pd.ProductID,
  3. pd.Name,
  4. pd.UnitPrice,
  5. od.Quantity,
  6. od.Price,
  7. }).ToList();

LINQ Pad Query

INNER JOIN among more than two tables

Like SQL, we can also apply join on multiple tables based on conditions as shown below.

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID join ct in dataContext.tblCustomers on od.CustomerID equals ct.CustID orderby od.OrderID select new { od.OrderID,
  2. pd.ProductID,
  3. pd.Name,
  4. pd.UnitPrice,
  5. od.Quantity,
  6. od.Price,
  7. Customer=ct.Name //define anonymous type Customer
  8. }).ToList();

LINQ Pad Query

INNER JOIN On Multiple Conditions

Sometimes, we required to apply join on multiple coditions. In this case, we need to make two anonymous types (one for left table and one for right table) by using new keyword then we compare both the anonymous types.

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID join ct in dataContext.tblCustomers on new {a=od.CustomerID,b=od.ContactNo} equals new {a=ct.CustID,b=ct.ContactNo} orderby od.OrderID select new { od.OrderID,
  2. pd.ProductID,
  3. pd.Name,
  4. pd.UnitPrice,
  5. od.Quantity,
  6. od.Price,
  7. Customer=ct.Name //define anonymous type Customer
  8. }).ToList();

LINQ Pad Query

NOTE

  1. Always remember, both the anonymous types should have exact same number of properties with same name and datatype other wise you will get the compile time error "Type inferencce failed in the call to Join".
  2. Both the comparing fields should define either NULL or NOT NULL values.
  3. If one of them is defined NULL and other is defined NOT NULL then we need to do typecasting of NOT NULL field to NULL data type like as above fig.

LEFT JOIN or LEFT OUTER JOIN

LEFT JOIN returns all records or rows from left table and from right table returns only matched records. If there are no columns matching in the right table, it returns NULL values.
In LINQ to achieve LEFT JOIN behavior, it is mandatory to use "INTO" keyword and "DefaultIfEmpty()" method. We can apply LEFT JOIN in LINQ like as :

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID into t from rt in t.DefaultIfEmpty() orderby pd.ProductID select new { //To handle null values do type casting as int?(NULL int)
  2. //since OrderID is defined NOT NULL in tblOrders
  3. OrderID=(int?)rt.OrderID,
  4. pd.ProductID,
  5. pd.Name,
  6. pd.UnitPrice,
  7. //no need to check for null since it is defined NULL in database
  8. rt.Quantity,
  9. rt.Price,
  10. }).ToList();

LINQ Pad Query

CROSS JOIN

Cross join is a cartesian join means cartesian product of both the tables. This join does not need any condition to join two tables. This join returns records or rows that are multiplication of record number from both the tables means each row on left table will related to each row of right table.
In LINQ to achieve CROSS JOIN behavior, there is no need to use Join clause and where clause. We will write the query as shown below.

C# Code

  1. var q = from c in dataContext.Customers from o in dataContext.Orders select new { c.CustomerID,
  2. c.ContactName,
  3. a.OrderID,
  4. a.OrderDate
  5. };

LINQ Pad Query

GROUP JOIN

Whene a join clause use an INTO expression, then it is called a group join. A group join produces a sequence of object arrays based on properties equivalence of left collection and right collection. If right collection has no matching elements with left collection then an empty array will be produced.

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID into t orderby pd.ProductID
  2. select new
  3. {
  4. pd.ProductID,
  5. pd.Name,
  6. pd.UnitPrice,
  7. Order=t
  8. }).ToList();

LINQ Pad Query

Basically, GROUP JOIN is like as INNER-EQUIJOIN except that the result sequence is organized into groups.

GROUP JOIN As SubQuery

We can also use the result of a GROUP JOIN as a subquery like as:

C# Code

  1. var q=(from pd in dataContext.tblProducts join od in dataContext.tblOrders on pd.ProductID equals od.ProductID into t from rt in t where rt.Price>70000 orderby pd.ProductID select new { rt.OrderID,
  2. pd.ProductID,
  3. pd.Name,
  4. pd.UnitPrice,
  5. rt.Quantity,
  6. rt.Price,
  7. }).ToList();

LINQ Pad Query

Active Directory in C#.Net


Microsoft Technet offers a script repository to work with AD User Accounts; however, I needed to work with .NET and I could not find samples for all the tasks I needed to program. I promised to myself that one day I would publish the code samples I found and created to help other developers who are working with Directory Services. So, I wish you a happy AD.NET programming and I hope my work saves you some time. The code samples I provide are written in C#.
1. Create a connection to Active Directory
/// <summary>/// Method used to create an entry to the AD./// Replace the path, username, and password./// </summary>/// <returns>DirectoryEntry</returns>public static DirectoryEntry GetDirectoryEntry()
{
DirectoryEntry de =
new
DirectoryEntry();
de.Path = LDAP://192.168.1.1/CN=Users;DC=Yourdomain;
de.Username = @"yourdomain\sampleuser";
de.Password = "samplepassword";
return
de;
}
2. Create a secure connection to Active Directory
To connect to the AD, you need a user account that belongs to the domain you want to connect to. Most user accounts have permissions to search the AD; however, to modify the AD, you need a user account that is a member of the group of Domain Administrators (DomainAdmin). An account that belongs to this group has high privileges and hardcoding the user and password of this account in your code can compromise the security of the AD. I don't recommend you to create directory entries where usernames and passwords are hardcoded. Try to connect to the AD using a secure connection.
/// <summary>/// Method used to create an entry to the AD using a secure connection./// Replace the path./// </summary>/// <returns>DirectoryEntry</returns>public static DirectoryEntry GetDirectoryEntry()
{
DirectoryEntry de =
new
DirectoryEntry();
de.Path = LDAP://192.168.1.1/CN=Users;DC=Yourdomain;
de.AuthenticationType = AuthenticationTypes.Secure;
return
de;
}

To connect to the AD using a secure connection, you need to delegate the permissions of a user account with DomainAdmin permissions to the thread that is running a program. For instance, I created an exe and I ran the program using the Run As command to start a program. I delegated the user's principal identity and culture to the current thread that runs the program. To delegate the principal identity and culture to the current thread, I used the following code:
/// <summary>/// Establish identity (principal) and culture for a thread./// </summary>public static void SetCultureAndIdentity()
{
AppDomain.CurrentDomain.SetPrincipalPolicy(PrincipalPolicy.WindowsPrincipal);
WindowsPrincipal principal = (WindowsPrincipal)Thread.CurrentPrincipal;
WindowsIdentity identity = (WindowsIdentity)principal.Identity;
System.Threading.Thread.CurrentThread.CurrentCulture =
new
CultureInfo("en-US");
}

3. Validate if a user exists
/// <summary>/// Method to validate if a user exists in the AD./// </summary>/// <param name="UserName"></param>/// <returns></returns>public bool UserExists(string UserName)
{
DirectoryEntry de = ADHelper.GetDirectoryEntry();
DirectorySearcher deSearch =
new
DirectorySearcher();
deSearch.SearchRoot =de;
deSearch.Filter = "(&(objectClass=user) (cn=" + UserName +"))";
SearchResultCollection results = deSearch.FindAll();
if
(results.Count == 0)
{
return false
;
}
else{return true;
}
}

4. Set user's properties
/// <summary>/// Helper method that sets properties for AD users./// </summary>/// <param name="de"></param>/// <param name="PropertyName"></param>/// <param name="PropertyValue"></param>public static void SetProperty(DirectoryEntry de, string PropertyName, string PropertyValue)
{
if(PropertyValue!=null
)
{
if
(de.Properties.Contains(PropertyName))
{
de.Properties[PropertyName][0]=PropertyValue;
}
else{
de.Properties[PropertyName].Add(PropertyValue);
}
}

5. Set user's country
To set the country property for a user was one of the tasks that took me some time to figure out. After some hours of research I realized that you need to know the ISO 3166 Codes for countries and set three properties to define a user's country: c, co, and countryCode.
// Set the co property using the name of the country.SetProperty(newuser,"co","MEXICO");// Set the c property using the two-letter country code (ISO 3166 A 2).SetProperty(newuser,"c","MX");// Set the countryCode property using the numeric value (ISO 3166 Number) of the country.SetProperty(newuser,"countryCode","484");}
6. Set user's password
Setting the password for a user requires some work. I will walk you through the steps I followed to set a password for a user:
a) Create or download a helper class that generates random passwords that comply with the strong password rules. I was short of time and couldn't develop one, so I downloaded the RandomPassword class created by Obviex.b) Create a method that consumes the RandomPassword helper class
/// <summary>/// Method that consumes a helper class library/// to generate random passwords./// </summary>/// <returns></returns>public string SetSecurePassword()
{
RandomPassword rp =
new
RandomPassword();return rp.Generate(8,8);
}
c) Set the password property using the usr.Invoke method.
/// <summary>/// Method to set a user's password/// <param name="path"></param>public void SetPassword(string path)
{
DirectoryEntry usr =
new
DirectoryEntry();
usr.Path = path;
usr.AuthenticationType = AuthenticationTypes.Secure;
object[] password = new object
[] {SetSecurePassword()};object ret = usr.Invoke("SetPassword", password );
usr.CommitChanges();
usr.Close();
}
The usr.Invoke method can be called once within the same AppDomain, otherwise your program will crash. If you place a call to the usr.Invoke method inside a for construct, the first run will be succesful, but the second one will crash the compiler. I created a workaround that helped me to solve this problem. I made a separate console application (SetPassword.exe) and I called and started the process programatically from the SetPassword method.

  • Download the SetPassword project.
  • Copy the SetPassword.exe file in your application.
  • Call and start SetPassword.exe from your application.
/// </summary>/// Method that calls and starts SetPassword.exe/// <param name="path"></param>/// <param name="password"></param>public void SetPassword(string path, string password)
{
StringBuilder args =
new
StringBuilder();
args.Append(path);
args.Append(" ");
args.Append(password);
ProcessStartInfo startInfo =
new
ProcessStartInfo("SetPassword.exe",args.ToString());
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
Process.Start(startInfo);
}
7. Enable a user account
/// <summary>/// Method to enable a user account in the AD./// </summary>/// <param name="de"></param>private static void EnableAccount(DirectoryEntry de)
{
//UF_DONT_EXPIRE_PASSWD 0x10000int exp = (int) de.Properties["userAccountControl"].Value;
de.Properties["userAccountControl"].Value = exp | 0x0001;
de.CommitChanges();//UF_ACCOUNTDISABLE 0x0002int val = (int) de.Properties["userAccountControl"].Value;
de.Properties["userAccountControl"].Value = val & ~0x0002;
de.CommitChanges();
}
8. Add a user to a group
/// <summary>/// Method to add a user to a group/// </summary>/// <param name="de"></param>/// <param name="deUser"></param>/// <param name="GroupName"></param>public static void AddUserToGroup(DirectoryEntry de, DirectoryEntry deUser, string GroupName)
{
DirectorySearcher deSearch =
new
DirectorySearcher();
deSearch.SearchRoot = de;
deSearch.Filter = "(&(objectClass=group) (cn=" + GroupName +"))";
SearchResultCollection results = deSearch.FindAll();
bool isGroupMember = false
;if (results.Count>0)
{
DirectoryEntry group = GetDirectoryEntry(results[0].Path);
object members = group.Invoke("Members",null
);foreach ( object member in (IEnumerable) members)
{
DirectoryEntry x =
new
DirectoryEntry(member);if (x.Name! = deUser.Name)
{
isGroupMember =
false
;
}
else{
isGroupMember =
true
;break;
}
}
if
(!isGroupMember)
{
group.Invoke("Add",
new object
[] {deUser.Path.ToString()});
}
group.Close();
}
return
;
}
9. Generate a mailbox for a user in Microsoft Exchange Server
You might need to create a mailbox for a user in Microsoft Exchange Server. Network configuration and server architecture can add complexity to the process of programmatically creating mailboxes for users, but you know, there's always a workaround. You can invoke a script that creates mailboxes from a remote machine. I will walk you through the steps I followed to create a mailbox for a user in Microsoft Exchange Server.

  • On the Domain Controller server, create the directory C:\TestRemoteMailbox.
  • Download the scripts MailBox.vbs and WSHControl.vbs.
  • Copy the script Mailbox.vbs to the directory C:\TestRemoteMailbox.
  • Note: Mailbox.vbs is a script that creates MailBoxes in Microsoft Exchange.
  • Copy the script WSHControl.vbs to your application directory.
  • Note: WSHControl.vbs is a script that invokes the MailBox.vbs script on a remote machine (Domain Controller)
  • From your application, call and start WSHControl.vbs.

/// <summary>/// Method that calls and starts a WSHControl.vbs/// </summary>/// <param name="userAlias"></param>public void GenerateMailBox(string userAlias)
{
StringBuilder mailargs =
new
StringBuilder();
mailargs.Append("WSHControl.vbs");
mailargs.Append(" ");
mailargs.Append(userAlias);
ProcessStartInfo sInfo =
new
ProcessStartInfo("Wscript.exe",mailargs.ToString());
sInfo.WindowStyle = ProcessWindowStyle.Hidden;;
Process.Start(sInfo);
}
10. Create a user account
/// <summary>/// Method that creates a new user account/// </summary>/// <param name="employeeID"></param>/// <param name="name"></param>/// <param name="login"></param>/// <param name="email"></param>/// <param name="group"></param>public void CreateNewUser(string employeeID, string name, string login, string email, string group)
{
Catalog catalog =
new
Catalog();
DirectoryEntry de = ADHelper.GetDirectoryEntry();
///
1. Create user accountDirectoryEntries users = de.Children;
DirectoryEntry newuser = users.Add("CN=" + login, "user");
///
2. Set propertiesSetProperty(newuser,"employeeID", employeeID);
SetProperty(newuser,"givenname", name);
SetProperty(newuser,"SAMAccountName", login);
SetProperty(newuser,"userPrincipalName", login);
SetProperty(newuser,"mail", email);
newuser.CommitChanges();
///
3. Set passwordSetPassword(newuser.Path);
newuser.CommitChanges();
///
4. Enable account EnableAccount(newuser);/// 5. Add user account to groupsAddUserToGroup(de,newuser,group);/// 6. Create a mailbox in Microsoft Exchange GenerateMailBox(login);
newuser.Close();
de.Close();
}
11. Disable a user account
/// <summary>/// Method that disables a user account in the AD and hides user's email from Exchange address lists./// </summary>/// <param name="EmployeeID"></param>public void DisableAccount(string EmployeeID)
{
DirectoryEntry de = GetDirectoryEntry();
DirectorySearcher ds =
new
DirectorySearcher(de);
ds.Filter = "(&(objectCategory=Person)(objectClass=user)(employeeID=" + EmployeeID + "))";
ds.SearchScope = SearchScope.Subtree;
SearchResult results = ds.FindOne();
if(results != null
)
{
DirectoryEntry dey = GetDirectoryEntry(results.Path);
int val = (int
)dey.Properties["userAccountControl"].Value;
dey.Properties["userAccountControl"].Value = val | 0x0002;
dey.Properties["msExchHideFromAddressLists"].Value = "TRUE";
dey.CommitChanges();
dey.Close();
}
de.Close();
}
12. Update user account
/// <summary>/// Method that updates user's properties/// </summary>/// <param name="employeeID"></param>/// <param name="department"></param>/// <param name="title"></param>/// <param name="company"></param>public void ModifyUser(string employeeID, string department, string title, string company)
{
DirectoryEntry de = GetDirectoryEntry();
DirectorySearcher ds =
new
DirectorySearcher(de);
ds.Filter = "(&(objectCategory=Person)(objectClass=user)(employeeID=" + employeeID + "))";
ds.SearchScope = SearchScope.Subtree;
SearchResult results = ds.FindOne();
if(results!=null
)
{
DirectoryEntry dey = GetDirectoryEntry(results.Path);
SetProperty(dey, "department", department);
SetProperty(dey, "title", title);
SetProperty(dey, "company", company);
dey.CommitChanges();
dey.Close();
}
de.Close();
}
13. Validate if a string has a correct email pattern.
/// <summary>/// Method that validates if a string has an email pattern./// </summary>/// <param name="mail"></param>/// <returns></returns>public bool IsEmail(string mail)
{
Regex mailPattern =
new
Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");return mailPattern.IsMatch(mail);
}
14. Extract a user alias from an email account.
/// <summary>/// Method to extract the alias from an email account./// dada una cuenta de correo electrónico/// </summary>/// <param name="mailAddress"></param>/// <returns></returns>public string GetAlias(string mailAddress)
{
if
(IsEmail(mailAddress))
{
return
mailAddress.Substring(0,mailAddress.IndexOf("@"));
}
else{return "";
}
}
15. Format dates to AD date format (AAAAMMDDMMSSSS.0Z)
/// <summary>/// Method that formats a date in the required format/// needed (AAAAMMDDMMSSSS.0Z) to compare dates in AD./// </summary>/// <param name="date"></param>/// <returns>Date in valid format for AD</returns>public string ToADDateString(DateTime date)
{
string
year = date.Year.ToString();int month = date.Month;int day = date.Day;
StringBuilder sb =
new
StringBuilder();
sb.Append(year);
if
(month <10)
{
sb.Append("0");
}
sb.Append(month.ToString());
if
(day <10)
{
sb.Append("0");
}
sb.Append(day.ToString());
sb.Append("000000.0Z");
return
sb.ToString();
}
16. Search users
When you use Directory Services, you can accomplish many interesting tasks such as searching and filtering users. The DirectorySearcher object allows you to query the AD. The following sample code queries the AD to search all the user accounts that were modified from a given date. The results are stored in a DataTable, so you can easily databind them.
/// <summary>/// Method that returns a DataTable with a list of users modified from a given date./// </summary>/// <param name="fromdate"></param>public DataTable GetModifiedUsers(DateTime fromdate)
{
DataTable dt =
new
DataTable();
dt.Columns.Add("EmployeeID");
dt.Columns.Add("Name");
dt.Columns.Add("Email");
DirectoryEntry de = GetDirectoryEntry();
DirectorySearcher ds =
new
DirectorySearcher(de);
StringBuilder filter =
new
StringBuilder();
filter.Append("(&(objectCategory=Person)(objectClass=user)(whenChanged>=");
filter.Append(date.ToADDateString());
filter.Append("))");
ds.Filter=filter.ToString();
ds.SearchScope = SearchScope.Subtree;
SearchResultCollection results= ds.FindAll();
foreach(SearchResult result in
results)
{
DataRow dr = dt.NewRow();
DirectoryEntry dey = GetDirectoryEntry(result.Path);
dr["EmployeeID"] = dey.Properties["employeeID"].Value;
dr["Name"] = dey.Properties["givenname"].Value;
dr["Email"] = dey.Properties["mail"].Value;
dt.Rows.Add(dr);
dey.Close();
}
de.Close();
return
dt;