Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically find used properties prior to them being used

Tags:

c#

reflection

I am looking for optimization of a pattern I am using for a dynamic forms application.

I have a repository class with a method:

public Entity Find(string objectId, List<string> includedProperties);

This returns an Entity object, with only the fields specified in the "includedProperties", as building the entire object for all purposes is unnecessary overhead in this case (some entities have hundreds of properties).

Example domain code using this repository often looks something like this:

var includedProperties = new List<string> {
    "FirstChildName" , 
    "FirstChildDob",
    "SecondChildName",
    "SecondChildDob"
}

I then fetch an object:

var person = repository.Find("123",includedProperties);

I then use the Properties with a GetProperty(string propertyName) method:

var firstChildDob = person.GetProperty("FirstChildDob").AsDateTime();
...etc

This all works fine, and fits well with the dynamic design of the application. However, I find it irritating that I always need to declare a list of "used" properties separately, prior to fetching the object.

So, my question is, through reflection or some other cleverness, can I simplify the building of the "Included Properties" by looking at which parameters get passed later in the code with the "GetProperty" method?

Using the above example, I'd like to build the list using a helper like this (or similar):

var includedProperties = HelperObject.GetFieldsUsedInCurrentCodeFile();

This would somehow pickup what string constants were passed to the "GetProperty()" method, saving the need for explicit declaration. Any suggestions welcome!

like image 739
Paul Grimshaw Avatar asked Jan 04 '14 00:01

Paul Grimshaw


2 Answers

I actually had a similar problem awhile back; the best I could come up with at the time was to define an enum that contained the names of the properties I wanted to use in the method.

Using this approach, you could build the list of included properties by cycling through the enum.

There are a couple of benefits to this approach vs. strings:

  1. Any property spelling issues or property name changes are made in a single location.

  2. If you are using a tool such as Resharper, you can determine when you have unused "properties" in the enum.

For example:

    private enum PersonMethodProperties
    {
        FirstChildName,
        FirstChildDob,
        SecondChildName,
        SecondChildDob
    }

    private void PersonMethod()
    {
        var includedProperties = GetIncludePropertiesFromEnum(typeof(PersonMethodProperties));

        var person = repository.Find("123", includedProperties);

        var firstChildDob = person.GetProperty(PersonMethodProperties.FirstChildDob.ToString()).AsDateTime();
    }

    private List<string> GetIncludePropertiesFromEnum(Type propertiesEnumType)
    {
        var includedProperties = new List<string>();

        foreach (var name in Enum.GetNames(propertiesEnumType))
        {
            includedProperties.Add(name);
        }

        return includedProperties;
    }
like image 91
competent_tech Avatar answered Sep 22 '22 12:09

competent_tech


Code analysis tools like Nitriq or NDepend unfortunately won't help you because in current version they do not capture method's arguments names and values.

But you can use Roslyn to create tool that will analyze your solution and generate a class containing list with used properties as a pre-build event. Code for solution analysis that finds all calls to className.methodName and returns its constant arguments (text in your case):

static IEnumerable<string> GetMethodCallParametersValues(string solutionName, 
                                                         string className, 
                                                         string methodName)
{
    var workspace = Workspace.LoadSolution(solutionName);
    var solution = workspace.CurrentSolution;

    var createCommandList = new List<ISymbol>();
    var @class = solution.Projects.Select(s => s.GetCompilation()
                                                .GetTypeByMetadataName(className))
                                  .FirstOrDefault();
    var method = @class.GetMembers(methodName)
                        .AsList()
                        .Where(s => s.Kind == CommonSymbolKind.Method)
                        .FirstOrDefault();
    var locations = method.FindReferences(solution)
                          .SelectMany(r => r.Locations);

    List<string> result = new List<string>();
    foreach (var location in locations)
    {
        var model = location.Document.GetSemanticModel();
        var token = location.Location
                            .SourceTree
                            .GetRoot()
                            .FindToken(location.Location.SourceSpan.Start);
        var invocation = token.Parent.FirstAncestorOrSelf<InvocationExpressionSyntax>();
        var arguments = invocation.ArgumentList.Arguments;
        result.AddRange(arguments.Select(a => model.GetConstantValue(a.Expression).Value.ToString()));
    }
    return result.Distinct();
}

and code usage:

var includedProperties = GetMethodCallParametersValues(@"c:\path\to\your.sln",
                                                       "SomeNamespace.SomeClass",
                                                       "GetProperty");

Note: Because of Roslyn's small bug in parsing Solution file, you probably will have to comment below lines in .sln file by adding # so they will look:

# VisualStudioVersion = 12.0.21005.1
# MinimumVisualStudioVersion = 10.0.40219.1
like image 24
Konrad Kokosa Avatar answered Sep 22 '22 12:09

Konrad Kokosa