2
\$\begingroup\$

I've written code to be called from Powershell like so:

Rename-DatabaseUser -Name 'someuser' -NewName 'somenewuser'

I'm wondering if there's another way I can approach this. I want to create objects of the same type with slightly differing property values.

Here's the code I'm currently using now (RenameDatabaseUserCmdlet.cs):

namespace Company.Powershell.Module.Cmdlets
{
    using System.Management.Automation;

    [Cmdlet(VerbsCommon.Rename, "DatabaseUser")]
    [Alias("rndbu")]
    public class RenameDatabaseUserCmdlet : PSCmdlet
    {
        [Parameter(ParameterSetName = "Object")]
        public DatabaseUser User { get; set; }

        [Parameter(ParameterSetName = "String")]
        public string Name { get; set; }

        [Parameter(ParameterSetName = "Object")]
        [Parameter(ParameterSetName = "String")]
        public string NewName { get; set; }

        protected override void ProcessRecord()
        {
            DatabaseUser user;

            if (ParameterSetName == "String")
            {
                var ps = PowerShell.Create(RunspaceMode.CurrentRunspace);
                ps.AddCommand("Get-DatabaseUser")
                  .AddParameter("Name", Name);

                foreach (var result in ps.Invoke())
                {
                    user = result?.BaseObject as DatabaseUser;
                    if (user == null)
                    {
                        // This is where I'm not sure I'm doing it right.
                        WriteError(DatabaseErrorRecord.UserNotFound(Name));
                        return;
                    }
                }
            }
            else
            {
                user = User;
            }

            // Still more to do here...
        }
    }
}

This is how I create the objects. In DatabaseErrorRecord I have multiple static methods which return an ErrorRecord object initialised slightly differently. These are some of the methods:

namespace Company.Powershell.Module
{
    using System;
    using System.Management.Automation;

    class DatabaseErrorRecord : ErrorRecord
    {
        // This is added just so it compiles, which is why I think I'm wrong.
        private DatabaseErrorRecord(Exception exception,
            string errorId,
            ErrorCategory errorCategory,
            object targetObject) : base(exception, errorId, errorCategory, targetObject)
        { }

        public static ErrorRecord UserNotFound(string userName)
        {
            return new ErrorRecord
            (
                new NullReferenceException($"The user '{userName}' could not be found in the database."),
                "UserNotFound",
                ErrorCategory.ObjectNotFound,
                null
            );
        }

        public static ErrorRecord DatabaseNotFound(string databaseName)
        {
            return new ErrorRecord
            (
                new NullReferenceException($"The database '{databaseName}' could not be found on the SQL Server instance."),
                "DatabaseNotFound",
                ErrorCategory.ObjectNotFound,
                null
            );
        }

        public static ErrorRecord PermissionConflict(string permissionA, string permissionB)
        {
            return new ErrorRecord
            (
                new InvalidOperationException($"Granting {permissionA} to this user would cause a conflict with {permissionB}."),
                "PermissionConflict",
                ErrorCategory.InvalidOperation,
                null
            );
        }
        // Other methods omitted...
    }
}

What is a good way to create objects of the same type, using slightly different arguments?

In each of these methods, when the ErrorRecord is created. The Exception, Error ID and Error Category will all be different. I would create these objects when I need them in code, but there's some instances where I would need the same object twice.

I'm still thinking this is wrong since I have to include the constructor despite the fact I will never use it.

Is there any other ways I could structure this?

\$\endgroup\$
0

2 Answers 2

4
\$\begingroup\$

There are no established scenarios for deriving from this class.

MSDN ErrorRecord

Which leads me to believe that there's no reason to derive a child class. Instead, change the name of DatabaseErrorRecord to DatabaseErrorRecordFactory and make it static. Do not derive from ErrorRecord. Instead, just have a static class with a bunch of static methods that return ErrorRecords. This will remove the need to have any constructor at all.

public static class DatabaseErrorRecord
{
    public static ErrorRecord UserNotFound(string userName)
    {
        return new ErrorRecord
        (
            new NullReferenceException($"The user '{userName}' could not be found in the database."),
            "UserNotFound",
            ErrorCategory.ObjectNotFound,
            null
        );
    }
    // Other methods omitted...
}

Side note: I really like that you're taking advantage of the new C# 6 features. I'm a big fan of the Elvis operator.

One other thing I'd consider changing is that NullReferenceException though. It doesn't feel appropriate. An argument exception might be okay, but I think I'd create my own custom exception for this one.

\$\endgroup\$
8
  • \$\begingroup\$ 'Elvis operator'. I chuckled. :) The only reason I'm using NullReferenceException there is because the method can't continue with a null object. I asked a question about custom exceptions before and was told it was a bit pointless if I didn't extend the functionality of the exception because otherwise I was essentially just 'aliasing' the class. \$\endgroup\$
    – Jake
    Mar 8, 2016 at 23:26
  • \$\begingroup\$ Yeah. I don't have very good argument against NRE other than it could be misleading. I expect NRE's to be a certain class of coding errors. This use of it would surprise me. \$\endgroup\$
    – RubberDuck
    Mar 8, 2016 at 23:53
  • 2
    \$\begingroup\$ KeyNotFoundException might be a reasonable choice. \$\endgroup\$
    – RobH
    Mar 9, 2016 at 11:19
  • 1
    \$\begingroup\$ Good call @RobH, but I'm thinking the fact that we're having this much trouble finding one in the framework may justify a custom one. \$\endgroup\$
    – RubberDuck
    Mar 9, 2016 at 11:24
  • 1
    \$\begingroup\$ @RubberDuck, maybe just create my own ObjectNotFoundException then? Since 'object' could refer to a database, user, domain account, role or permission. I won't be using Entity Framework in this project so hopefully it'll never clash. \$\endgroup\$
    – Jake
    Mar 9, 2016 at 14:12
2
\$\begingroup\$

Targeting only the ProcessRecord() method.

You have a flaw here, because if the given Name is not found then the resulting Collection<PSObject> from the call to ps.Invoke() won't likely contain any items, hence the foreach won't iterate. This will result in user being null.
The other thing I am concerned is, if it is possible that the user's name isn't unique, the user variable will be overwritten for each iteration of the loop.

If the Name is unique, you shouldn't use a foreach at all and change the code to

        var result = ps.Invoke();
        if (result.count == 0)
        {
            // Database user not found
            // write your error

            return;
        }
        // assuming unique `Name`
        user = (DatabaseUser)result[0].BaseObject;

I have replaced the soft cast with a regular cast because IMO we can be sure that a DatabaseUser is returned at this point.

\$\endgroup\$
1
  • \$\begingroup\$ That's actually pretty clever, thanks for that. Name is unique in this case because SQL Server won't let you create identical user names. \$\endgroup\$
    – Jake
    Mar 9, 2016 at 14:10

Your Answer

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

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