This project is read-only.

VITA Framework Highlights

Entities

Entities in VITA are .NET interfaces marked with Entity attribute:

  [Entity]
  public interface IBook {

    [PrimaryKey]
    Guid Id { get; set; }

    [Size(100), Index]
    string Title { get; set; } 

    [Unlimited, Nullable]
    string Description { get; set; }

    DateTime? PublishedOn { get; set; }

    IPublisher Publisher { get; set; }
  }

The attributes on properties are self explanatory - they provide an extra information for mapping to database objects. Entity, in ORM terms, is a representation of a row in the database table. One entity - one table, the mapping is simple.
Entities can reference each other - like IBook entity references IPublisher. This represents foreign key relation between database tables. The program code does not manipulate foreign keys directly - you assign entity references, and the framework takes care of foreign key columns.

One-to-many and many-to-many relations

The framework fully supports both types of relations. The IBook entity has a Publisher property pointing to IPublisher entity. Naturally, a publisher has an associated list of books:
  [Entity]
  public interface IPublisher {
  
    [PrimaryKey]
    Guid Id { get; set; }

    [Size(100), Index]
    string Name { get; set; } 
    
    IList<IBook> Books {get;}
  }
The Books property is automatically handled by the framework - the list will be loaded when we access the property.
In a similar fashion we can define a many-to-many relation with Authors. Many-to-many is handled in the database using a link entity:
  [Entity, PrimaryKey("Author,Book")]
  public interface IBookAuthor {
    IBook Book { get; set; }
    IAuthor Author { get; set; }
  }
Now we can define lists on IBook and IAuthor entities. Unlike one-to-many, we have to explicitly identify the link entity IBookAuthor:
  // In IBook entity: 
  [ManyToMany(typeof(IBookAuthor))]
  IList<IAuthor> Authors { get; }
  ... 
  // In IAuthor entity:
  [ManyToMany(typeof(IBookAuthor))]
  IList<IBook> Books { get; }   
Notice that properties have only 'get' accessor - you never assign it, the list is maintained automatically by the framework. It will be automatically instantiated and loaded when your code first reads the property.
Note that although the list properties appear in entity definition, the underlying table does not have a corresponding column - the property is maintained in c# code, through interpreting foreign keys in the database.

Database objects - everything happens automagically

When writing an application using VITA framework, you start with entity definitions. Once you've done this, you need a database with tables matching your entity definitions. You don't have to create anything manually, or run DDL scripts - VITA does this for you. Skipping some unimportant details, here's the process: you register all your entities with an entity application (based on class provided by VITA), provide a connection string to an empty database, and just say "RUN!". VITA will create database structures matching your entity interfaces - tables, columns, primary and foreign keys, indexes, constraints, even CRUD stored procedures.
The process is incremental - at any moment in the app lifecycle you can change entity interfaces, add/remove properties here and there, define new indexes, etc. At application start VITA will detect that there are changes - then it will go and update the database schema accordingly.
Database follows c#/VB code. You don't need to write and maintain DDL script for every model change and apply them on individual machines. You just modify entities, commit the code to source control server. Once the new build is out, on first run will update the database schemas - be it other developer's machine, staging server or production server.
Note: for production servers the automatic update can be disabled, and generated DDL scripts can be applied manually.
DB-first scenario is also supported - vdbtool application can generate VITA entity definitions from an existing database.

Application initialization - we skip it

We will skip over the details of initialization code - see sample code for code examples and VITA Quick Start Guide for more information. The initialization process results in a connected EntityApp object - a global singleton containing all meta-information (entity model) and serving as an access point to the physical database.

Querying entities

To start actually querying the database, we first must open an entity session - an object representing a connection to the physical database:
  var session = MyEntityApp.OpenSession(); 
We can get a list of books from the database table, optionally specifying paging parameters:
  var books = session.GetEntities<IBook>(skip: 0, take: 10);
We can query a book by its ID:
  var someBook = session.GetEntity<IBook>(bookId);
Having a book, we can access it's publisher:
  var pubName = someBook.Publisher.Name; 
Publisher entity (IPublisher interface) is a related entity residing in the Publisher table. You don't need any special preparations before you access the Publisher through the book - just read the book.Publisher property, and VITA will resolve a foreign key in Book entity, query the database, load the publisher entity and stick into the property - all automatic, in a lazy-load manner.
In the same way, you can access the authors list:
  var authCount = someBook.Authors.Count;
The list is loaded automatically, by querying the link entity IBookAuthor and selecting the list of authors for a particular book. You do not need to do anything extra - everything happens behind the scene, automatically.

LINQ

We can use LINQ to query the database:
var yearAgo = DateTime.Now.AddYears(-1);
var query = from bk in session.EntitySet<IBook>()
            where bk.Price < 20 && bk.PublishedOn > yearAgo
			select bk;
var cheapRecentBooks = query.ToList(); 			
You can use joins, Group-by, order by, Skip/Take clauses, create anonymous objects in output clauses - all facilities defined for the original Linq-to-Sql technology are fully supported.
You can use list properties we had seen previously, VITA's LINQ engine will correctly interpret them:
  var johnSharp = session.EntitySet<IAuthor>()
        .Where(a => a.LastName == "Sharp").First();
  var query = from bk in session.EntitySet<IBook>()
              where bk.Authors.Contains(johnSharp)
              select bk;
  var booksByJSharp = query.ToList(); 			
You can access the SQL executed by the LINQ query using the session.GetLastCommand() method - it returns the IDbCommand instance with CommandText (SQL) and parameters of the last executed query.
Translating c# expression trees into SQL queries is a time-consuming operation. VITA runtime maintains LINQ Query Cache - a cache for translated LINQ queries. When client code sends a new LINQ query, the runtime quickly and efficiently determines its 'shape', and looks up a translated query in cache. If cached query is found, it is reused with new parameter values. You don't need to do anything differently to take advantage of query caching - it is automatic, unlike other frameworks where you have to explicitly use a special facility like CompiledQuery class.

Search queries

Search forms require dynamic building of search SQL, with filtering predicates only for search terms/columns that user provided. VITA provides a PridicateBuilder that makes it quite easy to build such dynamic queries using strongly-typed LINQ expressions. See unit tests and sample books store project for samples of dynamic query building for search.

Object identity - single object for a row, no matter what

Entity session properly maintains object identity - for a given database row, there will be only one object instance representing it in client code if it is retrieved through the same entity session. No matter how you obtain an entity instance - through direct query, from other entity property, from LINQ query - the entity instance will be the same .NET object.

Update Operations

Entity session tracks all entity objects retrieved through it, and it tracks all entities modified by the client code. For updating data in the database, you just retrieve entities of interest, change their properties, insert new or delete existing entities, and finally call session.SaveChanges() method. The framework will submit all changes to the database in one transaction:
  var someBook = session.GetEntity<IBook>(bookId);
  someBook.Description += "( some notes )";
  var otherBook = session.GetEntity<IBook>(otherBookId);
  otherBook.Price = otherBook.Price * 0.8;
  var author = session.NewEntity<IAuthor>();
  author.FirstName = "Jessy";
  author.LastName = "Jones";
  someBook.Authors.Add(author); //this will create a link
  session.DeleteEntity<IBookReview>(reviewId); 
  session.SaveChanges();  
For referential integrity constraints, deletes/inserts should be properly sequenced - VITA does this automatically. The order of changes in c# code does not matter - the framework will re-order changes properly.
VITA uses stored procedures by default to perform updates - these are automatically generated at application startup. Alternatively, you can use plain SQL queries by adjusting configuration settings.

Object Tracking and memory

As we already mentioned, entity session tracks all objects loaded through it. This does not cause out-of-memory problem when you retrieve too many objects, like it happens in NHibernate. The objects are tracked through weak references, so once the client code drop the reference to an entity, it is collected by garbage collector. Modified entities are automatically switched to strong-reference tracking mode - so entity session never loses a pending change.

Batched updates

The framework supports a feature that is a huge performance booster - batched updates. One way to save changes for multiple entities is to send multiple CRUD commands to the database - one by one, one round-trip for each entity. That's how it is usually done.
VITA can do a much better thing - create one multi-line SQL text containing all calls to CRUD stored procedures, with proper values of parameters in the call, and then send this SQL to the database in one round-trip. The batch is enclosed in BeginTransaction/Commit statements - they are part of the text, so no extra round-trips for opening/committing the transaction.
Note that this happens automatically, the client code does not need to do anything - only enable batch update mode at configuration time, and then all session.SaveChanges() result in a single call to the database server, no matter how many entities are updated. This feature is a real performance booster - it speeds up the update operations by 5-10 times.
The batch mode is extremely beneficial in log-like activities. VITA log modules save log records on background threads in batch mode - all log entries accumulated within last 1 second are batched together and send to the server in one command.
To see batched updates in action, run the Extended unit tests project, and look at the log file (in bin/debug folder).

Identity columns

VITA fully supports identity columns. The problem with these columns is usually a need to retrieve the identity value for an inserted record, and then propagate it to referencing foreign keys in child rows about to be inserted. VITA runtime handles all this automatically. You can create one or even multiple interconnected entities with identity columns, and then submit them in one transaction. The runtime will perform inserts, retrieve and propagate identity values between entities. It even works in batch mode - the runtime uses OUTPUT parameters in batched stored procedure calls to pass identity values between the calls.

Direct database access

You can use DirectDbAccess facility to execute direct SQL statements. You can open connection and start/commit transactions, execute custom SQLs or call stored procedures, mix them with VITA's data operations (selects, LINQ queries or SaveChanges() calls) in the context of the single transaction.

Validation and client-fault handling

VITA offers a consistent conceptual framework and implementation for handling "soft" errors - user mistakes that result in operation cancellation. VITA provides methods for easy validation of input information, accumulating multiple errors in one container and then throwing ClientFaultException containing multiple errors. For Web service applications this exception can be automatically handled by VITA-provided HTTP message handler, which translates into BadRequest status response with detailed client faults serialized in response body (as Json or Xml).
Here is an example code validating a signup request:
  context.ValidateTrue(!string.IsNullOrEmpty(signup.UserName), 
      ClientFaultCodes.ValueMissing, "UserName", null, "UserName may not be empty");
  context.ValidateTrue(!string.IsNullOrEmpty(signup.Password), 
      ClientFaultCodes.ValueMissing, "Password", null, "Password may not be empty");
  context.ThrowValidation(); //throws ClientFaultException
'context' is an OperationContext instance - among other things, it serves as a container for accumulating input errors. If this is a data API controller code, and there is a WebCallContextHandler in the pipeline, then the exception will be translated into BadRequest response, with list of client faults in the body. The client application (ex: AngularJS) can use this information to present errors to the user in UI.
See more on Web stack integration in the section below.

Entity Cache - easy and powerful data caching

VITA implements powerful data caching solution. Most frameworks, ORM or specialized cache implementations, provide only 'cache-by-id' - caching single records by ID. So while requests like GetAuthor(authorId) are served from cache, the query like GetAuthor(a => a.LastName == someName) have to go to the database.
VITA's entity cache provides this, and also provides a unique full-table caching. You can specify at startup what tables should be fully cached in memory. The runtime pre-loads the tables and then serves all select queries - including LINQ queries (!) - from cached data. LINQ queries are rewritten on-the-fly into Linq-to-objects expressions which are executed against in-memory lists. Rewritten queries are cached in LINQ query cache, the same way SQL-translated query definitions are cached.
To use cached data, you don't need to request data from cache explicitly. Like other VITA facilities, cache works behind the scene, automatically and transparently - including automatic invalidation on update.

Authorization Framework

VITA implements powerful, easy to use and flexible authorization subsystem, unrivaled by any .NET library or development framework out there. You can setup data access rules up to row/column level, with connection to specific user role in the system and his/her relation to the data. All you need to do is open an entity session associated with particular user - and VITA's runtime start watching all data operations and checking if the user is in fact allowed to perform it. As soon as authorization rules are violated, it throws AccessDenied exception.
Here is a quick snapshot of how it works, in client code. Let's say we have an online book store, and users can write book reviews. Each user can read any review, while users can edit their own reviews. We have two users - Dora and Diego, and a review by Dora:
// User can create, update, delete reviews
  var secureSession = OpenSecureSession(dora); //Dora is current user
  var doraReview = secureSession.GetEntity<IBookReview>(doraReviewId);
  doraReview.Review += " (update: some more info)";
  secureSession.SaveChanges(); //Everything works fine

  // Now Diego tries to update Dora's review
  var secureSession = OpenSecureSession(diego); //Diego is user now
  var doraReview = secureSession.GetEntity<IBookReview>(doraReviewId); // goes OK
  doraReview.Review += " booo!"; //BANG! AccessDenied is thrown

Modular application construction

VITA is the first framework to enable composing data-connected applications from independently developed components. As we already described, VITA maintains the DB objects automatically, straight from entity definitions. DB model follows the c# code. This makes it possible to package a set of entities (interfaces) into a standalone module and then include the module as a whole into the application. Entities from module will be added to the application model, and corresponding database objects (tables, indexes, stored procedures) will be created automatically. This makes it possible to build data-connected applications as a composition of modules - independently developed and tested. The component-based code reuse is finally possible, just like we use pre-built UI controls in Windows Forms applications.
We strongly believe that absence of component-based architecture is the biggest drag and source of many of well know issues for database applications.

Standard modules

Using modular construction technology to the fullest, VITA provides a number of 'standard' pre-built application modules, ready to be used in any application. Some of these modules are:
  • Login module - provides advanced signup/login functionality. Stores usernames and passwords, with passwords stored as strong hashes, using BCrypt or RFC-2898 hashing.
    • Basic login/logout functionality
    • Multi-factor authentication and supporting workflow
    • Password recovery/reset feature with full confirmation workflow
    • Storage and management of secret questions and answers. A utility method can be used to easily import a list secret questions into your database. A list of good secret questions comes with VITA distribution (as a resource text file).
    • Maintaining a list of trusted devices for a user, so that certain computer can be automatically recognized as trusted, so no 2-Factor process in necessary
    • Automatic suspension of an account after certain number of failed login attempts, to prevent dictionary attacks
  • Logging modules - a number of separate but interconnected modules implemented various kinds of logs
    • Error log - for logging serious (fatal) exceptions, with extended details
    • Operation log - logs all data operations (SQL) with parameter values plus some statistics (execution time, record count). The log can be configured to run in 'sleep' mode - accumulate but do not persist if no exception; but if exception is thrown, all detailed information is persisted, with a link to error log entry.
    • Transaction log - writes a log entry for every update transaction (session.SaveChanges() call); records datetime, user id, user session id, and optionally a full list of changes made (list of 'entity-type/operation/pk-value' references). This change list information can be used for database synchronization - if we need a list of all records that had been changed (inserted/modified/deleted) since certain date, we just query the transaction log table. Also provides an easy way to add 'tracking columns' to other entities - columns that contain the ID of transaction that created or last updated the record; this facility is a efficient replacement for 'createdby', 'updatedby' etc columns usually added to database table for tracking. These injected columns are handled automatically by the runtime.
    • Incident Log - log for events that are not errors by themselves but may require some attention, especially if multiple events happen in some time period. The log allows setting triggers that fire custom code reacting to new incidents. Example - a failed login attempt. Multiple failed logins in a row may indicate a dictionary attack. A simple way to defend is to suspend the account for a few minutes. Login module defines LoginFailedTrigger that can be configured to do just that.
    • DbModelChange Log - log of all db model updates applied to the database. Logs DDL SQL scripts fired by VITA's automatic schema update facility.
    • WebCall log - logs all web calls; written by WebCallContextHandler - HTTP message handler provided by VITA. Logs all information about a web call - request URL, incoming/outgoing headers/cookies, HTTP request/response bodies, and SQLs executed during the call. May be configured to log only basic information for no-problem calls, but will automatically switch to log-all mode in case of exception.
  • DbInfo module - maintains a single table with a single record containing information about the database instance - its current model version, and installation type. The installation type flag allows altering the schema update behavior - if the database is production database, automatic updates are disabled. In this case, the schema should be updated by administrator by running DDL scripts.
  • PersistentSession module - maintains a persistent session for a logged in user, across multiple web calls. Integrated with web-related functionality (see next section)

Web stack integration

VITA provides a number of classes for easy integration with ASP.NET Web stack, geared more to RESTful data services built on Web API framework. These classes allow you to easily hook-up exception handling, error logging, web call logs and other facilities. HttpClientWrapper class makes it easier to make RESTful calls from the client - this component proved to be extremely useful in unit tests for web services.


Last edited Apr 20, 2015 at 10:15 PM by rivantsov, version 9

Comments

No comments yet.