Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ShouldBeEquivalentTo case insensitive property match #254

Closed
nick-randal opened this issue Apr 8, 2015 · 12 comments
Closed

ShouldBeEquivalentTo case insensitive property match #254

nick-randal opened this issue Apr 8, 2015 · 12 comments

Comments

@nick-randal
Copy link

How can I use ShouldBeEquivalentTo case insensitive property match. i.e. A.PersonId and B.PersonID.

@dennisdoomen
Copy link
Member

dennisdoomen commented Apr 8, 2015

Not out of the box. But what you can do is implement IMemberMatchingRule and pass that to the Using() call on the options parameter of ShouldBeEquivalentTo.

@dennisdoomen dennisdoomen added this to the v3.5 milestone Jun 23, 2015
@dennisdoomen dennisdoomen removed this from the v3.5 milestone Aug 3, 2015
@deadlydog
Copy link

deadlydog commented Nov 21, 2019

Any progress on this? I feel like it's a common enough scenario that it would be nice to have an out-of-the-box solution for it.

Also the link provided above is broken :(

@JSkimming
Copy link

I've just needed the same thing. If it helps anyone, here's my implementation that ignores case when matching properties.

public class CaseInsensitiveMatch : IMemberMatchingRule
{
    public SelectedMemberInfo Match(
        SelectedMemberInfo expectedMember,
        object subject,
        string memberPath,
        IEquivalencyAssertionOptions config)
    {
        List<PropertyInfo> properties =
            subject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
                .Where(pi => pi.Name.Equals(expectedMember.Name, StringComparison.OrdinalIgnoreCase))
                .ToList();

        PropertyInfo property = properties.Count > 1
            ? properties.SingleOrDefault(p => p.PropertyType == expectedMember.MemberType)
            : properties.SingleOrDefault();

        SelectedMemberInfo match = SelectedMemberInfo.Create(property);
        return match;
    }
}

@deadlydog
Copy link

Cool, thanks for the code @JSkimming :)

I also went with my own implementation that I'll share here. I chose to just not use the BeEquivalentTo method and just used this bit I found on Stack Overflow.

actualStringList.Should().Equal(expectedStringList, (s1, s2) => string.Equals(s1, s2, StringComparison.OrdinalIgnoreCase));

@rynkevich
Copy link
Contributor

I've been thinking on this for some time and can't come up with a case where this could be really useful, as usually you won't compare two object of different types. You create an expected object of the same type as actual and compare them. If you can't create it, you make your own class with the same member names that you have in actual. @nick-randal @JSkimming @deadlydog, can you please share your use cases for this feature?

However, if this feature is to be implemented, I'd like to have an ability to perform case-insensitive matches not just considering letters in lower and upper case equivalent, but also ignoring the differences in naming conventions like snake_case or CamelCase. This may look like

foo.Should().BeEquivalentTo(bar, options => options.IgnoringCaseInMemberNames(ignoreUnderscores: true));

Since cases like SCREAMING_SNAKE_CASE, camelCase would be covered by default and kebab-case could not be used to create a valid C# identifier, the only thing required to support such comparison is to ignore underscores during the comparison. However, there's a corner case with identifiers like _foo, __Bar, Baz__ etc.

@JSkimming
Copy link

JSkimming commented Jun 3, 2020

@rynkevich the use case is when I want to compare two objects where I either can't or don't want to change one to be consistent with the other.

For instance, if I'm using a third-party library to retrieve some data, and then save it to an Entity Framework DB Context.

The third part library has an object with the properties FirstName and Lastname, but my existing EF models use Firstname and Lastname.

I could:

  1. Change my EF models to use the inconsistent naming convention of the third party library.
  2. Make FluentAssertions compare properties ignoring case.
  3. Create an expected object just for my tests to compare against my actual.

I prefer option 2 because I don't want to propagate the inconsistent naming convention to my models, and it is less work to maintain the tests than option 3. BTW, I usually adopt option 3 where the source and destination objects are suitably different. But for a simple case where only the case is different, option 2 makes for simpler and more maintainable tests.

I can also imagine a scenario where the third party library names things with underscores (I've found this some generated third party libraries that are built on frameworks that use the underscore convention). I agree that it would also be useful. But the simple case adding an option to enable case insensitive appears, on the surface, to be a relatively simple option to allow.

@rynkevich
Copy link
Contributor

@JSkimming, thanks for the example. I guess, it's all about just providing a more convenient API for specific usage scenarios. The question is don't we try to make FluentAssertions perform more and more complex mapping logic? This isn't AutoMapper or something.

@dennisdoomen, could you share your thoughts on that?

Speaking about third party libraries with strange conventions, an example that comes to my mind is PayPal SDK. They use underscores for property names and that could be where this feature could be applied, if you write some abstration.

@dennisdoomen
Copy link
Member

I think it's a fair question to ask for something like that, but considering its edgy scenarios, I don't think it really belongs in the library. So I would not reject a PR that tries to implement it, but I also don't feel the need to do this myself.

@nick-randal
Copy link
Author

nick-randal commented Jun 4, 2020

Its fair to say you should not have to implement it but I don't agree that it doesn't belong. As the .net landscape has changed heavily to open source, we are going to have collisions with 3rd party interfaces and coding styles. I don't think it would be out of character for this highly utilitarian library to avoid being opinionated and not put the burden on the developer to find a work around for this casing problem.

Sending a PR into highly developed library is always intimidating. I am will to give it a shot but @dennisdoomen
can you at least tell me if I am headed in the right direction? Building off of @JSkimming example.

public static class CaseInsensitivePropertyMatchExtensions
{
	public static EquivalencyAssertionOptions<T> CaseInsensitiveProperties<T>(this EquivalencyAssertionOptions<T> options)
	{
		options.Using(new CaseInsensitivePropertyMatch());
		return options;
	}
}

public sealed class CaseInsensitivePropertyMatch : IMemberMatchingRule
{
	public SelectedMemberInfo Match(
		SelectedMemberInfo expectedMember,
		object subject,
		string memberPath,
		IEquivalencyAssertionOptions config)
	{
		var subjectType = subject.GetType();
		var expectedKey = $"{subjectType.FullName}.{expectedMember.Name}";

		if (KeyedMembers.TryGetValue(expectedKey, out var selectedMemberInfo))
			return selectedMemberInfo;
		
		var properties = subjectType
			.GetProperties(BindingFlags.Public | BindingFlags.Instance)
			.ToList();

		foreach (var property in properties)
		{
			var propertyKey = $"{subjectType.FullName}.{property.Name}";
			KeyedMembers.TryAdd(propertyKey, SelectedMemberInfo.Create(property));
		}

		if (KeyedMembers.TryGetValue(expectedKey, out selectedMemberInfo))
			return selectedMemberInfo;

		throw new InvalidOperationException($"Failed to find member {expectedMember.Name} in {expectedMember.DeclaringType.FullName} for case-insensitive member equivalency match.");
	}

	private static readonly ConcurrentDictionary<string, SelectedMemberInfo> KeyedMembers = new ConcurrentDictionary<string, SelectedMemberInfo>(StringComparer.OrdinalIgnoreCase);
}

@rynkevich
Copy link
Contributor

@nick-randal, here's my two cents:

  • This feature probably should be more than just case insensitive match for properties. To support other scenarios also addressed by other features in library we should check IEquivalencyAssertionOptions to see which members have to be included in match. There's an example here.
  • A new configuration method could be added directly to EquivalencyAssertionOptions.

@dennisdoomen
Copy link
Member

Yep. the direction is fine and the remarks from @rynkevich are completely valid.

@dennisdoomen
Copy link
Member

This is solved by #1742

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants