3
\$\begingroup\$

I'm looking for some opinions on improvement/technique of code in C# calling to a Neo4j database to perform a multi-faceted query looking for users. My goals were to lookup by first/last name, phone number, birthday, and e-mail address. I wanted case insensitivity, and I want the query to respect certain privacy settings that users can set (i.e. can users look me up via phone number?), and a block list.

I use regexes and some CultureInfo code to put things into a standard format, but I feel that my code is a little bloated and could be made smaller or more optimized.

// Social Function, Non-Throwing //
// Searches for any and all users matching full name, first name, last name, e-mail, birth date, or phone number in the given search query.
private static Regex _emailRegex = new Regex("[-0-9a-zA-Z.+_]+@[-0-9a-zA-Z.+_]+\\.[a-zA-Z]{2,4}");
private static Regex _dateRegex = new Regex(@"^((0?[13578]|10|12)(-|\/)(([1-9])|(0[1-9])|([12])([0-9]?)|(3[01]?))(-|\/)((19)([2-9])(\d{1})|(20)([01])(\d{1})|([8901])(\d{1}))|(0?[2469]|11)(-|\/)(([1-9])|(0[1-9])|([12])([0-9]?)|(3[0]?))(-|\/)((19)([2-9])(\d{1})|(20)([01])(\d{1})|([8901])(\d{1})))$");
private static Regex _phoneRegex = new Regex("[0-9]+");
public List<User> Find(string query)
{
    // internal search query
    List<User> results = doFind(query);

    // return empty results if we found no one
    if (results.Count == 0)
        return results;

    // remove users from results if we blocked them or they blocked us
    results.RemoveAll(x => x.IsBlocked(this.Id));

    return results;
}

private static List<User> doFind(string query)
{      
    query = query.Trim();

    if (_emailRegex.IsMatch(query)) // input was an e-mail address
    {
        try 
        {
            return Graph.Instance.Cypher
                .Match("(user:User)")
                .Where((User user) => user.Email == query)                        
                .AndWhere("user.Privacy_AllowEMailLookup = true")
                .Return(user => user.As<User>())
                .Results.ToList();
        }
        catch { return new List<User>(); }
    }
    else if (_dateRegex.IsMatch(query.Replace(".","/").Replace("-","/"))) // input was a birthday
    {
        query = query.Replace(".","/").Replace("-","/");

        DateTimeOffset dto = DateTimeOffset.Parse(query);

        try
        {
            return Graph.Instance.Cypher
                .Match("(user:User)")
                .Where((User user) => user.DateOfBirth.ToString() == dto.Date.ToString())                        
                .Return(user => user.As<User>())
                .Results.ToList();
        }
        catch { return new List<User>(); }
    }
    else if (_phoneRegex.IsMatch(query.Replace("(","").Replace(")","").Replace("-","").Trim()))
    {
        try
        {
            query = query.Replace("(", "").Replace(")", "").Replace("-", "").Trim();

            return Graph.Instance.Cypher
                .Match("(user:User)")
                .Where((User user) => user.PhoneNumber == query)
                .AndWhere("user.Privacy_AllowPhoneLookup = true")
                .Return(user => user.As<User>())
                .Results.ToList();
        }
        catch { return new List<User>(); }
    }
    else 
    {
        try
        {
            query = WebUtility.HtmlDecode(query);
            query = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(query);

            ICypherFluentQuery q = Graph.Instance.Cypher
                .Match("(user:User)")
                .Where((User user) => user.FullName == query)
                .OrWhere((User user) => user.FirstName == query)
                .OrWhere((User user) => user.LastName == query)
                .OrWhere((User user) => user.Handle == query.ToLower())
                .OrWhere((User user) => user.Id.ToString() == query);

            List<string> tokens = query.Split(new Char[] { ' ', ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();

            if (tokens != null && tokens.Count > 0)
            {
                foreach (string token in tokens)
                {
                    string t = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(token);

                    q = q.OrWhere((User user) => user.FullName == t)
                    .OrWhere((User user) => user.FirstName == t)
                    .OrWhere((User user) => user.LastName == t)
                    .OrWhere((User user) => user.Handle == t)
                    .OrWhere((User user) => user.Id.ToString() == t.ToLower());
                }
            }

            return q
                .ReturnDistinct(user => user.As<User>())
                .Results.ToList();

        }
        catch (Exception ex)
        {
            Logger.Error(ex);
            return new List<User>();
        }
    }
}
\$\endgroup\$
0
2
\$\begingroup\$

doFind should be PascalCase.


I would make each of those cases their own method, e.g. GetUserByEmail etc. That way your 100+ lines long doFind becomes more manageable, especially if there are new filters added in the future.


I see a lot of repeated code:

Graph.Instance.Cypher
.Match("(user:User)")

And:

.Return(user => user.As<User>())
.Results.ToList();

Isn't it possible to keep this logic, as well as the try...catch in the doFind and have the various checks return an Expression or a Query that you then can use, so you'd end up with something like this:

private static List<User> doFind(string query)
{      
    ICypherFluentQuery whereExpression;

    if (_emailRegex.IsMatch(query)) // input was an e-mail address
    {
        whereExpression = GetUsersByEmail();
    }
    else if (_dateRegex.IsMatch(query.Replace(".","/").Replace("-","/"))) // input was a birthday
    {
        whereExpression = GetUsersByBirthDay();
    }
    else if (_phoneRegex.IsMatch(query.Replace("(","").Replace(")","").Replace("-","").Trim()))
    {
        whereExpression = GetUsersByPhone();
    }
    else 
    {
        whereExpression = GetUsers();
    }

    try 
    {
        return Graph.Instance.Cypher
            .Match("(user:User)")
            .Apply(whereExpression)
            .ReturnDistinct(user => user.As<User>())
            .Results.ToList();
    }
    catch { return new List<User>(); }
}

This is example code -- I don't know neo4j, so I don't know if this is possible.

Also: is that try...catch really necessary? It certainly looks dangerous: an exception is thrown and it is just ignored and an empty list is return.


.Replace(".","/").Replace("-","/") and .Replace("(","").Replace(")","").Replace("-","") are repeated, and thus they should be moved to methods.


Is this the proper way to do this: user.DateOfBirth.ToString() == dto.Date.ToString()? Can't you just compare these dates directly?


This looks odd:

catch (Exception ex)
{
    Logger.Error(ex);
    return new List<User>();
}

At least this time you're logging the exception (unlike above), but again you're returning an empty list. So unless someone checks the error log, no one is any the wiser that an exception has occurred.

\$\endgroup\$
3
  • \$\begingroup\$ I appreciate this response, though I'm long off this project it is always nice to get an answer. All of your advice is great, in regards to the neo4j specific stuff: neo4j's wrapper in .NET throws an exception when there's no results to return, hence the empty list that is returned. The date comparison is interpreted into a Cypher query string, so you can't compare .NET types, only primitive types, so the dates have to be cast to strings and compared that way (terrible I know), or to UNIX time and compared that way (probably cleaner and safer). The whole try catch system was my best solution. \$\endgroup\$ Oct 28 '15 at 16:23
  • \$\begingroup\$ @TylerHarden I suspected that neo4j could be responsible (throwing an exception when there are no results? Who thinks up such an implementation?). The late review is due to an attempt to get rid of "zombie" questions. \$\endgroup\$
    – BCdotWEB
    Oct 28 '15 at 16:35
  • \$\begingroup\$ I'm in agreement. If I hadn't moved from .NET to Java I would have just changed the wrapper and did a PR, but the project owners let it go pretty much. That implementation is absolutely asinine. But cheers for answering, accepting. \$\endgroup\$ Oct 28 '15 at 17:09

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.