Search code examples
c#asp.netasmx

block multiple request from same user id to a web method c#


I have a web method upload Transaction (ASMX web service) that take the XML file, validate the file and store the file content in SQL server database. we noticed that a certain users can submit the same file twice at the same time. so we can have the same codes again in our database( we cannot use unique index on the database or do anything on database level, don't ask me why). I thought I can use the lock statement on the user id string but i don't know if this will solve the issue. or if I can use a cashed object for storing all user id requests and check if we have 2 requests from the same user Id we will execute the first one and block the second request with an error message so if anyone have any idea please help


Solution

  • Blocking on strings is bad. Blocking your webserver is bad.

    AsyncLocker is a handy class that I wrote to allow locking on any type that behaves nicely as a key in a dictionary. It also requires asynchronous awaiting before entering the critical section (as opposed to the normal blocking behaviour of locks):

    public class AsyncLocker<T>
    {
        private LazyDictionary<T, SemaphoreSlim> semaphoreDictionary = 
            new LazyDictionary<T, SemaphoreSlim>();
    
        public async Task<IDisposable> LockAsync(T key)
        {
            var semaphore = semaphoreDictionary.GetOrAdd(key, () => new SemaphoreSlim(1,1));
            await semaphore.WaitAsync();
            return new ActionDisposable(() => semaphore.Release());
        }
    }
    

    It depends on the following two helper classes:

    LazyDictionary:

    public class LazyDictionary<TKey,TValue>
    {
        //here we use Lazy<TValue> as the value in the dictionary
        //to guard against the fact the the initializer function
        //in ConcurrentDictionary.AddOrGet *can*, under some conditions, 
        //run more than once per key, with the result of all but one of 
        //the runs being discarded. 
        //If this happens, only uninitialized
        //Lazy values are discarded. Only the Lazy that actually 
        //made it into the dictionary is materialized by accessing
        //its Value property.
        private ConcurrentDictionary<TKey, Lazy<TValue>> dictionary = 
            new ConcurrentDictionary<TKey, Lazy<TValue>>();
        public TValue GetOrAdd(TKey key, Func<TValue> valueGenerator)
        {
            var lazyValue = dictionary.GetOrAdd(key,
                k => new Lazy<TValue>(valueGenerator));
            return lazyValue.Value;
        }
    }
    

    ActionDisposable:

    public sealed class ActionDisposable:IDisposable
    {
        //useful for making arbitrary IDisposable instances
        //that perform an Action when Dispose is called
        //(after a using block, for instance)
        private Action action;
        public ActionDisposable(Action action)
        {
            this.action = action;
        }
        public void Dispose()
        {
            var action = this.action;
            if(action != null)
            {
                action();
            }
        }
    }
    

    Now, if you keep a static instance of this somewhere:

    static AsyncLocker<string> userLock = new AsyncLocker<string>();
    

    you can use it in an async method, leveraging the delights of LockAsync's IDisposable return type to write a using statement that neatly wraps the critical section:

    using(await userLock.LockAsync(userId))
    {
        //user with userId only allowed in this section
        //one at a time.
    }
    

    If we need to wait before entering, it's done asynchronously, freeing up the thread to service other requests, instead of blocking until the wait is over and potentially messing up your server's performance under load.

    Of course, when you need to scale to more than one webserver, this approach will no longer work, and you'll need to synchronize using a different means (probably via the DB).