About .Net assembly versioning ..

You make a cool reusable component, publish it and get it used by M people in N projects. Everything is good and under control.. until a CHANGE HAPPENS. And a few bugs from end-users. And you tweak some internal bits and add another feature.. Unless you have a good versioning practices in place, you'll be in dll-hell and everyone will hate you for making the messy never-working and poorly supported component in the first place.

The following describes a practice I use for versioning reusable components and staying in control (closer to sane).

What do I Need?

The absolute must is to understand from the dll itself which code it was compiled from.

For example if a user sends you email witha question or a problem you want to know what is the EXACT code they used. This is especially important to help ensure you can reproduce the problem they had.

Assembly identity must be unique enough without requiring constant effort from dev.

Thirdly, you want to control when assembly can be upgraded without recompile of caller assemblies. This is especially important when you have a chain of dependent assemblies one of the bottom layer ones get a non-breaking change - there is no need to recompile and re-release every other component in the chain. Even more when fully qualified names are stored in configuration. Obviously this must be used with care, as it is really bad when loader loads only partially compatible assembly and you get a runtime exception for missing a method or similar avoidable error.

imageA strong plus would be if you could check the version info on any computer, not requiring the availability or use of less known command line tools. Dll file properties in OS is a good place.

Optionally it would be cool to easily see from dll the exact date&time when assembly was created, by whom, where and which compilation flags were used (Debug/Release). This makes the assembly origin even more obvious.

what tools do I have?

Obviously there are attributes to attach metadata to my assembly:

    • This is used by the runtime loader to determine which assembly to load
    • Show in windows file properties as "File version" but only if there is no FileVersionAttribute value.
    • Format: forced 4-part numbered, i.e. '1.0.123.23425'
    • ignored by assembly loading
    • Some setup tools use it to decide if assembly should be upgraded or "already exists".
    • Show in windows file properties as "File version".
    • Format: free text but OS recognizes only 4-part numbered format.
    • Dumb metadata, show in windows file properties as "Product version".
    • Format: free text
  • AssemblyConfigurationAttribute and AssemblyDescription
    • Dumb metadata, readable only by .net tools.
    • Format: free text

Note that while assemblyVersion supports asterisk to autogenerate last 2 components, it does not work for FileAttribute so it is of no use.

Windows explorer properties has Details view which exposes some metadata about dll versions:

What version SCHEME to use?

I suggest every assembly to be identified by 4-part numbered version number:

  • <major> - increment on major functional change
  • <minor> - increment on every breaking functional change
  • <date> - compile timestamp for uniqueness
  • <time> - compile timestamp for uniqueness

AssemblyVersion: '<major>.<minor>.0.0'.
FileVersion: '<major>.<minor>.<date>.<time>'
InformationalVersion can contain everything else.

<date> and <time> part should be human-readable and if possible ever-increasing. Since the numbers are limited by UInt16 max value then some compromise is probably necessary.

For example: there may be assembly release 1.2.0.0, deployed as compiled file 1.2.30425.1148, stating that it was released by me on my PC A, today before lunch.

I don't see a real need to have more than 2 parts for AssemblyVersion. For:

  • Hotfix (no breaking API change) - change only compile timestamp.
  • non-breaking functional change - depending on change consider incrementing <minor> or just treating it as hotfix.
  • breaking functional change - increment <minor> or optionally <major>.

I suggest against using AssemblyDescriptionAttribute or configurationAttribute for versioning info as it is more difiicult to browse this info for random dlls.

How to get it automatically?

Obviously AssemblyVersion must be changed manually by devs. Not automatics needed there.

[assembly: AssemblyVersion("1.2")]

On the other hand FileVersion and InformationalVersion should be regenerated on each build. For this I created a T4 template AssemblyInformationalVersion.tt:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ output extension="gen.cs"#>
<#
    string informationalVersion;
    string assemblyVersion;
    string fileVersionAddition;
    try
    {
        var path = this.Host.ResolvePath("AssemblyInfo.cs");
        string versionLine = File.ReadAllLines(path).Where(line => line.Contains("AssemblyVersion")).First();
        assemblyVersion = versionLine.Split('"')[1];
        if (assemblyVersion.Split('.').Length != 2) { throw new Exception("AssemblyVersion must be in format '<major>.<minor>'"); }

        DateTime compileDate = DateTime.Now;
        int yearpart = compileDate.Year % 10 %6; //last digit mod 6 (max value in version component is 65536)
        fileVersionAddition = yearpart + compileDate.ToString("MMdd") + "." + compileDate.ToString("HHmm");

        informationalVersion = String.Format ("{0} (on {1} by {2} at {3})",
            assemblyVersion,
            DateTime.Now.ToString("s"),
            Environment.UserName,
            Environment.MachineName
        );
    }
    catch ( Exception exception)
    {
        WriteLine("Error generating output : " + exception.ToString());
        throw;
    }

 #>
using System.Reflection;

//NB! this file is automatically generated. All manual changes will be lost on build.
// Generated based on assembly version: <#= assemblyVersion #>
[assembly: AssemblyFileVersion("<#= assemblyVersion #>.<#= fileVersionAddition #>")]
[assembly: AssemblyInformationalVersion("<#= informationalVersion #>")]

.. and trigger it on each build, for example by pre-build event:

"%COMMONPROGRAMFILES(x86)%\microsoft shared\TextTemplating\11.0\TextTransform.exe" -out "$(ProjectDir)\Properties\AssemblyInformationalVersion.gen.cs" "$(ProjectDir)\Properties\AssemblyInformationalVersion.tt"

So, on each build I will have detailed versioning info generated for my assembly automatically:

using System.Reflection;

//NB! this file is automatically generated. All manual changes will be lost on build.
// Generated based on assembly version: 1.2
[assembly: AssemblyFileVersion("1.2.30415.1128")]
[assembly: AssemblyInformationalVersion("1.2 (on 2013-04-15T11:28:42 by MyUser at MYMACHINE)")]

There is a one-time, simple copy-paste setup and then it just works as long as you use Visual Studio for compilation. Not 100% sure if AssemblyInfo.cs file path is resolved by build server hosts.

Sellest kuidas lugeda väga suuri faile..

Vahel juhtub, et keerad mõnele automaatteenusele selja ja enne kui arugi saad, on tekkinud 10GB logifail. Windows-platvormi notepad ei ole ehk väga hea mõte selle faili avamiseks.

Sobivaks vahendiks on aga Large Text File Viewer.

Hea:

  • väga kerge ja kiire!
    • näiteks 2GB+ fail avaneb hetkeliselt ja programm võtab mälu vaid ca 20MB.
  • freeware, tingimused siin
  • staatiline binaar. ei mingit installimist.
    • kogu programm (koos madonna jm piltidega) <1 MB
  • split-screen sama faili erinevate kohtade võrdlemiseks

Halb:

  • ei mingit failide muutmist
  • dokumentatsiooni järgi on tugi ainult ASCII & Utf-16, ka UTF-8 korral võib tulla ebameeldivaid üllatusi.
  • Otsing on aeglane, ma ei taha teada mis juhtub kui väga koledate regulaaravaldist kasutada 10GB failil.
  • sisaldab mõttetuid featuure
    • näiteks editori taustapilt, isekeriv help-tekst abi aknas jms.

Kokkuvõtvalt: kaugel ideaalist kuid kitsas hiigelsuure faili lugemise rollis päris hea tööriist.

Sellest, kuidas panna TFS workspace tuvastama failisüsteemis toimunud muudatusi..

TFS ei ole versioneerimisprogramm, vaid Visual Studio laiendus, mis tegeleb versioneerimisega (ja mille külge on ehitatud ohtralt muid tulesid-vilesid). Seetõttu on minu kui CVS/SVN pealt Microsofti maailma sattunule alati pinnuks silmas, et ma ei saa faile muuta-lisada failisüsteemi tasemel või mõne teise programmi abil. Failid on read-only ja isegi kui sa neid muudad, siis Visual studio ei saa aru, et need on muutunud. Vähemalt mitte enne kui sa oled kõiki neid muutnud KA Visual Studios.

Osutus, et olukord pole lootusetu ja teised hädalised on TFSile kargud teinud: Team Foundation Power Tools, mis sisaldab toredat utiliiti nimega tfpt, mis muuhulgas võrdleb workspace kaustu versioonihalduse infoga ja lisab lisandunud-muutunud-kustutatud failid pending muudatusena.

C:\WM\TFS\SomePath\src>tfpt online /diff /recursive /adds SomeProject

Getting your pending changes from the server...
Checking the status of C:\WM\TFS\SomePath\src\SomeProject... Done
Walking C:\WM\TFS\SomePath\src\SomeProject... Found 20

Mille peale leitakse muutunud failid ja kuvatakse VS sarnases aknas:
image

Valitud muudatuste kohta jääb konsooliaknasse ka logi:

Edits:

SomeProject:
www_otsing.asp
www_paring.asp
www_teade.asp

Adds:

SomeProject\DBKontroll:
SomeSql.sql

Loomulikult saab ka antud aknast loobuda ja kõik välja checkida kasutades võtit /noprompt.

Mina pean seda funktsionaalsust regulaarselt googeldama, ehk nüüd on kergem leida..

PS: Power tools võimaldab ka explorer integration vahendeid, mis võiks sarnased tegevusd veel mugavamaks teha, kuid neid ma ei ole julgenud proovida. Nagunii hakkab soovimatutel hetkedel päringutega servereid pommitama.

About WebDAV: do it yourself ..

Facing an integration task using WebDAV brought me to take a look closer at WebDAV and corresponding tools for .Net platform. This is part of a short series:

-----

Why pay others do have fun and use the protocol? How hard could be to implement just the few pieces I need ..

What do I need ?

The simplest scenario you can get from WebDAV:

  • List folder contents, get name/urls, size, differentiate files & subfolders
  • upload a file from memory
  • download a file to memory

For testing purposes I use IIS 7.5 as my WebDAV server.

What IS WebDAV ?

Basically an extension of HTTP protocol to use web servers as file systems. It is described in IETF RFC 4918. A Quick overview can be get from wiki and there’s an official-like site webdav.org which provides more links and resources. 

how to Get List of contents ?

I could simply make an HTTP GET request and a html page from get web server containing links to its contents. But this is ugly and there is the webdav way using PROPFIND instead of GET.

PROPFIND relies on xml query sent with http request. RFC contains also some example queries. The query I need would be something like this:

<?xml version="1.0" encoding="utf-8"?>
<D:propfind xmlns:D="DAV:" xmlns:ns0="DAV:">
<D:prop>
<ns0:displayname/>
<ns0:iscollection/>
<ns0:getcontentlength/>
</D:prop>
</D:propfind>

To send the query to web server using PROPFIND in simple:

var request = (HttpWebRequest)HttpWebRequest.Create(targetUri);
request.UseDefaultCredentials = true;
request.Method = "PROPFIND";

var bytes = Encoding.UTF8.GetBytes(query);
using (var requestStream = request.GetRequestStream())
{
requestStream.Write(bytes, 0, bytes.Length);
}
request.ContentType = "text/xml";

But now you’ll get an error which is not the most helpful:
System.Net.WebException: The remote server returned an error: (403) Forbidden.

The resolve is to read the spec and find the mandatory “Depth” header which determines if you want properties only on the targeted item (value 0), with direct children (value 1) or recursively (value “infinity”). To add that header:

request.Headers.Add("Depth", "1");

Now the request is set up, reading the response gives as requested data in xml format:

using (var response = (HttpWebResponse)request.GetResponse())
using (var responseStream = response.GetResponseStream())
using (var responseContents = new MemoryStream(2048))
{
responseStream.CopyTo(responseContents, 2048);
return Encoding.UTF8.GetString(responseContents.ToArray());
}

The result could be something like this:

<D:multistatus xmlns:D="DAV:">
<D:response>
<D:href>http://imrepyhvel/webDavTest/</D:href>
<D:propstat>
<D:status>HTTP/1.1 200 OK</D:status>
<D:prop>
<D:displayname>webDavTest</D:displayname>
<D:getcontentlength>0</D:getcontentlength>
<D:iscollection>1</D:iscollection>
</D:prop>
</D:propstat>
</D:response>
<D:response>
<D:href>http://imrepyhvel/webDavTest/bb.txt</D:href>
<D:propstat>
<D:status>HTTP/1.1 200 OK</D:status>
<D:prop>
<D:displayname>bb.txt</D:displayname>
<D:getcontentlength>4</D:getcontentlength>
<D:iscollection>0</D:iscollection>
</D:prop>
</D:propstat>
</D:response>
</D:multistatus>

The tricky part is that different servers may return data in slightly different ways. For example folders may not have some properties (like “getcontentlength”) in which case there’s another “propstat”-block with different “D:status". RFC describes the possible statuses.

Another suggestion is to help clean up the HTTP communication and use these optional settings for HTTP request which obviously remove HTTP 100 and continuous handshaking:

request.PreAuthenticate = true;
request.ServicePoint.Expect100Continue = false;

How to download A file ?

Heey, you already know that. That's what HTTP GET method is about ..

How to upload a file?

This is less common but still simple:

WebRequest request = WebRequest.Create(targetPath);
request.UseDefaultCredentials = true;
request.Method = "PUT";

using (Stream stream = request.GetRequestStream())
{
contents.CopyTo(stream, 2048);
}
request.GetResponse();

What do I think ?

Most is simple HTTP action. The only tricky part is working with metadata (PROPFIND) but with help of RFC and google, you won't get stuck. If you are on low budget or only need simple scenarios then doing WebDav by yourself is not a problem.

The good:

  • what you do yourself is free
    • No licence fees
    • no limitations on deployment
    • no copyright or copyleft unless you want to create one.
  • Seems simple enough
  • know-how can be applied using every language capable of creating-sending-receiving HTTP traffic
  • Performance & traffic is as efficient as you write it
    • 3 tasks = auth challenge + 3 request-response pairs
    • approx 41 ms

image

The bad:

  • Requires some time to explore RFC and write/google the proof of concept codes
    • Some hours will do for simple scenarios.
  • Requires more time in case of problems
    • no support/bugfixes from outside
    • the reasons behind those uninformative 403s are hard to track down.
  • Requires more time to test
    • there will be quirks on different WebDAV servers as experienced by running my code in both IIS and on livelink folders.

About WebDAV: using Independentsoft client ..

Facing an integration task using WebDAV brought me to take a look closer at WebDAV and corresponding tools for .Net platform. This is part of a short series:

-----

Another commercial client - WebDAV .NET from Independentsoft, priced 299 EUR and up.

What do I need ?

The simplest scenario you can get from WebDAV:

  • List folder contents, get name/urls, size, differentiate files & subfolders
  • upload a file from memory
  • download a file to memory

For testing purposes I use IIS 7.5 as my WebDAV server.

What Does it do?

Homepage promises compatibility to WebDAV RFC 2518.

Only interesting fact from homepage is support for ANY .Net Framework, including Compact Framework and Mono.

What did I get ? / How did I do it ?

Once again, I achieved my supersimple goals.

Initializing the client:

var session = new WebdavSession();
session.Credentials = CredentialCache.DefaultNetworkCredentials;
var resource = new Resource(session);

This Resource thingy is weird, but I hope it makes sense to somebody. Now, fetching contents and showing only files ..

var selectProperties = new PropertyName[] { DavProperty.DisplayName, DavProperty.IsCollection, DavProperty.GetContentLength };
foreach (var item in resource.List(folderUri.ToString(), selectProperties))
{
if (item.Properties[DavProperty.IsCollection].Value == "1") { continue; }
Console.WriteLine("{0} ({1} bytes)",
item.Properties[DavProperty.DisplayName].Value,
item.Properties[DavProperty.GetContentLength].Value
);
}

I really like that I can enumerate the properties I’m interested in and the choice of valid names are accessible using DavProperty container. Downside is that the seemingly simple and universal API requires me to check for the existance of given properties in output (not done in sample) and parse-interpret the values. It is straightforward considering the protocol, but the resulting code is not the cleanest. Now, to downloading..

using (var stream = new MemoryStream())
{
resource.Download(folderUri.ToString() + "/bb.txt", stream);
var contents = Encoding.Default.GetString(stream.ToArray());
}

Almost as simple as it gets – download that URI into that stream. How about upload?

var newFileName = "NewFile_" + DateTime.Now.ToString("dd-HHmmss") + ".txt";
var content = "Whatever content";

using (var stream = new MemoryStream(Encoding.Default.GetBytes(content)))
{
resource.Upload(folderUri.ToString() + "/" + newFileName, stream);
}

Once again elegant -  data from this stream to this url.

What do I think ?

Simple, straightforward, efficient – exactly the way you want your libraries to be. On the downside for trickier parts you better (get to) know what happens in the protocol level. Using the default configuration it provided seemingly optimal traffic. If I had the licence, I would use it for my WebDAV needs.

The good:

  • decent API
    • default scenarios are made quite simple.
    • with xmldoc.
  • Commercial product but
    • Has evaluation option (30 days)
    • even entry level licence has royalty-free redistribution with your software.
    • source code option is available (for extra fee)
  • Seems fast
    • For example: list properties you are interested.
    • with default settings: 3 tasks = auth challenge + 3 request-response pairs in HTTP track
    • approx 42 ms

image

  • Supports Compact Framework & Mono
    • official Mono support is no common. A good cause!
  • Nice attitude from support
    • On evaluation request I got a mail from a specific developer in Independentsoft stating that if I have problems or need examples then he’ll try to help (“usually in few minutes”). It was an automated message, but still – encouraging!

The bad:

  • Commercial product
    • 300 EURs for starter is a bit heavy for simple needs in a single project
    • They do want your email for evaluation version.
  • Homepage says it is based on RFC 2518 which is an obsolete specification and is marked to be superseded by RFC 4918.
    • Not sure what’s the difference in real world but it is a sign of worry, because RFC 4918 seems to be from June 2007.
  • Code may be straightforward but not always as clean as it could be.
    • for example : reading property values after PROPFIND

About WebDav: using IT Hit .Net library ..

Facing an integration task using WebDAV brought me to take a look closer at WebDAV and corresponding tools for .Net platform. This is part of a short series:

-----

Even if inventing your own wheel could be fun, it’s not always reasonable thing to do. I couldn’t find any freeware trustworthy-looking clients with brief googling so the first candidate is a commercial client - WebDAV client library from IT Hit, whose pricing starts from 350 USD.

What do I need ?

The simplest scenario you can get from WebDAV:

  • List folder contents, get name/urls, size, differentiate files & subfolders
  • upload a file from memory
  • download a file to memory

For testing purposes I use IIS 7.5 as my WebDAV server.

What Does it do?

Homepage promises compatibility to WebDAV RFC 2518.

More interesting features include:

  • requires .Net Framework 2.0 or .Net Compact Framework 2.0 SP1
  • high-level API
  • versioning support (DeltaV)
  • resumable uploads-downloads

What did I get ? / How did I do it ?

As expected, I achieved my supersimple goals.

Initializing the client:

string license = File.ReadAllText("WebDav-.net client_License.xml");
var session = new WebDavSession(license);
session.Credentials = CredentialCache.DefaultNetworkCredentials;
IFolder folder = session.OpenFolder(folderUri); //work in this folder.

Note that licence information has to be applied in-code in xml format.

Creating a IFolder is not mandatory for all tasks but as I happen to work in a single specific folder then I’m reusing the same object and declaring it next to the master-object session. Now, let’s see what’s in that folder..

foreach (IHierarchyItem item in folder.GetChildren(false))
{
if (item.ItemType != ItemType.Resource) { continue; }
Console.WriteLine("{0} ({1} bytes)",
item.DisplayName,
((IResource)item).ContentLength
);
}

Note that API is not the MOST readable: that “false” means non-recursive, files are called “resource” and even though most common properties are present in IHierarchyItem, file size is not. Code sample excerpts on home page were of no use. Also, I could not find it from object during runtime because the assembly code is obfuscated. Whatever, solution was found only after digging into IT Hit sample application. Let’s go download a text file contents..

var downloadResource = folder.GetResource("bb.txt");
using (Stream stream = downloadResource.GetReadStream())
using (var reader = new StreamReader(stream))
{
var contents = reader.ReadToEnd();
}

There was sample code present and the simple task gets simple solution. I’m getting used to looking for Resources. Possibility to get a file by local name (as opposed to always working with full URI) is a nice touch. Next task is to upload a file ..

var newFileName = "NewFile_" + DateTime.Now.ToString("dd-HHmmss") + ".txt";
var content = "Whatever content";

var uploadResource = folder.CreateResource(newFileName);
using (var datastream = new MemoryStream(Encoding.Default.GetBytes(content)))
using (Stream uploadstream = uploadResource.GetWriteStream(datastream.Length))
{
datastream.CopyTo(uploadstream);
}

Create resource in folder and get stream to write to that resource – seems reasonable. The downside is that if I do not know my data stream length in advance then I could be in trouble. There probably are some workarounds, but these API calls just happen to have that limitation. Just beware.

What do I think ?

The API is high level and using code exercpts in IT Hit home page you could get most of your tasks done relatively fast. I would trust it to do the job, espectially trickier stuff.  Still - I didn’t get the feeling that “this is The Tool to use for WebDAV”. Far from it.

The good:

  • decent API
    • you really don’t need to know much about webDAV.
    • Some helpful abstractions and hiding of webdav properties are a helpful for quickstarters
  • Commercial product but
    • Has evaluation option (1 month)
    • even entry level has royalty-free redistribution with your software.
    • source code option is available (for extra fee ofc)
  • Seems reliable
    • I ran into very few error messages and the ones I did see were informative.
    • testapp worked without modifications also on Livelink WebDAV folder.
  • support Compact Framework

The bad:

  • Commercial product
    • You can’t even get evaluation version without registration.
  • Homepage says it is based on RFC 2518 which is an obsolete specification and is marked to be superseded by RFC 4918.
    • Not sure what’s the difference in real world but it is a sign of worry, because RFC 4918 seems to be from June 2007.
  • no xml doc
    • I’m not going to go to their website or open up a separate help-file just to see comments on parameters and to find out that class PropertyName is “WebDAV property name”. I want intellisense so I wouldn’t waste my time!
  • API could be better
    • I could not perform ANY tasks by intuition alone. Not even the “download that file”-task. Even worse – I had to dive into sample application code just to know how to get file size.
  • Code is obfuscated
    • runtime debugging won’t help you much (af.g==false? Oh, really?!)

image

  • default protocol usage is chatty and non-optimal.
    • for example: it queries for all properties when you care only about a few.
    • example of chattiness for the 3 simple tasks (took approx 100 ms)

image

Sellest, et dünaamiline SQL on vahel vajalik..

.. näiteks siis kui sul on vaja N väga sarnase objekti kohta koostada päringud/trigeri/SP väljatöötatud mustri järgi, aga sa EI taha seda N-realist lohet ringi kopeerida ja kukkuda iga muudatuse peale N korda rohkem (nüri) tööd tegema.  Kui N läheneb kümnetele või sadadele, siis dünaamiline SQL võib olla su sõber.

KURI on seda aga liiast kasutada, sest tulemuse loetavus on vähegi keerukama loogika korral väga halb, optimeerijale on see black box ja abivahendid ei oska su dünaamilisest päringust ilma kõikidele loogikaharudele vastavaid päringuid käivitamata hoiatusi/vigu välja noppida.

Hea ülevaate, mis on võimalik, mis ok ja mis mitte avastasin näiteks siit:

http://www.sommarskog.se/dynamic_sql.html

MSSQL-spetsiifiline, aga mulle sobib ..

Sellest, kuidas parsida C# koodifaile..

Laisk inimene ei tee käsitööd vaid kulutab (halvemal juhul sama suurusjärku) aja loomaks vahendeid, mis nüri töö tema eest ära teeks. Mul oli vaja iga koodifaili kohta teada, mis nimeruume ta deklareerib ja millised klasse ta sisaldab. Failide arv on paari tuhande ringis, seega käsitsi üle käimine ei olnud kindlasti plaanis.

Regexp ?

Esimene mõte oli vanad head regulaaravaldised. Nende lugemine ja debugimine on küll tükk vaeva, aga päris palju saab ära teha. Lihtsustatud stsenaariumi nimeruumide leidmine on mõõdukal lihtne : need on faili alguses, enne deklaratsiooni on üldjuhul vaid using-käsud ja erinevad kommentaarid, võib-olla ka hunnik whitespace’i. Aga sisemised nimeruumid ? mitu nimeruumi failis?  atribuudid ? Klasside leidmine oleks sel meetodil veel kordades keerukam, sundides arvestama ka näiteks string-konstantides sisalduvaga jms..

Või parser ?

Ja siis taipasin – nii tore kui jalgratta leiutamine ja regulaaravaldiste kirjutamine ka ei ole, antud probleem on ju juba ammu lahendatud. Iga C# koodiparser teeb oma põhitöö seas just seda, mida mul vaja. Näiteks proovisin SharpDevelop’i (vabavarane IDE c# jm jaoks) koosseisu kuuluvat NRefactory-nimelist parserit.

Samm1: muretse NRefactory

NRefactory näib olevat kättesaadava ainult SharpDevelop koosseisus, mille installeri  saab siit: http://www.icsharpcode.net/OpenSource/SD/Download/, misjärel on teek leitav sõltuvalt installeerimise kohast, näiteks siit:

C:\Program Files (x86)\SharpDevelop\4.0\bin\ICSharpCode.NRefactory.dll

Kui sa ei taha SharpDevelopiga tutvuda, siis dll saab loomulikult ka SharpDevelopi lähtekoodist.

Samm2: Kood

using ICSharpCode.NRefactory;
using ICSharpCode.NRefactory.Ast;

Parseri loomisel ei ole just palju valikuid, kuid mõned siiski. kuna meid huvitavad ainult klassid-nimeruumid, siis sobib näiteks selline parseri loomine.

IParser parser = ParserFactory.CreateParser(@"C:\Somepath\somecodefile.cs");
parser.ParseMethodBodies = false;
parser.Parse();

if (parser.Errors.Count > 0)
{
throw new Exception(String.Format("Parsing of '{0}' failed, Errors: {1}.",
codeFile.FullName, parser.Errors.ErrorOutput));
}

// this is my method to query the information.
WalkFile(parser.CompilationUnit.Children);


Meetod WalkFile() käib rekursiivselt läbi AST (Abstract Syntax Tree) ja viskab mind huvitava informatsiooni konsooli. Loomulikult oleks kena rekursiooni vältida, aga see ei ole praegu oluline:

private static void WalkFile(List<INode> nodes)
{
foreach (var node in nodes)
{
var asNamespace = node as NamespaceDeclaration;
if (asNamespace != null)
{
Console.WriteLine("Namespace found: '{0}' from {1}",
asNamespace.Name, asNamespace.StartLocation.ToString());
}

var asType = node as TypeDeclaration;
if (asType != null) {
Console.WriteLine("Type found: '{0}' from {1}",
asType.Name, asType.StartLocation.ToString());
}
WalkFile(node.Children);
}
}

Ja tulemuseks näiteks midagi sarnast:

Namespace found: 'SomeRoot.Parent' from (Line 6, Col 1)
Type found: 'ClassInParent' from (Line 8, Col 2)
Type found: 'SubClass' from (Line 10, Col 3)
Namespace found: 'ChildNamespace' from (Line 23, Col 2)
Type found: 'ClassInChild' from (Line 25, Col 3)

Mõistmaks, mis klass-nimeruum, mille sees asub, peaks muidugi vanemate ahelat vms infot meeles pidama, aga see ei ole enam midagi keerulist.

mis edasi ?

Koodifailide parsimine võimaldab teha koodi pealt päris huvitavaid päringuid. Näiteks kontrollida:

  • kas klassid jms ikka kasutavad kokkulepitud nimetamisreegleid ?
  • millised klassid ei järgi kokkulepitud public/internal/private-reegleid?
  • millised koodifailid ei järgi “1 file = 1 class” põhimõtet ?
  • millised koodifailid ei asu õiges kaustas (näiteks nimeruumi järgi) ?
  • mitu rida on pikim meetod ?

Mitte kõike neist ei saa teha reflectioni abil ja erinevalt reflectionist ei ole sul vaja kompileerimist, vaid piisab märgatavalt vähematest eeldustest: ainult vaadatavad koodifailid peab olema süntaktiliselt korrektsed. Puuduvad teegid, või kompileerimist takistavad vead ei takista teisi faile analüüsimast. Või suvalist faili.

Võimalik on teha näiteks koodifailide konverteerijat, mis näiteks eemaldab või lisab atribuute, muudab klassivälju, näiteks field –> property jne. Üks väike video sel teemal on näiteks siin. AST abil koodifailide muutmisel on küll miinuseks see, et kommentaarid lähevad kaotsi, mistõttu on ehk kaval kasutada parsimist informatsiooni pärimisks ja kasutada parseri antud asukohainfot automaatsete muudatuste tegemiseks muid vahendeid kasutades.

About Excel exporting using Nikasoft NativeExcel..

I’m still looking at Excel components. From the same series so far:

This time I’ll try Nikasoft NativeExcel and follow the same procedure as in previous posts of the series.

What does it do ?

Works without Excel, creates Excel 97-2003 documents, which may contain formulas, images, formatting etc. See the feature list yourself. It seems to support everything I need.

What do I need ?

Support for data types, unicode, basic formatting, formulas, cell merge, column width – the same things as in my previous excel-component test.

What did I get ?

As already becoming a norm - functionally I got everything I wanted. Again, as I used demo version then it overwrote the top-left cell with warning “This worksheet was created by demo version of NativeExcel library”. I didn’t like that it overwrote my own text in there without warning which is more invasive than Winnovative’s extra sheet. In production I hope they’ll somehow lose it, I hope.

The file was a Excel 97-2003 xls file, And even though it provided tools to export Excel 5 and 97 version as well as CSV and a few more, it does NOT support xlsx. Not a big thing but a sign.

image 

How did I do it ?

using System;
using System.Globalization;
using System.Threading;
using NativeExcel;

public class NativeExcelTest
{
private const int DummyRows = 15;

public static void Build()
{
//prepare sheet
IWorkbook book = NativeExcel.Factory.CreateWorkbook();
IWorksheet sheet = book.Worksheets.Add();
sheet.Name = "Sample";

//create data
int currentRow = 1;
//we can use excel style range instead of coordinates.
sheet.Cells["A1:D1"].Merge();
sheet.Cells["A1"].Value = "demonstrate merge";

BuildHeaderRow(sheet, ++currentRow);
for(int i = 1; i <= DummyRows; i++)
{
BuildDataRow(sheet, i, ++currentRow);
}
BuildSummaryRow(sheet, ++currentRow);

//corrigate column widths:
sheet.Cells.Autofit();

//output result
book.SaveAs("Native_result.xls");
}

private static void BuildHeaderRow(IWorksheet sheet, int toRow)
{
sheet.Cells[toRow, 1].Value = "index";
sheet.Cells[toRow, 2].Value = "1/index";
sheet.Cells[toRow, 3].Value = "name";
sheet.Cells[toRow, 4].Value = "datetime";
sheet.Cells[toRow, 1, toRow, 4].Font.Bold = true;
}

private static void BuildDataRow(IWorksheet sheet, int dataIndex, int toRow)
{
//generate dummy data row using dataIndex for values:
sheet.Cells[toRow, 1].Value = dataIndex;
sheet.Cells[toRow, 2].Value = 1.0M / dataIndex;
sheet.Cells[toRow, 3].Value = "Ilus õõvaöö nr "
+ dataIndex.ToString(CultureInfo.InvariantCulture);
sheet.Cells[toRow, 4].Value = DateTime.Today.AddDays(dataIndex);
}

private static void BuildSummaryRow(IWorksheet sheet, int toRow)
{
sheet.Cells[toRow, 1].Formula = String.Format("=AVERAGE(A{0}:A{1})", toRow - DummyRows, toRow - 1);
sheet.Cells[toRow, 2].Formula = String.Format("=SUM(B{0}:B{1})", toRow - DummyRows, toRow - 1);
sheet.Cells[toRow, 4].Formula = String.Format("=D{0}+1", toRow - 1);
//it didn't get the formula type correctly.
sheet.Cells[toRow, 4].NumberFormat
= Thread.CurrentThread.CurrentCulture.DateTimeFormat.ShortDatePattern;
}
}


Some subjective numbers:

  • ~64 lines of code (including empty lines, usings, etc)
  • runs approx 200-240 ms on E8300 @2.83GHz, 4GB RAM.

What do I think ?

Having previously tried Winnovative component, the transition was smooth and without serious problems. All differences for my simple testcase were rather small and syntactic. Maybe I’m biased, but it felt a bit messier than Winnovative component. But as there is also price difference then better try it out yourself before choosing one or the other.

The good

  • mostly managed code, .Net fw 2.0
    • Requires windows as there’s reference to GDI32.dll
  • relatively cheap
    • $140 (registered user licence)
      • it seems there it may be with source code but without the right to modify
      • not sure if licencees are named developers or how the number of development machines matter.
      • unlimited deploy
    • $450 (site licence)
      • source code + right to recompile
      • All employees can develop
      • unlimited deploy
  • Probably single dll deployment (~0.5 MB)
  • Seems to do everything I need
  • decent API
    • friendly and flexible cell referencing tools, similar to those of Winnovative.
      • indexed (row 1, col 1) or “A1”-style
    • no adding of empty cells/rows as for CarlosAG.
  • Some documentation in web (mostly in the form of code samples

The bad:

  • Not free
    • see the prices mentioned above. Not much but more than nothing.
  • No xlsx
    • It probably is not so important to end-users, but I like the idea of the new format. Not to mentioned this is the format to be supported by Excel longer and better than the older one.
  • No xmldoc included with demo dll
    • The documentation is in web though, maybe will be included after buying the licence.
  • slower than any component I’ve seen
    • 200ms-250ms for the trivial testcase. This is 4-6 times slower than other components I’ve tested. Uh-oh, this is no small difference.
  • Some weird things in API
    • prefixes, like “xl”, “mso”, “H”, “V”. This smells of bad style.
    • interfaces & factories for everything. Seems unneccessary complex design for simple things. Debugging and code readability suffers because of actual class hiding.
  • Lots of releases. 
    • started in 2006, but since the start of 2009 there has been 19 releases, including sometimes with just 1 day apart. They are either really fast at responding to bug notices or there are quality problems. Not sure which.
  • Uncertainty for demo vs licenced version
    • No information if/how the demo version and registered assemblies differ or how licence existance is verified at runtime, does the assembly remain the same, etc.

About avoiding Excel Exporting with Aspose Cells for .NET ..

I’m still looking at Excel components. From the same series so far:

I also wanted to try Aspose Cells for .NET but gave up before even downloading the code, because:

  • Their website was annoyingly heavyweight
    • which reminded me of hard times working with Infragistics components with feature overflow obstructing productivity. Aspose website smelled the same.
  • The dll only download was 13 MB.
    • That’s quite a lot.
    • There’s also 42 MB version, I better not use my imagination what it will install on my machine.
  • The prices START from $900 and go up to $10k+.
    • this without doubt the most expensive component compared to those I’ve seen so far. Too expensive.
  • They required me to register to download the trial version.
    • Why would they need registration for trial version ?
    • Registration form included mandatory fields for my address and telepone..
    • Sorry, I want to try your software not to give you my personal data for something I probably will not end up using. Aspose, this is a really unfriendly way to get new customers!

So I didn’t try their component. Don’t get me wrong. The compnent may be good and powerful and shine with quality and support worthy of Corporate tag, but I’m looking a simple & small & cheap.. I already have better alternatives without registration or before even looking at their API or features.