NonTransactionalReads is a specific Session option that allows to read once fetched entities right from Session cache without the requirement to have an open transaction. This mode might be useful for desktop application that have only one Session instance for the entire lifetime of application or for services like auditing, logging, etc. that need to read persistent fields without having any idea of sessions, transactions, etc.
To demonstrate the approach, let's start with a sample. Here is the persistent class with one lazy loading field:
[HierarchyRoot]
public class Country : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field(Length = 30)]
public string Name { get; set; }
[Field(Length = 3)]
public string Code { get; set; }
[Field(LazyLoad = true)]
public string Description { get; set; }
public Country(Session session)
: base(session) {}
}
Say, we want to have a cache of countries in our application. We create some countries on Domain start, e.g.:
var domainConfiguration = DomainConfiguration.Load("Default");
var domain = Domain.Build(domainConfiguration);
using (var session = domain.OpenSession())
using (var tx = session.OpenTransaction()) {
new Country(session) {
Code = "AND",
Name = "Andorra",
Description = "A tiny country in the middle of Europe"
};
new Country(session) {
Code = "ATA",
Name = "Antarctica",
Description = "Icy paradise for penguins"
};
// other countries...
tx.Complete();
}
To implement some sort of cache in earlier version of DataObjects.Net you would load countries in every transaction or avoid frequent loading of persistent objects from database by converting them into some kind of POCO and cache them.
In DataObjects.Net 5.0 you can do this in a more comfortable way using the new NonTransactionalReads flag.
// Configuring session
var options = new SessionConfiguration(SessionOptions.Default | SessionOptions.NonTransactionalReads);
var session = domain.OpenSession(options);
Dictionary<string, Country> cache;
// Loading cache with data in one transaction
using (var tx = session.OpenTransaction()) {
cache = session.Query.All<Country>().ToDictionary(i => i.Code);
tx.Complete();
}
// accessing data from another transaction
using (var tx2 = session.OpenTransaction()) {
// cached objects are not being reloaded from database. they are consumed as is
var antarctica = cache["ATA"];
Console.WriteLine(antarctica.Name);
// accessing a lazy loading field. session loads it on demand
Console.WriteLine(antarctica.Description);
tx2.Complete();
}
// disposing session. Data is not accessible anymore
session.Dispose();
What is more interesting, the same behavior can be achieved without using any transactions at all. Let's re-write the sample:
// Configuring session
var options = new SessionConfiguration(SessionOptions.Default | SessionOptions.NonTransactionalReads);
var session = domain.OpenSession(options);
Dictionary<string, Country> cache;
// Loading cache with data without any transaction
cache = session.Query.All<Country>().ToDictionary(i => i.Code);
// accessing data
var antarctica = cache["ATA"];
Console.WriteLine(antarctica.Name);
// accessing a lazy loading field. session loads it on demand
Console.WriteLine(antarctica.Description);
// disposing session. Data is not accessible anymore
session.Dispose();
So, the key points of the NonTransactionalReads mode:
- No matter how data is loaded from database (with the help of transaction or not), it is accessible from outside the boundaries of the transaction, if any.
- Data is cached on Session level. As long as the Session is alive (not disposed), the data will be available from its cache.
- In case of accessing LazyLoad fields, Entity references and EntitySets, data is automatically fetched from database on demand.
In the previous post we explained DataObjects.Net built-in logging capabilities. Now it is time to show how popular logging libraries like log4net and NLog can be integrated with it.
We would need the list of names of built-in logs:
Xtensive.Orm - logs Session & Transaction-related events and exceptions.
Xtensive.Orm.Building - logs events during the Domain building process.
Xtensive.Orm.Sql - logs SQL statements sent to database server.
Xtensive.Orm.Upgrade - logs events during database schema upgrade.
log4net
- Add DataObjects.Net logging provider for log4net. It will automatically add log4net.
- Set up log provider in Xtensive.Orm configuration section
<Xtensive.Orm>
<domains>
<domain ... >
</domain>
</domains>
<logging provider="Xtensive.Orm.Logging.log4net.LogProvider, Xtensive.Orm.Logging.log4net">
</Xtensive.Orm>
- Configure log4net. Use the above-mentioned loggers' names, e.g.:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
...
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
</configSections>
<log4net>
<appender name="ConsoleAppernder" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger %message%newline" />
</layout>
</appender>
<logger name="Xtensive.Orm">
<level value="ALL" />
<appender-ref ref="ConsoleAppernder" />
</logger>
</log4net>
</configuration>
NLog
- Add DataObjects.Net logging provider for NLog. It will automatically add NLog.
- Set up log provider in Xtensive.Orm configuration section
<Xtensive.Orm>
<domains>
<domain ... >
</domain>
</domains>
<logging provider="Xtensive.Orm.Logging.NLog.LogProvider, Xtensive.Orm.Logging.NLog">
</Xtensive.Orm>
- Configure NLog. Use the above-mentioned loggers' names, e.g.:
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="console" xsi:type="Console" />
</targets>
<rules>
<logger name="Xtensive.Orm" minlevel="Debug" writeTo="console" />
</rules>
</nlog>
Pretty simple, huh? You can even write your own log provider with ease. Just take a look at the implementation of these two in DataObjects.Net Extensions repository, default branch.
Yeah, we have to admit it - logging in DataObjects.Net 4.x was implemented quite odd and its API was not clear even to some of product developers, not to mention customers who were wondering whether the bloody logging works at all and how the hell it is supposed to be configured. Finally, these days are gone and starting from DataObjects.Net 5.0 we re-implemented the logging.
So, what has changed? The answer is - almost all, including inconsistent loggers' names, tricky xml configuration and peculiar log formats. Imho, the best characteristic for the new logging will be "brain-dead obvious" "extremely simple".
OK, let's dive into the details.
Loggers
These are named logs that record messages from specific parts of DataObjects.Net. There are 4 of them:
Xtensive.Orm - logs Session & Transaction-related events and exceptions.
Xtensive.Orm.Building - logs events during the Domain building process.
Xtensive.Orm.Sql - logs SQL statements sent to database server.
Xtensive.Orm.Upgrade - logs events during database schema upgrade.
Log writers
Console - writes messages to application's console window, if any. Useful for small & test projects.
DebugOnlyConsole - the same as Console but writes log data only when a project is run in Debug mode.
path_to_file - appends log messages to a specified file. If file is absent, it will be created. Useful for development & production environments. path_to_file can be either absolute or relative to the application location.
None - writes to /dev/null.
Configuration
Configuration of built-in logging is made in application configuration file (app.config or web.config). Logger configuration takes 2 parameters: logger name as source and log writer name as target.
Example: Logging everything to Console
<Xtensive.Orm>
<domains>
<domain name="Default".../>
</domains>
<logging>
<log source="Xtensive.Orm" target="Console"/>
<log source="Xtensive.Orm.Building" target="Console"/>
<log source="Xtensive.Orm.Sql" target="Console"/>
<log source="Xtensive.Orm.Upgrade" target="Console"/>
</logging>
</Xtensive.Orm>
Example: Logging everything to files
<Xtensive.Orm>
<domains>
<domain name="Default".../>
</domains>
<logging>
<log source="Xtensive.Orm" target="C:\Orm.log"/>
<log source="Xtensive.Orm.Building" target="C:\Orm.Building.log"/>
<log source="Xtensive.Orm.Sql" target="C:\Orm.Sql.log"/>
<log source="Xtensive.Orm.Upgrade" target="C:\Orm.Upgrade.log"/>
</logging>
</Xtensive.Orm>
To simplify things in case you want to write messages from all loggers into the same stream, you may want to use our magic asterisk, like this:
Example: Logging everything to a file
<Xtensive.Orm>
<domains>
<domain name="Default".../>
</domains>
<logging>
<log source="*" target="C:\Orm.log"/>
</logging>
</Xtensive.Orm>
In addition to the built-in log writers DataObjects.Net 5 is shipped with 2 extensions allowing to redirect logging output to NLog & log4net. Read about that in the second part.
I'm not sure whether anyone remembers, but in the beginning of DataObjects.Net 4.0 epoch we had a semi-successful attempt to provide our own in-memory database implementation, which existed for several years but eventually was removed from the product by various reasons.
And guess what? Starting from DataObjects.Net 4.6.4, in-memory database support is back! But this time it is not our own version, but SQLite in-memory mode.
To connect to in-memory database use the following connection string:
<domain
name="Default"
provider="sqlite"
connectionString="Data Source=:memory:"
...>
<domain>
The advantages of such kind of storage are the highest possible speed of database-related operations and no necessity to bother with database files on disk. On the contrary, there are also disadvantages, like the absence of multiple connections to database and no real persistence.
SQLite in-memory mode has some requirements that DataObjects.Net must have met. The most important one is that as soon as connection to in-memory database is closed, the database is destroyed and all data is lost.
Before version 4.6.4, DataObjects.Net always built database scheme in a separate connection which was closed after that procedure. Moreover, the ORM didn't keep any connections open at all, closing them as soon as corresponding session is disposed to free up resources and return connection to connection pool. To support the SQL in-memory mode we had to change the connection management layer and make it more flexible and configurable.
And so we did it. Now we open connection to SQL in-memory database only once on domain build and keep it open until domain is disposed, protecting the database from being destroyed, so your data is safe while domain is alive. In the meantime, you may open and close sessions as usual except you shouldn't open concurrent sessions as only one connection is allowed at a time.
Bad practice: using nested sessions with SQLite in-memory database. Session use the same connection concurrently.
using (var session1 = domain.OpenSession()) {
using (var t1 = session1.OpenTransaction()) {
using (var session2 = domain.OpenSession()) {
// Here you'll get InvalidOperationException
// that the connection is used by another session
Good practice: using subsequent sessions. Both sessions use the same connection, but not concurrently.
using (var session1 = domain.OpenSession()) {
using (var t1 = session1.OpenTransaction()) {
// Do some stuff
t1.Complete();
}
}
using (var session2 = domain.OpenSession()) {
using (var t2 = session2.OpenTransaction()) {
// Do some stuff
t2.Complete();
}
}
This article starts a series of posts covering advanced mapping capabilities introduced in DataObjects.Net 4.6.
Idea
The idea of advanced mappings is simple — to have a way to spread groups of persistent classes among different database schemas and even different databases according to number of mapping rules.
Basics
Advanced mapping configuration is based on set of mapping rules, each of them defines how to map a particular entity. Mapping rule consists of two logical parts:
- condition (defines when the rule is applied)
- mapping (specifies in what database/schema to place the target)
The rules could be specified in configuration file or right in code using fluent API. They are processed in the order they appear, so the first matching rule wins.
In addition to collection of rules, two properties specify default mapping: DefaultSchema and DefaultDatabase. These properties are used as fallback values if no rule matches entity being processed. Note, that 'defaultSchema' option is mandatory if any rules for schemas are specified.
XML configuration
OK, we are done with theory. Let's write some real mapping configurations. The most trivial one is setting the default schema. This is already supported by DataObjects.Net since version 4.3. Let's recall it:
<domain defaultSchema="myapp" ... />
Starting from version 4.6, we can write more precise mapping rules. They are defined in <mappingRules> element inside domain configuration.
<domain defaultSchema="myapp" ... >
<mappingRules>
<rule namespace="MyApp.Model.Foo" schema="myapp_foo" />
</mappingRules>
</domain>
The above example maps all types from namespace 'MyApp.Model.Foo' to schema 'myapp_foo'. All other types are mapped to schema 'myapp' (fallback rule). If you have model with multiple assemblies in might be convenient to map to assembly names instead of namespaces:
<domain defaultSchema="mysite" ... >
<mappingRules>
<rule assembly="MySite.Model.Blog" schema="mysite_blog" />
<rule assembly="MySite.Model.Forum" schema="mysite_forum" />
</mappingRules>
</domain>
The above example splits model into two schemas. One for persistent types related to blog module. The other one is for forum module. The rest is mapped to 'mysite' schema.
Fluent configuration
As alternative to XML-based configuration you can programmatically define mapping in code. Mapping rules could be added by using DomainConfiguration.MappingRules collection. Two examples from previous section will look like this:
domainConfiguration.DefaultSchema = "myapp";
var rules = domainConfiguration.MappingRules;
rules.Map("MyApp.Model.Foo").ToSchema("myapp_foo");
domainConfiguration.DefaultSchema = "mysite";
var rules = domainConfiguration.MappingRules;
blogAssembly = typeof (MySite.Model.Blog.Post).Assembly;
forumAssembly = typeof (MySite.Model.Forum.Thread).Assembly;
rules.Map(blogAssembly).ToSchema("mysite_blog");
rules.Map(forumAssembly).ToSchema("mysite_forum");
The last example uses hypothetical 'MySite.Model.Blog.Post' and 'MySite.Model.Forum.Thread' types to discover the corresponding assemblies.
To be continiued
This post covered basic ideas as well as mapping to multiple schemas. The second part will cover mapping to multiple databases and related application design techniques. Stay tuned.
While DataObjects.Net 4.4.1 & 4.3.8 Final installers are being prepared and tested, here is one more post about one not so well known feature of effective domain modelling and how that feature is extended in the upcoming release.
DataObjects.Net includes a set of rules for defining the default value for a persistent property.
The rules are the following:
Let T is the type of a persistent property, then:
1. T is a primitive typeIf T is a primitive type, Guid or String then the default value for the property is default(T) and the type of the underlying column is T.
public class Animal : Entity {
...
[Field]
public int Age { get; set; }
CREATE TABLE Animal (
...
[Age] [int] NOT NULL
2. T is Nullable<>If T is Nullable<> then the default value is NULL and the type of the underlying column is Nullable T.
public class Animal : Entity {
...
[Field]
public int? Legs { get; set; }
CREATE TABLE Animal (
...
[Legs] [int] NULL
3. T is a reference typeIf T is a reference type then the default value is NULL and the type of the underlying column is the type of primary key of the referenced Entity.
public class Animal : Entity {
...
[Field]
public Person Owner { get; set; }
CREATE TABLE Animal (
...
[Owner.Id] [int] NULL
These rules are more or less obvious and straightforward but what if the overwhelming majority of animals in your application universe have 4 legs and you don't want to set this property manually again and again? Or there should not be homeless animals? Then I guess, you should have the opportunity to override these rules.
This can be done with the help of [Field] attribute.
1. Setting default value for a primitive persistent propertypublic class Animal : Entity {
...
[Field(DefaultValue = 4)]
public int? Legs { get; set; }
CREATE TABLE Animal (
...
[Legs] [int] NULL
...
ALTER TABLE [dbo].[Animal] ADD CONSTRAINT [DF_Animal_Legs] DEFAULT ((4)) FOR [Legs]
2. Changing nullability of a reference field.public class Animal : Entity {
...
[Field(Nullable = false)]
public Person Owner { get; set; }
CREATE TABLE Animal (
...
[Owner.Id] [int] NOT NULL
This setting leads to NOT NULL column which means that the value of the property must be set prior to any Session.Persist() call while constructing the Animal entity. Therefore, the right way to set such properties is inside entity's constructor, before executing any queries.
3. Setting default value for a reference persistent property.Note, this feature is implemented in the upcoming version of DataObjects.Net (by upcoming I mean 4.3.8 or 4.4.1 branches).
public class Animal : Entity {
...
[Field(DefaultValue = 1)] // 1 here is the key of the default animal owner
public Person Owner { get; set; }
CREATE TABLE Animal (
...
[Owner.Id] [int] NULL
...
ALTER TABLE [dbo].[Animal] ADD CONSTRAINT [DF_Animal_OwnerId] DEFAULT ((1)) FOR [Owner.Id]
This makes sense in a scenario when the key of the default referenced entity is already known on a compilation step or it is a well-known constant identifier so there is an Entity with that key in the domain.
I hope these little tricks will help you in modelling your domains with even less efforts.
In this post I'm going to describe one of the most advanced and appreciated features of DataObjects.Net — automatic database schema upgrade.
You know, automatic database schema generation for Code-First & Model-First ORMs is a vital requirement, however while the task is not trivial by itself, it is not so difficult in comparison with continuous domain model & database schema synchronization. One can imagine all kind of operations on domain model: creating, renaming, moving, splitting, merging, deleting types, fields, associations, field options, index options, validation constraints, etc. All these types of changes must be propagated by ORM to database schema more or less transparently and without any possible loss of data.
In DataObjects.Net this goal is achieved with the help of mature upgrading framework that had been polished for years in version 3.x branch (starting from 2003 year) and then was migrated to version 4.x one with numerous core updates and extensions.
This could be surprising, but the core component of the upgrade framework is not translation of upgrade actions to SQL or making changes to a database schema, but the effective comparison of 2 abstract models. I'm talking about Xtensive.Modelling that was invented for describing and comparing models of any thing. In case of DataObjects.Net it is being used to compare 2 models of a storage.
Here is how the whole process is constructed. We build domain model and convert it to a unified model of storage. In parallel, we extracting metadata about database schema and converting it to the model of existing storage. After that we compare these two and additionally pass to the comparison procedure a set of upgrade hints that are provided by a user. As a result, we get a difference between 2 models and a sequence of upgrade actions that must be executed in order to convert the existing storage to the new one.
Upgrade actions are taken place only when a domain is being built in DomainUpgradeMode.Perform or PerformSafely. These two have one principal distinction: while Perform silently alters whatever is required even with data loss, PerformSafely guarantees that nothing will be removed unless is explicitly declared by a user by using special upgrade hints and attributes. The hints are nonetheless important and useful in both scenarios as they provide additional information for the comparison routine on what changes are made to domain model.
What are these hints?
- RenameTypeHint — for renaming a persistent type
- RenameFieldHint — for renaming a persisting field
- RemoveTypeHint — for removing a persistent type
- RemoveFieldHint — for removing a persistent field
- ChangeFieldTypeHint — for a situation when type of a persistent field is changed
- MoveFieldHint — for moving a persistent field from one type to another
- CopyFieldHint — for copying a persistent field from one type to another
The hints mechanism is intended to be used in custom implementation of UpgradeHandler class. This can be included into an assembly that is going to be upgraded from version "1.0.0.0" to another one and can look like this:
public class MyUpgradeHandler : UpgradeHandler
{
public override bool CanUpgradeFrom(string oldVersion)
{
return oldVersion == "1.0.0.0";
}
protected override void AddUpgradeHints()
{
var hintSet = UpgradeContext.Hints;
hintSet.Add(
new RenameTypeHint("MyProduct.Model.Customer", typeof (Person)));
hintSet.Add(
new RenameFieldHint(typeof (Person), "Name", "FullName"));
}
}
More about the hints and usage examples in our manual.
The upgrade hints infrastructure serves well for the overwhelming majority of scenarios but there are relatively rare situations where a change in domain model can't be described with these hints. We were gathering information and analyzing such cases and afterall, decided to extend the interface of UpgradeHandler class to provide an additional point where the upgrade routine can be corrected by user. On the picture above there is the last point, where upgrade actions are generated based on the storage models comparison result. Until now, these actions were automatically applied to a database schema and there was no way to control this process.
We are adding a method to UpgradeHandler class that exposes a sequence of upgrade actions before thay are applied to database scheme:
public class UpgradeHandler : IUpgradeHandler
// ...
public virtual void OnBeforeExecuteActions(UpgradeActionSequence actions)
{
// In overridden method actions can be added, edited, removed, etc.
}
}
UpgradeActionSequence contains all actions that will be played against a database scheme, split in groups. An action is a regular SQL command. In this method any action can be removed, edited, moved from one group to another, new actions can be added to the sequence and so on.
The actions are executed in the following order:
01. NonTransactionalEpilogueCommands
02. Open transaction
03. CleanupDataCommands
04. PreUpgradeCommands
05. UpgradeCommands
06. CopyDataCommands
07. PostCopyDataCommands
08. CleanupCommands
09. Commit transaction
10. NonTransactionalEpilogueCommands
Note the transaction boundaries: all commands except those that don't support transactional execution are run in one transaction so on any error the scheme will stay valid and integral.
The new bits are being tested now and will be available soon as nightly builds. After the thorough testing, the updated DataObjects.Net will be available as usual on our website.
Thanks for your attention.
Today DataObjects.Net team is happy to publish the third beta of 4.5 version. This release mostly contains changes in the security system as well as bug fixes merged from the stable branch.
After the Beta 2 was published, we received great feedback concerning the security system and we are very glad for that. Thanks to all of you who dropped us a line concerning this stuff.
Changes from Beta 2First and the most important one is that roles are made persistent. There were numerous requests for this feature, moreover the idea of Terje Myklebust that he formulated as a templates for roles fits to this pattern very well — a persistent class serves as a template, so you can have as many parameterized instances of it as you need. Here is the updated Role hierarchy:
public interface IRole : IEntity
{
[Field]
string Name { get; }
[Field]
[Association(PairTo = "Roles", OnOwnerRemove = OnRemoveAction.Clear, OnTargetRemove = OnRemoveAction.Clear)]
EntitySet Principals { get; }
IList Permissions { get; }
}
[Index("Name", Unique = true)]
public abstract class Role : Entity, IRole
{
[NotNullConstraint(Mode = ConstrainMode.OnSetValue)]
[Field(Length = 128)]
public string Name { get; protected set; }
[Field]
public EntitySet Principals { get; private set; }
public IList Permissions { get; }
protected void RegisterPermission(Permission permission);
protected abstract void RegisterPermissions();
protected Role(Session session)
: base(session)
{
Name = GetType().Name;
}
}
Xtensive.Practices.Security contains the abstract class Role that implements all the required infrastructure for permission registration and handling. Each instance of Role must contain unique name. By default, Role class sets name property to name of the concrete type. In case you want more than one instances of a role of the same type, you must give them unique names. Role and Principal now are connected via paired entitysets. So a principal knows all its roles and vice versa.
Note that the Role class doesn't define hierarchy root and key fields. You must do this manually as you do in case of Principal, for example:
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
public abstract class EmployeeRole : Role
{
[Field, Key]
public int Id { get; set; }
protected override void RegisterPermissions()
{
// This is base role for every employee
// All employees can see products
RegisterPermission(new Permission());
// All employees can see employees
RegisterPermission(new Permission());
}
protected EmployeeRole(Session session)
: base(session)
{
}
}
All other roles inherit EmployeeRole. Note that in case of roles the SingleTable inheritance scheme is the best option as all role types are similar and can be effectively placed inside a single table.
Other changes:Session.ValidatePrincipal method is renamed to Session.Authenticate.
IPrincipalValidationService is renamed to IAuthenticationService
GenericPrincipalValidationService is renamed to GenericAuthenticationService
GenericRoleProvider, PrincipalRole, PrincipalRoleSet, RoleSet are discarded as useless.
Roles as templatesThere was a fruitful discussion after the previous post and the idea of role templates was suggested. Here is how it can be implemented in the Beta 3. I'll take the same scenario with branches and office manager roles.
Fisrt, let's define branches
[HierarchyRoot]
public class Branch : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field]
public string Name { get; set; }
public Branch(Session session)
: base(session)
{}
}
And then, define a branch office manager role
public class BranchOfficeManagerRole : EmployeeRole
{
[Field]
public Branch Branch { get; set; }
private IQueryable GetCustomers(ImpersonationContext context, QueryEndpoint query)
{
return query.All()
.Where(c => c.Branch == Branch);
}
protected override void RegisterPermissions()
{
RegisterPermission(new Permission(true, GetCustomers));
}
public BranchOfficeManagerRole(Session session, Branch branch)
: base(session)
{
Branch = branch;
Name = branch.Name + "OfficeManager";
}
}
See, the role class is not tied to a concrete branch office instance, it only declares a connection between a role type and a branch type instead. The concrete Branch instance is passed in the constructor. Also note how Name property is formed in the constructor to avoid non-uniqueness.
Having these classes declared we can easily use them in creating the appropriate role instances as many as we need. Moreover, we can create branches and branch-dependent roles in runtime.
// Branches
var southBranch = new Branch(session) { Name = "South"};
var northBranch = new Branch(session) { Name = "North"};
// Roles
var southBranchOfficeManagerRole = new BranchOfficeManagerRole(session, southBranch);
var northBranchOfficeManagerRole = new BranchOfficeManagerRole(session, northBranch);
// Employees
var user1 = new Employee(session);
user1.Roles.Add(southBranchOfficeManagerRole);
var user2 = new Employee(session);
user2.Roles.Add(northBranchOfficeManagerRole);
// By adding both roles to an employee we can give him a superset of permissions
var user3 = new Employee(session);
user3.Roles.Add(southBranchOfficeManagerRole);
user3.Roles.Add(northBranchOfficeManagerRole);
DownloadDataObjects.Net 4.5 Beta 3 can be downloaded from the website.
P.S.
Dear DataObjects.Net users and contributors. The Beta 3 is very close to the Release Candidate. But before the main branch is frozen, I want to clarify
a question: should permissions be persistent or not? Is it vital? If yes, how filter expressions should be stored?
If the question is given a sensible answer, we'll continue implementing persistent permissions and make Beta 4; otherwise we will proceed to RC 1.
Thank you.
P.P.S.
Dear Malisa Ncube,
It seems that after these alterations you'll have to update the SalesPoint database creation script for MySQL. Sorry for that, hope this won't tale too much time.
Today the DataObjects.Net Team is releasing the second beta of the upcoming 4.5 version of DataObjects.Net. The beta is mostly dedicated to the security system.
All security-related classes are located in a separate assembly called Xtensive.Practices.Security. Let's explore what types to be aware of the assembly includes:
- IPrincipal, Principal & GenericPrincipal
- Role & RoleSet
- Permission & PermissionSet
- IHashingService with several implementations
- IPrincipalValidationService with a generic implementation
- ImpersonationContext
- some Session extension methods
Most of them have been already discussed in the previous posts on the topic. However, let's recall the main principals & usage scenarios on a security sample called SalesPoint.
Adding the security system to your project
1. Add reference to Xtensive.Practices.Security assembly
2. Register types from Xtensive.Practices.Security in your domain.
<domain connectionurl="sqlserver://./SalesPoint"upgrademode="Skip">
<types>
<add assembly="SalesPoint">
<add assembly="Xtensive.Practices.Security">
</types>
</domain>
Xtensive.Practices.Security contains the following persistent types: Principal, GenericPrincipal (both are abstract) and PrincipalRole which is used for storing role names for a principal.
3. Make your own persistent type that describe a user of your application. Inherit it from Principal or GenericPrincipal classes.
Principal class defines the minimum IPrincipal implementation and can be used for all kind of users, no matter whether you use login/password authorization scheme, Windows-based one or your own.
GenericPrincipal class is specifically designed for login/password authorization scheme. It already contains all the required properties and methods for storing password hash, for setting password, etc. Moreover, generic principal validation service is oriented only on GenericPrincipal class and its inheritors. So, in the overwhelming majority of scenarios you should inherit from GenericPrincipal class.
In the sample the login/password authorization scheme is used, so the Employee class is inherited from the GenericPrincipal one. Note that you have total control on properties of user hierarchy: you define the hierarchy inheritance scheme as well as the structure of Key fields.
[HierarchyRoot]
public class Employee : GenericPrincipal
{
[Field, Key]
public int Id { get; private set; }
// Other fields
}
More about principal types: part 5.
4. Define which hashing algorithm will be used. In this version md5, sha1, sha256, sha384, sha512 hashing algorithms are provided plus plain one that allows to store passwords as is, without hashing. This might be useful for testing purposes but it is strongly recommended to use true hashing one in the real life applications. In the sample the plain one is used, here is how it can be configured:
<configSections>
<section name="Xtensive.Security" type="Xtensive.Practices.Security.Configuration.ConfigurationSection, Xtensive.Practices.Security" />
</configSections>
<Xtensive.Security>
<hashingService name="plain"/>
</Xtensive.Security>
If no hashing service is set, then the system will fall back to the 'plain' one.
Managing passwords and validating usersHaving the above-mentioned steps done, you are getting the ability to manage users, set their passwords and validate them. Here is how:
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = new Employee(session);
employee.Name = "Steve Ballmer";
employee.SetPassword("developers, developers, developers, developers");
trx.Complete();
}
}
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = session.ValidatePrincipal("Steve Ballmer", "developers, developers, developers, developers");
Assert.IsNotNull(employee);
trx.Complete();
}
}
Defining rolesWhereas user management is the essential part of any security system, roles sometimes are used as an optional addition, thus loosing all their power. But it's you who decide whether to employ them or not. In Xtensive.Practices.Security roles are optional too.
Roles are ordinary classes, not persistent ones, and so are permissions. In order to utilize the roles mechanism you inherit from base Role<T> class and declare permissions for domain model types, like here:
public class EmployeeRole : Role
{
public EmployeeRole()
{
// This is base role for every employee.
// It defines read-only access to products and employees for all staff.
// All employees can see products
RegisterPermission(new Permission<Product>());
// All employees can see employees
RegisterPermission(new Permission<Employee>());
}
}
public class StockManagerRole : EmployeeRole
{
public StockManagerRole()
{
// Stock manager inherits all permissions from Employee role
// In addition, it declares write access to products
// Stock managers can see and edit products
RegisterPermission(new Permission<Product>(canWrite:true));
}
}
Here is more advanced role declaration. You can declare secure queries for persistent types and use you own permission classes as well:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Order> GetOrdersQuery(ImpersonationContext context, QueryEndpoint query)
{
// Sales representative role has access to its own orders only
return query.All<Order>()
.Where(o => o.Employee == context.Principal);
}
public SalesRepresentativeRole()
{
// Sales representative inherits Employee permissions
// Sales representative can see and edit customers
RegisterPermission(new Permission<Customer>(canWrite:true));
// Sales representative can see and edit sale orders but not approve
RegisterPermission(new OrderPermission(canWrite:true, canApprove:false, GetOrdersQuery));
}
}
Important: for the ease of use give your roles parameterless constructors. If this is unacceptable, then you should provide to a framework a list of role instances. I'll describe this this later.
More about roles and permissions: part 2, part 3, part 4.
ImpersonationThe next question after we've successfully defined users, authentication scheme, roles and permissions is "How the hell will these parts work together?". Let's see.
 You already know how to validate user by the pair of name and password.
If the validation is successful, you may impersonate current Session with user's account. After this is done, the security framework will provide you with all necessary infrastructure on what permission the user has, which roles he is in.
using (var session = domain.OpenSession()) {
using (var trx = session.OpenTransaction()) {
var employee = session.ValidatePrincipal("Steve Ballmer", "developers, developers, developers, developers");
// Opening an impersonation context
var context = session.Impersonate(employee);
// Checking permissions
context.Permissions.Contains<Permission<Customer>>(p => p.CanRead);
context.Permissions.Contains<OrderPermission>(p => p.CanApprove);
// Closing the context
context.Undo();
trx.Complete();
}
}
Here is how permissions are used to restrict screens of the sample application:
Note that currently logged in employee is in StockManager role that prohibits access to Customers and Orders. The main menu just checks whether current impersonation context contains the appropriate permission and acts accordingly by enabling or disabling controls.
Probably, the most exciting and important feature of the framework is automatic application of secure filters to every query that is being executed through Session.Query endpoint.
For example, if we log in with an employee who is in SalesRepresentative role, which restricts all orders to his own orders only, we will see the following picture:
We logged in as a Robert King, a sales representative. He can see only his own orders as the filter is declared in SalesRepresentative role and is applied automatically. The application doesn't know about users, permissions, roles, etc. All this stuff is provided by Xtensive.Practices.Security layer. Note that Customers and Orders views are available to him, but the "Approve" button is not, because the role doesn't declare access to "Approve" action.
What will happen if we log in as SalesManager? Let's see:
The user also has access to Customers and Orders as SalesManager role inherits it from SalesRepresentative one. Moreover, the role doesn't have such restriction for Orders, the employee see not only his own orders but the orders of employees from his department. In addition, the button "Approve" is enabled.
More on applying security filters to queries: part 6
A few notes about the impersonation context
ImpersonationContext class implements IDisposable, so you can employ using pattern. The context also supports nesting.
Current impersonation context instance can be accessed through Session, so you don't have to pass the reference to the context everywhere you might need it.
var context = session.GetImpersonationContext();
How to build the SalesPoint sample application
SQL script that builds the required "SalesPoint" database is included into the solution "Samples" and is located in the root folder of the SalesPoint project. Run the script against your database server and check that the database is successfully created. After that update the connectionUrl in app.config file according to the
path of your database server.
DownloadDataObjects.Net 4.5 Beta 2 can be downloaded from the website.
P.S.
Dear users of DataObjects.Net!
This is DataObjects.Net Beta 2, so I'm asking for your feedback. Play with the sample or try creating your own one, try applying the security framework to your real projects, do anything you want with it including the most weird scenarios (I know you can), I'll be happy to receive any comment no matter good or bad from you. Any of these will definitely help to make the product better and keep our requirements for high quality product.
Thank you.
It seems that in the end of the previous post I was a bit wrong. One more topic left for detailed discussion. And its name is "Query conflicts resolving".
Let me explain it a bit. Say, there is a consulting firm that is managed by 2 managers. First of them, Dan, is responsible for negotiations with automobile companies, while the second one, John, works with aircraft enterprises. According to the approach that is described in the previous set of posts the security domain model would contains of two roles: AutomobileManagerRole and AircraftManagerRole, each of them would include a correspondent restrictive query.
public class AutomobileManagerRole : Role
{
private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
{
return query.All<Customer>()
.Where(c => c.IsAutomobileIndustry);
}
public AutomobileManagerRole()
{
RegisterPermission(new CustomerPermission(true, GetCustomers));
}
}
public class AircraftManagerRole : Role
{
private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
{
return query.All<Customer>()
.Where(c => c.IsAircraftIndustry);
}
public AircraftManagerRole()
{
RegisterPermission(new CustomerPermission(true, GetCustomers));
}
}
Dan would be associated with AutomobileManagerRole and John - with AircraftManagerRole. The roles effectively avoid both of them to see customers that belong to industry other than a manager is responsible for.
But imagine a situation when one of the managers, let it be Dan, left on a vacation. To continue automobile customers management process John should get access to customers of Dan for the vacation period. To achieve this, a system security administrator goes to a security administration console and temporarily adds AutomobileManagerRole to John's account. As a result, John's account starts containing 2 roles, both of them with restrictive query for customers.
So, what should happen when John will execute a query for customers? How the conflict between 2 restrictive queries should be resolved?
The answer is that if the first role grants access to X set of entities and the second role grants it to Y set of entities then the result of combination of these roles should be a union of X and Y, not intersect or except.
Applying this approach to the situation with the managers, John will get access to the result of the following query:
query.All<Customer>()
.Where(c => c.IsAutomobileIndustry)
.Union(query.All<Customer>()
.Where(c => c.IsAircraftIndustry));
This approach will be used always when impersonation context is active and user account contains more then one role with a restrictive query for a querying persistent type.
After releasing DataObjects.Net 4.5 Beta 1 with Firebird & MySQL providers as well as with LINQPad provider, the team switched back to the security-related stuff.
In the previous part we discussed the roles and permissions in terms of restricting LINQ queries. This time let's focus on users, passwords and validation.
First of all, according to the requirements to the security system listed in the very first post concerning this topic, we announced that it would be good if we integrate somehow with the core .NET security interfaces, like IPrincipal & IIdentity. As a result, our core IPrincipal interface inherits System.Security.IPrincipal interface and adds 2 persistent fields and 1 method:
public interface IPrincipal : IEntity, System.Security.Principal.IPrincipal
{
[Field]
string Name { get; }
[Field]
[Association(PairTo = "Principal", OnOwnerRemove = OnRemoveAction.Cascade)]
PrincipalRoleSet PrincipalRoles { get; }
bool IsInRole(Role role);
}
Field 'Name' is used for storing identity information in storage. 'PrincipalRoles' is a set of roles that a principal owns. The PrincipalRole type is just a auxiliary type that help associate a Principal with its roles. A role itself is not persistent type, so we use its name as a reference.
[HierarchyRoot]
[KeyGenerator(KeyGeneratorKind.None)]
public class PrincipalRole : Entity
{
[Field, Key(0)]
public IPrincipal Principal { get; private set; }
[Field(Length = 50), Key(1)]
public string Name { get; private set; }
public PrincipalRole (Session session, IPrincipal principal, string name)
: base(session, principal, name) {}
}
In order not to realize all functionality of IPrincipal interface by yourselves, there is 2 base implementations: Principal & GenericPrincipal. The first one is pure implementation of the IPrincipal interface with no additional stuff. Here it is:
public abstract class Principal : Entity, IPrincipal
{
[NotNullConstraint(Mode = ConstrainMode.OnValidate)]
[Field(Length = 50, Indexed = true)]
public string Name { get; set; }
public virtual IIdentity Identity
{
get { return new GenericIdentity(Name); }
}
[Field]
public PrincipalRoleSet PrincipalRoles { get; private set; }
public bool IsInRole(string role)
{
return PrincipalRoles.Any(r => r.Name == role);
}
public bool IsInRole(Role role)
{
return PrincipalRoles.Contains(role);
}
}
The second persistent class called GenericPrincipal is designed for use in scenarios with username/password matching:
public abstract class GenericPrincipal : Principal
{
[Field(Length = 50)]
public string Password { get; protected set; }
public virtual void SetPassword(string password);
}
Note that both classes neither define Key fields nor apply HierarchyRoot attribute because no prediction can be made about such properties in your domain models. However, this means that you should add your own persistent type called User or similar and inherit from Principal or GenericPrinipal types in order to use all features of the security system. No additional coding is required, just inherit one of them, define Key field(s) and apply HierarchyRoot on top and that's all.
As GenericPrincipal type is intended to be used in username/password scenarios, the next question is: who will be responsible for possible encryption of passwords and how users will be validated/authenticated?
The previous version of DataObjects.Net provided several password encryption algorithms you could choose from, however the possibility to implement your own one was absent. This time we are going to provide a generic interface for string encryption as well as several common implementations. Customers will have the ability to make their own implementations and plug them in. Here is the interface:
public interface IEncryptionService : IDomainService
{
string Encrypt(string value);
}
It is absolutely simple and straightforward. For now there are 2 realizations: PlainEncryptionService that actually doesn't make any encryption at all and could be used for testing purposes, and Md5EncryptionService. I'll show how to configure the services later.
The next part is user authentication. Keeping in mind that DataObjects.Net-based applications might operate with various types of principals, for example, Windows principal or generic one with username/password authorization scheme, we provide the following generic interface for this task:
public interface IPrincipalValidationService : ISessionService
{
IPrincipal Validate(IIdentity identity, params object[] args);
IPrincipal Validate(string name, params object[] args);
}
The method that takes IIdentity as the first parameter is more generic, it might take WindowsIdentity as well as any other IIdentity descendants. The second argument is an array of values that are used for authentication, these could be passwords, tokens, tickets, you name it. The framework will provide base user validation service for username/password authentication scheme, and you'll have the ability to make your own ones.
It seems that there is nothing left to describe. In the next part we'll try making our first sample with integrated security. Stay tuned.
This is the forth part in a series of posts that is dedicated to security system concept in DataObjects.Net. In the previous part I described Role & Permission types and their relationships in context of entity access based on entity type. This part of the concept answers the question whether a user has access to entities of a persistent type or not and what kind of access if yes (read, write, approve, you name it).
In the part where the requirements were listed there was another set that defines some business rules applied to the set of entities that are accessible to a user. Remember, Sales Representatives can edit only their own Order instances, while Sales Managers can edit both their own orders as well as orders of stuff from their Sales Department and so on.
Let me recall the matrix:
So, we should have an option to restrict the set of Customers and Orders visible for Sales Representative & Sales Manager roles. These requirements are more like full-fledged business rules applied to the entire set of Customers & Orders than just a security issue, thus they rarely can be implemented in terms of ACL model.
Having Permission<T> class we can add there a property that will hold an IQueryable<T> object that will be used to describe the entire set of entities of type T that is accessible to a role that contains the permission.
Here is the definition of the Permission<T> class:
public class Permission<T> : Permission where T : class, IEntity
{
public Func<SecurityContext, IQueryable<T>> Query { get; protected set; }
...
public Permission(bool canWrite)
: base(typeof(T), canWrite)
{
// If not set explicitly, all entities are accessible by default
Query = context => context.Session.Query.All<T>();
}
public Permission(bool canWrite, Func<SecurityContext, IQueryable<T>> query)
: base(typeof(T), canWrite)
{
Query = query;
}
}
Having such an option, we can use it to explicitly define the accessibility-related business rules. Let's start with Sale Representative. This role restricts access to Order entities to those that are created by particular sales representative. So we have to define a rule that will build an IQueryable<Order> and register it properly. Here is how:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Order> GetOrdersQuery(SecurityContext context)
{
// Sales representative role has access to its own orders only
return context.Session.Query.All<Order>()
.Where(o => o.Employee == context.User);
}
public SalesRepresentativeRole()
{
RegisterPermission(new OrderPermission(canWrite:true, canApprove:false, GetOrdersQuery));
}
}
I won't focus on SecurityContext for now, The only thing that we should know about it is that it contains a Session and a User.
Note that we use Func<SecurityContext, IQueryable<T>> because we need to dynamically resolve the IQueryable<T> depending on the security context, currently active session, etc.
Using the same technique we can define restrictions for Sales Manager role:
public class SalesManagerRole : SalesRepresentativeRole
{
private static IQueryable<Order> GetOrdersQuery(SecurityContext context)
{
// Sales manager role has access to its own orders as well as to orders of his department
return context.Session.Query.All<Order>()
.Where(
o =>
o.Employee == context.User ||
o.Employee.In(
context.Session.Query.All<Employee>().Where(e => e.ReportsTo == context.User)));
}
public SalesManagerRole()
{
// Sales manager can do sale orders approval, in addition
RegisterPermission(new OrderPermission(canWrite:true, canApprove:true, GetOrdersQuery));
}
}
Note that actually we don't need ACLs or something artificial for defining the restrictions. Imagine a database with thousands and billions of entities and every single one must have one or more records that store entity's owner & all users that should have access to it. That's not an option. Pure business objects from the domain model and their relationships are enough for business rules of almost arbitrary complexity.
Now let's define business rules for Sales Representative & Sales Manager roles for accessing Customer objects. There is a rule that says that both of them should deal only with those customers that are from their local region. In this sample there are 2 sales departments, one is located in London, another is in Seattle. Thus, I split all customers into 2 groups: customers from the Old World & customers from the New World. The first group is served by the London sales department, the second one is by the Seattle one.
To simplify things I added a helper class that contains 2 groups of countries:
public static class WellKnown
{
public static IList<string> NewWorldCountries;
public static IList<string> OldWorldCountries;
static WellKnown()
{
NewWorldCountries = new List<string>()
{
"Argentina", "Australia", "Brazil", "Canada",
"Mexico", "USA", "Venezuela"
};
OldWorldCountries = new List<string>()
{
"Austria", "Belgium", "Denmark", "Finland",
"France", "Germany", "Ireland", "Italy",
"Japan", "Netherlands", "Norway", "Poland",
"Portugal", "Singapore", "Spain", "Sweden",
"Switzerland", "UK"
};
}
}
Having that done, I can describe the customers-related business rule as follow:
public class SalesRepresentativeRole : EmployeeRole
{
private static IQueryable<Customer> GetCustomersQuery(SecurityContext context)
{
// Sales representative role has access to local customers only
var employee = (Employee)context.User;
if (employee.Address.Country.In(WellKnown.NewWorldCountries))
return context.Session.Query.All<Customer>()
.Where(c => c.Address.Country.In(WellKnown.NewWorldCountries));
else
return context.Session.Query.All<Customer>()
.Where(c => c.Address.Country.In(WellKnown.OldWorldCountries));
}
public SalesRepresentativeRole()
{
// Sales representative can see and edit customers
RegisterPermission(new Permission<Customer>(canWrite:true, GetCustomersQuery));
}
}
Is't that good? As the role structure is hierarchical, this business rule is automatically inherited by all descendants of Sale Representative role, if not overridden. This means that there is no need to duplicate it in Sales Manager role. However, as Sales President role shouldn't have such a restriction we properly override that permission:
public class SalesPresidentRole : SalesManagerRole
{
public SalesPresidentRole()
{
// Overriding the inherited permission with restriction, so this role will have access to all customers
RegisterPermission(new Permission<Customer>());
...
}
}
That's all for today. In the next part I'll describe the SecurityContext class and related stuff in details.
Big note for all:
The security concept I'm talking about is a prototype. Its purpose is to find out the better way to implement the security in DataObjects.Net. The only way I can see this can be done right is to criticize & discuss the thing before it is implemented and released. I know that the most of DataObjects.Net users are mature and experienced developers that have numerous properly built applications that have their own security systems. Therefore, I'm hoping that it will be better to join our efforts and make the security system that will perfectly fit our needs.
For now, I would like to specially thank Vlad Klekovkin for his critics of the prototype. Vlad also shared a concept of his own security system built on top of DataObjects.Net.
Thanks a lot, Vlad!
This is the third part in a series of posts that are dedicated to security system concept in DataObjects.Net. The first part contained common considerations and bits of theory, the second one was about the approach in development and the requirements to security system.
In this part I'll describe main blocks of security system architecture, i.e. roles & permissions.
First of all, let's start with roles. As you might remember, there is a list of positions in our imaginary sales organization:
- Stock manager
- Sales representative
- Sales manager
- Sales president
I won't list their particular duties and access permissions again as this information can be found in the previous post. The thing that must be mentioned here is that each position executes one or more roles within the company, for example, Stock manager is also an employee, so Stock manager position includes some basic duties as well as duties specific to stock manager. The same goes for Sales manager, he not only have to approve orders but also acts like a sale representative as well as a common employee. Therefore, these positions can be arranged into accurate hierarchical structure according to the rule of inheritance. Luckily, RBAC level 2 declares support for roles hierarchies, and so does our security model.
Role hierarchy for the imaginary sales company:
Note, that each role is defined as a separate class. This approach as any other one has advantages and well as drawbacks. It is less dynamic, but provides natural inheritance, doesn't require persistence and is more intuitive, visual and understandable. Moreover, as roles in a company are mostly static, I've found the approach highly advantageous.
Look, EmployeeRole inherits a Role class. This is the base role class, it provides inheritors with necessary infrastructure: a set of Permissions for a role, name of a role and a method for permission registration.
To continue with roles hierarchy configuration we should define a permission first. According to the access matrix in the previous post, there are 2 common permissions that are actual for all kind of entities: Read permission & Write permission. In addition to these basic ones, there might be extended permissions as well, like Approve permission defined for Order entity type. Thus, we've defined a base Permission type and OrderPermission inheritor that provides requested functionality:
Let's see how these roles and permission are intended to be used. Let's start with the EmployeeRole first. As it is mentioned above, all company positions have at least read-only access to employees & products, we can define the base Role node for this organization and call it EmployeeRole. Let this role is given to any employee in the company.
Here is how EmployeeRole is declared:
public class EmployeeRole : Role
{
public EmployeeRole()
{
// This is base role for every employee
// All employees can read products
RegisterPermission(new Permission<Product>());
// All employees can read employees
RegisterPermission(new Permission<Employee>());
}
}
That's it. Now all employees have a permission to read Products & Employees.
What about the Stock manager role? Stock manager should inherit the base EmployeeRole and add permission for editing products.
public class StockManagerRole : EmployeeRole
{
public StockManagerRole()
{
// Stock manager inherits Employee permissions
// Stock managers can read and edit products
RegisterPermission(new Permission<Product>(canWrite:true));
}
}
As I've already mentioned, the advantage of the class-based approach in role hierarchy definition is in its essential inheritance support. Derived role can inherit all permissions and other options of the base class and override something if is necessary. This is achievable in the most natural way for developers: make an inheritor & override all you need.
On the other branch of roles hierarchy, Sales representative role also inherits the base EmployeeRole and should add permissions to edit customers and orders. In order to add Order permission we should define OrderPermission first, where a property CanApprove should be included. Here it is:
public class OrderPermission : Permission<Order>
{
public bool CanApprove { get; private set; }
public OrderPermission(bool canWrite, bool canApprove)
: base(canWrite)
{
CanApprove = canApprove;
}
}
Having declared the OrderPermission, we can proceed with SaleRepresentativeRole class:
public class SalesRepresentativeRole : EmployeeRole
{
public SalesRepresentativeRole()
{
// Sales representative inherits Employee permissions
// Sales representative can read and edit customers
RegisterPermission(new Permission<Customer>(canWrite:true));
// Sales representative can read and edit sale orders but not approve
RegisterPermission(new OrderPermission(canWrite:true, canApprove:false));
}
}
As for Sales manager role, it inherits Sales representative role and adds permission to approve orders. Here is how:
public class SalesManagerRole : SalesRepresentativeRole
{
public SalesManagerRole()
{
// Sales manager inherits Sales representative permissions
// Sales manager can do sale orders approval, in addition
RegisterPermission(new OrderPermission(canWrite:true, canApprove:true));
}
}
The last one is Sales president role. It in turn has access to all entities in read-only mode plus write permission to Employees, so we have to override permissions declared in its ansector (SalesManagerRole):
public class SalesPresidentRole : SalesManagerRole
{
public SalesPresidentRole()
{
// Sales president inherits Sales manager permissions
// Sales President can read all, but can't alter any details, except employees. They usually need read-only aggregated reports, actually
RegisterPermission(new Permission<Customer>());
RegisterPermission(new Permission<Order>());
RegisterPermission(new Permission<Product>());
RegisterPermission(new Permission<Employee>(canWrite:true));
}
}
Note the clarity and the ease of permissions overriding.
That's all for today. For now we have permissions and roles defined. Hence, we've got a mechanism that can be used to describe the access matrix from the previous post in full manner:
This is the second part in a series of posts that are dedicated to security system concept in DataObjects.Net. The first part contained common considerations and bits of theory.
In this part I'll tell you about the development approach we chose.
To avoid the situation when a framework is being built according to some pure theoretical ideas and as a result it is hard to use it in real world scenarios we had decided to start from the other side: try implementing an application that seems to be more or less real, put into practice our security concepts and thus develop the security system.
We chose Northwind database model as a playground and slightly modified it to add some complexity in company staff relationships.
Here is the updated organization chart for our Northwind company.
There are 2 sales departments: first is located in Seattle and second is in London. Both of them are managed by a sales manager, each of them has 2 sales representatives and 1 stock manager. Sales representatives report to sales manager, both sales managers report to sales president.
In this application model we defined the following main types of entities to secure:
- Customer
- Order
- Product
- Employee
There could be much more types to secure but there is no necessity as those four is enough to build more or less authentic model.
Next we have to define which company staff will have access to these secured entities, and which won't. Moreover, there might be several levels of access, for example, read, write and any other more specific ones.
According to the organization chart, we define 4 main roles:
- Stock manager
- Sales representative
- Sales manager
- Sales president
Each of them has its own set of duties, responsibilities, permissions and limitations. In our model there is the following distribution of all that stuff:
- All staff has read-only access to employees data.
- Stock managers manage products. They doesn't have access to customers and orders.
- Sales representatives have read-only access to products, have full access to customers of their sales department and their own orders. They don't have access to order approval.
- Sales managers also have full access to customers of the sales department, to their own orders and well as to orders of sales representatives in their sales department. Moreover, sales managers have access to the order approval operation.
- Sales president has access to all kind of information without limitation, but in read-only mode. In addition, he can manage employees (hire, dismiss, so he also has write access).
Here is the graphical matrix that shows access rights to entity types:
This matrix that demonstrates the additional limitations that must be met as well.
So, here is the deal: we need to provide a flexible and efficient security framework that can be used in domain models like this one with all above-mentioned permissions and limitations. In the next post I'll show you whether we managed to achieve the goal and how we did it.
This is an introductory post to the security system design and implementation in DataObjects.Net. Here we'd wanted to define common terms, considerations and requirements to the upcoming security system.
Bits of theoryAlmost any access control model can be stated formally using the notions of users (subjects), objects, operations, and permissions, and the relationships between these entities.
- The term user refers to people who interface with the computer system directly or not and on behalf of whom some actions are being taken by a computer program or a process.
- An object in terms of classic OR/M can be any entity or a group of entities accessible within the mapped database(s).
- An operation is a standalone action invoked by the user on the objects.
- Permissions (or privileges) are authorizations to perform some action on
the objects. The term permission refers to some combination of object and operation.
The role-based access control model (RBAC) adds one more fundamental term to the list — a role. A role is essentially a collection of permissions. Within an organization, roles are relatively stable, while users and permissions are both numerous and may change rapidly. Controlling all access through roles simplifies the management and review of access controls, therefore we'd prefer to follow role-based security model where users receive permissions only through the roles to which they are assigned.
Another advantage of the role-based access control model is the fact that roles are initially hierarchical — roles can inherit permissions from other roles. As a result, appropriate role hierarchies can be flexibly defined for any business process workflow, for example:
As a conclusion: although any access control system has its own advantages and limitations, we've chosen the RBAC one as a base for access control model in DataObjects.Net because of its flexibility and efficiency in the most usage scenarios.
Requirements and other considerationsFirst of all, we don't want to reinvent the wheel (again). If any core part of the standard .NET security system can be consumed, then it should be consumed. Mainly, I imply core interfaces such as IPrincipal, IIdentity, etc. This might help to use Thread.CurrentThread.Principal property in the same way as we use Thread.CurrentThread.CurrentCulture in localization extension, as well as more tightly integrate with system authentication services.
Other considerations:
- Security-related data mustn't be stored in serialized way in blob fields or something. It must be accessible via plain SQL.
- If this is possible, security system should be implemented as as extension (separate assembly) to the core framework.
- Security policy shouldn't be automatically applied to all persistent types. Only selectively chosen and configured persistent types should be subject for security system. This could be done with the help of special interface marker, attribute usage or similar.
- Authentication part should be extensible with custom types of authentication services (LDAP, WebServices, etc.).
- LINQ queries should be transparently re-written by security system to apply effective permissions.
- ASP.NET membership provider should be implemented as well.
This list doesn't pretend to be complete. Something might got out from our sight. If so, please don't hesitate to post a comment.
In the next posts of the series I'll try describing several aspects of the system in more detailed manner.
I'd like to share an early link to feature-based ORM comparison we're working on: "The Most Comprehensive Feature-Based Object-Relational Mapping Tool Comparison Ever :)™"
The document is incomplete yet:
- some cells are empty - i.e. their content is currently unknown;
- there can be some mistakes (it wasn't checked by community yet);
as you might suspect, a copy of this document is edited by ORMBattle.net participants, so its version including most of the tools tested there must also appear soon. It won't appear "as-is" at our own web site, but we'll use ~ the same columns from it (direct comparison with commercial competitors in marketing materials is normally not acceptable).
On the other hand, the feature map is already quite comprehensive: there are about 270 features organized into hierarchical structure. It is far more detailed then any other ORM comparison we were able to find (likely, this one is the most detailed, but really ancient predecessor).
Likely, the document is currently a bit biased toward DataObjects.Net from the point of selected features, but I feel this will be "automatically fixed" by the community shortly: vendors are allowed to add any non-duplicating features and sections there, as well as propose to exclude the non-important ones.
On the other hand, it's clearly much less biased document as e.g. this one (although I understand it doesn't pretend to be a real comparison). I.e. it can be hardly called as promotional material.
Our final goal is to develop a feature map including major ORM tools and features that are mutually agreed by various ORM vendors, where each vendor is responsible for contents of his own column (i.e. cheating is possible, but I suspect users & competitors won't accept this well); the table you see is our initial investment into this process.
Availability of such comparison should help developers to choose the tools they need based on their own requirements, as well as understand the relationships between features better (hierarchy seems really helpful here - I already got few quite positive comments related to the structure of the document).
An accompanying document commenting each section there and describing DataObjects.Net advantages / disadvantages in comparison to other tools should also appear soon.
Don't forget to study the comments at the bottom of the first sheet, as well as "Remarks" sheet.
I just finished writing pretty large document: "Atomicity of visible state change in complex action sequences in DataObjects.Net"
The article describes one of important features of event notification system in DataObjects.Net, that is used in during synchronization of paired (inverse) associations and entity removals.
Any comments, error reports and suggestions are welcome. Russian-speaking developers can read the article in Russian.
The article (in IFrame):
This post actually starts a sequence of posts dedicated to DataObjects.Net design - more precisely, to its design goals. I decided to start the cycle from the practical example, since picture frequently worth more then thousands of words. Certain amount of advertisement of our outsourcing team is just side effect of this post, although if have some serious project for these guys, you're welcome.
SeverRegionGaz is regional natural gas provider. This is a big organization, that, although being a subdivision of Gazprom itself, has a set of branches as well. There is a bunch of legacy software systems there, varying from pretty old to new. Some of them have very similar functionality: earlier SeverRegionGaz branches were independent from each other, and thus they use partially equivalent software.
Most of data they maintain is related to:
- Billing - obviously.
- Equipment. E.g. they know exactly what's installed at each particular location (home, office, etc.).
- Incidents and customer interaction history.
- Bookkeeping. Unfortunately for us, they wanted to see certain information from two different instances of 1C Enterprise 7.5 there as well.
The main goal of "Single Window" system is to provide a single access point allowing to browse all this data, and, importantly, search for any piece of information there.
Let me illustrate the importance of this goal: earlier, to find necessary piece of information (e.g. billing and interaction history for a particular person), they should identify one or few of these legacy systems first, and then request necessary information from appropriate people (nearly no one precisely knows all these systems). In short, getting the information was really complex and long process -- and we were happy to change this.
There were few other, minor goals - e.g. it was necessary to:
- Provide web interface allowing customers to report the values of natural gas consumption counters and interact with support staff.
- Integrate with external payment processing system provided by their bank and automatically process the payments made by customers.
- Implement reporting. Only a part of reports needed by SeverRegionGaz was available in legacy systems, so we should implement the missing ones.
To stress this, we should develop an application allowing to browse really huge database (I'll explain this later). Its editing capabilities should be pretty limited - mainly, because:
- Most of this information must be imported from external sources. If we'd be asked to support editing, it would bring the complexity to a completely different level. In fact, we should either be capable of syncing back all the changed (that's really hard, taking into account that none of legacy systems is ready for syncing, and almost no one knows how these systems exactly work at all), or, alternatively, replace all the legacy systems there by our own (hard as well).
- That fact that different systems are still necessary for editing (mainly, data entry) is acceptable for SeverRegionGaz. The people working with them are used to them; single person there normally deals with a single system. Having "Single Window" there, they should study just one more system to be capable of accessing all the data - that's much better then ~10.
- Full editing support (likely, a complete replacement of most of legacy systems) is primary goal for "Single Window v2" - and the idea of movement to this big goal step-by-step is really good. For now it's ok if we allow to edit only the data we fully control.
That was a story behind "Single Window" project. Now some facts about its implementation:
- Time: 7 months -- February 2010 ... July 2010. First 1.5 months were spent almost completely on specifications.
- People: initially -- 3.5 developers + 1.5 managers (Alex Ustinov was playing both roles there); closer to completion -- 6 developers.
- Database: ~ 12 GB of data, 507 tables, 440 types! (all are unique, i.e. there are no generic-based tables)
- External data sources: 8, full data import is implemented for all of them; in additional, continuous change migration is implemented for 3 of them.
- Other elements: hundreds of lists, forms and reports. I suspect, totally - almost 1 thousand.
- Complexities: lots of, but mostly they were related to ETL processes, starting from some funny ones and ending up with real problems.
- Used technologies:
- DataObjects.Net 4 - btw, it's our first really big application based on it. And that's why I write this post :) - LiveUI - it would be really hard to generate that huge UI without this framework. Btw, Alex Ilyin adopted its core part to WPF pretty fast. We also intensively used T4 to - WCF - we've implemented 3-tier architecture relying on WCF as communication protocol. - WPF - an obvious choice for UI, and lots of other stuff, ending up with pretty exotic COM+ (used for integration with 1C).
Screenshots
The main tab is designed in very minimalistic fashion:
That's what happens when user hits "Search" button:
As you see, we use full-text capabilities of DO4 in full power here. In fact, we index all of objects, which content is interesting from the point of search. Full-text indexing here is implemented in nearly the same way as it was in v3.9 - i.e. there is a single special type for full text document, per-type full-text content extractors running continuously in background, and so on.
Here is a typical list (take a look at grid settings and search box):
Some forms:
The list of actions in left panel is actually pretty long - i.e. you see may be 30% of it. There is no scrollbar, but you may find "Up" and "Down" arrows indicating the list will automatically scroll up or down when mouse pointer approaches its top and bottom edges.
Integration services control list:
Web site, customer's home page:
Pages for registration of natural gas consumption counter value and interaction with support staff:
And finally, a single screenshot exposing the complexity of domain model:
As you see, our team made a huge job, that is directly related to DataObjects.Net.
The main point of this post is: DataObjects.Net is designed to develop really complex business applications fast. Why? Well, that's the topic for my subsequent posts, but for now I'd like to touch key points:
- Code-only approach allows developers to work fully independently without caring about schema changes at all - even in different branches.
- Integrated schema upgrade capabilities are ideal for unit testing (and testing in general). It's easy to launch unit tests for any part of your application.
- Rich event and interaction model simplifies development of shared logic, such as full-text indexing and change tracking.
- Excellent LINQ support brings significant advantages, when you start writing reports. Queries there might require really good translator.
- Can you imagine dealing with 500+ types in EF? Actually, I just tried to find some reports about this on the web, but found only "avoid this" statements, with tons of reasons. The most funny one is about performance and usability of IntelliSence when you type "dataContext.".
To be frank, of course I got lots of issue reports during these months from our "Single Window" team, and actually, still get them. Mostly they were related to LINQ and schema upgrade. So if you use DO, you can say "thanks" to Alex Ilyin - he simply tortured me and Alexis Kochetov, and still does this. E.g. mainly because of him:
- DO4 translates really complex LINQ expressions involving DTOs (custom types and anonymous typs).
- Schema upgrade works really well and fast now. Domain.Build(...) performance is 2-3 times higher there (Alex hates waiting for launch). E.g. now our 440-type Domain requires ~ 4 seconds to be built in Skip mode on my moderate office PC (Core 2 Duo). To achieve this, I should parallelize some stages of build process.
Let me finish with one more screenshot:
P.S. I'm really interested in examples of complex applications relying on popular ORM tools. If you know some, please share the link. I got an impression that people still avoid using ORM tools in such cases (there are opinions like "ORM is not for enterprise!"). So I'd like to "measure the length" in terms of model complexity (count of tables, types, etc.) - just for fun, of course. You should know we like to measure various features of ORM tools.
|
|