1
\$\begingroup\$

Background

I often see blogs/articles split out the implementation of the Query and QueryHandler classes. I had briefly asked a question on Stack Overflow about whether or not a self-executing Query class would be a violation of SOLID design principals (I deleted the question as it was feeling like the wrong place to ask for feedback). My opinion was that a Query class represents a single operation within a system, and that the separation of parameters/execution was unnecessary in most cases...

I understand that in some cases there may be legitimate reasons for doing so (i.e., using Query object AS your view model, not to my taste). One comment on the original question suggested the inability to cleanly apply decorators to a merged Query class in order to enhance query execution and although I had considered the suggestions at the time to be the responsibility of a QueryProcessor, it got me thinking: as modification of one will more often than not result in modification of the other. Perhaps nesting the handler class would be a happy middle ground as shown in the implementation below.

Implementation

/// <summary>
/// Query definition used to retrieve a specific user by their username.
/// </summary>
public sealed class GetUserByUsernameQuery : Query<User>
{
    /// <summary>
    /// The unique user username to locate.
    /// </summary>
    public Username Username { get; }

    /// <summary>
    /// Initializes a new instance of <see cref="GetUserByUsernameQuery"/>.
    /// </summary>
    /// <param name="username">The unique user username to locate.</param>
    public GetUserByUsernameQuery(Username username)
    {
        if (username == null) throw new ArgumentNullException(nameof(username));

        Username = username;
    }

    /// <summary>
    /// Handles processing of a <see cref="GetUserByUsernameQuery "/> instance.
    /// </summary>
    internal sealed class Handler : SqlQueryHandler<GetUserByUsernameQuery, User>
    {
        /// <summary>
        /// Executes the specified <paramref name="query"/> instance.
        /// </summary>
        /// <param name="query">The query instance to process.</param>
        protected override IEnumerable<User> Execute(GetUserByUsernameQuery query)
        {
            using (var connection = OpenConnection())
                return connection.Query<User>("SELECT * FROM [dbo].[GetUserByUsername](@username);", query);
        }
    }
}

NOTE: No comments regarding SELECT *. The Inline Table Function explicitly selects only the required columns for the result set and I see no reason to define the columns again in the code.


I know some people consider inner classes to be evil, however in this case my thinking is that an inner class provides a nice balance between a self-executing Query and full separation between the Query/QueryHandler classes (i.e., easy to edit within a single file).

My rational:

  • There will only ever be a single handler class per query.
  • The handler will often have to be modified if the query class were to change.
  • Allows for decorators etc to be applied to the QueryHandler if required.
  • The QueryHandler class really cannot exist without the Query class.

Are there any major downsides to going down this path? Any other suggestions on how to achieve balance between the two ends of the spectrum? I would also be interested in feedback on using inline table functions for Dapper queries. I typically place my SQL in embedded resource files or a resource file, but I thought this might be better option?

NOTE: My use case does not require that the Query/QueryHandler ever need to live in different assemblies; thus dependencies are not a concern. And worst case, I could easily refactor if required.

\$\endgroup\$
2
\$\begingroup\$

I would say that we should be able to execute different query handlers given the same query parameters – for testing purposes. Simple data transfer object (query) being passed to the repository interface allows us to do it in a well-known and understandable way.

The same time the following works well for me (SSAS MDX queries)…

To define MDX I use a partial class in multiple files (example):

// SalesSummary.cs
public static partial class MdxQueries
{
    public static IEnumerable<TradeSummary> SalesSummary(DateTime day, string department) =>
        new MdxQuery($@"MDX SCRIPT WITH PARAMETER={day} AND PARAMETER={department}")
            .Select(r => new TradeSummary
            {
                Product = r["Product"],
                Total = r["Total"]
            });
}

// PurchaseSummary.cs
public static partial class MdxQueries
{
    public static IEnumerable<TradeSummary> PurchaseSummary(DateTime day, string department) =>
        new MdxQuery($@"MDX SCRIPT WITH PARAMETER={day} AND PARAMETER={department}")
            .Select(r => new TradeSummary
            {
                Product = r["Product"],
                Total = r["Total"]
            });
}

Where library classes are:

public class MdxQuery : IEnumerable<MdxRow>
{
    static string ConnectionString => ConfigurationManager.ConnectionStrings["SSAS"]
        .ConnectionString;

    public MdxQuery(string mdx)
        : this(mdx, ConnectionString)
    {
    }

    public MdxQuery(string mdx, string connectionString)
    {
        _mdx = mdx;
        _connectionString = connectionString;
    }

    readonly string _mdx;
    readonly string _connectionString;

    public IEnumerator<MdxRow> GetEnumerator()
    {
        using (var connection = new AdomdConnection(_connectionString))
        {
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = _mdx;
            var reader = command.ExecuteReader();
            while (reader.Read())
                yield return new MdxRow(reader);
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public static implicit operator MdxQuery(string mdx) =>
        new MdxQuery(mdx);
}

Where:

public struct MdxRow
{
    internal MdxRow(AdomdDataReader reader)
    {
        Values = Enumerable.Range(0, reader.FieldCount)
            .ToDictionary(i => reader.GetName(i), i => new MdxValue(reader[i]));

        ValuesByIndex = Enumerable.Range(0, reader.FieldCount)
            .ToDictionary(i => i, i => new MdxValue(reader[i]));
    }

    public MdxValue this[string column] => Values[$"[Measures].[{column}]"];
    public MdxValue this[int column] => ValuesByIndex[column];
    Dictionary<string, MdxValue> Values { get; }
    Dictionary<int, MdxValue> ValuesByIndex { get; }
}

Where:

public struct MdxValue
{
    public MdxValue(object value)
    {
        Value = value;
    }

    object Value { get; }

    public static implicit operator string(MdxValue value) =>
        Convert.ToString(value.Value);

    public static implicit operator int(MdxValue value) =>
        Convert.ToInt32(value.Value ?? 0);

    public static implicit operator double(MdxValue value) =>
        Convert.ToDouble(value.Value ?? 0);

    public static implicit operator int? (MdxValue value) =>
        value.IsNull ? (int?)null : (int)value;

    public static implicit operator double? (MdxValue value) =>
        value.IsNull ? (double?)null : (double)value;

    public static implicit operator DateTime(MdxValue value) =>
        DateTime.Parse((string)value ?? DateTime.MinValue.ToString());

    public static implicit operator DateTime? (MdxValue value) =>
        value.IsNull ? (DateTime?)null : (DateTime)value;

    public bool IsNull => Value == null;
}

All this stuff is used to implement repositories, which depend on DTO.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I would not plan to use a separate QueryHandler for testing, as I could easily configure the backing store to be an in-memory database or mock out the underlying connection if preferred (i.e., no need for a fake implementation). As such, I stand by my single QueryHandler statement... that being said, the class shown above does not handle DI, so that did need to be addressed. I opted to split out the class and conform to the masses; mostly because .NET Core projects auto nest files and that makes the artificial separation tolerable). Leaving open for now pending additional feedback. \$\endgroup\$ – Chris Baxter Jun 12 '16 at 0:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.