4
\$\begingroup\$

I have a function that generates a URL using a stringbuilder:

    internal String GenerateUrl()
    {
        StringBuilder urlBuilder = new StringBuilder("http://");
        urlBuilder.Append(IpAddress);
        urlBuilder.Append("/set.cmd?" + "user=" + Username + "+");
        urlBuilder.Append("pass=" + Password + "+");

        switch (Command)
        {
            case CommandType.Undefined:
                throw new ArgumentException("Command type has not been set");

            case CommandType.SetPower:
                urlBuilder.Append("cmd=setpower+");

                for (int i = 0; i < (Ports.Length-1); i++)
                {
                    urlBuilder.Append(BooleanToStringPortRemap(Ports[i], i + 1) + "+");
                }

                urlBuilder.Append(BooleanToStringPortRemap(Ports[Ports.Length - 1], Ports.Length));
                break;

            case CommandType.ReadPower:
                urlBuilder.Append("cmd=getpower");
                break;

            case CommandType.ReadCurrent:
                urlBuilder.Append("cmd=getcurrent");
                break;
        }

        return urlBuilder.ToString();
    }

This scores quite badly for maintainability and cyclomatic complexity. My question is, is there a better way to construct a URL or if not is there a better way to construct one using a Stringbuilder than what I am currently doing?

\$\endgroup\$
  • 2
    \$\begingroup\$ What kind or url uses a + as a separator? The only allowed ones are ; and & as far as I know. + is a url encoded space. \$\endgroup\$ – RobH Nov 13 '15 at 10:52
  • \$\begingroup\$ @RobH It looks like they are parameters send to a command-line tool or something like that. \$\endgroup\$ – BCdotWEB Nov 13 '15 at 13:11
  • 4
    \$\begingroup\$ Please tell me you aren't sending user credentials as URL parameters. Even if it's not over the internet. Even if it's not actually across a network, but just localhost. That password will be in the clear, in log files, in multiple places, several of which are not generally protected. User credentials should be sent in the body, over a properly-secured connection, using TLS. \$\endgroup\$ – anaximander Nov 13 '15 at 15:44
  • 1
    \$\begingroup\$ I would consider utilizing the Uri class. dotnetperls.com/uri You'll have to create an extension method to easily add parameters but it's not too bad. stackoverflow.com/a/10836145/1603799 \$\endgroup\$ – MiniRagnarok Nov 13 '15 at 16:35
  • \$\begingroup\$ 3 answers and the only mention of the URI class or the sending of plain text credentials is in the comments? \$\endgroup\$ – RubberDuck Nov 14 '15 at 11:38
12
\$\begingroup\$

This is a mess:

StringBuilder urlBuilder = new StringBuilder("http://");
urlBuilder.Append(IpAddress);
urlBuilder.Append("/set.cmd?" + "user=" + Username + "+");
urlBuilder.Append("pass=" + Password + "+");

Why concatenate when there's an AppendFormat method?

StringBuilder urlBuilder = new StringBuilder(1024);
urlBuilder.AppendFormat("http://{0}/set.cmd?user={1}+pass={2}+", IpAddress, Username, Password);

Considering the repeated use of the key=value pattern, I wonder: why not use a Dictionary<string, object>, fill that with all of the necessary values, and at the end use string.join to combine them all? Something like this:

var keyValues = new Dictionary<string, object>();
keyValues.Add("user", UserName);
keyValues.Add("pass", Password);
keyValues.Add("cmd", commandName);

return "?" + string.Join("+", keyValues.Select(x=>string.Format("{0}={1}", x.Key, x.Value)));
\$\endgroup\$
6
\$\begingroup\$
StringBuilder urlBuilder = new StringBuilder("http://");

Instead of using this constructor you should use the one taking the initial capacity as an argument. By default the StringBuilder is initialized with a capacity of 16 which is doubled each time the internal buffer is too small. Using a higher initial capacity will reduce the number of operations to increase the buffer size.

urlBuilder.Append(IpAddress);
urlBuilder.Append("/set.cmd?" + "user=" + Username + "+");
urlBuilder.Append("pass=" + Password + "+");

As you are using a StringBuilder you should use the provided fluent way to use its methods. The methods of the StringBuilder like e.g the Append() method returns the StringBuilder itself.

You should check whether the Command is CommandType.Undefined as the very first operation in that method; in this way no unneeded work will be done.

Instead of using Ports.Length-1 you should extract it to a variable. However, this could also be done in a better way if we knew how the BooleanToStringPortRemap() method looks.

internal String GenerateUrl()
{
    if (Command == CommandType.Undefined)
    {
        throw new ArgumentException("Command type has not been set");
    } 

    StringBuilder urlBuilder = new StringBuilder(1024);

    urlBuilder.Append("http://")
              .Append(IpAddress)
              .Append("/set.cmd?user=").Append(Username).Append("+")
              .Append("pass=").Append(Password).Append("+");

    switch (Command)
    {
        case CommandType.SetPower:

            urlBuilder.Append("cmd=setpower+");

            int portsCount = Ports.Length - 1;

            for (int i = 0; i < portsCount; i++)
            {
                urlBuilder.Append(BooleanToStringPortRemap(Ports[i], i + 1))
                          .Append("+");
            }

            urlBuilder.Append(BooleanToStringPortRemap(Ports[portsCount], Ports.Length));
            break;

        case CommandType.ReadPower:

            urlBuilder.Append("cmd=getpower");
            break;

        case CommandType.ReadCurrent:

            urlBuilder.Append("cmd=getcurrent");
            break;
    }

    return urlBuilder.ToString();
}
\$\endgroup\$
  • \$\begingroup\$ How have I not noticed StringBuilder's Fluent API before? \$\endgroup\$ – Nick Udell Dec 21 '15 at 14:39
4
\$\begingroup\$

Your cyclomatic complexity is high because of the switch statement! We can get rid of it using two solutions. The first one would be inheritance of command builders. But that's overkill, you don't want to have 3 classes for 3 builders that simply returns a string. But maybe you'd accept having 3 methods for 3 builders. (As long as you don't have too many ways to build an URL, that'd be good!)

I used some C#6 features but it's nothing can be changed a little to work with other versions of C#!

First, as @BCdotWEB wrote, you should use Format, which I did using String interpolation in C#6. (You can use String.Format if you can't use C#6!)

I created a test class to show you how I'd do it.

public class Test
{
    private readonly Dictionary<CommandType, Func<String>> commandBuilders;

    //Properties that you already have : IpAddress,Username,Password,Ports

    public Test()
    {
        commandBuilders = new Dictionary<CommandType, Func<String>>();
        commandBuilders.Add(CommandType.SetPower, BuildSetPower);
        commandBuilders.Add(CommandType.ReadPower, BuildReadPower);
        commandBuilders.Add(CommandType.ReadCurrent, BuildReadCurrent);
    }

    private string GetUrlBaseFormat() => $"http://{IpAddress}/set.cmd?user={Username}+pass={Password}+";

    private string BuildSetPower()
    {
        var urlBuilder = new StringBuilder(GetUrlBaseFormat());
        urlBuilder.Append("cmd=setpower+");

        for (int i = 0; i < (Ports.Length-1); i++)
        {
            urlBuilder.Append(BooleanToStringPortRemap(Ports[i], i + 1) + "+");
        }

        urlBuilder.Append(BooleanToStringPortRemap(Ports[Ports.Length - 1], Ports.Length));

        return urlBuilder.ToString();
    }

    private string BuildReadPower() => $"{GetUrlBaseFormat()}cmd=getpower";
    private string BuildReadCurrent() => $"{GetUrlBaseFormat()}cmd=getcurrent";

    internal String GenerateUrl()
    {
        Func<string> urlBuilder;
        bool builderExists = commandBuilders.TryGetValue(Command, out urlBuilder);

        if(!builderExists)
        {
            throw new ArgumentException("Command type has not been set");
        }

        return urlBuilder();
    }
}

That's a simple example I made that could work out well in your code. We use a Dictionary to hold which Func<String> to call when given a certain Command.

Now, each way to build an URL has its own method, most of them are one-liners.

This way, you don't have the switch in your code, and if you happen to have more ways to build URL your switch wouldn't grow in complexity! (But you'd have more methods, solution incoming!)

You could argue that I'm just moving the problem. You don't have an high complexity now, but you have more methods in your class. You could move these methods to a static class that would help to build URLs. And if that ever becomes a problem, you could use inheritance to have multiple instance of.. IUrlBuilder. These are the solutions to deal with your high cyclomatic complexity.

Finally, as said @Heslacher, if we knew what is inside the BooleanToStringPortRemap method, we could rework it to use something like String.Join instead of using a for loop to join your ports.

\$\endgroup\$
  • \$\begingroup\$ Can I ask what the $ operator is for? I have seen a few other posters using it as well. I am running VS2015 and this works fine, but when I build using VS2013 the $ operator seems to cause problems. Is it new to C#6? \$\endgroup\$ – JNH Nov 16 '15 at 14:28
  • \$\begingroup\$ The $ operator is used for String Interpolation, which is a C#6 feature. C#6 isn't available with VS2013 I think. \$\endgroup\$ – IEatBagels Nov 16 '15 at 14:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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