SimpleRules.Net - Easy to use Rules Engine






4.86/5 (26 votes)
SimpleRules.Net is a rules engine that works based on attributes decorated on the properties of a class
Agenda
- Introduction
- Installation
- Terminology
- Basic Usage
- Specify Metadata Using an External Class
- Specify Metadata During Setup
- Creating Custom Rules
- Discover Handlers Dynamically Using Assembly Markers
- Create new Rules for Existing Handlers
- Multiple Validation Rules
- Usage in a .Net Core MVC Project
- Internals
- Rule Attributes & their Hierarchy
- Sequence of Operations
- Expression Generation using Handlers
- Closer look at the RegexRuleHandler
- Dealing with Nullable Properties
- Handler Discovery
- Some pointers on Compatibility
Introduction
SimpleRules.Net is a rules engine, as you guessed probably based on the name! SimpleRules.Net was born out of a discussion I had at my workplace. The idea was to come up with a library that does not require me or anybody to write a gazillion lines of code to validate a set of instances (a List<T>
or T specifically) and this led to the development of SimpleRules.Net. In order to define rules for a certain class or instance, all you have to do is decorate the class with pre-defined rule attributes. The basic usage section will get you started. And, it does not end there. Check out the sections after the basic usage section to know more!
SimpleRules.Net can be used in console, web applications or anything else for that matter. The sample included uses an MVC project to demonstrate how to use this library. SimpleRules.Net is NOT intended to be a replacement for the data annotations features that MVC provides, which are part of the System.ComponentModel.DataAnnotations
namespace see Using data annotations.
Installation
To install SimpleRules.Net from NuGet, run the following command in the package manager console or from the nuget user interface.
PM> Install-Package SimpleRules
Terminology
Before we get on with anything, I would like to define some terminology that will be used across this file. Consider the snippet below:
public class User
{
public string Password { get; set; }
[EqualTo("Password")]
public string ConfirmPassword { get; set; }
}
In the above class the property that is decorated with the EqualTo
attribute will be referred to as the "source" and the property identified by the argument to this attribute (Password
in this case) will be referred to as the "target". For any source there could be multiple targets (i.e. rules).
Basic Usage
Lets say you have a User object and you need to validate if the Password
property matches the ConfirmPassword
property and also if the EmailAddress
and PhoneNumber
properties match their appropriate pattern, as suggested by their names. Here is what you could do:
public class User
{
public string Username { get; set; }
public string Password { get; set; }
[EqualTo("Password")]
public string ConfirmPassword { get; set; }
[EmailMatchRegex]
public string EmailAddress { get; set; }
[UsPhoneNumberRegex]
public string PhoneNumber { get; set; }
}
As evident from the snippet above, all you had to do was decorate the ConfirmPassword
property with the EqualTo
attribute, by passing in the name of the property that has to be matched with, in this case Password
. And for the EmailAddress
and PhoneNumber
properties, use the in-built EmailMatchRegex
and UsPhoneNumberRegex
attributes! With this done, when you have an instance of user, it can be validated as shown below:
var user = new User {
Username = "jdoe",
Password = "john",
ConfirmPassword = "johndoe",
EmailAddress = "abc",
PhoneNumber = "123"
};
var simpleRulesEngine = new SimpleRulesEngine();
var result = simpleRulesEngine.Validate(user);
Needless to say, you can also pass in a list in order to validate a list of User
objects (List<User>
). An illustration is shown below:
var users = ...; // Method that returns a list of users: List<User>
var simpleRulesEngine = new SimpleRulesEngine();
var results = simpleRulesEngine.Validate(users);
The result will be a ValidationResult
that contains an Errors
property with 3 errors in this case - one each for mismatching passwords, invalid email address and invalid phone number. In the next section, you will see how the rule metadata can be seperated out of the class to keep the entities and the rules separate, in order to achive loose coupling.
One important thing to note is that when a rule is applied on a certain property (the source), the data types of the "target" property (or properties) should match that of the source. This is applicable when validating against a constant too. Simply, if the EqualTo
attribute is applied on a DateTime
property, the "target" property should also be a DateTime
(or DateTime?
).
Specify Metadata Using an External Class
In the earlier section you saw how easy it is to setup and run a validation using the SimpleRules engine. In this section, we are going to see how the rules can be declared externally, like I said in the last section to support loose coupling. To do this, just create a metadata class called UserMetadata
and decorate your class with the RuleMetadata
attribute, as shown below:
[RuleMetadata(typeof(UserMetadata))]
public class User
{
public string Username { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
public string EmailAddress { get; set; }
public string PhoneNumber { get; set; }
}
public class UserMetadata
{
public string Username { get; set; }
public string Password { get; set; }
[EqualTo("Password")]
public string ConfirmPassword { get; set; }
[EmailMatchRegex]
public string EmailAddress { get; set; }
[UsPhoneNumberRegex]
public string PhoneNumber { get; set; }
}
As of now the rule metadata class has to contain the same set of properties (or more) that are present in the class that you intend to validate (User
). And with respect to the data types, it can also be just an object
. When you make the same call to Validate
as before with a list of users, you will get the same results as before, an Errors
property with 3 errors. Next section deals how the rules engine can be informed of the link between the class to be validated and the metadata.
Specify Metadata During Setup
Let's say you don't want to specify the metadata class in the class you want to validate. In that case, you just have to use the RegisterMetadata
extension to register the metadata for a class, as shown below:
var user = new User {
Username = "jdoe",
Password = "john",
ConfirmPassword = "johndoe",
EmailAddress = "abc",
PhoneNumber = "123"
};
var simpleRulesEngine = new SimpleRulesEngine()
.RegisterMetadata<User, UserMetadata>();
var result = simpleRulesEngine.Validate(user);
The result of the validation is just the same as discussed before! Okay, we have seen enough of validation. Note that if you attempt to register the same concrete type, metadata type again, it will throw a DuplicateMetadataRegistrationException
exception. Lets move on to possibilities of extending the rules engine using custom rules. With the help of custom rule handlers, there is no limit what can be validated!
Creating Custom Rules
SimpleRules.Net supports extensibility with the help of "handlers" to define new rules. That was one of the most important design considerations for me - to support the ability to define custom rules easily. As you probably guessed this rule engine is based on expressions. So to get futher with the handlers, you need to have a working knowledge of expressions. Let me try to explain the ability to create custom rules with the help of a rule that validates if a certain property is between a certain minimum and maximum value. In order to do this, first you need to create the attribute that contains the metadata for the rule - RangeRuleAttribute
.
public class RangeRuleAttribute : BaseRuleAttribute
{
public RangeRuleAttribute(int minValue, int maxValue)
{
MinValue = minValue;
MaxValue = maxValue;
}
public int MinValue { get; set; }
public int MaxValue { get; set; }
}
As evident from the snippet above, the rule attribute defines a MinValue
and a MaxValue
to get the necessary metadata for the rule. The next step is to define a handler that implements the IHandler
interface, as shown below:
public class RangeRuleHandler : IHandler
{
public EvaluatedRule GenerateEvaluatedRule<TConcrete>(BaseRuleAttribute attribute, PropertyInfo targetProp)
{
var rangeAttr = attribute as RangeRuleAttribute;
var input = Expression.Parameter(typeof(TConcrete), "a");
var propName = targetProp.Name;
var comparison = Expression.And(
Expression.GreaterThan(Expression.PropertyOrField(input, propName), Expression.Constant(rangeAttr.MinValue)),
Expression.LessThan(Expression.PropertyOrField(input, propName), Expression.Constant(rangeAttr.MaxValue))
);
var lambda = Expression.Lambda(comparison, input);
return new EvaluatedRule
{
MessageFormat = string.Format("{0} should be between {1} and {2}", propName.AddSpaces(), rangeAttr.MinValue, rangeAttr.MaxValue),
RuleType = RuleType.Error,
Expression = lambda
};
}
public bool Handles(BaseRuleAttribute attribute)
{
return typeof(RangeRuleAttribute).IsAssignableFrom(attribute.GetType());
}
}
The Handles
method simply returns a boolean to indicate that this handler infact deals with a RangeRuleAttribute
. And the GenerateEvaluatedRule
method constructs an expression which uses the rule metadata to construct an expression tree that evaluates the same. In order to explain the expression tree generated by this method, let me define a class that will use this rule.
public class Activity
{
public int Id { get; set; }
public string Name { get; set; }
[RangeRule(10, 30)]
public int Capacity { get; set; }
}
The Func
given below explains the intended effect I wish to achieve. Given an activity, check if the property Capacity
is between the numbers 10 and 30.
Func<Activity, bool> func = a => a.Capacity > 10 && a.Capacity < 30;
The return value of the GenerateEvaluatedRule
method needs to return an object of type EvaluatedRule
which contains the evaluated rule itself along with other additional properties to help the core rules engine decide on things, like the message to be displayed if the rule is not met and whether its an error or a warning. If you notice the line where the message is constructed, I have used the extension method AddSpaces
to make the property names readable, rather than being a string of text. For example StartDate
will be transformed to "Start Date" and so on. Its not over once you create the rule attribute and the handler. You need to register it with the rules engine. More on this is in the next section.
Discover Handlers Dynamically Using Assembly Markers
In the previous section you saw have new rules/handlers can be defined. In order to put them to use, there are couple of ways. First one is by registering it with the rules engine as shown below:
var activities = new List<Activity> { new Activity { Id = 1, Name = "Indoor", Capcaity = 45 } };
var engine = new SimpleRulesEngine()
.RegisterCustomRule<RangeRuleHandler>();
var results = engine.Validate<Activity>(acitivities);
This might get tedious if you have multiple handlers defined. So, in order to let the rules engine automatically discover the handlers defined, use the DiscoverHandlers
method, as shown below:
var engine = new SimpleRulesEngine()
.DiscoverHandlers(new[] { typeof(Marker) });
Marker
is simply a class that exists in the assembly that contains the defined handlers. With this method called, the handlers are all automatically discovered and registered and note that during the life cycle of the rules engine instance it may be called any number of times (handlers already registered will be ignored).
Create new Rules for Existing Handlers
Its also possible to extend existing rules in order to support reuse. For example, consider the MatchRegexRule
. This can be used to validate the value of a property against a regular expression. There are already rules like the EmailMatchRegexRule
, UsPhoneNumberRegex
etc to validate properties, but you can create your own rules based on this too. For example, the following code creates a rule that validates a password. I lifted the password validation regex straight out of google and it validates if the "password matching expression. match all alphanumeric character and predefined wild characters. password must consists of at least 8 characters and not more than 15 characters".
public class PasswordMatchRegexAttribute : MatchRegexAttribute
{
public PasswordMatchRegexAttribute()
: base(@"^([a-zA-Z0-9@*#]{8,15})$")
{
}
}
With this rule you can validate a class that contains this rule decorated on a particular property.
Multiple Validation Rules
You can also decorate a property with multiple rule attributes in order to validate it against a number of other properties. A sample is shown below:
public class Student
{
[EntityKey]
[GreaterThan("", constantValue: 100)]
public int Id { get; set; }
[LessThan("StartDate")]
public DateTime RegistrationDate { get; set; }
public DateTime StartDate { get; set; }
[LessThan("StartDate", canBeNull: true)]
public DateTime? EndDate { get; set; }
[LessThan("RegistrationDate")]
[LessThan("StartDate")]
[LessThan("EndDate")]
public DateTime DateOfBirth { get; set; }
}
In the above snippet note the DateOfBirth
property. It has been decorated with 3 validation rules to check whether it is less than 3 other properties: RegistrationDate
, StartDate
and EndDate
.
Another thing of interest in the above snippet is the EntityKey
attribute decorated on the Id
property. You can use this attribute on a property to indicate that this value has to be returned as the value for the Key
property in every ValidationResult
instance returned with the results of the validation of a certain entity.
Usage in a .Net Core MVC Project
The sample project provided in the solution has an example of how the rules engine can be used in a MVC project. This section provides a quick introduction of the same. In this case, the Startup
class is used to create an instance of the rules engine as a singleton and configured in the ConfigureServices
method, as shown below:
public void ConfigureServices(IServiceCollection services)
{
// ...
var simpleRulesEngine = new SimpleRulesEngine()
.DiscoverHandlers(new [] { typeof(Startup) })
.RegisterMetadata<Registration, RegistrationMetadata>()
.RegisterMetadata<Activity, ActivityMetadata>();
services.AddSingleton(typeof (SimpleRulesEngine), simpleRulesEngine);
// ...
}
With this done, the SimpleRulesEngine can be injected in to any controller where you intend to do validation, like it is done in the case of the HomeController
.
public class HomeController : Controller
{
private readonly SimpleRulesEngine _rulesEngine;
public HomeController(SimpleRulesEngine rulesEngine)
{
_rulesEngine = rulesEngine;
}
// ...
}
Internals
Based on what you have seen so far, at the core of the rules engine various attributes and expression trees are used extensively to interpret the rules defined on a class. When a class is declared and decorated with even one rule attribute, a lot of things happen behind the scenes. This section intends to discuss about some of these things in moderate detail. The subsequent sections are NOT intended to be a tutorial on expression trees. Rather, the intention is to show how it is used to get the required results. Read on!
Rule Attributes & their Hierarchy
Since rule attributes play an important role in this library, its important that we take a look in to them and understand them better. Consider the illustration below that describes the attributes already available to you:
System.Attribute [fw] | BaseRuleAttribute -----> Should be used for custom rule attributes / \ RuleAttribute [a] RegexRuleAttribute [a] | | | | | | | | GreaterThanAttribute ... EmailRegexRuleAttribute ... Legend: fw -> framework a -> abstract
At the root of the attribute based rule engine framework of the api is the BaseRuleAttribute
, which is an abstract class that inherits from System.Attribute
class that comes with the .net framework. Every other rule attribute has to derive from the BaseRuleAttribute
in order to be discoverable by the rule engine core. The reason would be evident if you look at the GetRules
method (and its overload) in the SimpleRulesEngineExtensions
class. One of them is shown below:
public static IEnumerable<Tuple<BaseRuleAttribute, PropertyInfo>> GetRules(this Type srcType)
{
var attrList = srcType.GetProperties(publicPropFlags)
.SelectMany(c => c.GetCustomAttributes<BaseRuleAttribute>(), (prop, attr) => new { prop, attr })
.Select(p => new Tuple<BaseRuleAttribute, PropertyInfo>(p.attr, p.prop));
return attrList;
}
The generic type constraint to the GetCustomAttributes
method is the BaseRuleAttribute
class, so that for the given concrete type all the rule attributes decorated on various properties are identified. So when you are creating custom handlers, you need to ensure that the attribute used in the Handles
method uses a class that derives from the BaseRuleAttribute
or any other class that derives from the BaseRuleAttribute
class - for example, say, the MatchRegexAttribute
. A more detailed discussion on this follows further down this document and it delves in to a lot more expressions and reflection.
Sequence of Operations
The most important methods exposed to the world by the rules engine are:
- Validate<TConcrete>
- RegisterMetadata<TConcrete, TMeta>
- RegisterCustomRule<Rhandler>
- DiscoverHandlers
To begin with, lets start with the Validate<TConcret>
method since other methods can be considered "add ons". When you call the Validate<TConcrete> method, Every possible rule declared in various properties of the TConcrete
class are discovered using the GetRules
method. The rules could have been provided using a number of methods, like, using inline rule attributes or using a metadata class declared inline in the class or externally using the RegisterMetadata
method. For the sake of simplicity, lets assume the rules are declared inline in the class itself, like this:
public class Activity
{
[EntityKey]
public int Id { get; set; }
public string Name { get; set; }
public DateTime StartDate { get; set; }
[GreaterThan("StartDate")]
public DateTime EndDate { get; set; }
[LesserThan("MaxCapacity")]
public int Capacity { get; set; }
public int MaxCapacity { get; set; }
}
The GetRules
(SimpleRulesEngine
) method first checks a local cache that may contain the list of evaluated rules. In this case, since the method is being called for the first time, the cache won't have an entry for the TConcrete
type and so it must move on to the next step of discovering the rules attributes.
The GetMetadataType
(SimpleRulesEngineExtensions
) is an extension method that simply checks and returns all the rules decorated for every property in the class. It takes in two parameters as the input - one is a dictionary that acts as a cache of discovered rule attributes and the next is the concrete type itself. The order in which GetMetadataType
discovers the rule attributes are:
- Passed in dictionary is checked first, to return the type that contains the rule metadata. This would be true if the rule metadata was registered using the
RegisterMetadata
method - The type's (note, not properties) attributes are checked and if there is a
RuleMetadata
attribute, that is returned - Finally, if 1 & 2 fails, the type's properties are investigated to find out if they have the rule attributes. If so they are returned
If all the 3 steps fail, a null
is returned to indicate no rules were discovered, causing an exception to be thrown to inform the user that rule validation cannot on a class without any rule attributes.
In the case of our Activity
class, there are rule attributes, so we move on to the next step. If you go back to the Activity
class, you will notice that the Id
property has the EntityKey
attribute decoration. This just indicates that the value of this property must be used to populate the ValidationResult.Key
property in the result returned for every entry in the input. This helps you build a more helpful message to the end user.
As of now, I have a list of EvaluatedRule
instances and so I am ready to move on to the actual process of generating the expressions!
Expression Generation using Handlers
With the help of the list of rules, I now use the ProcessRule
(ExpressionGenerationExtensions
) extension to identify the associated instance of IHandler
. If a handler is not found, an exception is thrown and execution is halted. In this case the GreaterThan
and LessThan
attributes are used and so these two will evaluate to be handled by the SimpleRuleHandler
. With this in tow the ProcessRule
method calls the SimpleRuleHandler.GenerateEvaluatedRule
method to create an instance of EvaluatedRule
, which contains the evaluated expression tree, a message that has to be displayed if the rule is not matched, rule type (error or warning). It also contains a read only Delegate
that will be used during the actual evaluation of the rules.
With the rules evaluated, they are stored in the evaluated rules cache and returned to the Validate<TConcrete>
method. At this point all that remains is to cast the delegates to a Func<TConcrete, bool>
and evaluate them! Every rule is evaluated and the results are aggregated in to a ValidationResult
, either in the Errors
property or the Warnings
property based on the rule type.
Closer look at the RegexRuleHandler
It might be interesting at this point to look at the RegexRuleHandler
in more detail. This handler deals with validating a certain property in a class against a regular expression. The Handles
method acknowledges that any attribute that uses the RegexRuleAttribute
is acceptable for this handler. Here is the definition of the RegexRuleHandler
.
public abstract class RegexRuleAttribute : BaseRuleAttribute
{
public abstract string RegularExpression { get; }
}
Its an abstract class so that it cannot be directly used. Instead the MatchRegexAttribute
has to be used to decorate propeties to validate against a regular expression. The handler uses the regular expression passed to construct an expression block that operates on the TConcrete
instance. Here is the complete handler for these type of attributes.
public class RegexRuleHandler : IHandler
{
public EvaluatedRule GenerateEvaluatedRule<TConcrete>(BaseRuleAttribute attribute, PropertyInfo targetProp)
{
var regexAttr = attribute as RegexRuleAttribute;
var input = Expression.Parameter(typeof(TConcrete), "i");
var ctor = typeof(Regex).GetConstructors().SingleOrDefault(c => c.GetParameters().Count() == 1);
var method = typeof(Regex).GetMethods()
.SingleOrDefault(m => m.Name == "IsMatch" &&
!m.IsStatic &&
m.GetParameters().Count() == 1);
var leftExpr = Expression.PropertyOrField(input, targetProp.Name);
var block = Expression.Block(
Expression.Call(
Expression.New(
ctor,
Expression.Constant(regexAttr.RegularExpression)), method, leftExpr)
);
var expression = Expression.Lambda(block, input);
return new EvaluatedRule
{
RuleType = RuleType.Error,
MessageFormat = string.Format("{0} does not match the expected format", targetProp.Name.AddSpaces(), regexAttr.RegularExpression),
Expression = expression
};
}
// ...
}
Note the expression generation step, the block
variable. It simply generates an instance of the Regex
object by passing the regular expression defined as a constructor to the MatchRegexAttribute
and calls the IsMatch
method by passing the property that has to be compared against the regular expression.
There are some more attributes like UsPhoneNumberRegexAttribute
, EmailMatchRegexAttribute
are syntactic sugars for the MatchRegexAttribute
to validate a property against the regular expression for US phone number and email respectively.
Dealing with Nullable Properties
Even though I don't want to go over the handler that deals with simple relational operators, I would like to throw light on how I deal with nullable properties. For the sake of this section, lets call the property that has the rule attribute decoration as the "source" and the property its compared against the "target". With that said, there are few cases to consider.
- "source" is nullable, "target" is not
- "target" is nullable, "source" is not
- both "source" and "target" are nullable
In all these cases, even if one of source or target is null, its important to use the Convert
method of the Expression
class to convert the properties to have the same PropertyType
as shown below (lines 3 & 4 in the method body):
public static BinaryExpression CreateBinaryExpression(this Expression leftExpr,
Expression rightExpr,
ExpressionType exprType,
PropertyInfo propertyInfo,
PropertyInfo otherPropInfo = null)
{
var isNullable = propertyInfo.IsNullable() || (otherPropInfo != null ? otherPropInfo.IsNullable() : true);
var propType = propertyInfo.PropertyType;
var leftExprFinal = isNullable ? Expression.Convert(leftExpr, propertyInfo.PropertyType) : leftExpr;
var rightExprFinal = isNullable ? Expression.Convert(rightExpr, propertyInfo.PropertyType) : rightExpr;
return Expression.MakeBinary(
exprType,
leftExprFinal,
rightExprFinal
);
}
Closer look into RegisterCustomRule Methods
Next up, lets look at the RegisterCustomRule
(SimpleRulesEngine
) method. There is not much to it in terms of implementation. But it plays an important role in how all the things come together. The RegisterCustomRule
method is a generic method that takes in a single type constraint, which is the class that implements IHandler
. Notice the new ()
constraint added so that I can ensure that there is a parameterless constructor for this class. With the type constraint passed, I have another dictionary that acts as a cache to stop users from registering the same handler multiple times. The handlers have a Handles
method that takes the responsibility of identifying or subscribing to the type of attributes this handler will process. Its important what attribute you use in this method. Consider the case of SimpleRuleHandler
as shown below:
public bool Handles(BaseRuleAttribute attribute)
{
return typeof(RelationalOperatorAttribute).IsAssignableFrom(attribute.GetType());
}
Attributes like GreaterThan
, LessThan
etc derive from the RelationalOperatorAttribute
class, which in turn derives from the BaseRuleAttribute
. BaseRuleAttribute
should be the class that any rule derives from to be able to be used in the rules engine. Getting back to the Handles
method, rather than checking if GreaterThan
, LessThan
attributes satisfy the IsAssignableFrom
check, you should check if the RelationalOperatorAttribute
attribute satisfies the requirement.
Handler Discovery
In order to automatically identify custom handlers, the rules engine provides the DiscoverHandlers
method and it looks like this:
public SimpleRulesEngine DiscoverHandlers(params Type[] assemblyMarkers)
{
var discoveredHandlerTypes = assemblyMarkers.FindHandlerTypesInAssemblies();
foreach (var handler in discoveredHandlerTypes)
{
if (!handlerMapping.ContainsKey(handler))
handlerMapping.Add(handler, handler.CreateInstance());
}
return this;
}
The FindHandlerTypesInAssemblies
extension method is used to "look" in to the assemblies passed and identify any types that implements the IHandler
interface. This method uses reflection to achieve this. Once the types are inferred, its passed on to the calling method so that they can be registered in the dictionary that caches the handlers for quick retrieval. The rules engine itself uses this method to discover the handlers that are provided by default like the one for relational operator rules and regular expression rules. Here is the corresponding method from the SimpleRulesEngine
class:
private void AddDefaultHandlers()
{
this.DiscoverHandlers(typeof (SimpleRulesEngine));
}
Some pointers on Compatibility
I wanted this library to be available for multiple frameworks - namely .net standard 1.6, .net 4.5 and .net 4.6. In order to achieve this I had to do a few things. One of them is in the project.json file where the frameworks
property has all the frameworks listed. The most important thing of interest is the Compatibility directory in the simple rules engine project. Take a look at that - here is the hierarchy of that directory:
Compatibility | | - GENERAL | - NETSTANDARD
This is needed because the classes under the System.Reflection
namespace has been changed in the .net standard framework. For example you cannot use the IsInterface
property of a System.Type
object to identify if the type is an interface anymore. Instead, the System.Type
instance has a GetTypeInfo
method to get all of the information about the type like whether its an interface, or whether its generic and so on. In order to deal with this and not duplicate code, the compatibility directory has been created and if you recall looking at the project.json file, you will notice that being indicated, as shown below (see 'define' property):
{
"net461": {
"buildOptions": {
"define": [ "GENERAL" ]
},
"dependencies": {
"NETStandard.Library": "1.6.0",
"System.Reflection": "4.0.10"
}
},
"netstandard1.6": {
"imports": "dnxcore50",
"buildOptions": {
"define": [ "NETSTANDARD" ]
},
}
}