BLOGas.lt
Sukurk savo BLOGą Kitas atsitiktinis BLOGas

Skaitliukas per laiko vienetą

Parašė Sergejus | 2011-06-15 18:56

Šiandien darbe prisireikė galimybės apriboti kreipinių į serverį kiekį per laiko vienetą (pvz., per sekundę). Vaizdžiai, jeigu leidžiama daugiausiai 100 kreipinių į sekundę, o per tą pačią sekundę ateina 150 kreipinių – pirmi 100 bus aptarnauti, o likę 50 – ne. Taip gimė labai naudinga klasė Throttler:

public abstract class Throttler
{
    private readonly long _limit;
    private readonly TimeSpan _timePeriod;
    private DateTime _time;

    protected Throttler(long limit, TimeSpan timePeriod)
    {
        _limit = limit;
        _timePeriod = timePeriod;
        _time = DateTime.UtcNow;
        Lock = new ReaderWriterLockSlim();
    }

    protected ReaderWriterLockSlim Lock { private set; get; }

    public bool Throttle()
    {
        var now = DateTime.UtcNow;

        if (now >= Time.Add(_timePeriod))
        {
            Lock.EnterWriteLock();
            try
            {
                if (now >= _time.Add(_timePeriod))
                {
                    _time = now;
                    ResetCounter(context);
                }
            }
            finally
            {
                Lock.ExitWriteLock();
            }
        }

        var count = IncrementCounter(context);

        return count > _limit;
    }

    private DateTime Time
    {
        get
        {
            Lock.EnterReadLock();
            try
            {
                return _time;
            }
            finally
            {
                Lock.ExitReadLock();
            }
        }
    }

    protected abstract void ResetCounter(TContext context);

    protected abstract long IncrementCounter(TContext context);
}

Jos pagrindu aukščiau pateiktas uždavinys sprendžiamas ypač paprastai:

public class TotalCountThrottler : Throttler
{
    private long _count;

    public TotalCountThrottler(long limit, TimeSpan timePeriod)
        : base(limit, timePeriod)
    {
    }

    protected override void ResetCounter(HttpContextBase context)
    {
        Interlocked.Exchange(ref _count, 0);
    }

    protected override long IncrementCounter(HttpContextBase context)
    {
        return Interlocked.Increment(ref _count);
    }
}

Panaudojimas yra elementarus:

public class ThrottlingHandler : IHttpHandler
{
    private readonly TotalCountThrottler _throttler;

    public ThrottlingHandler()
    {
        _throttler = new TotalCountThrottler(100, TimeSpan.FromSeconds(1));
    }

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        if (_throttler.Throttle())
        {
            context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
            context.Response.End();
        }
    }
}

Pažymėtina, kad minėta klasė gali veikti tiek su Web aplikacijomis, tiek su bet kokiomis kitomis aplikacijomis.

Rodyk draugams

Patogesnis darbas su ConfigurationManager klase

Parašė Sergejus | 2010-07-16 23:45

ConfigurationManager yra viena svarbiausių dažniausiai naudojamų .NET klasių, nes būtent ji yra naudojama nustatymų iš web.config ar app.config nuskaitymui. Vienas dalykas kuris paskutiniu metu šioje klasėje man nepatinka – kodo ilgis, reikalingas pasiekti nustatymą iš appSettings skilties ar prisijungimo eilutę iš connectionStrings skilties. Pavyzdžiui, norint nuskaityti nustatymą Name, reikia tokio kodo:

var name = ConfigurationManager.AppSettings["Name"];

PrisijungimoEilutės Connection nuskaitymas reikalauja dar daugiau kodo:

var connection = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;

Šiandien noriu pasiūlyti jums supaprastiną ConfigurationManager klasę Config. Klasės interfeisus (reikalavimus) aprašiau kaip testus:

[TestClass]
public class ConfigTest
{
    [TestMethod]
    public void NamePropertyReturnsSameValueAsAppSettings()
    {
        var actual = Config.Current.Name;
        var expected = ConfigurationManager.AppSettings["Name"];

        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void ConnectionPropertyReturnsSameValueAsConnectionStringWhenNoAppSettingsWithSameKeyExist()
    {
        var actual = Config.Current.Connection;
        var expected = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;

        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void DatabasePropertyReturnsSameValueAsAppSettingsEvenWhenConnectionStringWithSameKeyExist()
    {
        var actual = Config.Current.Database;
        var expected = ConfigurationManager.AppSettings["Database"];

        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    public void NewInstanceOfConfigCanBeCreatedAndUsedToAccessAppSettingsWithKeyName()
    {
        dynamic config = new Config();
        var actual = config.Name;
        var expected = ConfigurationManager.AppSettings["Name"];

        Assert.AreEqual(expected, actual);
    }

    [TestMethod]
    [ExpectedException(typeof(RuntimeBinderException))]
    public void NonExistingAppSettingsKeyAndConnectionStringKeyThrowsException()
    {
        var actual = Config.Current.NonExisting;
    }
}

Testams naudojamas toks app.config failas:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="Connection" connectionString="StubConnectionString"/>
    <add name="Database" connectionString="StubDatabaseConnectionString"/>
  </connectionStrings>
  <appSettings>
    <add key="Name" value="Sergejus"/>
    <add key="Database" value="StubDatabaseSetting"/>
    <add key="Age" value="25"/>
  </appSettings>
</configuration>

Kaip matote, pagrindinis noras buvo pasiekti tiek AppSettings, tiek ConnectionStrings nustatymus kaip savybes. Tai ypač lengva realizuoti naudojant C# 4.0 dinaminius objektus:

public class Config : DynamicObject
{
    private static readonly Config config = new Config();

    public static dynamic Current
    {
        get
        {
            return config;
        }
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = ConfigurationManager.AppSettings[binder.Name];

        if (result == null && ConfigurationManager.ConnectionStrings[binder.Name] != null)
        {
            result = ConfigurationManager.ConnectionStrings[binder.Name].ConnectionString;
        }

        return result != null;
    }
}

O ką jus manote apie tokios klasės naudą? Kaip visada laukiu komentarų…

Rodyk draugams

Patogus internal klasių testavimas

Parašė Sergejus | 2010-07-08 20:42

ATNAUJINTA

Jeigu jus turite pilną prieigą ir galimybę papildyti testuojamą rinkinį, gali būti patogiau naudoti ne mano metodą, bet pasiūlytą Sauliaus – atributą InternalsVisibleTo.

 

Šiandien norėjau ištestuoti vieną iš savo klasių, bet pasirodė, jos pasiekiamumo lygis yra internal. Tai reiškia, kad iš kitų rinkinių (angl. assembly) ši klasė tiesiog nėra matoma, kas sukelia pakankamai nemažai nepatogumų norint ją ištestuoti.

Tarkime, mes norime ištestuoti labai paprastą klasę Switch:

internal class Switch
{
    public Switch(bool isOn)
    {
        IsOn = isOn;
    }

    public bool IsOn
    {
        get;
        private set;
    }

    public bool Invert()
    {
        IsOn = !IsOn;
        return IsOn;
    }
}

Vienas iš galimų variantų gali būti tiesiog panaudoti refleksiją (angl. reflection). Tarkime, mes norime ištestuoti ar perdavus į konstruktorių reikšmę true ir vėliau iškvietus savybę IsOn, mes gausime tą pačią reikšmę true. Naudojant paprastą refleksiją testavimo metodas galėtų atrodyti taip:

[TestMethod]
public void SwitchIsOnWhenCreatedWithArgumentSetToTrue()
{
    var assembly = Assembly.GetAssembly(typeof(AnyPublicClass));
    var type = assembly.GetType("Code.Switch");
    var ctor = type.GetConstructor(new[] { typeof(bool) });
    var obj = ctor.Invoke(new object[] { /*is on*/ true });
    var isOn = type.GetProperty("IsOn").GetValue(obj, null);

    Assert.IsTrue(Convert.ToBoolean(isOn));
}

Kaip matote, užrašymas tikrai nėra labai patogus (AnyPublicClass – tai bet kokia public klasė, kuri randasi tame pačiame rinkinyje kaip ir testuojama internal klasė). Kitas galimas variantas, kurį aš atsimyniau iš MVP Summit pristatymų, panaudoti C# 4 dinaminius objektus. Jų pagalba galima „apgaubti“ refleksiją į paprastą metodų ar savybių iškvietimą. Vieną iš tokių objektų realizacijų aš radau David Ebbo tinklaraštyje (klasė PrivateReflectionDynamicObject). Šios klasės pagalba bet kurį objektą (įskaitant ir internal), galima paversti dinaminiu AsDynamic() metodo pagalba. Tam kad būtų dar patogiau, aš aprašiau praplėtimo metodą New(), leidžiantį kurti reikalingo tipo dinaminius objektus:

public static class TestExtensions
{
    private const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Public |
                                       BindingFlags.Static | BindingFlags.Instance;

    public static dynamic New(this Assembly assembly, string typeName, params object[] ctorArgs)
    {
        var type = assembly.GetTypes().First(item => item.Name == typeName);
        var ctorArgTypes = ctorArgs.Select(arg => arg.GetType()).ToArray();
        var ctor = type.GetConstructor(Flags, null, ctorArgTypes, null);

        return ctor != null ? ctor.Invoke(ctorArgs).AsDynamic() : null;
    }
}

Perrašytas testavimo metodas atrodo taip:

[TestMethod]
public void SwitchIsOnWhenCreatedWithArgumentSetToTrue()
{
    var assembly = Assembly.GetAssembly(typeof(AnyPublicClass));
    dynamic obj = assembly.New("Switch", /*is on*/ true);

    Assert.IsTrue(obj.IsOn);
}

Sutinkate, kad metodas atrodo daug paprasčiau? Mes ne tik žymiai paprasčiau galime kurti objektus, bet ir „natūraliai“ kviesti objekto savybę IsOn! Žemiau pateiktas kitas pavyzdys, kuris parodo kaip galima kviesti metodus:

[TestMethod]
public void SwitchStateChangedToOffWhenInvertCalledForOn()
{
    var assembly = Assembly.GetAssembly(typeof(AnyPublicClass));
    dynamic obj = assembly.New("Switch", /*is on*/ true);

    obj.Invert();
    Assert.IsFalse(obj.IsOn);
}

C# 4 dinaminių objektų pagalba kaip niekada paprastai ir patogiai mes galime testuoti internal klases!

P.S.

Be abejo visas pateiktas kodas tinka ir private klasių testavimui, kitas klausimas ar tikrai logiška testuoti private klases..

P.P.S.

Visą kodą galite rasti ir mano SBToolkit projekte.

Rodyk draugams

Bazinė Windows servisų klasė

Parašė Sergejus | 2010-07-07 18:31

Paskutiniu metu darbe teko nemažai rašyti įvairiausius Windows servisus (integracinius, asinchroninių užduočių ir pan.). Kaip ir daugelis jūsų, tam aš turėjau nuosavą bazinę klasę, kuri tebuvo .NET Framework klasės ServiceBase abstrakcija. Prieš kelias savaites nusprendžiau iš esmės perrašyti savo bazinę klasę, kad apart standartinio ServiceBase funkcionalumo, ji spręstų dvi pagrindines Windows servisų problemas:

  • Galimybė paleisti kodą tiek kaip konsolinę aplikaciją (derinimui), tiek kaip Windows servisą
  • Galimybė instaliuoti / išinstaliuoti servisą nesinaudojant pagalbine InstallUtil programa, o tiesiog nurodant parametrus servisas.exe /install ir servisas.exe /uninstall.

Viso sprendimo kodo nekomentuosiu (jis pakankamai aiškus), tik pažymėsiu kelias įdomias vietas:

Pavyzdinis Windows servisas galėtų atrodyti taip:

public class TestService : WindowsServiceBase
{
    public const string Name = "TestService";

    public TestService() : base(Name)
    {
    }

    private static void Main(string[] args)
    {
        Run<TestService>(args);
    }

    protected override void Execute()
    {
        Console.WriteLine("Bus spausdinama kas 60 sek.");
    }
}

Pavyzdinio Windows serviso diegėjas:

[RunInstaller(true)]
public class Installer : WindowsServiceInstaller
{
    public Installer() : base(TestService.Name)
    {
    }
}

Bazinės WindowsServiceBase klasės kodas pateiktas žemiau:

public abstract class WindowsServiceBase : ServiceBase
{
    private readonly Timer timer;

    private int isWorking;

    protected WindowsServiceBase(string name)
    {
        if (String.IsNullOrEmpty(name))
        {
            throw new ArgumentNullException("name");
        }

        timer = new Timer(PollInterval) { AutoReset = true };
        timer.Elapsed += (sender, args) => DoExecute();

        ServiceName = name;
        CanPauseAndContinue = false;
    }

    protected ILogger Logger { get; set; }

    protected int PollInterval
    {
        get
        {
            return Int32.Parse(ConfigurationManager.AppSettings["PollInterval"] ?? "60") * 1000;
        }
    }

    protected abstract void Execute();

    protected override void OnStart(string[] args)
    {
        Logger.Information("Starting service " + ServiceName);

        base.OnStart(args);
        timer.Start();
    }

    protected override void OnStop()
    {
        Logger.Information("Stopping service " + ServiceName);

        base.OnStop();
        timer.Stop();

        CleanUp();
    }

    protected static void Run<TService>(params string[] args) where TService : WindowsServiceBase, new()
    {
        var service = new TService();

        if (args != null && args.Length > 0)
        {
            for (int i = 0; i < args.Length; i++)
            {
                args[i] = args[i].Trim().ToLowerInvariant();
            }

            var exeName = Path.GetFileName(Assembly.GetCallingAssembly().Location);
            if (args.Contains("uninstall"))
            {
                using (var installer = new AssemblyInstaller(exeName, null) { UseNewContext = true })
                {
                    installer.Uninstall(null);
                    return;
                }
            }
            if (args.Contains("install"))
            {
                using (var installer = new AssemblyInstaller(exeName, null) { UseNewContext = true })
                {
                    installer.Install(null);
                    installer.Commit(null);
                    return;
                }
            }
        }

        if (Environment.UserInteractive)
        {
            service.OnStart(args);

            Console.WriteLine("Service is running... Press Ctrl+C to exit.");

            while (true)
            {
                // loop to execute service, sleep for 0.1 second to save CPU ticks
                Thread.Sleep(100)
            }
        }
        else
        {
            Run(service);
        }
    }

    protected virtual void CleanUp()
    {
        timer.Dispose();
    }

    private void DoExecute()
    {
        if (Interlocked.Exchange(ref isWorking, 1) == 0)
        {
            Logger.Information("Execution process started...");
            try
            {
                Execute();
            }
            catch (Exception e)
            {
                Logger.Error(e.Message, e);
            }
            Logger.Information("Execution process completed...");

            Interlocked.Exchange(ref isWorking, 0);
        }
    }
}

Čia naudojamas bendro pobūdžio interfeisas ILogger:

public interface ILogger
{
    void Information(string message);
    void Warning(string message);
    void Error(string message);
    void Error(string message, Exception ex);
}

Bazinė WindowsServiceInstaller klasė atrodo taip:

public class WindowsServiceInstaller : Installer
{
    private readonly ServiceProcessInstaller serviceProcessInstaller;

    private readonly ServiceInstaller serviceInstaller;

    public WindowsServiceInstaller(string name)
    {
        serviceProcessInstaller = new ServiceProcessInstaller
        {
            Account = ServiceAccount.LocalSystem,
            Password = null,
            Username = null,
        };

        serviceInstaller = new ServiceInstaller
        {
            DisplayName = name,
            ServiceName = name,
            StartType = ServiceStartMode.Automatic
        };

        Installers.AddRange(new Installer[]
            {
                serviceProcessInstaller,
                serviceInstaller
            });
    }
}

Tokia nesudėtinga bazinė klasė leido man žymiai efektyviau ir patogiau kurti bei naudoti Windows servisus, tikiuosi pravers ir jums. Kaip visada, laukiu jūsų pastabų / siūlymų / komentarų.

P.S.

WindowsServiceBase ir WindowsServiceInstaller kodą galite rasti ir mano SBToolkit projekte.

Rodyk draugams

Tikslios SQL klaidos gavimas iš SqlException

Parašė Sergejus | 2010-05-27 22:52

Dirbant su duomenų bazėmis, kartais svarbu žinoti kokia tiksliai SQL klaida įvyko serveryje. Iš T-SQL pusės tai sužinoti labai paprasta – SELECT @@ERROR. Iš C# pusės mes gauname SqlException, bet savybė ErrorCode nėra @@ERROR atitikmuo.

Jeigu jums, pavyzdžiui, reikia apdoroti SQL klaidą ‘Delete statement conflicted with column reference…’, mes pirma turime sužinoti šios klaidos numerį SQL serveryje. Tai daroma užklausos SELECT * FROM master.sys.sysmessages pagalba. Prieš tai minėtos klaidos kodas yra 547. Toliau rašome paprastą C# praplėtimo metodą IsSqlDeleteConflictException:

public static class ExceptionExtensions
{
    public static bool IsSqlDeleteConflictException(this Exception exception)
    {
        var errorNumber = 547;
        var e = exception as SqlException;

        return e != null &&
               e.Errors.Cast<SqlError>().Any(err => err.Number == errorNumber);
    }
}

Rodyk draugams

Pasikeitimų kolekcijoje aptikimas

Parašė Sergejus | 2010-05-17 20:29

Dirbant su kolekcijomis, dažna užduotis yra palyginti kaip jos pasikeitė su laiku: kokie įrašai buvo pridėti, kokie ištrinti, o kokie iš viso nesikeitė. Tai ypatingai dažnai tenka daryti atvaizduojant 1:N ir N:M ryšius formose (puslapiuose). Šiai užduočiai atlikti geriausia panaudoti aibių teoriją, o kur aibių teorija – ten ir LINQ (metodai Except bei Union). Šiandien nusprendžiau pasirašyti pagalbinę klasę Diff, kuri kiek palengvina pakeitimų sąraše aptikimą:

public class Diff<TType>
{
    private readonly IEnumerable<TType> original;

    private readonly IEnumerable<TType> current;

    private readonly IEqualityComparer<TType> comparer;

    public Diff(IEnumerable<TType> original, IEnumerable<TType> current)
    {
        if (original == null)
        {
            throw new ArgumentNullException("original");
        }

        if (current == null)
        {
            throw new ArgumentNullException("current");
        }

        this.original = original;
        this.current = current;
    }

    public Diff(IEnumerable<TType> original, IEnumerable<TType> current, IEqualityComparer<TType> comparer)
        : this(original, current)
    {
        if (comparer == null)
        {
            throw new ArgumentNullException("comparer");
        }

        this.comparer = comparer;
    }

    public IEnumerable<TType> Added
    {
        get
        {
            return comparer == null ? current.Except(original) : current.Except(original, comparer);
        }
    }

    public IEnumerable<TType> Removed
    {
        get
        {
            return comparer == null ? original.Except(current) : original.Except(current, comparer);
        }
    }

    public IEnumerable<TType> Unchanged
    {
        get
        {
            return comparer == null ? current.Except(Added) : current.Except(Added, comparer);
        }
    }
}

 

Kaip matyti, vienas iš Diff klasės konstruktorių priima IEqualityComparer<T> interfeisą, kuris leidžia aprašyti elementų palyginimo taisykles. Tai nėra aktualu primityviems tipams (int, string, double), bet pasidaro svarbu dirbant su objektais. Iš praktikos galiu pasakyti, kad tokiais atvejais dažnai norima lyginti ne pagal visas objekto savybes, bet tik pagal konkrečią. Šiai operacijai palengvinti, galima pasinaudoti pagalbine DefaultComparer<T> klase. Norint TTipas tipo objektus lyginti, tarkime, pagal savybę Name, šios klasės pagalba jums užtenka nurodyti DefaultComparer<TTipas>.For(obj => obj.Name). DefaultComparer<T> realizacija pateikta žemiau:

public static class DefaultComparer<T>
{
    public static IEqualityComparer<T> For<V>(Func<T, V> getValue)
    {
        return new DefaultComparer<T, V>(getValue);
    }
}

public sealed class DefaultComparer<T, V> : IEqualityComparer<T>
{
    private readonly Func<T, V> getValue;

    public DefaultComparer(Func<T, V> getValue)
    {
        this.getValue = getValue;
    }

    public bool Equals(T x, T y)
    {
        return EqualityComparer<V>.Default.Equals(getValue(x), getValue(y));
    }

    public int GetHashCode(T obj)
    {
        var value = getValue(obj);
        if (value == null)
        {
            return 0;
        }

        return EqualityComparer<V>.Default.GetHashCode(value);
    }
}

Tikiuosi bent daliai jūsų šios klasės pravers norint aptikti pasikeitimus kolekcijose.

Rodyk draugams