LINQ provider built on top of System.DirectoryServices.Protocols for interacting with LDAP servers.
There are tons of examples on how to access Active Directory via System.DirectoryServices and System.DirectoryServices.AccountManagement. However, with the introduction of LINQ all of those examples felt completely outdated. The goal of LINQ to LDAP is to simplify the process of getting information in an out of a directory server using LINQ.
Below you'll find the fastest way to get up and running with LINQ to LDAP. There's also an examples project here with a MVC and WPF example that connects to a live OpenLDAP server. You can also reference the Documentation.
Add LINQ to LDAP to your project by getting it from NuGet.
public class User
{
public const string NamingContext = "CN=Users,CN=Employees,DC=Northwind,DC=local";
public string DistinguishedName { get; set; }
public string CommonName { get; set; }
public Guid Guid { get; set; }
public SecurityIdentifier Sid { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime WhenCreated { get; set; }
public DateTime LastChanged { get; set; }
public void SetDistinguishedName()
{
DistinguishedName = string.Format("CN={0},{1}", CommonName, NamingContext);
}
}
By Attributes
[DirectorySchema(NamingContext, ObjectCategory = "Person", ObjectClass = "User")]
public class User
{
public const string NamingContext = "CN=Users,CN=Employees,DC=Northwind,DC=local";
[DistinguishedName]
public string DistinguishedName { get; set; }
[DirectoryAttribute("cn", ReadOnlyOnAdd = true, ReadOnlyOnSet = true)]
public string CommonName { get; set; }
[DirectoryAttribute("objectguid", ReadOnlyOnAdd = true, ReadOnlyOnSet = true)]
public Guid Guid { get; set; }
[DirectoryAttribute("objectsid", ReadOnlyOnAdd = true, ReadOnlyOnSet = true)]
public SecurityIdentifier Sid { get; set; }
[DirectoryAttribute]
public string Title { get; set; }
[DirectoryAttribute("givenname")]
public string FirstName { get; set; }
[DirectoryAttribute("sn")]
public string LastName { get; set; }
[DirectoryAttribute(ReadOnlyOnAdd = true, ReadOnlyOnSet = true)]
public DateTime WhenCreated { get; set; }
[DirectoryAttribute(ReadOnlyOnAdd = true, ReadOnlyOnSet = true)]
public DateTime WhenChanged { get; set; }
public void SetDistinguishedName()
{
DistinguishedName = string.Format("CN={0},{1}", CommonName, NamingContext);
}
}
By Mapping
public class UserMapping : ClassMap<User>
{
public override IClassMap PerformMapping(string namingContext = null,
string objectCategory = null, bool includeObjectCategory = true,
IEnumerable<string> objectClasses = null, bool includeObjectClasses = true)
{
NamingContext(User.NamingContext);
ObjectCategory("Person");
ObjectClass("User");
DistinguishedName(x => x.DistinguishedName);
Map(x => x.CommonName).Named("cn").ReadOnly();
Map(x => x.Guid).Named("objectguid").ReadOnly();
Map(x => x.Sid).Named("objectsid").ReadOnly();
Map(x => x.Title);
Map(x => x.FirstName).Named("givenname");
Map(x => x.LastName).Named("sn");
Map(x => x.WhenCreated).ReadOnly();
Map(x => x.WhenChanged).ReadOnly();
return this;
}
}
Put this in your Startup.cs, Global.asax.cs or App.xaml.cs
var config = new LdapConfiguration()
.MaxPageSizeIs(1000);
//add mapping
config.AddMapping(new UserMapping());
// OR
config.AddMapping(new AttributeClassMap<User>());
//configure connecting to the directory
config.ConfigureFactory("companydirectory.com")
.AuthenticateBy(AuthType.Negotiate)
.AuthenticateAs(CredentialCache.DefaultNetworkCredentials)
.ProtocolVersion(3)
.UsePort(389);
//store the configuration
//alternately you can register it with your IoC container of choice.
config.UseStaticStorage();
//this only works when using static storage
IDirectoryContext context = new DirectoryContext();
//create it from a configuration
IDirectoryContext context = new DirectoryContext(config);
IDirectoryContext context = new DirectoryContext();
var user = new User()
{
CommonName = "ABC User",
FirstName = "ABC",
LastName = "User",
Title = "Test User"
};
user.SetDistinguishedName();
context.Add(user);
user = context.Query<User>()
.Single(u => u.FirstName == user.FirstName && u.LastName == user.LastName);
user.Should().Not.Be.Null();
user.Title = "Freshly updated";
context.Update(user);
context.Delete(user.DistinguishedName);
IDirectoryContext context = new DirectoryContext();
IDirectoryAttributes user = new DirectoryAttributes("CN=ABC User,CN=Users,CN=Employees,DC=Northwind,DC=local");
user.Set("givenname", "ABC")
.Set("sn", "User")
.Set("title", "Test User");
context.Add(user);
user = context.Query("CN=Users,CN=Employees,DC=Northwind,DC=local")
.Select("title", "cn", "givenname", "sn", "objectGuid", "objectSid")
.Single(_ => Filter.Equal(_, "givenname", "ABC", shouldCleanValue: true) &&
Filter.Equal(_, "sn", "User", shouldCleanValue: true));
user.Should().Not.Be.Null();
user.GetGuid("objectGuid").Should().Not.Be.Null();
user.Set("title", "Freshly updated");
context.Update(user);
context.Delete(user.DistinguishedName);
The LdapConfiguration class is designed to be a singleton. You should configure it once and then store it with your IoC container of choice or call the UseStaticStorage method. I don't recommend using DirectoryContext as a singleton, but it is possible depending on your usage scenario. There may be limitations to how long a connection to your LDAP server can be maintained.
LdapConfiguration is only thread safe after it has been configured. DirectoryContext can be used across threads, but the underlying LdapConnection is not guaranteed to be thread safe so proceed with caution.
Calling dispose is not explicitly required for DirectoryContext. Both it and the LdapConnection from S.DS.P have a finalizer that will get called when the garbage collector runs. However, depending on your connection factory and usage scenario, you may run out of connections to your LDAP server.
LDAP currently does NOT support transactions so there is no way to wrap modifications in a unit-of-work.
DirectoryContext is mocking friendly via the IDirectoryContext interface. Its methods can be mocked, however, when using LINQ to LDAP specific expressions in queries (custom filters) it will be necessary to use the MockQuery under TestSupport.
var array = new[]() { "one" };
var context = new Mock<IDirectoryContext>();
var query = new MockQuery<IDirectoryAttributes>(new List<object> { array });
context.Setup(x => x.Query("test", SearchScope.Subtree, null, null, null))
.Returns(query);
var expression = PredicateBuilder.Create<IDirectoryAttributes>()
.And(x => Filter.Equal(x, "x", "y", false))
.Or(x => Filter.Equal(x, "a", "b", true));
var result = context.Object.Query("test")
.Where(expression)
.Select(x => x.GetString("whatever"))
.ToArray();
query.MockProvider.ExecutedExpressions.Should().Have.Count.EqualTo(1);
query.MockProvider.ExecutedExpressions[0].ToString()
.Should().Contain("Equal(x, \"x\", \"y\", False)")
.And.Contain("OrElse")
.And.Contain("Equal(x, \"a\", \"b\", True)")
.And.Contain("x => x.GetString(\"whatever\")");
result.Should().Have.SameSequenceAs(array);
- Microsoft Lightweight Directory Services
- Microsoft Active Directory (Read Only Testing)
- OpenLDAP (Read Only Testing)
- IBM Tivoli Directory (Read Only Testing)
- Siemens DirX (Read Only Testing)
- Apache Directory (Read Only Testing)
- OpenDJ (User Tested)
This project is provided as is. I have tested the project primarily using Lightweight Directory Services, but each server will have different features and capabilities. I recommend thorough testing before using this project in a production environment.