This project is read-only.

VITA Tutorial

Part 5. Entity modules, custom methods, error log service

Outline

In this part of the tutorial we are going to look at an advanced concept of the VITA framework - Entity modules. The goal is to break and "componentize" large entity models into smaller functional components independently designed and tested. The application is later assembled at runtime (at startup in fact) from a set of pre-built modules.
In addition to being containers for entity definitions and associated logic, modules provide facilities for creating custom stored procedures (or SQL queries) - they are compiled from LINQ queries defined directly in c# code.

EnityModule class

EntityModule defined by VITA core framework is a base class to be used for custom entity modules. So lets start packing our books model into a new BooksModule class:

  public class BooksModule : EntityModule {
    //constructor
    public BooksModule(EntityArea area) : base(area, "Books", "Books module") {
      // We need to register only one root type, 
      // the rest will be discovered automatically
      this.RegisterEntities(typeof(IBook)); 
      //Register companion types that describe keys and indexes on entities
      this.RegisterCompanionTypes(
          typeof(IBookKeys), typeof(IAuthorKeys), typeof(IPublisherKeys));
    }
    . . . 
  }

The constructor takes just one parameter (EntityArea) which it passes to the base constructor, along with module name and description. The EntityArea parameter is used to group multiple modules together and place them in one database - the details are not important for us now, just ignore this parameter. We will discuss areas in the next part of the tutorial.
The first thing we do is registering our book entities as belonging to this module. We actually need to register a single entity IBook - the rest will be discovered automatically by following the entity references in properties and attributes. Notice that while IBook does not reference IAuthor, it is still discovered by using the reference in ManyToMany attribute on Authors property.

Companion types

The next call in the BooksModule constructor is RegisterCompanionTypes. It registers the so-called companion types (interfaces) that we define in this part of the tutorial with the purpose of moving all database-related attributes to these companion types. Here is how we do it.
For better "separation of concerns", we create a separate file EntityKeys.cs and put the following definitions there:

  [Index("Publisher,Category,Title")]
  public interface IBookKeys : IBook {
    [Index]
    new string Title { get; set; }
    [ClusteredIndex]
    new DateTime PublishedOn { get; set; }
  }

  public interface IPublisherKeys : IPublisher {
    [Unique]
    new string Name { get; set; }
  }

  public interface IAuthorKeys : IAuthor {
    [Index]
    new string LastName { get; set; }
  }


We simply move some attributes that we defined on our entities to new "key" entities and placed them in this file. Now we need to tell the system to check these key types for attributes - that is what RegisterCompanionTypes call is doing. When these types are registered, the system will them book entities by looking at their "bases". Then it will use all attributes defined on key entities as if they were defined on the entities themselves. Now this file, containing only database-related attribute, can be handled by our database expert, and other developers do not need to even see this stuff.
Please notice two things. First, we do not put Entity attribute on these companion types - this is not needed, they are not real entities. Secondly, we use "new" keyword for properties that we "reintroduce" on the derived interfaces - this is c# language rule.

Custom commands

Let's get back to the BooksModule class. We will now create custom commands for the module. These commands will execute custom database operations. We define custom commands using LINQ expressions, and they are translated into SQLs and then into stored procedures; they will be available to the application code as plain c# methods. We are going to create two commands:
  • A command that selects all books for author(s) with a given last name
  • A command that modifies the price of all books in a given category by a fixed factor.
First, we need to define two class-level fields to hold our commands (inside the BooksModule class):

    private IEntityCommand _getBooksByAuthorCommand, _changePriceCommand;

IEntityCommand is an interface that represents a custom command over entities. You don't need to know what it is exactly - your code mostly passes it around. To actually create the command objects, we need to override the CompileCommands virtual method:

  public override void CompileCommands(IEntityCommandCompiler compiler) {   . . .

The compiler parameter is the object that we will use to communicate with VITA's engine to compile the procedures. We will use LINQ to express the "logic" of the procedures.
If you ever used Linq2Sql, you know that the starting point of writing query is getting the original entity set(s) representing a table in the database. The DataContext.GetTable<T>() is the method that returns a "table" or "entity" that may be used in LINQ expression. The compiler parameter provides similar method that returns the reference to original entity sets. Our first action in the CompileCommands method is to get hold on the entity sets that we will use in our queries:

      var books = compiler.EntitySet<IBook>();
      var pubs = compiler.EntitySet<IPublisher>();
      var authors = compiler.EntitySet<IAuthor>();
      var bookAuthors = compiler.EntitySet<IBookAuthor>();

Now we are ready to write a query for the first method that returns books by author's last name:

      var queryGetByAuthor = from bk in books
                             join ba in bookAuthors on bk equals ba.Book
                             join au in authors on ba.Author equals au
                             where au.LastName == "?"

The question mark in quotes is a placeholder; all constants in the query will be converted into parameters (that's the way Linq2Sql works, and VITA's current implementation uses Linq2Sql under the hood for converting queries into SQLs).
We now use the query to compile the entity command:

      _getBooksByAuthorCommand = compiler.CompileCommand<IBook>(CommandKind.Select, 
                        this.Area, "BooksGetByAuthorLastName", queryGetByAuthor);

The IBook generic argument specifies the type of entity returned by the query. The Area argument identifies the "schema" of the stored procedure (same as the schema for Books model entities). The third string argument is the name of the command. It is the name of the stored procedure - if the database setup is set to use stored procedures. (It is also the name to use in the URL when calling this method through RESTful interface). The last argument is the query that specifies the actual logic.
We finish the definition by setting the method description and adding a parameter:

      _getBooksByAuthorCommand.Description = 
               "Gets books for author(s) with a given last name.";
      _getBooksByAuthorCommand.AddParameter("authorLastName", typeof(string), 30);

The description will be used in the help pages for the RESTful API - we will discuss this in the later parts of this tutorial.
Now we have the command ready to be used in the application code. Once the application startup is finished, a new stored procedure part5.BooksGetByAuthorLastName will appear in the database. To call it, we define a method in the BooksModule class:

    public IList<IBook> GetBooksByAuthor(IEntitySession session, string lastName) {
      return session.ExecuteQuery<IBook>(_getBooksByAuthorCommand, lastName);
    }

Believe it or not, but calling this method at runtime executes the stored procedure, reads the results and returns the list of books!
Now let's get back to the CompileCommands method - we need to compile another command for updating the books price:

      var factor = 1.1d;
      var bkCat = BookCategory.Kids; 
      var changePriceQuery = from p in books
                             where p.Category == bkCat
                             select new { Id = p.Id, Price = p.Price * factor };
      _changePriceCommand = compiler.CompileCommand<IBook>(CommandKind.Update, Area,
                    "BooksChangePrice", changePriceQuery);
      _changePriceCommand.AddParameter("category", typeof(BookCategory));
      _changePriceCommand.AddParameter("priceFactor", typeof(double));
      _changePriceCommand.Description = "Changes prices of books in a given category.";

The command we are creating changes the price of all books in a given category by a given factor. We start by creating two placeholders for query parameters. We do not have to do this - as we already said, all constants in the query are translated into parameters - but we do it for code clarity.
Next, the query itself. It returns an anonymous type containing two properties: Id - book Id, and Price - a new price. This is how the update is encoded in the LINQ query. VITA supports creating custom methods for all four basic types: select, update, insert and delete. The following are the rules for the type returned by the LINQ query for each method:
  • SELECT - the entity itself
  • UPDATE - an anonymous type containing all properties constituting the primary key plus all new values for the properties being updated.
  • INSERT - an anonymous type containing all values for the inserted entity
  • DELETE - an anonymous type containing the primary key of the entity to be deleted.

After defining the query, we call the compiler.CompileCommand method to actually compile it. We finally add parameters for the command and its description.
For executing this command we create a method in BooksModule class:

    public void ChangePrice(IEntitySession session, BookCategory category, 
                                        double changePerc) {
      var factor = (100.0 + changePerc) / 100.0d;
      session.ExecuteNonQuery(_changePriceCommand, category, factor);
    }

The method accepts a book category and price change percentage. It transforms this percentage parameter into a price factor, and then executes the command. Note that in this case we use ExecuteNonQuery method (not ExecuteQuery used for the select-by-author command), because the query does not return any entities, just updates them.
In the next section we will see how to use the module we just defined and its custom methods.

Sample startup code

We come to actually using the module we defined in the sample code. First, we need to declare static variables holding "global" objects we will use:

    private static IEntityStore _entityStore;
    private static BooksModule _booksModule; // our custom module
    private static ErrorLogService _errorLog; // log service 

The entity store is the same entity store we had seen before - we will use it to open sessions and to query/update entities. We also need an instance of the books module to use the custom methods we just created.
The new thing is the error log service. This service is implemented by the ErrorLogModule that is part of VITA code base - it is located in the Vita.Modules assembly. This module defines a single entity (IErrorInfo) that is simply an error log entry. Our purpose here is to demonstrate the power of the modules concept. We will merge the ErrorLogModule into our application, the underlying table will "automagically" appear in our database, and we'll have a ready-to-use error logging service. This service will be "blended" into our sample application, as if we had built it from scratch, together with the books module and the entire sample application - but it is in fact imported as a "standard" component.
Now we are going to setup the application. We will no longer use the helper method that we used in the previous parts of the tutorial - we have a module now, which requires more elaborate code. The following code sets up our books module, error log module, creates an entity store with the underlying database, and finally creates the error logging service:

      var modelSetup = new EntityModelSetup("VitaTutorial");
      var defaultArea = modelSetup.AddArea("Part5", "part5"); 
      _booksModule = new BooksModule(defaultArea);
      var errLogModule = new ErrorLogModule(defaultArea); 
      var connString = SetupHelper.GetConnectionString();
      _entityStore = modelSetup.CreateEntityStoreMsSql(connString, updateSchema: true);
      _errorLog = new ErrorLogService(_entityStore);

Do not worry if you do not quite understand what is going on here - we will leave it until the next part of the tutorial to explain the details of the solution structure and setup. For now, it's enough to know that we just create the instances of our global variables - entity store, books module and error log service.

Executing custom commands

Let's use the custom commands we defined in the books module. The following code prints the titles of the books by John Sharp:

      session = _entityStore.OpenSession(); 
      var booksByJohnSharp = _booksModule.GetBooksByAuthor(session, "Sharp");
      foreach (var book in booksByJohnSharp)
        Console.WriteLine("  Book: " + book.Title);

Let's now imagine we have a sale in our book store and for faster sale we want reduce prices on all programming books by 20%. The following code does the job:

      _booksModule.ChangePrice(session, BookCategory.Programming, -20);

Using the error log service

Finally, let's use the error log service to log an exception:

      var errorMessage = "Demo Error, date/time=" + DateTime.Now.ToString();
      try {
        throw new Exception(errorMessage);
      } catch (Exception ex) {
        _errorLog.LogError(ex, null, Environment.UserName, "VitaTutorial");
      }

We use a time-stamped error message, so we can easily verify that exception was logged - the full sample contains the verification code.

Composing application from entity modules

We had seen how entity modules are used for encapsulating groups of entities into a single component, together with some custom logic and specialized methods. When it comes to assembling an application from multiple modules, there are several features you may use to extend and/or integrate the modules:
  • Extending module entities: you may need to add a few properties/columns to an entity defined in a module you use
  • Linking entities from different modules: you may need to make an entity in one module reference an entity in another module through some property.
These customizations are done using a set of facilities implemented by EntityModule class, mainly ReplaceEntity method. You define an extended version of the module-defined entity (by inheriting the interface), and then replace the original entity type with your new extended version.
See more details here: Entity Modules Integration Sample.

Conclusion

We reviewed the entity modules functionality in this part of the tutorial. We created custom commands for querying and updating the data. These custom commands are translated into stored procedures in the database. Again, stored procedures are optional - the commands may be in plain SQL executed from the client. In the future parts of this tutorial we will see how to expose the custom commands through RESTful API.
The other nice feature I would like to mention here is the viewer facility for the exception log implemented by error log module. It displays the exception log in the browser as an HTML page:

tut_part5_errlog.jpg

Clicking on the error message link brings the details page. We will discuss this viewer in the future part of this tutorial when we get to building RESTful Web services with VITA.
The following image shows the output of the sample code execution for this section:

tut_part5_output.jpg


Tutorial Home

Last edited Mar 14, 2012 at 7:36 PM by rivantsov, version 41

Comments

No comments yet.