Wiki

Clone wiki

DataBag / Home

DataBag

DataBag provides an easy way of resolving string expressions based on registered variables. The library allows you to:

  • Parse string expressions

  • Perform automatic type conversions

  • Define default/test data

    flow — kopia.png

License

DataBag is Open Source software and is released under the MIT license

Installation

To install DataBag, run the following command in the Package Manager Console

PM> Install-Package DataBag
https://www.nuget.org/packages/DataBag/

To install DataBag.EntityFramework, run the following command in the Package Manager Console

PM> Install-Package DataBag.EntityFramework
https://www.nuget.org/packages/DataBag.EntityFramework/

How to start

In order to start using DataBag, create a new instance of IDataBag by using the built in factory:

#!c#
IDataBag bag = DataBagFactory.GetInstance();

After that, you can register variables which can be later included in text by using [name] notation.

#!c#
bag.Register("name", "John");
bag.Register("forename", "Smith");
bag.Register("activation code", 1234);

var emailTemplate= File.ReadAllText("EmailTemplate.txt");

var message = bag.Resolve(emailTemplate);
// Hello John Smith! Your activation code is: 1234

Resource file: EmailTemplate.txt

Hello [name] [forename]! Your activation code is: [activation code].

Variables

Variables can be defined in two ways:

  • as a constant value (calculated one time when registering),
  • as a dynamic value (calculated each time when resolving).

The value of the variable can be either an object (of any type, including null) or a string expression. If the expression contains a reference to another variable, the variable will be resolved first in order to calculate the final value. This also means that it is possible to define expressions that have references to another expressions and so on (nested expressions).

#!c#
bag.Register("header", "Hello [name]! ");
bag.Register("registration text", "Click the activation link in order to complete the registration process. ");
bag.Register("disabled account text", "Your account has been disabled. ");
bag.Register("footer", "[today]");

var registrationMail = bag.Resolve("[header] [registration text] [footer]");
// Hello John! Click the activation link in order to complete the registration process. 01/01/2017

var accountDisabledMail = bag.Resolve("[header] [disabled account text] [footer]");
// Hello John! Your account has been disabled. 01/01/2017

To resolve a single variable (not expression), you can use Get() method and pass only the variable name (without [ and ]):

#!c#
bag.Register("registration mail", "[header] [registration text] [footer]");

var header = bag.Get("header");
// Hello John!

var mail = bag.Get("registration mail");
// Hello John! Click the activation link in order to complete the registration process. 01/01/2017

Note:

  • Variables can be registered at any time in any order.
  • Registering variable multiple times under the same name, will update its value (then you can use Restore() method to bring back the previous value).
  • By default, you have access to few predefined variables (null, today, yesterday, and so on).

Type Converters

During resolving an object/expression, you can optionally specify the output type that will be used to convert the resolved value.

#!c#
int intFromString = bag.Resolve<int>("100");
// 100

bool boolFromString = bag.Resolve<bool>("True");
// true

bag.Register("strNumber", "999");
int intFromExpression = bag.Resolve<int>("[strNumber]");
// 999

Based on the type of resolved value and the given output type, DataBag is looking for a matching type converter. If the converter is found, it is used to convert the value, if not, a default conversion is performed.

The default conversion is appropriate when:

  • converting to string,
  • converting to enum,
  • converting to/from nullable types.

By default, Databag contains few standard type converters (string to int, string to bool, etc.). However, if you want to change the default conversion logic, or define a conversion between new types, you can create your own type converter:

#!c#
bag.DefineTypeConverter<MyFirstType, MySecondType>((firstTypeObj) =>
{
    // TODO: your logic
    // return convertedObject
});

MyFirstType myFirstType = new MyFirstType();
MySecondType mySecondType = bag.Resolve<MySecondType>(myFirstType);
// <converted value>

Important:

When resolving an expression which isn't only made of a single variable (1), all the variables within the expression will be automatically converted first to string.

(1) - contains multiple variables or additional text

#!c#

bag.Register("birth date", new DateTime(1980, 1, 1));

// Resolving expression with only a single variable returns the original object (without conversion)
DateTime birthDate = bag.Resolve("[bith date]");
// 01-01-1980 (as DateTime)

// Resolving expression with multiple variables (or additional text) returns string (automatic conversion to string)
string birthDate = bag.Resolve("Birth date is: [birth date]");
// Birth date is: 01-01-1980 (as string)

// Resolving expression with multiple variables (automatic conversion to string) with additional conversion
bag.Register("strNumber", "1");
bag.Register("intNumber", 2);
int number = bag.Resolve<int>("[strNumber][intNumber]3")
// 123 (as int)

Functions

Variables are used to store dynamic data, but sometimes you need to perform additional operations before returning the value.

In order to do that, you can use a function in the variable reference by adding : character followed by the function name.

#!c#
bag.Resolve("Hello [name:toUpper()]")
// Hello JOHN

Some functions can also take parameters (in case of multiple parameters, they should be separated by comma).

#!c#
bag.Resolve("Tomorrow is [today:addDays(1)]");
// Tomorrow is 02/01/2017

You can also invoke multiple functions one after another. In such case, the result of the previous function will be passed as the input object of the next one.

#!c#
bag.Register("age", 17);

bag.Resolve("You was born in [today:addYears(-[age]):year()]");
// You was born in 01/01/2000

In the above example:

  1. the most nested variable age will be resolved first (then variable today)
  2. by using addYears() function, today will be decreased by 17 years (-[age])
  3. by using year() function, the Year property of the DateTime object will be returned
  4. returned object is of type int, so it will be automatically converted to string

DataBag comes with several built in functions (for types: string, int, DateTime). If there are no functions that satisfy your needs, you can create a new one:

#!c#
bag.DefineFunction<string>("myCustomFunction", (input, args) =>
{
    return input + "Updated";
});

bag.Resolve("[name:myCustomFunction()]")
// JohnUpdated

Using Strongly-Typed Objects

All the above examples show how to register/resolve a variable by using the name given in a string parameter. This approach is appropriate when registering a temporary variable or when adding new variables in the runtime (for example based on input data).

However if you know exactly what variables are required, it is recommend to define a class with a property for each variable

#!c#
public class User
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
}

Then you can use lambda expressions to both register and get the variables (instead of passing string parameters).

#!c#
bag.Register<User>(u => u.Id, Guid.NewGuid())
   .Register<User>(u => u.Name, "John")
   .Register<User>(u => u.DateOfBirth, new DateTime(1980, 1, 1));

bag.Get<User>(u => u.Name);
// John

bag.Get<User>(u => u.DateOfBirth);
// 01/01/1980

Important:

The class in not used to store variables, but to create a mapping (by using naming convention) between the class properties and the values in DataBag. Bear in mind, that you can register variables with types different than the ones specified in the class.

Based on the registered variables, it is also possible to resolve the entire object with the prepopulated properties. In such case, if the property type is different than the one in corresponding variable, the automatic conversion will be performed.

#!c#

var user = bag.GetFor<User>();
user.Name;
// John

user.BirthOfDate;
// 01/01/1980

In order to reference a variable (registered in the above way) in expressions, you need to use the [className.propertyName] convention.

#!c#
bag.Resolve("Hello [User.Name]! You was born in [User.BirthOfDate] year")
// Hello John! You was born in 01/01/1980 year

To change the default naming convention of variables (when using strongly-typed approach), you can use the following attributes:

  • For class - [SkipClassNameInVariables] - in order to use only property name as the variable name
  • For property - [VariableName("myNewName")] - in order to use custom name

Entity Framework Integration

DataBag can be integrated with Entity Framework to provide default data in entity classes. To do this, follow the steps below:

  • create entity classes as usual,
  • define default data by implementing IRegisterVariables interface,
  • use [FindBy] attribute to indicate which properties should be used when searching for existing records (something similar to sql where statement). Thanks to this, all foreign keys (navigation properties in EF) will be automatically resolved when adding new record.

Let's consider the following scenario - two entities with one to many relationship and default data assigned to each of them:

#!c#
public partial class Employer : IRegisterVariables
{
    [FindBy]
    public string Name { get; set; }

    public int EmployerId { get; set; }
    public string Code { get; set; }
    public bool Enabled { get; set; }
    public virtual ICollection<Employee> Employees { get; set; }

    public void Register(IDataBag dataBag)
    {
        dataBag.Register<Employer>(e => e.Name, "CompanyX")
               .Register<Employer>(e => e.Code, "C123")
               .Register<Employer>(e => e.Enabled, true);
    }
}
#!c#
public partial class Employee : IRegisterVariables
{
    [FindBy]
    public string Name { get; set; }
    [FindBy]
    public string Surname { get; set; }

    public int EmployeeId { get; set; }
    public int EmployerId { get; set; }
    public DateTime DateOfBirth { get; set; }
    public virtual Employer Employer { get; set; }

    public void Register(IDataBag dataBag)
    {
        dataBag.Register<Employee>(e => e.Name, "John")
               .Register<Employee>(e => e.Surname, "Smith")
               .Register<Employee>(e => e.DateOfBirth, new DateTime(1960, 1, 13);
    }
}

Now, to add new records:

  • create new instance of DataBag (use the built in factory),
  • register variables - this can be done automatically by providing assembly where entity classes have been defined,
  • create DbContext object,
  • use extension method Add<T>
#!c#
var dbContext = new MyCompanyContext();
var dataBag = DataBagFactory
    .GetInstance()
    .RegisterVariablesFromAssembly(typeof(Employer).Assembly);

dbContext.Add<Employer>(dataBag);
dbContext.Add<Employee>(dataBag);

As you can see in the example, you don't have to specify any values as well as foreign keys. Both entities will be created with default data, and employee will be assigned to the correct employer record (first record that matches properties with [FindBy])

If you want to change the default data or add more records, you can update proper variables as follow (using statement has been used to restore original values at the end):

#!c#

using (dataBag.ForTempChanges())
{
    for (int er = 0; er < 5; er++)
    {
        // Update employer name
        dataBag.Register<Employer>(e => e.Name, $"Company {er}");
        dbContext.Add<Employer>(dataBag);

        for (int ee = 0; ee < 10; ee++)
        {
            // Update employee surname
            dataBag.Register<Employee>(e => e.Surname, $"Smith {ee}");
            dbContext.Add<Employee>(dataBag);
        }
    }
}

If you want to check whether a record with default data exists, use the Find<T> method

#!c#
var defaultEmployerExists = dbContext.Find<Employer>(dataBag) != null;

SpecFlow Integration

DataBag can be also easily integrated with other libraries, especially with SpecFlow.

Gherkin script:

Scenario: Integrate DataBag with SpecFlow and Entity Framework
    Given Employer exists
    And Employee exists
    | Name | Surname |
    | John | Smith   |
    | Bob  | Johnson |
    When I log in
    Then 'Welcome to [Employer.Name:toUpper()]' is displayed
    # resolved parameter will be 'Welcome to COMPANYX'

Sample implementation:

#!c#
[Binding]
public class SpecFlowSteps
{
    private Assembly assembly;
    private IDataBag dataBag;
    private MyDBContext dbContext;

    public SpecFlowSteps()
    {
        assembly = typeof(SpecFlowSteps).Assembly;
        dataBag = DataBagFactory.GetInstance().RegisterVariablesFromAssembly(assembly);
        dbContext = new MyDBContext();
    }

    [Given(@"(.+) exists")]
    public void GivenDefaultRecordExists(string entity)
    {
        var type = assembly.GetTypes().First(t => t.Name.EndsWith(entity));
        dbContext.Add(dataBag, type);
    }

    [Given(@"(.+) exists")]
    public void GivenCustomRecordsExist(string entity, Table customValues)
    {
        var type = assembly.GetTypes().First(t => t.Name.EndsWith(entity));
        using (dataBag.ForTempChanges())
        {
            foreach (var row in customValues.Rows)
            {
                foreach (var field in row.Keys)
                {
                    var variableName = DataBagExtensions.GetVariableName(type.Name, field);
                    dataBag.Register(variableName, row[field]);
                }

                dbContext.Add(dataBag, type);
            }
        }
    }

    [Then(@"'(.*)' is displayed")]
    public void ThenTextIsDisplayed(string text)
    {
        text = dataBag.Resolve<string>(text);
        // TODO: add assertion
    }
}

Updated