Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / SQL

MDX. Slowly changing value with adjustment. ADOMD.Net. MDX usage in C#

4.50/5 (4 votes)
1 Dec 2009CPOL12 min read 39.4K   1.4K  
MDX query for Get last valid value for measure before the date and then calculate all values that contribute to this value till given date. ADOMD.Net. MDX usage in C#.

Get last valid value for measure before the date and then calculate all values that contribute to this value till given date

MDX, C# 2.0, ADOMD.Net, SQL Service Analysis Services 2005

MDX



Introduction

Article covers:

  • MDX Query, Subquery, Set, Dimension, Axis, Tuple, Member, Property
  • WITH, MEMBER, IIF, NOT IsEmpty, NonEmpty, Tail, Item(), CurrentMember, Name, PrevMember, StrToMember, IS NULL, SELECT, ON ROWS, ON COLUMNS, FROM, WHERE
  • using Microsoft.AnalysisServices.AdomdClient; Microsoft.AnalysisServices.AdomdClient.AdomdConnection, Microsoft.AnalysisServices.AdomdClient.AdomdCommand, Microsoft.AnalysisServices.AdomdClient.AdomdCommand.ExecuteCellSet(), Microsoft.AnalysisServices.AdomdClient.AdomdParameter, Microsoft.AnalysisServices.AdomdClient.CellSet, Microsoft.AnalysisServices.AdomdClient.Tuple

Sample content and how to deploy samles on your PC you can see in related section

Slowly Changing values with adjustment. What do I mean when I am talking this? This is all values which are difficult to organize constant measuring of them, so they are calculated somehow. I.e. these are values which contain a lot (sometimes even huge number) of elements or different sources of information, for instance, this is results of stock-taking or inter-payments verification between enterprise and its counteragents. This is "snap of reality". After this "real" measuring occured decision making persons still require most recent values. It is possible to calculate them basing on values that cause to changing of interesting ones. But due in time calculated value differs from the actual state of affairs, because of, say, stealing or operational mistakes. Thus, there are such "strong bases" in informaional system.

This article covers retrieving most real level of such value using MDX in SSAS and getting it in C# using ADOMD.Net. It doesn't assume any prior experience with OLAP and Microsoft Analysis Services, and ADOMD.Net objects as well, but you should be able to backup database from backup and to deploy database to Analysis Service from Business Intelligence Development Studio (BI DevStudio).It is needed to see query at work.

I get stocktake level as an example.

The main problem with attempting to get the stock level is determining when the last stocktake for the current product occurred. And then it is getting of valid time dimension member to pass it to query.

MDX Query

Conception

The basic idea is to get a cube that calculates how much stock is on hand for a given product, at a given location, at any given time. The idea is to:

  • Get the last stocktake before the given date and time.
  • Add and subtract all stock changes since this date time.

There are several tables that contribute to the stock level. We will use them all while of adjustment.

Dimensions

There are three dimensions:

  • Time, it is represented by seconds when any action occurs
  • Location, this could be warehouse or any else site where products are stored.
  • Product, this is interesting products.

We should to get stocktake level of product wich is on hand in certain location at certain moment of time

Measures

There are a lot of measures. Now we are interesing in [Measures].[Last Stocktake Qty] which comes from annual or half-yearly stock-taking.

Get the last stocktake before the given date and time

Solvation of this part of task is private case of "Slowly Changing Values" wich is described in book "MDX Solutions" at p.84. There are two methodth how to do this, we will use both of them. One in first version (recursion) now and one in second version (tail of filterd ordered set) later in ADOMD.Net client. Instead of book version we get time of last stated stocktake level, really it is unique name of Time dimension member. We act so because further we should accumulate sums of other values tables that contribute to the stock level and which have been occurring in period from last update till interesting moment of time.

A calculated members that does the work for us is:

MDX
-- last update of Stock level Second
MEMBER [Measures].[Last Update of Stock Level] AS
'IIF (
	NOT IsEmpty ([Measures].[Last Stocktake Qty]),
	[Time].[Second].CurrentMember.Name,
	IIF ( 
		[Time].[Second].PrevMember IS NULL,
		NULL,
		([Measures].[Last Update of Stock Level], [Time].[Second].PrevMember)
	)
)'

This calculated member tests Stocktake Quantity on emptiness, but returns Second of first non empty Stocktake level.

MDX
NOT IsEmpty ([Measures].[Last Stocktake Qty]),
[Time].[Second].CurrentMember.Name,

In case Stocktake level does not exist at current second we switch to previous second. For that purpose we determine time as [Time].[Second].PrevMember explicitly when we refer to previous secon, elsewise we refer to the same cell.

MDX
([Measures].[Last Update of Stock Level], [Time].[Second].PrevMember)

And, of cause we have to take account of boundary handling - previous second must exist.

MDX
[Time].[Second].PrevMember IS NULL

To complete this block we determine interesting stocktake level.

MDX
-- Last measured stocktake level
MEMBER [Measures].[Last Measured Stock Level] AS
'(
	StrToMember("[Time].[Second].[" 
		+ [Measures].[Last Update of Stock Level]
		+ "]"
		), 
	[Measures].[Last Stocktake Qty]
)'

We have to use StrToMember() function because [Measures].[Last Update of Stock Level] is string but tuple consists of members, so we give a member.

Add and subtract all stock changes since this date time

At this point we have got second of last update of stock level that precedes interesting second. To sum facts that contribute to stock level we can use sum function.

MDX
-- SaleQty
MEMBER [Measures].[SaleQty Later] AS
'SUM(
	StrToMember("[Time].[Second].&[" 
			+ [Measures].[Last Update of Stock Level]
			+ "]")
		: [Time].[Second].CurrentMember, [Measures].[Sale Qty])'

But this operation is too expense. It will be better to subtract two numbers: accumulated value at the end of period and accumulated value at the date of last update. To do this we should to create calculated member for each value that contribute to Stocktake level. In BI DevStudio project you choose node 'Cubes', you expand it, you double-click at 'Amicus Cub', then you choose 'Calculations' tab where you can create calculated measure for cube.

CREATE MEMBER [Measures].[SaleQtyToDate] As 'sum(null : [Time].[Second].currentmember, [Measures].[Sale Qty])'

Or use MDX script.

MDX
CREATE MEMBER [Measures].[SaleQtyToDate] As 
'Sum(null : [Time].[Second].currentmember, [Measures].[Sale Qty])'

In AS2005 in expressions which represents range Set null indicates first member of current level (explicit ramge member determines current level).

And then changes that have occured in period of "uncertainty" can be obtained using following calculated member:

MDX
-- SaleQty
MEMBER [Measures].[Diffs SaleQty] AS
'([Time].[Second].CurrentMember, [Measures].[SaleQtyToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[SaleQtyToDate])'

It is not necessary to use [Time].[Second].CurrentMember in first tuple. But, as for me, this way of writing is more transparent. [Time].[Second].CurrentMember is value of interesting second.

We do similar for each value that influences stocktake level:

  • Receipts
  • NegativeAdjustment
  • PositiveAdjustment
  • TransferIn
  • TransferOut

Collect altogether

Our goal is to get stocktake level of certain product at certain location at certain moment of time. In MDX slicer serves for this purpose. It clarifies cell context. It means it determines CurrentMembers. In our case we get stocktake level of product STEVE, for 1st location at '2008-10-29 08:55:15'th second (to get full identifire of certain member, or any, you can select interesting object in Objects exlorer tree, then click right button, and choose 'Copy').

MDX
-- Slice to product, location, second
WHERE (
	[Product].[Product].&[STEVE], 
	[Location].[Location].&[1], 
	[Time].[Second].&[2008-10-29 08:55:15]
)

Now we put parts of desired value altogether:

MDX
-- Real stock level
MEMBER [Measures].[Real Stock Level] AS
'
[Measures].[Last Measured Stock Level]
- [Measures].[Diffs SaleQty]
 + [Measures].[Diffs Receipts]
 - [Measures].[Diffs NegativeAdjustment]
 + [Measures].[Diffs PositiveAdjustment]
 - [Measures].[Diffs TransferOut]
 + [Measures].[Diffs TransferIn]
'

And whole code:

MDX
WITH 
-- last update of Stock level Second
MEMBER [Measures].[Last Update of Stock Level] AS
'IIF (
	NOT IsEmpty ([Measures].[Last Stocktake Qty]),
	[Time].[Second].CurrentMember.Name,
	IIF ( 
		[Time].[Second].PrevMember IS NULL,
		NULL,
		([Measures].[Last Update of Stock Level], [Time].[Second].PrevMember)
	)
)'
-- Diferences in calculated members after last update date
-- SaleQty
MEMBER [Measures].[Diffs SaleQty] AS
'([Time].[Second].CurrentMember, [Measures].[SaleQtyToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[SaleQtyToDate])'
-- Receipts
MEMBER [Measures].[Diffs Receipts] AS
'([Time].[Second].CurrentMember, [Measures].[ReceiptsToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[ReceiptsToDate])'
-- NegativeAdjustment
MEMBER [Measures].[Diffs NegativeAdjustment] AS
'([Time].[Second].CurrentMember, [Measures].[NegativeAdjustmentToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[NegativeAdjustmentToDate])'
-- PositiveAdjustment
MEMBER [Measures].[Diffs PositiveAdjustment] AS
'([Time].[Second].CurrentMember, [Measures].[PositiveAdjustmentToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[PositiveAdjustmentToDate])'
-- TransferIn
MEMBER [Measures].[Diffs TransferIn] AS
'([Time].[Second].CurrentMember, [Measures].[TransferInToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[TransferInToDate])'
-- TransferOut
MEMBER [Measures].[Diffs TransferOut] AS
'([Time].[Second].CurrentMember, [Measures].[TransferOutToDate])
	- (   
			StrToMember("[Time].[Second].&[" 
					+ [Measures].[Last Update of Stock Level]
					+ "]"), 
			[Measures].[TransferOutToDate])'
-- Last measured stocktake level
MEMBER [Measures].[Last Measured Stock Level] AS
'(
	StrToMember("[Time].[Second].[" 
		+ [Measures].[Last Update of Stock Level]
		+ "]"
		), 
	[Measures].[Last Stocktake Qty]
)'
-- Real stock level
MEMBER [Measures].[Real Stock Level] AS
'
[Measures].[Last Measured Stock Level]
- [Measures].[Diffs SaleQty]
 + [Measures].[Diffs Receipts]
 - [Measures].[Diffs NegativeAdjustment]
 + [Measures].[Diffs PositiveAdjustment]
 - [Measures].[Diffs TransferOut]
 + [Measures].[Diffs TransferIn]
'
-- Query 
SELECT [Measures].[Measures].[Real Stock Level] ON COLUMNS
FROM [Amicus Dev]
-- Slice to product, location, second
WHERE (
	[Product].[Product].&[STEVE], 
	[Location].[Location].&[1], 
	[Time].[Second].&[2008-10-29 08:55:15]
)

Final Edition

Before continue we consider second method how to get last previous value if it is absent for current time point (I used 'point' because columns, rows, pages, chapters, sections, etc. are axes, so members are points).

MDX
--member for second of last stocktake check
Member  AS
'Tail(
	NonEmpty( 
		[ExistingSeconds],
		([Measures].[Last Stocktake Qty])
	),
	1
).Item(0).Item(0)'

Observant reader will notice two thing. One is named Set. It represents all Members of [Time].[Second] level, in other words, all seconds. Last is that calculated member [Time].[Second].[Last Stocktake Check] relates to [Time].[Second] dimention and, in the main, returns member instead of name of member. First allows us to use this member in places where [Time].[Second] could be used. Second delivers us from usage of StrToMember(), and, as result, gives a win in performance. Note: use members and named sets as far as possible, because member or set comiles just once and then you may use them everwhere.

But most observant reader will notice that calculated member adduced above always returns last second in time dimention. It is because slicer (members in Whereclause) determines cell context, but does not cut calculation area down. It was a trap for me, because of my comming from SQL where Where is filter option. We have to use subcube instead:

MDX
( 
	SELECT
	{ NULL : 
		[Time].[Second].&[2008-10-29 08:54:25]		//10
																//Time dimension
	} on 0
	, {[Product].[Product].&[STEVE] } on 1						//Product dimension
	, {[Location].[Location].&[1] } on 2						//Location dimension
	From
	[Amicus Dev]
)

And to force an usage of 'filtered' context in sets and members with aid of key word Existing:

MDX
--period from begining till interesting date
Set [ExistingSeconds] AS 
'Existing [Time].[Second].Members'

And at this point of exposition we have member from [Time].[Second] dimension for last update before date. But it happens that when we solve one problem we create other one. It is exactly this case: now we have lost CurrentMember for [Time].[Second] dimension. We can get it from ExistingSeconds set:

MDX
--interesting second value
Member [Time].[Second].[InterestingSecond] AS
'[ExistingSeconds].Item([ExistingSeconds].Count - 1).Item(0)'

We take first member (.Item(0)) of last tuple (.Item([ExistingSeconds].Count - 1)) among filtered seconds ([ExistingSeconds]). Do you remember that we have filtered this dimention with aid of subquery/subcube by interesting second? I ask to say that, in case I have forgot to say previously.

So we solve this problem too. It is our luck that it heppaned to be easer than that we had solved before, because it is quite the contrary in most cases.

Final version:

MDX
With 
--period from begining till interesting date
Set [ExistingSeconds] AS 
'Existing [Time].[Second].Members'
--member for second of last stocktake check
Member [Time].[Second].[Last Stocktake Check] AS
'Tail(
	NonEmpty( 
		[ExistingSeconds],
		([Measures].[Last Stocktake Qty])
	),
	1
).Item(0).Item(0)'
--interesting second value
Member [Time].[Second].[InterestingSecond] AS
'[ExistingSeconds].Item([ExistingSeconds].Count - 1).Item(0)'
--actual stocktake quantity (last checked) 		 
Member [Measures].[Actual Stocktake Qty] AS
'(
	[Time].[Second].[Last Stocktake Check],
	[Measures].[Last Stocktake Qty]
)'
-- Diferences in calculated members after last update date
-- SaleQty
MEMBER [Measures].[Last Sale Qty] AS
'([Time].[Second].[InterestingSecond], [Measures].[SaleQtyToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[SaleQtyToDate])'
-- Receipts
MEMBER [Measures].[Last Receipts] AS
'([Time].[Second].[InterestingSecond], [Measures].[ReceiptsToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[ReceiptsToDate])'
-- NegativeAdjustment
MEMBER [Measures].[Last Negative Adjustment] AS
'([Time].[Second].[InterestingSecond], [Measures].[NegativeAdjustmentToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[NegativeAdjustmentToDate])'
-- PositiveAdjustment
MEMBER [Measures].[Last Positive Adjustment] AS
'([Time].[Second].[InterestingSecond], [Measures].[PositiveAdjustmentToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[PositiveAdjustmentToDate])'
-- TransferIn
MEMBER [Measures].[Last Transfer In] AS
'([Time].[Second].[InterestingSecond], [Measures].[TransferInToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[TransferInToDate])'
-- TransferOut
MEMBER [Measures].[Last Transfer Out] AS
'([Time].[Second].[InterestingSecond], [Measures].[TransferOutToDate])
	- ([Time].[Second].[Last Stocktake Check], [Measures].[TransferOutToDate])'
-- Real stock level
MEMBER [Measures].[Most Real Stocktake Qty] AS
'
[Measures].[Actual Stocktake Qty]
- [Measures].[Last Sale Qty]
 + [Measures].[Last Receipts]
 - [Measures].[Last Negative Adjustment]
 + [Measures].[Last Positive Adjustment]
 - [Measures].[Last Transfer Out]
 + [Measures].[Last Transfer In]
'
Select
{[Measures].[Most Real Stocktake Qty]} on 0
From
( 
	SELECT
	{ NULL : 
		[Time].[Second].&[2008-10-29 08:54:25]		//10
																//Time dimension
	} on 0
	, {[Product].[Product].&[STEVE] } on 1						//Product dimension
	, {[Location].[Location].&[1] } on 2						//Location dimension
	From
	[Amicus Dev]
)

Additionally this version of query is more compact. Let's code in C# now!

C#/ADOMD.Net application to retrieve value from AS2005

C# application to retrieve value from AS2005 usging MDX query

This is part of article where you ll find code to connect to SQL Server Analysis Services and to get MDX query result in our C#/.Net code.

I do not know about you, but I prefer to determine date and time in natural way, i.e. it is choosing date from calndar and typing time in ##:##:## format. It is first challenge for us, because of possibility to query by existing dimension members only. And this is natural, because any request about non existing information is vagueness (null). We could ask user to choose interesting second from list of existing in Storage values, but it would be ugly. So there is nothing for it, we have to find best match. In case of time, best match is closest (at the left) second to the given one.

C#
/// <summary>
/// Query to get biggest valid member of [Time].[Second] dimension that less than given datetime
/// </summary>
private const string queryGetLastSecondButGiven =
    @"With Member [Measures].x AS 'CDate([Time].[Second].CurrentMember.Name)' "
    + @"Select Tail(Filter(Existing [Time].[Second].Children, [Measures].x <= @Second), 1) on 0, { } on 1 From [Amicus Dev] "
    + @"Where (StrToMember(@Location), StrToMember(@Product))";

Interesting places:

  • Member [Measures].x AS 'CDate([Time].[Second].CurrentMember.Name)'

    In MDX query VBA functions are accessible. It is CDate() among them which convert string to date. String goes from Name of current member of [Time].[Second] dimension.

  • Tail(Filter(Existing [Time].[Second].Children, [Measures].x <= @Second), 1) on 0

    Let`s consider this block step by step:

    1. @Second - paremeter that becomes numeric value when it is query executing.
    2. Filter(... [Time].[Second].Children, [Measures].x <= @Second) gets all members from [Time].[Second] dimension that are less (earlier) than given date-time.
    3. Existing [Time].[Second].Children takes children accordingly to current slicer context only. It forces applying of query context in calculated members also.
    4. Tail(..., 1) takes last tuple from set
  • { } on 1 tells to MDX to get Axis 0 only, null set on rows. It is necessary because of retrieving measure by default. And this is way to say "enough" to MDX. Because any redundant action costs CPU time.
  • From [Amicus Dev] - data source.
  • StrToMember(@Location), StrToMember(@Product) - location and product members passed as a string respectively.
  • Where (...) - Slicer. It determines cell context.

And C# code to perform this task of time member getting. It is more preferable to get second in string format to ease its usage later.

C#
/// <summary>
/// Gets biggest member of [Time].[Second] dimension but less than given date boundary
/// </summary>
/// <param name="upperBoundary">date time upper boundary</param>
/// <param name="product">product to filter</param>
/// <param name="location">location to filter</param>
/// <returns>uniquer name of existant second member or null</returns>
string GetNearestValidSecondMember(DateTime upperBoundary, string product, string location)
{
    using (AdomdConnection conn = new AdomdConnection(activeConnectionString))
    {
        CellSet cs;     // cell set //query result
        try
        {
            conn.Open();
            AdomdCommand cmd = conn.CreateCommand();
            cmd.CommandText = queryGetLastSecondButGiven;

            cmd.Parameters.Add(new AdomdParameter("Second", upperBoundary));
            cmd.Parameters.Add(new AdomdParameter("Location", location));
            cmd.Parameters.Add(new AdomdParameter("Product", product));

            cs = cmd.ExecuteCellSet();
        }
        finally
        {
            conn.Close();
        }

        return cs.Axes[0].Set.Tuples[0].Members[0].UniqueName;
    }
    return null;
}

All details are described later. I just want to mention about line 'cs.Axes[0].Set.Tuples[0].Members[0].UniqueName'. CellSet of this query is always empty. So to retrieve dimension name we have to access via Axes collection that keeps axes, then it is Set, Tuples, and there is Member that consist of UniqueName - member identifier. We have to get exactly UniqueName, because we will send it to StrToMember as parameter. It will save us from redundant typing.

In the similar way we take product and location members to populate respective dropdown comboboxes to allow user selection (see details in source code, GetDimMembers(...) method).

When all data from user has been collected it is time to get stocktake level.

Query. It is the same as in Final Edition exept it is quoted and explicit dimension members for time, product and location is replaced by parameters:

C#
    /// <summary>
    /// Query to get stocktake level of product at location for datetime
    /// </summary>
    private const string queryGetRealStocktakeLevel =
        @"With Set [ExistingSeconds] AS 'Existing [Time].[Second].Members' "
...
        + @"Select {[Measures].[Most Real Stocktake Qty]} on 0 From ( SELECT { NULL : "
    + @"StrToMember(@Second) "   //[Time].[Second].&[2008-10-29 08:54:25]       "    //Time dimension
        + @"} on 0 , {"
    + @"StrToMember(@Product) " //[Product].[Product].&[STEVE]" //Product dimension
        + @"} on 1, {"
    + @"StrToMember(@Location) " //[Location].[Location].&[1]"      //Location dimension
        + @"} on 2 From [Amicus Dev])";

C# code to retreive data from AS2005, to make code workable we have to add reference to Microsoft.AnalysisServices.AdomdClient assembly, of cause:

C#
using Microsoft.AnalysisServices.AdomdClient;
	
	//code from btGet_Click(...) method
	...
			//query
			using (AdomdConnection conn = new AdomdConnection(activeConnectionString))
			{
				try
				{
					conn.Open();
					AdomdCommand cmd = conn.CreateCommand();
					cmd.CommandText = queryGetRealStocktakeLevel;
					
					cmd.Parameters.Add(new AdomdParameter("Second", second));
					cmd.Parameters.Add(new AdomdParameter("Location", location));
					cmd.Parameters.Add(new AdomdParameter("Product", product));

					CellSet cs = cmd.ExecuteCellSet();

					if (cs.Cells.Count > 0)
					{
						Cell cell = cs.Cells[0];
						teStoktakeLevel.EditValue = cell.FormattedValue;
					}					
				}
				catch (System.Exception ex)
				{
					MessageBox.Show(ex.Message);
				}
				finally
				{
					conn.Close();
				}
			}
	...

Value for activeConnectionString is "Provider=msolap;Data Source=local;Initial Catalog=AmicusAnalysisServices;Cube=Amicus Dev"

Briefly, we create connection to OLAP using AdomdConnection object, we open it (.Open()), we create special for OLAP command object (instance of AdomdCommand) using AdomdConnection.CreateCommand(), then we assign query text to created command, we add parameters to command (cmd.Parameters.Add(new AdomdParameter("...", ...)), we execute command (ExecuteCellSet). And finally we get interesting cell (.Cells(0)) and cell value (cell.FormattedValue). We do not iterate over cell set because we now that it should consist of one cell, so we just access to this alone cell. By the way, ExecuteScalar is not implemented.

Sample Deployment

databaseSource.bak file. This is relatinal database-source. Create empty 'Cubes' database and backup it from given file (However, you may use any name for DB if you want to bore yourself by changing connection string in BI DevStudio AmicusAnalysisServices solution, in 'Amicus Dev.ds' node).

AmicusAnalysisServices.sln. This is BI Solution to create database, and cube in it, in SQL Server Analysis Services. To Deploy cube you have to open solution in Visual Studio and then run 'Build/Deploy AmicusAnalysisServices' from main menu.

GetStockTakeLevel.sln. This is Demo application that connects to AS2005 and retrievs Stocktake level accordingly selected paramters

MDX. These is text files with article MDX queries all.

My demo sample uses DevXpress components for GUI. There are dowload version with reqiered DevXpress.DLLs and without them (Light) in downloads. If you have got no Xpress components and do not want to download it, be comfortable - sample core is independent on these DLLs.

Gratitudes

Thanks for reading.

Points of Interest

At the moment of article writting I am interesting in software/web projects as developer, project manager or remote team founder. It is projects with use of Microsoft Technologies, DB/ERP/Financial mainly. ...but I do not bound yourself in applying of my skills and opportunity to make this word easier and more comfortable for people.

History

Article was created at 14th of November, 2009.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)