HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Select query helper for nolock and no change tracking

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
trackinghelperqueryfornolockandselectchange

Problem

I often need to switch connection strings and databases (dev/prod) and I need to be able to execute queries with the NOLOCK flag.

For this purpose I create a few utilities that should make this a little bit easier.

The main class is the QueryService that encapsulates the enitre setup process that I had to repeat for each query/database. Most databases that I use are readonly so I do not need hte change tracking and thus the property allowing to disable it. I use model-first for all of them.

public abstract class QueryService where TDbContext : DbContext
{
    protected QueryService(string connectionStringName, string environmentName = null)
    {
        if (string.IsNullOrEmpty(connectionStringName)) { throw new ArgumentNullException("connectionStringName"); }
        if (string.IsNullOrEmpty(environmentName)) { throw new ArgumentNullException("environmentName"); }

        ConnectionStringName = connectionStringName;
        EnvironmentName = environmentName;
        AutoDetectChangesEnabled = true;
    }
    public Action Log { get; set; }

    public string ConnectionStringName { get; private set; }

    public string EnvironmentName { get; private set; }

    public bool AutoDetectChangesEnabled { get; set; }

    public TResult Execute(Func query, bool nolock = true)
    {
        if (query == null) { throw new ArgumentNullException("query"); }

        var connectionStringFullName = ConnectionStringName + (string.IsNullOrEmpty(EnvironmentName) ? string.Empty : "." + EnvironmentName);

        using (var context = DbContextFactory.Create(connectionStringFullName))
        {
            context.Configuration.AutoDetectChangesEnabled = AutoDetectChangesEnabled;
            context.Database.Log = Log;
            return nolock ? context.AsNolock(query) : query(context);
        }
    }
}


It's supported by two other utilities.

a DbContextFactory - its job is to create the context that must have a constructor accepting a connection string name.

Solution

OK, let’s try to approach it in a SOLID way (everything is immutable and thread safe):

public class FooContext : DbContext
{
    public virtual ICollection Strings { get; set; }
}

public static class FooQueries
{
    static IConnection Connection => new Connection("connectionName", "environment")
        .Log(Console.WriteLine)
        .NoTracking();

    public static IList GetStrings() => Connection
        .Query((FooContext c) => c.Strings.ToList())
        .WithNoLock()
        .Execute();
}


Where we use:

public interface IConnection
{
    T ToContext() where T : DbContext;
}

public interface IQuery
{
    TResult Execute();
}


With extension class:

public static class Queries
{
    public static IConnection Log(this IConnection connection, Action log) =>
        new ConfiguringConnection(connection, c => c.Database.Log = log);

    public static IConnection NoTracking(this IConnection connection) =>
        new ConfiguringConnection(connection, c => c.Configuration.AutoDetectChangesEnabled = false);

    public static IQuery Query(this IConnection connection, Func selector)
        where TContext : DbContext =>
        new Query(connection, selector);

    public static IQuery WithNoLock(this IQuery query) =>
        new NoLockQuery(query);
}


And:

public class Connection : IConnection
{
    public Connection(string name, string environment = null)
    {
        Name = name;
        Environment = environment;
    }

    public T ToContext()
        where T : DbContext =>
        (T)Activator.CreateInstance(typeof(T), ToString());

    public override string ToString() =>
        string.IsNullOrEmpty(Environment) ? Name : Name + "." + Environment;

    string Name { get; }
    string Environment { get; }
}


And:

class ConfiguringConnection : IConnection
{
    public ConfiguringConnection(IConnection parent, Action setup)
    {
        Parent = parent;
        Setup = setup;
    }

    public T ToContext() where T : DbContext
    {
        var context = Parent.ToContext();
        Setup(context);
        return context;
    }

    protected IConnection Parent { get; }
    protected Action Setup { get; }
}


And:

class NoLockQuery : IQuery
{
    public NoLockQuery(IQuery parent)
    {
        Parent = parent;
    }

    public TResult Execute()
    {
        using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions
        {
            IsolationLevel = IsolationLevel.ReadUncommitted
        }))
        {
            var result = Parent.Execute();
            scope.Complete();
            return result;
        }
    }

    IQuery Parent { get; }
}


And:

class Query : IQuery 
    where TContext : DbContext
{
    public Query(IConnection connection, Func selector)
    {
        Connection = connection;
        Selector = selector;
    }

    public TResult Execute() => Selector(Connection.ToContext());
    IConnection Connection { get; }
    Func Selector { get; }
}


P.S. I hope all this stuff works :)

Code Snippets

public class FooContext : DbContext
{
    public virtual ICollection<string> Strings { get; set; }
}

public static class FooQueries
{
    static IConnection Connection => new Connection("connectionName", "environment")
        .Log(Console.WriteLine)
        .NoTracking();

    public static IList<string> GetStrings() => Connection
        .Query((FooContext c) => c.Strings.ToList())
        .WithNoLock()
        .Execute();
}
public interface IConnection
{
    T ToContext<T>() where T : DbContext;
}

public interface IQuery<TResult>
{
    TResult Execute();
}
public static class Queries
{
    public static IConnection Log(this IConnection connection, Action<string> log) =>
        new ConfiguringConnection(connection, c => c.Database.Log = log);

    public static IConnection NoTracking(this IConnection connection) =>
        new ConfiguringConnection(connection, c => c.Configuration.AutoDetectChangesEnabled = false);

    public static IQuery<TResult> Query<TContext, TResult>(this IConnection connection, Func<TContext, TResult> selector)
        where TContext : DbContext =>
        new Query<TContext, TResult>(connection, selector);

    public static IQuery<TResult> WithNoLock<TResult>(this IQuery<TResult> query) =>
        new NoLockQuery<TResult>(query);
}
public class Connection : IConnection
{
    public Connection(string name, string environment = null)
    {
        Name = name;
        Environment = environment;
    }

    public T ToContext<T>()
        where T : DbContext =>
        (T)Activator.CreateInstance(typeof(T), ToString());

    public override string ToString() =>
        string.IsNullOrEmpty(Environment) ? Name : Name + "." + Environment;

    string Name { get; }
    string Environment { get; }
}
class ConfiguringConnection : IConnection
{
    public ConfiguringConnection(IConnection parent, Action<DbContext> setup)
    {
        Parent = parent;
        Setup = setup;
    }

    public T ToContext<T>() where T : DbContext
    {
        var context = Parent.ToContext<T>();
        Setup(context);
        return context;
    }

    protected IConnection Parent { get; }
    protected Action<DbContext> Setup { get; }
}

Context

StackExchange Code Review Q#132931, answer score: 2

Revisions (0)

No revisions yet.