Tuesday, April 15, 2008

A limitation of Archive Center

FileArchiveCenter has a limitation: only ascii encoded files work,
even though it should not.

For archiving process, it is no problem with file binary content all the way down to the disk. However, when it comes to retrieving a file, it doesn't work that way. Here is how it works:

An archived file first get read by GpgWrapper, which reads the file decrypted content from the console output in text format and returns it as a string.

Then, the ArchiveManager encodes the content into binary with ASCII standard encoding:

return Encoding.ASCII.GetBytes(strFileContent);

Next, the binary is passed through soap protocol to the client, behind the scene, it is converted into text and back to binary with Base64 encoding, because soap is a text based protocol.


Finally, the client proxy class convert the binary back to string with ASCII encoding,

ASCIIEncoding encoding = new ASCIIEncoding();
return encoding.GetString(fileBinaryContent);

Clearly, with this process, only ascii files work. It is a limitation.

To solve the problem, two preconditions are to be met:

1. GnpPGWrapper rather return the decrypted binary then the console output of text; but it needs to seek a way let the GnuPG commandline out a decrypted file in a temp location;

2. Based on the binary stream of a file's content, .Net framework has the function to discover the encoding - it makes sense that .net has this sort of function.

Now that we can successfully refactor the code to archive text file with any kind of encoding. Bear in mind, we have to check all the client application which retrieve files from archive service, because we changed the damn interface!


By Kevin Luan
April 15, 2008

Thursday, March 20, 2008

Web service access network shared folder

By Kevin Luan
Mar 19, 2008


When my web service accesses a network mapping drive, a privilege-related exception happened.

To be specific, the problem is that the Directory.CreateDirectory(path) method in .Net framework failed because of lack of authority to access the target folder. The message is reading like "part of the path can not be found."

The server which hosts my web service and the server where the folder is shared and mapped are not in a domain environment. They are in a network workgroup.

I did the following steps to solve this problem:

1. In the folder sharing server, designate a user
2. Expose approperate accessability from the sharing folder to the designated user

3. Create a matching account in the web service hosting server

4. Assign the matching account to the application pool that hosts my web service.

The background information

In the first place, I changed the user identity in machine.config file, processModel section to "system", which is supposed to have almost all authority to access all resources in the server.
it doesn't work. the reason I thought was that IIS 6.0, which is different from IIS 5.0, uses application pool with an associated user account. it is the application pool user under which my web service process is runing, rather than the user defined in machine.config.

coming next, I set the application pool with a user id of "local system", which is supposed to have plenty of authority to access the server's resources. it still doesn't work. I believed the reason was that "local system", as the name implies, is a local account, is not the "local system" in the sharing folder server. It is a concern what specific user id is used to make request toer is mapped to connect to a network resource such as our mapping folder.

In a domain environment, the "local system" has certain domain role which has the authority to access the resource, but it is different in a workgroup environment.


Finally I tried using matching credentials and it worked. I created and used exactly same user account in both servers.


Remember, use UNC path to access the shared network folder, rather than mapping to a network drive, because the mapping drive is user session dependent.

Session state in a webfarm

In a web farm environment, one concern is how to keep the session state.

we may wire up a session state-server or maintain session in a database.

The easiest solution is implementing session affinity. In the switch, where the load balancing is implemented, configure the filter with parameters of both ip address and session id. As such, any request from a specific ip pertaining a given session id would be directed to a server where the request consistency is guaranteed.

Monday, February 11, 2008

Web application access mapped network folder

Asp.Net runs in a process as runned by aspnet_user, so web application by default has little privilege accessing OS's file system, and have no right to access network mapped folders.

If requested to do that, you just need to change the user of the process. This can be done by changing machine.config file of the Asp.net version installed and being used for your website.

In , defines the process. by default the user is not specified. you can explicitely specify a username as following:



or you even can specify a password attribute.

Wednesday, January 23, 2008

Create logs in database with EntLib

INTRODUCTION
This lab is demonstrating Logging Block of Enterprise Library 3.0. Specifically it sets up
a logging to database framework. The following 5 steps are all needed for this job.

STEP 1: Set the configure entries
The sample configuration is as follow.

<configSections>
<section name="loggingConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Logging.Configuration.LoggingSettings, Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.9.9.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=2.9.9.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</configSections>

<loggingConfiguration name="Logging Application Block" tracingEnabled="true"
defaultCategory="General" logWarningsWhenNoCategoriesMatch="true">
<listeners>
<add databaseInstanceName="Connection String" writeLogStoredProcName="WriteLog"
addCategoryStoredProcName="AddCategory" formatter="Text Formatter"
listenerDataType="Microsoft.Practices.EnterpriseLibrary.Logging.Database.Configuration.FormattedDatabaseTraceListenerData, Microsoft.Practices.EnterpriseLibrary.Logging.Database, Version=2.9.9.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
traceOutputOptions="None" type="Microsoft.Practices.EnterpriseLibrary.Logging.Database.FormattedDatabaseTraceListener, Microsoft.Practices.EnterpriseLibrary.Logging.Database, Version=2.9.9.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
name="Database Trace Listener" />
</listeners>
<formatters>
<add template="Timestamp: {timestamp}&#xD;&#xA;Message: {message}&#xD;&#xA;Category: {category}&#xD;&#xA;Priority: {priority}&#xD;&#xA;EventId: {eventid}&#xD;&#xA;Severity: {severity}&#xD;&#xA;Title:{title}&#xD;&#xA;Machine: {machine}&#xD;&#xA;Application Domain: {appDomain}&#xD;&#xA;Process Id: {processId}&#xD;&#xA;Process Name: {processName}&#xD;&#xA;Win32 Thread Id: {win32ThreadId}&#xD;&#xA;Thread Name: {threadName}&#xD;&#xA;Extended Properties: {dictionary({key} - {value}&#xD;&#xA;)}"
type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter, Microsoft.Practices.EnterpriseLibrary.Logging, Version=2.9.9.2, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
name="Text Formatter" />
</formatters>
<categorySources>
<add switchValue="All" name="General">
<listeners>
<add name="Database Trace Listener" />
</listeners>
</add>
</categorySources>
<specialSources>
<allEvents switchValue="All" name="All Events" />
<notProcessed switchValue="All" name="Unprocessed Category" />
<errors switchValue="All" name="Logging Errors &amp; Warnings">
<listeners>
<add name="Database Trace Listener" />
</listeners>
</errors>
</specialSources>
</loggingConfiguration>

<connectionStrings>
<add name="Connection String" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=|DataDirectory|\Database1.mdf;Integrated Security=True;User Instance=True"
providerName="System.Data.SqlClient" />
</connectionStrings>



STEP 2: COPY AND CREATE REFERENCE FOR ENTLIB 3.0
Create a folder EntLib3 under the website and copy the assmplies, the add reference for the website.

The assemblies and files used:
Microsoft.Practices.EnterpriseLibrary.Common.xml
Microsoft.Practices.EnterpriseLibrary.Common.dll
Microsoft.Practices.EnterpriseLibrary.Data.xml
Microsoft.Practices.EnterpriseLibrary.Data.dll
Microsoft.Practices.EnterpriseLibrary.Logging.Database.xml
Microsoft.Practices.EnterpriseLibrary.Logging.Database.dll
Microsoft.Practices.EnterpriseLibrary.Logging.xml
Microsoft.Practices.EnterpriseLibrary.Logging.dll
Microsoft.Practices.ObjectBuilder.dll


STEP 3: THE CODE SAMPLE FOR WRITING INTO LOG

// import EntLib assembly's namespace
using Microsoft.Practices.EnterpriseLibrary.Logging;

// write an log entry by the Logger class
Logger.Write("Test","General");


STEP 4: CREATE BACKEND DATABASE STRUCTURE
There are three tables need to be created:

- Category
- CategoryLog
- Log

/****** Object: Table [dbo].[Category] Script Date: 01/16/2008 00:12:59 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Category](
[CategoryID] [int] IDENTITY(1,1) NOT NULL,
[CategoryName] [nvarchar](64) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
CONSTRAINT [PK_Categories] PRIMARY KEY CLUSTERED
(
[CategoryID] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]


/****** Object: Table [dbo].[CategoryLog] Script Date: 01/16/2008 00:17:56 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[CategoryLog](
[CategoryLogID] [int] IDENTITY(1,1) NOT NULL,
[CategoryID] [int] NOT NULL,
[LogID] [int] NOT NULL,
CONSTRAINT [PK_CategoryLog] PRIMARY KEY CLUSTERED
(
[CategoryLogID] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]

GO
USE [C:\EXAMPLES2\ENTERPRISE LIBRARY\LOGGING\LOGTODATABASE\LOGTODATABASE\APP_DATA\DATABASE1.MDF]
GO
ALTER TABLE [dbo].[CategoryLog] WITH CHECK ADD CONSTRAINT [FK_CategoryLog_Category] FOREIGN KEY([CategoryID])
REFERENCES [dbo].[Category] ([CategoryID])
GO
ALTER TABLE [dbo].[CategoryLog] WITH CHECK ADD CONSTRAINT [FK_CategoryLog_Log] FOREIGN KEY([LogID])
REFERENCES [dbo].[Log] ([LogID])

/****** Object: Table [dbo].[Log] Script Date: 01/16/2008 00:18:49 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE TABLE [dbo].[Log](
[LogID] [int] IDENTITY(1,1) NOT NULL,
[EventID] [int] NULL,
[Priority] [int] NOT NULL,
[Severity] [nvarchar](32) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Title] [nvarchar](256) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[Timestamp] [datetime] NOT NULL,
[MachineName] [nvarchar](32) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[AppDomainName] [nvarchar](512) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[ProcessID] [nvarchar](256) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[ProcessName] [nvarchar](512) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
[ThreadName] [nvarchar](512) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Win32ThreadId] [nvarchar](128) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[Message] [nvarchar](1500) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
[FormattedMessage] [ntext] COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
CONSTRAINT [PK_Log] PRIMARY KEY CLUSTERED
(
[LogID] ASC
)WITH (IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

STEP 5: CREATE THE ACCESSING STORED PROCEDURES
They are:
- AddCategory
- ClearLogs
- InsertCategoryLog
- WriteLog

--AddCategory
CREATE PROCEDURE [dbo].[AddCategory]
-- Add the parameters for the function here
@CategoryName nvarchar(64),
@LogID int
AS
BEGIN
SET NOCOUNT ON;
DECLARE @CatID INT
SELECT @CatID = CategoryID FROM Category WHERE CategoryName = @CategoryName
IF @CatID IS NULL
BEGIN
INSERT INTO Category (CategoryName) VALUES(@CategoryName)
SELECT @CatID = @@IDENTITY
END

EXEC InsertCategoryLog @CatID, @LogID

RETURN @CatID
END

--ClearLogs
CREATE PROCEDURE [dbo].[ClearLogs]
AS
BEGIN
SET NOCOUNT ON;

DELETE FROM CategoryLog
DELETE FROM [Log]
DELETE FROM Category
END

--InsertCategoryLog
CREATE PROCEDURE [dbo].[InsertCategoryLog]
@CategoryID INT,
@LogID INT
AS
BEGIN
SET NOCOUNT ON;

DECLARE @CatLogID INT
SELECT @CatLogID FROM CategoryLog WHERE CategoryID=@CategoryID and LogID = @LogID
IF @CatLogID IS NULL
BEGIN
INSERT INTO CategoryLog (CategoryID, LogID) VALUES(@CategoryID, @LogID)
RETURN @@IDENTITY
END
ELSE RETURN @CatLogID
END

--WriteLog
CREATE PROCEDURE [dbo].[WriteLog]
(
@EventID int,
@Priority int,
@Severity nvarchar(32),
@Title nvarchar(256),
@Timestamp datetime,
@MachineName nvarchar(32),
@AppDomainName nvarchar(512),
@ProcessID nvarchar(256),
@ProcessName nvarchar(512),
@ThreadName nvarchar(512),
@Win32ThreadId nvarchar(128),
@Message nvarchar(1500),
@FormattedMessage ntext,
@LogId int OUTPUT
)
AS

INSERT INTO [Log] (
EventID,
Priority,
Severity,
Title,
[Timestamp],
MachineName,
AppDomainName,
ProcessID,
ProcessName,
ThreadName,
Win32ThreadId,
Message,
FormattedMessage
)
VALUES (
@EventID,
@Priority,
@Severity,
@Title,
@Timestamp,
@MachineName,
@AppDomainName,
@ProcessID,
@ProcessName,
@ThreadName,
@Win32ThreadId,
@Message,
@FormattedMessage)

SET @LogID = @@IDENTITY
RETURN @LogID

Other issues:
1. Add more categories
It may be important to define a number of categories for logs
this can be done by the configuration file.


<categorySources>
<add switchValue="All" name="General">
<listeners>
<add name="Database Trace Listener"/>
</listeners>
</add>
<add switchValue="All" name="MyCategory">
<listeners>
<add name="Database Trace Listener"/>
</listeners>
</add>
<add switchValue="All" name="MyCategory2">
<listeners>
<add name="Database Trace Listener"/>
</listeners>
</add>
</categorySources>



Once new categorySources defined, we can use it in code where to write a log:

// indicate which category that this log belongs to
Logger.Write("This is my test", "MyCategory2");

the EntLib code will create database record for the categories and the mapping records between logs and categories.

2. define custom fields
if we are not only logging a message, we'd like to log information composed by a number of fields, we can use ExtendedProperties.

LogEntry ent = new LogEntry();
ent.ExtendedProperties.Add("key1", "value1");
ent.ExtendedProperties.Add("key2", "value2");
ent.Categories.Add("MyCategory2");
Logger.Write(ent);


these collection will be serialized to a string and stored to FormattedMessage column of Log table.
the format of the string is defined by a text formatter in configure file:

<formatters>
<add template="Timestamp: {timestamp}&#xD;&#xA;
Message:{message}&#xD;&#xA;
Category: {category}&#xD;&#xA;
Priority: {priority}&#xD;&#xA;
EventId: {eventid}&#xD;&#xA;
Severity: {severity}&#xD;&#xA;
Title:{title}&#xD;&#xA;
Machine: {machine}&#xD;&#xA;
Application Domain: {appDomain}&#xD;&#xA;
Process Id: {processId}&#xD;&#xA;
Process Name: {processName}&#xD;&#xA;
Win32 Thread Id: {win32ThreadId}&#xD;&#xA;
Thread Name: {threadName}&#xD;&#xA;
Extended Properties: {dictionary({key} - {value}&#xD;&#xA;)}"

type="Microsoft.Practices.EnterpriseLibrary.Logging.Formatters.TextFormatter,
Microsoft.Practices.EnterpriseLibrary.Logging, Version=3.1.0.0, Culture=neutral, PublicKeyToken=null"
name="Text Formatter" />