Monday, December 10, 2007

Short-lived object manager

I don't know if I'm re-inventing the wheel here, but I refactored a couple of server-side objects to arrive at this base class:




using System;
using System.Collections.Generic;
using System.Threading;

class ShortLived : IDisposable
{
Guid key;
DateTime lastAccess = DateTime.Now;
int disposed = 0;
static Timer gcThread = null;
static Dictionary<Guid, ShortLived> objects = new Dictionary<Guid, ShortLived>();
static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
static readonly TimeSpan GcInterval = TimeSpan.FromMinutes(1);
static object syncRoot = new object();

public Guid Key
{
get
{
return key;
}
}

public static object SyncRoot
{
get
{
return syncRoot;
}
}

public static T GetObject<T>(Guid key) where T : ShortLived
{
ShortLived result = null;
if (objects.TryGetValue(key, out result))
return result as T;
return default(T);
}

public static IEnumerable<T> GetObjects<T>() where T : ShortLived
{
List<T> results = new List<T>();
lock (SyncRoot)
{
foreach (ShortLived obj in objects.Values)
{
if (obj is T)
results.Add((T)obj);
}
}
return results;
}

void IDisposable.Dispose()
{
Dispose(Interlocked.CompareExchange(ref disposed, 1, 0) == 1);
}

protected virtual void Dispose(bool disposed)
{
}

protected ShortLived()
{
key = Guid.NewGuid();
Init();
}

protected ShortLived(Guid key)
{
this.key = key;
Init();
}

private void Init()
{
lock (SyncRoot)
objects.Add(key, this);
Timer newCollector = new Timer(GarbageCollect, null, GcInterval, GcInterval);
if (Interlocked.CompareExchange(ref gcThread, newCollector, null) == null)
gcThread.Change(GcInterval, GcInterval);
else
newCollector.Dispose();
}

protected void KeepAlive()
{
lastAccess = DateTime.Now;
}

private static void GarbageCollect(object context)
{
List<Guid> expired = new List<Guid>();
DateTime now = DateTime.Now;
lock (SyncRoot)
{
foreach (KeyValuePair<Guid, ShortLived> kv in objects)
{
if (now - kv.Value.lastAccess > Lifetime)
expired.Add(kv.Key);
}
foreach (Guid key in expired)
{
IDisposable obj = objects[key];
objects.Remove(key);
obj.Dispose();
}
if (objects.Count == 0)
{
gcThread.Change(Timeout.Infinite, Timeout.Infinite);
Interlocked.Exchange(ref gcThread, null);
}
}
}
}

To use, just inherit from ShortLived, and call KeepAlive whenever the object is accessed. Don't store references to your object; store a reference to its Key property, and use that to look it up when you want it again.

Wednesday, December 05, 2007

WCF Services alongside legacy ASMX

I guess the situation I found myself in recently is pretty common. So we have an intranet application which uses .NET 1.1 webservices hosted in IIS 6. Now we want to add a new batch of WCF services, without clobbering the environment too much. Transaction support is definitely a must, and obviously we need to be able to determine the client's credentials. There's a lot of legacy code that refers to HttpContext.Current.User.Identity.Name. Sounds familiar?

Right, so the first thing you discover is that only http bindings are supported when the service is hosted in IIS. So you try basicHttpBinding, but that doesn't support transactions. That leaves wsHttpBinding. Now you get an error along the lines of "the service requires Anonymous access but it is not configured in IIS..." After a bit of digging, you find out that IIS is set up for NTLM authentication only. wsHttpBinding supports this, but only at a transport level. Which means https. So you hack up a certificate for the server and get everything working, but... now transactions can't be flowed because the DTC doesn't have WS-AtomicTransaction support enabled. More research, and it looks like getting this to happen involves a certificate for every single client.

It was about that time that I noticed that girl scout was 6 storeys... I mean I decided to look for alternatives. To cut a long story short, here's my web.config:

<configuration>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
<bindings>
<customBinding>
<binding name="iisat">
<transactionFlow transactionProtocol="OleTransactions"/>
<textMessageEncoding/>
<httpTransport authenticationScheme="Ntlm"/>
</binding>
</customBinding>
</bindings>
<services>
<service name="(censored)">
<endpoint
address=""
binding="customBinding"
bindingConfiguration="iisat"
contract="(censored)" />
</service>
</services>
</system.serviceModel>
</configuration>


You're also going to want to apply the AspNetCompatibilityRequirementsAttribute to your service implementations. That gets you access to HttpContext. Also you need to ensure that the DTC is configured for network access on all participating machines (including the clients). This is a bit of a pain, but the relevant configuration is at Control Panel/Administrative Tools/Component Services. Under Component Services/Computers, right-click on My Computer and choose properties. There's an MSDTC tab. Click "Security Configuration" and check the relevant tick boxes. Bear in mind that the client generally initiates and therefore hosts the transaction.

HTH.

Labels: