Using fluent assertions, I would like to assert that a given string contains either one of two strings:
actual.Should().Contain("oneWay").Or().Should().Contain("anotherWay");
// eiter value should pass the assertion.
// for example: "you may do it oneWay." should pass, but
// "you may do it thisWay." should not pass
Only if neither of the values is contained, the assertion should fail. This does NOT work (not even compile) as there is no Or() operator.
This is how I do it now:
bool isVariant1 = actual.Contains(#"oneWay");
bool isVariant2 = actual.Contains(#"anotherWay");
bool anyVariant = (isVariant1 || isVariant2);
anyVariant.Should().BeTrue("because blahblah. Actual content was: " + actual);
This is verbose, and the "because" argument must get created manually to have a meaningful output.
Is there a way to do this in a more readable manner? A solution should also apply to other fluent assertion types, like Be(), HaveCount() etc...
I am using FluentAssertions version 2.2.0.0 on .NET 3.5, if that matters.
I would make it an extension to the string assertions. Something like this:
public static void BeAnyOf(this StringAssertions assertions, string[] expectations, string because, params string[] args) {
Execute.Assertion
.ForCondition(expectations.Any(assertions.Subject.Contains))
.BecauseOf(because, args)
.FailWith("Expected {context:string} to be any of {0}{reason}", expectations);
}
You could even fork the repository and provide me with a Pull Request to make it part of the next version.
Should not this work?
actual.Should().BeOneOf("oneWay", "anotherWay");
Worked for me using v3.1.229.
You could make it a little more readable by writing a simple string extension:
public static class StringExt
{
public static bool ContainsAnyOf(this string self, params string[] strings)
{
return strings.Any(self.Contains);
}
}
Then you could do this:
actual.ContainsAnyOf("oneWay", "anotherWay").Should().BeTrue("because of this reason");
Unfortunately this doesn't help with the "reason" part of the message, but I think it's a little better.
Related
I'm trying to create a simple way to test string parameters in unit tests, for most string parameters, I want to check the behaviour when the parameter is Null, Empty or consists only of whitespaces.
In most of the cases, I'm checking the parameters with string.IsNullOrWhiteSpace(), and throw an exception if it has one of those three values.
Now for unit testing, it seems I have to write, for every string parameter, three unit tests. One for null values, one for empty values and one for Whitespaces.
Imagine a method with 3 or 4 string parameters, then I need to write 9 or 12 unit tests...
Can anyone come up with a simple way to test this? Maybe using AutoFixture?
To avoid duplicating the same test multiple times you can write a parameterized test.
If you're using xUnit, you would write a so called theory. A theory means you're proving a principle, that is that a certain function behaves as expected when given different samples of the same kind of input data. For example:
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Should_throw_argument_exception_when_input_string_is_invalid(string input)
{
Assert.Throws<ArgumentException>(() => SystemUnderTest.SomeFunction(input));
}
The xUnit runner will run this test multiple times, each time assigning the input parameter to one of the values specified in the [InlineData] attribute.
If the function being tested has more than one parameter, you might not care what values are passed to the remaining ones, as long as at least one of them is a string that's either null, empty or contains only whitespace.
In this case, you can combine parameterized tests with AutoFixture. AutoFixture is designed to give you general-purpose test data that is good enough to use in most scenarios when you don't necessarily care about what the exact values are.
In order to use it with xUnit theories, you'll need to add the AutoFixture.Xunit NuGet package (or AutoFixture.Xunit2 depending on which version you're using) to your test project and use the InlineAutoData attribute:
[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_first_input_string_is_invalid(
string input1,
string input2,
string input3)
{
Assert.Throws<ArgumentException>(() =>
SystemUnderTest.SomeFunction(input1, input2, input3));
}
Only the input1 parameter will have a specific value, while the rest will be assigned random strings by AutoFixture.
One thing to note here is that the values passed through the [InlineAutoData] attribute are assigned to the test parameters based on their position. Since we need to test the same behavior for all three parameters individually, we'll have to write three tests:
[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_second_input_string_is_invalid(
string input2,
string input1,
string input3)
{
Assert.Throws<ArgumentException>(() =>
SystemUnderTest.SomeFunction(input1, input2, input3));
}
[Theory]
[InlineAutoData(null)]
[InlineAutoData("")]
[InlineAutoData(" ")]
public void Should_throw_argument_exception_when_the_third_input_string_is_invalid(
string input3,
string input1,
string input2)
{
Assert.Throws<ArgumentException>(() =>
SystemUnderTest.SomeFunction(input1, input2, input3));
}
A word on property-based testing
I can't help but think that these kinds of test scenarios are a perfect match for property-based testing. Without going into too much detail, property-based testing is about proving a specific behavior (or "property") of a function by running it multiple times with generated input.
In other words:
Property-based tests make statements about the output of your code
based on the input, and these statements are verified for many
different possible inputs.
In your case, you could write a single test to verify that the function throws an ArgumentException whenever at least one of the arguments is either null, an empty string or a string that only contains whitespace.
In .NET, you can use a library called FsCheck to write and execute property-based tests. While the API is primarily designed to be used from F#, it can also be used from C#.
However, in this particular scenario, I think you're better off sticking with regular parameterized tests and AutoFixture to achieve the same goal. In fact, writing these tests with FsCheck and C# would end up being more verbose and wouldn't really buy you much in terms of robustness.
Update: AutoFixture.Idioms
As #RubenBartelink pointed out in the comments, there is another option. AutoFixture encapsulates common assertions in a small library called AutoFixture.Idioms. You can use it to centralize the expectations on how the string parameters are validated by a method and use them in your tests.
While I do have my reservations on this approach, I'll add it here as another possible solution for the sake of completeness:
[Theory, AutoData]
public void Should_throw_argument_exception_when_the_input_strings_are_invalid(
ValidatesTheStringArguments assertion)
{
var sut = typeof(SystemUnderTest).GetMethod("SomeMethod");
assertion.Verify(sut);
}
public class ValidatesTheStringArguments : GuardClauseAssertion
{
public ValidatesTheStringArguments(ISpecimenBuilder builder)
: base(
builder,
new CompositeBehaviorExpectation(
new NullReferenceBehaviorExpectation(),
new EmptyStringBehaviorExpectation(),
new WhitespaceOnlyStringBehaviorExpectation()))
{
}
}
public class EmptyStringBehaviorExpectation : IBehaviorExpectation
{
public void Verify(IGuardClauseCommand command)
{
if (!command.RequestedType.IsClass
&& !command.RequestedType.IsInterface)
{
return;
}
try
{
command.Execute(string.Empty);
}
catch (ArgumentException)
{
return;
}
catch (Exception e)
{
throw command.CreateException("empty", e);
}
throw command.CreateException("empty");
}
}
public class WhitespaceOnlyStringBehaviorExpectation : IBehaviorExpectation
{
public void Verify(IGuardClauseCommand command)
{
if (!command.RequestedType.IsClass
&& !command.RequestedType.IsInterface)
{
return;
}
try
{
command.Execute(" ");
}
catch (ArgumentException)
{
return;
}
catch (Exception e)
{
throw command.CreateException("whitespace", e);
}
throw command.CreateException("whitespace");
}
}
Based on the expectations expressed in NullReferenceBehaviorExpectation, EmptyStringBehaviorExpectation, and WhitespaceOnlyStringBehaviorExpectation AutoFixture will automatically attempt to invoke the method named "SomeMethod" with null, empty strings and whitespace respectively.
If the method doesn't throw the correct exception – as specified in the catch block inside the expectation classes – then AutoFixture will itself throw an exception explaining what happend. Here's an example:
An attempt was made to assign the value whitespace to the parameter
"p1" of the method "SomeMethod", and no Guard Clause prevented this.
Are you missing a Guard Clause?
You can also use AutoFixture.Idioms without parameterized tests by simply instantiating the objects yourself:
[Fact]
public void Should_throw_argument_exception_when_the_input_strings_are_invalid()
{
var assertion = new ValidatesTheStringArguments(new Fixture());
var sut = typeof(SystemUnderTest).GetMethod("SomeMethod");
assertion.Verify(sut);
}
I am implementing a caching layer between my database and my C# code. The idea is to cache the results of certain DB queries based on the parameters to the query. The database is using the default collation - either SQL_Latin1_General_CP1_CI_AS or Latin1_General_CI_AS, which I believe based on some brief googling are equivalent for equality, just different for sorting.
I need a .NET StringComparer that can give me the same behavior, at least for equality testing and hashcode generation, as the database's collation is using. The goal is to be able to use the StringComparer in a .NET dictionary in C# code to determine whether a particular string key is already in the cache or not.
A really simplified example:
var comparer = StringComparer.??? // What goes here?
private static Dictionary<string, MyObject> cache =
new Dictionary<string, MyObject>(comparer);
public static MyObject GetObject(string key) {
if (cache.ContainsKey(key)) {
return cache[key].Clone();
} else {
// invoke SQL "select * from mytable where mykey = #mykey"
// with parameter #mykey set to key
MyObject result = // object constructed from the sql result
cache[key] = result;
return result.Clone();
}
}
public static void SaveObject(string key, MyObject obj) {
// invoke SQL "update mytable set ... where mykey = #mykey" etc
cache[key] = obj.Clone();
}
The reason it's important that the StringComparer matches the database's collation is that both false positives and false negatives would have bad effects for the code.
If the StringComparer says that two keys A and B are equal when the database believes they are distinct, then there could be two rows in the database with those two keys, but the cache will prevent the second one ever getting returned if asked for A and B in succession - because the get for B will incorrectly hit the cache and return the object that was retrieved for A.
The problem is more subtle if the StringComparer says that A and B are different when the database believes they are equal, but no less problematic. GetObject calls for both keys would be fine, and return objects corresponding to the same database row. But then calling SaveObject with key A would leave the cache incorrect; there would still be a cache entry for key B that has the old data. A subsequent GetObject(B) would give outdated information.
So for my code to work correctly I need the StringComparer to match the database behavior for equality testing and hashcode generation. My googling so far has yielded lots of information about the fact that SQL collations and .NET comparisons are not exactly equivalent, but no details on what the differences are, whether they are limited to only differences in sorting, or whether it is possible to find a StringComparer that is equivalent to a specific SQL collation if a general-purpose solution is not needed.
(Side note - the caching layer is general purpose, so I cannot make particular assumptions about what the nature of the key is and what collation would be appropriate. All the tables in my database share the same default server collation. I just need to match the collation as it exists)
I've recently faced with the same problem: I need an IEqualityComparer<string> that behaves in SQL-like style. I've tried CollationInfo and its EqualityComparer. If your DB is always _AS (accent sensitive) then your solution will work, but in case if you change the collation that is AI or WI or whatever "insensitive" else the hashing will break.
Why? If you decompile Microsoft.SqlServer.Management.SqlParser.dll and look inside you'll find out that CollationInfo internally uses CultureAwareComparer.GetHashCode (it's internal class of mscorlib.dll) and finally it does the following:
public override int GetHashCode(string obj)
{
if (obj == null)
throw new ArgumentNullException("obj");
CompareOptions options = CompareOptions.None;
if (this._ignoreCase)
options |= CompareOptions.IgnoreCase;
return this._compareInfo.GetHashCodeOfString(obj, options);
}
As you can see it can produce the same hashcode for "aa" and "AA", but not for "äå" and "aa" (which are the same, if you ignore diacritics (AI) in majority of cultures, so they should have the same hashcode). I don't know why the .NET API is limited by this, but you should understand where the problem can come from.
To get the same hashcode for strings with diacritics you can do the following: create implementation of IEqualityComparer<T> implementing the GetHashCode that will call appropriate CompareInfo's object's GetHashCodeOfString via reflection because this method is internal and can't be used directly. But calling it directly with correct CompareOptions will produce the desired result:
See this example:
static void Main(string[] args)
{
const string outputPath = "output.txt";
const string latin1GeneralCiAiKsWs = "Latin1_General_100_CI_AI_KS_WS";
using (FileStream fileStream = File.Open(outputPath, FileMode.Create, FileAccess.Write))
{
using (var streamWriter = new StreamWriter(fileStream, Encoding.UTF8))
{
string[] strings = { "aa", "AA", "äå", "ÄÅ" };
CompareInfo compareInfo = CultureInfo.GetCultureInfo(1033).CompareInfo;
MethodInfo GetHashCodeOfString = compareInfo.GetType()
.GetMethod("GetHashCodeOfString",
BindingFlags.Instance | BindingFlags.NonPublic,
null,
new[] { typeof(string), typeof(CompareOptions), typeof(bool), typeof(long) },
null);
Func<string, int> correctHackGetHashCode = s => (int)GetHashCodeOfString.Invoke(compareInfo,
new object[] { s, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0L });
Func<string, int> incorrectCollationInfoGetHashCode =
s => CollationInfo.GetCollationInfo(latin1GeneralCiAiKsWs).EqualityComparer.GetHashCode(s);
PrintHashCodes(latin1GeneralCiAiKsWs, incorrectCollationInfoGetHashCode, streamWriter, strings);
PrintHashCodes("----", correctHackGetHashCode, streamWriter, strings);
}
}
Process.Start(outputPath);
}
private static void PrintHashCodes(string collation, Func<string, int> getHashCode, TextWriter writer, params string[] strings)
{
writer.WriteLine(Environment.NewLine + "Used collation: {0}", collation + Environment.NewLine);
foreach (string s in strings)
{
WriteStringHashcode(writer, s, getHashCode(s));
}
}
The output is:
Used collation: Latin1_General_100_CI_AI_KS_WS
aa, hashcode: 2053722942
AA, hashcode: 2053722942
äå, hashcode: -266555795
ÄÅ, hashcode: -266555795
Used collation: ----
aa, hashcode: 2053722942
AA, hashcode: 2053722942
äå, hashcode: 2053722942
ÄÅ, hashcode: 2053722942
I know it looks like the hack, but after inspecting decompiled .NET code I'm not sure if there any other option in case the generic functionality is needed.
So be sure that you'll not fall into trap using this not fully correct API.
UPDATE:
I've also created the gist with potential implementation of "SQL-like comparer" using CollationInfo.
Also there should be paid enough attention where to search for "string pitfalls" in your code base, so if the string comparison, hashcode, equality should be changed to "SQL collation-like" those places are 100% will be broken, so you'll have to find out and inspect all the places that can be broken.
UPDATE #2:
There is better and cleaner way to make GetHashCode() treat CompareOptions. There is the class SortKey that works correctly with CompareOptions and it can be retrieved using
CompareInfo.GetSortKey(yourString, yourCompareOptions).GetHashCode()
Here is the link to .NET source code and implementation.
UPDATE #3:
If you're on .NET Framework 4.7.1+ you should use new GlobalizationExtensions class as proposed by this recent answer.
Take a look at the CollationInfo class. It is located in an assembly called Microsoft.SqlServer.Management.SqlParser.dll although I am not totally sure where to get this. There is a static list of Collations (names) and a static method GetCollationInfo (by name).
Each CollationInfo has a Comparer. It is not exactly the same as a StringComparer but has similar functionality.
EDIT: Microsoft.SqlServer.Management.SqlParser.dll is a part of the Shared Management Objects (SMO) package. This feature can be downloaded for SQL Server 2008 R2 here:
http://www.microsoft.com/download/en/details.aspx?id=16978#SMO
EDIT: CollationInfo does have a property named EqualityComparer which is an IEqualityComparer<string>.
The following is much simpler:
System.Globalization.CultureInfo.GetCultureInfo(1033)
.CompareInfo.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth)
It comes from https://learn.microsoft.com/en-us/dotnet/api/system.globalization.globalizationextensions?view=netframework-4.8
It computes the hashcode correctly given the options given.
You'll still have to trim trailing spaces manually, as they as discarded by ANSI sql but not in .net
Here is a wrapper that trims spaces.
using System.Collections.Generic;
using System.Globalization;
namespace Wish.Core
{
public class SqlStringComparer : IEqualityComparer<string>
{
public static IEqualityComparer<string> Instance { get; }
private static IEqualityComparer<string> _internalComparer =
CultureInfo.GetCultureInfo(1033)
.CompareInfo
.GetStringComparer(CompareOptions.IgnoreCase | CompareOptions.IgnoreKanaType | CompareOptions.IgnoreWidth);
private SqlStringComparer()
{
}
public bool Equals(string x, string y)
{
//ANSI sql doesn't consider trailing spaces but .Net does
return _internalComparer.Equals(x?.TrimEnd(), y?.TrimEnd());
}
public int GetHashCode(string obj)
{
return _internalComparer.GetHashCode(obj?.TrimEnd());
}
static SqlStringComparer()
{
Instance = new SqlStringComparer();
}
}
}
SQL Server's Server.GetStringComparer may be of some use.
I have the following xmlnode (string) whose value would be of few given type i.e:
"Ansi_nulls","Encrypted","Quoted_identifer" etc.
I want to test the xmlnode using xmlNode.Contains ("xxx","yyy") ..
What is the correct syntax ?
If you are testing the full (complete) value of the node, you can do it by reversing the call; check that a list of known values contains your current node value:
new[]{"Ansi_nulls","Encrypted","Quoted_identifer", ...}.Contains(xmlNode);
I would create an extension method using sehes solution:
public static bool Contains(this string source, params string[] values)
{
return values.Any(value => source.Contains(value));
}
That way you can call:
xmlNode.Contains("string", "something else");
Contains takes a string to test for.
One way to solve your problem would be to use a simple regular expression
using System.Text.RegularExpressions;
if (Regex.IsMatch(nodevalue, "(Ansi_nulls|Encrypted|Quoted_identifer)")...
if (new[] {"xxx","yyy"}.Any(n => xmlNode.Contains(n)))
A simple if statement will work:
if (xmlNode.Contains("xxx") && xmlNode.Contains("yyy"))
{
// your work
}
I have a unit test to check whether a method returns the correct IEnumerable. The method builds the enumerable using yield return. The class that it is an enumerable of is below:
enum TokenType
{
NUMBER,
COMMAND,
ARITHMETIC,
}
internal class Token
{
public TokenType type { get; set; }
public string text { get; set; }
public static bool operator == (Token lh, Token rh) { return (lh.type == rh.type) && (lh.text == rh.text); }
public static bool operator != (Token lh, Token rh) { return !(lh == rh); }
public override int GetHashCode()
{
return text.GetHashCode() % type.GetHashCode();
}
public override bool Equals(object obj)
{
return this == (Token)obj;
}
}
This is the relevant part of the method:
foreach (var lookup in REGEX_MAPPING)
{
if (lookup.re.IsMatch(s))
{
yield return new Token { type = lookup.type, text = s };
break;
}
}
If I store the result of this method in actual, make another enumerable expected, and compare them like this...
Assert.AreEqual(expected, actual);
..., the assertion fails.
I wrote an extension method for IEnumerable that is similar to Python's zip function (it combines two IEnumerables into a set of pairs) and tried this:
foreach(Token[] t in expected.zip(actual))
{
Assert.AreEqual(t[0], t[1]);
}
It worked! So what is the difference between these two Assert.AreEquals?
Found it:
Assert.IsTrue(expected.SequenceEqual(actual));
Have you considered using the CollectionAssert class instead...considering that it is intended to perform equality checks on collections?
Addendum:
If the 'collections' being compared are enumerations, then simply wrapping them with 'new List<T>(enumeration)' is the easiest way to perform the comparison. Constructing a new list causes some overhead of course, but in the context of a unit test this should not matter too much I hope?
Assert.AreEqual is going to compare the two objects at hand. IEnumerables are types in and of themselves, and provide a mechanism to iterate over some collection...but they are not actually that collection. Your original comparison compared two IEnumerables, which is a valid comparison...but not what you needed. You needed to compare what the two IEnumerables were intended to enumerate.
Here is how I compare two enumerables:
Assert.AreEqual(t1.Count(), t2.Count());
IEnumerator<Token> e1 = t1.GetEnumerator();
IEnumerator<Token> e2 = t2.GetEnumerator();
while (e1.MoveNext() && e2.MoveNext())
{
Assert.AreEqual(e1.Current, e2.Current);
}
I am not sure whether the above is less code than your .Zip method, but it is about as simple as it gets.
I think the simplest and clearest way to assert the equality you want is a combination of the answer by jerryjvl and comment on his post by MEMark - combine CollectionAssert.AreEqual with extension methods:
CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray());
This gives richer error information than the SequenceEqual answer suggested by the OP (it will tell you which element was found that was unexpected). For example:
IEnumerable<string> expected = new List<string> { "a", "b" };
IEnumerable<string> actual = new List<string> { "a", "c" }; // mismatching second element
CollectionAssert.AreEqual(expected.ToArray(), actual.ToArray());
// Helpful failure message!
// CollectionAssert.AreEqual failed. (Element at index 1 do not match.)
Assert.IsTrue(expected.SequenceEqual(actual));
// Mediocre failure message:
// Assert.IsTrue failed.
You'll be really pleased you did it this way if/when your test fails - sometimes you can even know what's wrong without having to break out the debugger - and hey you're doing TDD right, so you write a failing test first, right? ;-)
The error messages get even more helpful if you're using AreEquivalent to test for equivalence (order doesn't matter):
CollectionAssert.AreEquivalent(expected.ToList(), actual.ToList());
// really helpful error message!
// CollectionAssert.AreEquivalent failed. The expected collection contains 1
// occurrence(s) of <b>. The actual collection contains 0 occurrence(s).
Take this non-compiling code for instance:
public string GetPath(string basefolder, string[] extraFolders)
{
string version = Versioner.GetBuildAndDotNetVersions();
string callingModule = StackCrawler.GetCallingModuleName();
return AppendFolders(basefolder, version, callingModule, extraFolders);
}
private string AppendFolders(params string[] folders)
{
string outstring = folders[0];
for (int i = 1; i < folders.Length; i++)
{
string fixedPath = folders[i][0] == '\\' ? folders[i].Substring(1) : folders[i];
Path.Combine(outstring, fixedPath);
}
return outstring;
}
This example is a somewhat simplified version of testing code I am using. Please, I am only interested in solutions having directly to do with the param keyword. I know how lists and other similar things work.
Is there a way to "explode" the extraFolders array so that it's contents can be passed into AppendFolders along with other parameters?
Just pass it. The folders parameter is an array first. the "params" functionality is a little bit of compiler magic, but it's not required.
AppendFolders(extraFolders);
Now, it this particulat instance, you'll have to add some things to that array, first.
List<string> lstFolders = new List<string>(extraFolders);
lstFolder.Insert(0, callingModule);
lstFolder.Insert(0, version);
lstFolder.Insert(0, basefolder);
return AppendFolders(lstFolders.ToArray());
I'll quibble with the term "collapse", since it seems you really want to "expand". And I'm not sure what you mean by solutions "having directly to do with params keyword" and that "you're not interested in workarounds". In the end, you either have to pass a number of strings - which the compiler will magically package into an array - or an array of strings directly. That being said, my solution (without changing the interface) would go something like:
return AppendFolders(new string[] { basefolder, version, callingModule }.Concat(extraFolders).ToArray());
Edit:
While you can't add an operator via extension methods, you could do:
return AppendFolders(new string[] { baseFolder, callingModuleName, version }.Concat(extraFolders));
public static T[] Concat<T>(this T[] a, T[] b) {
return ((IEnumerable<T>)a).Concat(b).ToArray();
}
But, if we're going to go that far - might as well just extend List<T> to handle this elegantly:
return AppendFolders(new Params<string>() { baseFolder, callingModuleName, version, extraFolders });
class Params<T> : List<T> {
public void Add(IEnumerable<T> collection) {
base.AddRange(collection);
}
public static implicit operator T[](Params<T> a) {
return a.ToArray();
}
}
One option is to make the params parameter an object[]:
static string appendFolders(params object[] folders)
{ return (string) folders.Aggregate("",(output, f) =>
Path.Combine( (string)output
,(f is string[])
? appendFolders((object[])f)
: ((string)f).TrimStart('\\')));
}
If you want something more strongly-typed, another option is to create a custom union type with implicit conversion operators:
static string appendFolders(params StringOrArray[] folders)
{ return folders.SelectMany(x=>x.AsEnumerable())
.Aggregate("",
(output, f)=>Path.Combine(output,f.TrimStart('\\')));
}
class StringOrArray
{ string[] array;
public IEnumerable<string> AsEnumerable()
{ return soa.array;}
public static implicit operator StringOrArray(string s)
{ return new StringOrArray{array=new[]{s}};}
public static implicit operator StringOrArray(string[] s)
{ return new StringOrArray{array=s};}
}
In either case, this will compile:
appendFolders("base", "v1", "module", new[]{"debug","bin"});
A quick and dirty solution would be to build a List<string> from the items and then pass that (with ToArray()).
Note that you don't need to test for the backslash. Path.Combine handles the dirty things rather fine.
I think OregonGhost's answer is probably the way you want to go. Just to elaborate on it, he's suggesting doing something like this:
public string GetPath(string basefolder, string[] extraFolders)
{
string version = Versioner.GetBuildAndDotNetVersions();
string callingModule = StackCrawler.GetCallingModuleName();
List<string> parameters = new List<string>(extraFolders.Length + 3);
parameters.Add(basefolder);
parameters.Add(version);
parameters.Add(callingModule);
parameters.AddRange(extraFolders);
return AppendFolders(parameters.ToArray());
}
And I don't mean that as a lesson on how to use Lists, just as a little clarification for anybody who may come along looking for the solution in the future.