2
\$\begingroup\$

I created a web service which can be called from a form to add new users to the system. The final service will add users to Active Directory and Exchange in the required format (please note that FirstName and LastName are required fields in the form, so they will never be null):

using System.Collections.Generic;
using System.Web.Services;

namespace NewStarterWebService
{
[WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class NewStarter : WebService
    {
        [WebMethod]
        public void CreateNewUser(
            string firstName, 
            string middleName, 
            string lastName, 
            string jobTitle, 
            string department, 
            string office, 
            string role,
            string manager)
        {
            var newUser = new NewUser(firstName, middleName, lastName, jobTitle, department, office, role, manager);
            newUser.Create();
        }
    }
}

The NewUser Class only has 1 method - Create() and a constructor:

using System;
using System.Collections.Generic;

namespace NewStarterWebService
{
    public class NewUser
    {
        public readonly string FullName;
        public readonly string FirstName;
        public readonly string MiddleName;
        public readonly string LastName;
        public readonly string Department;
        public readonly string Office;
        public readonly string JobTitle;
        public readonly string Role;
        public readonly string Manager;
        public readonly string Email; 
        public readonly string DotName;
        public readonly string HomeDirectory;
        public readonly string Initials;

        public readonly Dictionary<string, string> DefaultProperties;
        private const string Password = "Password99";

        public NewUser(
            string firstName,
            string middleName,
            string lastName,
            string jobTitle,
            string department,
            string office,
            string role,
            string manager)
        {
            Office = office;
            Department = department;
            FirstName = firstName.Trim();
            MiddleName = middleName.Trim();
            LastName = lastName.Trim();
            JobTitle = jobTitle.Trim();
            Role = role.Trim();

            Manager = Ad.GetManagerDistinguishedName(manager);
            FullName = $"{FirstName} {LastName}";
            Email = $"{FirstName}{LastName}@domain.com";
            DotName = $"{FirstName}.{LastName}";
            HomeDirectory = $@"\\File\Home$\{DotName}";
            try
            {
                Initials = $"{FirstName[0]}{MiddleName[0]}{LastName[0]}";
            }
            catch (Exception ex) when (
                ex is ArgumentOutOfRangeException || 
                ex is NullReferenceException || 
                ex is ArgumentNullException)
            {
                Initials = $"{FirstName[0]}{LastName[0]}";
            }

            // Set the default properties for AD
            DefaultProperties = new Dictionary<string, string>
            {
                {"userprincipalname",           Email},
                {"samaccountname",              DotName },
                {"sn",                          LastName},
                {"givenname",                   FirstName },
                {"displayname",                 FullName },
                {"description",                 JobTitle },
                {"mail",                        Email },
                {"homedirectory",               HomeDirectory },
                {"homedrive",                   "H:" },
                {"physicalDeliveryOfficeName",  Office },
                {"Manager",                     Manager },
                {"Initials",                    Initials }
            };
        }

        public void Create()
        {
            Ad.CreateUser(this);
            Ps.AddExchangeUser(this);
        }     
    }
}

In the constructor, Manager is initialised with a call to the AD class, because the manager's DisplayName is sent through, but for my purposes I need this to be the DsitinguishedName.

The Create() method makes a call to Ad (Active Directory) and Ps (PowerShell) to do the AD and Exchange creation. I didn't post this code because I thought that it would maybe be a bit much but if people think it would help to put the rest of it in context I will add it!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ You say that FirstName and LastName never may be null, but could they be empty ? \$\endgroup\$
    – Heslacher
    Jun 9, 2016 at 14:29
  • \$\begingroup\$ @Heslacher You are so right ... Just tried submitting the form where FirstName and LastName are made up of some spaces and the form tried to submit - I will have to handle that in the code \$\endgroup\$
    – Bassie
    Jun 9, 2016 at 14:40

3 Answers 3

3
\$\begingroup\$

I would not put dependency on singleton in this class – it means two responsibility for NewUser - format properties and orchestrate AD API. Let’s define this helper class to be more explicit on your validation scenarios:

static class Trimming
{
    public static string TrimRequired(this string value, [CallerMemberName] string property = "")
    {
        if(string.IsNullOrWhiteSpace(value))
            throw new InvalidOperationException($"Mailformed property {property}.");

        return value.Trim();
    }

    public static string TrimOptional(this string value)
    {
        if (value == null)
            return null;

        return value.Trim();
    }
}

And define UserBuilder (It makes sense to prefer properties over ctor parameters when we have so many):

public class UserBuilder
{
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public string JobTitle { get; set; }
    public string Department { get; set; }
    public string Office { get; set; }
    public string Role { get; set; }
    public string Manager { get; set; }

    public IDictionary<string, string> ToProperties() =>
        new Dictionary<string, string>
        {
            {"userprincipalname", RequiredEmail},
            {"samaccountname", RequiredDotName },
            {"sn", RequiredLastName},
            {"givenname", RequiredFirstName },
            {"displayname", RequiredFullName },
            {"description", OptionalJobTitle },
            {"mail", RequiredEmail },
            {"homedirectory", RequiredHomeDirectory },
            {"homedrive", "H:" },
            {"physicalDeliveryOfficeName", OptionalOffice },
            {"Manager", RequiredManager },
            {"Initials", RequiredInitials }
        };

    string RequiredFullName => $"{RequiredFirstName} {RequiredLastName}";
    string RequiredEmail => $"{RequiredFirstName}{RequiredLastName}@domain.com";
    string RequiredDotName => $"{RequiredFirstName}.{RequiredLastName}";
    string RequiredHomeDirectory => $@"\\File\Home$\{RequiredDotName}";

    string RequiredInitials
    {
        get
        {
            try
            {
                return $"{RequiredFirstName[0]}{OptionalMiddleName[0]}{RequiredLastName[0]}";
            }
            catch (Exception ex) when (
                ex is ArgumentOutOfRangeException ||
                ex is NullReferenceException ||
                ex is ArgumentNullException)
            {
                return $"{RequiredFirstName[0]}{RequiredLastName[0]}";
            }
        }
    }

    string OptionalOffice => Office.TrimOptional();
    string OptionalDepartment => Department.TrimOptional();
    string RequiredFirstName => FirstName.TrimRequired();
    string OptionalMiddleName => MiddleName.TrimOptional();
    string RequiredLastName => LastName.TrimRequired();
    string OptionalJobTitle => JobTitle.TrimOptional();
    string RequiredRole => Role.TrimRequired();
    string RequiredManager => Manager.TrimRequired();
}

P.S. Generally speaking, it makes sense to do not hold an intermediate state of you calculations (like trimmed string values) – just an original data + define functions/properties to process them. It makes your design more flexible.

P.P.S. It might be useful to have UserName class:

   class UserName
   {
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        public string LastName { get; set; }

        public string FullName => ...
        public string Initials => ...
   }

... and use in UserBuilder.

\$\endgroup\$
3
\$\begingroup\$

My biggest concerns are that I miss some data validation. Sure you say that FirstName and LastName won't be null, but they may be either Empty or have only whitespace.

But what about the passed in middleName ? If middleName is null then a call to MiddleName = middleName.Trim(); would blow in your face wich is the same for jobTitle and role.


Using the classname NewUser will make me wonder what you will do if you need a user to be updated or deleted. Will you then add another class EditedUser and DeletableUser?

Why don't you name it ADUser or ActiveDirectoryUser instead ?

\$\endgroup\$
1
  • \$\begingroup\$ I should have mentioned that JobTitle and Role are both submitted via dropdown boxes, so I felt safe about these. With everything else you are totally right - maybe I will just rename the class User as they will be added to various systems eventually \$\endgroup\$
    – Bassie
    Jun 9, 2016 at 15:05
2
\$\begingroup\$

This annoys me:

public readonly string FullName;

And that's because readonly is a modifier for fields. Instead, I'd expect this:

public string FullName {get; private set; }
\$\endgroup\$
4
  • \$\begingroup\$ I was thinking exactly this but had no idea why I should change it - didn't feel right to make a change if I didn't have a real reason. I still can't really tell why properties are better when a readonly field also disallows public setting. I guess making it a property lets methods other than the constructor make changes to it \$\endgroup\$
    – Bassie
    Jun 9, 2016 at 15:16
  • 1
    \$\begingroup\$ If OP is using C# 6, it could be as simple as public string FullName { get; } and it would behave just like his readonly field. Using private set opens the field up to be modified within the class, not exactly the same semantics as OP's code. \$\endgroup\$
    – RubberDuck
    Jun 9, 2016 at 23:12
  • \$\begingroup\$ @RubberDuck Does this mean that excluding private set; allows for modification in the constructor and no-where else (similar to readonly) in C#6 ? \$\endgroup\$
    – Bassie
    Jun 10, 2016 at 11:54
  • \$\begingroup\$ @Bassie simple-talk.com/dotnet/.net-framework/whats-new-in-c-6 \$\endgroup\$
    – BCdotWEB
    Jun 10, 2016 at 12:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.