Skip to content

Commit

Permalink
Add ability to intercept some SQL operations done through SqlHelper a…
Browse files Browse the repository at this point in the history
…nd EntityConnectionExtensions by implementing ISqlOperationInterceptor and/or IRowOperationInterceptor in the mock connection class.
  • Loading branch information
volkanceylan committed Dec 18, 2024
1 parent 3a1edcc commit 4058bd2
Show file tree
Hide file tree
Showing 20 changed files with 583 additions and 367 deletions.
11 changes: 2 additions & 9 deletions serene/src/Serene.Web/Modules/Administration/User/UserHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,15 @@ public static string GenerateHash(string password, ref string salt)

public static MyRow GetUser(IDbConnection connection, BaseCriteria filter)
{
var row = new MyRow();
if (new SqlQuery().From(row)
return connection.TryFirst<MyRow>(query => query
.Select(
Fld.UserId,
Fld.Username,
Fld.DisplayName,
Fld.PasswordHash,
Fld.PasswordSalt,
Fld.IsActive)
.Where(filter)
.GetFirst(connection))
{
return row;
}

return null;
.Where(filter));
}

public static bool IsInvariantLetter(char c)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.0" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
</ItemGroup>
<Target Name="CopySergenToArtifacts" AfterTargets="CopyFilesToOutputDirectory">
<Target Name="CopySergenToArtifacts" AfterTargets="Pack">
<ItemGroup>
<_OutputFilesToCopy Include="$(OutDir)\**\*" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Serenity.Data;

/// <summary>
/// An interface that makes it possible to intercept basic SQL operations on connections
/// (e.g. SqlHelper extensions) mostly for testing purposes. Note that this does not
/// intercept all SQL operations, only the ones that are done through SqlHelper extensions.
/// It does not intercept Dapper operations, for example.
/// This interface should be implemented by the mock connection class used in tests.
/// </summary>
public interface ISqlOperationInterceptor
{
/// <summary>
/// Intercepts SqlHelper.Execute(SqlDelete/SqlUpdate/SqlInsert) method.
/// <param name="commandText">Command text</param>
/// <param name="parameters">Parameters</param>
/// <param name="query">The query.</param>
/// <param name="expectedRows">Expected rows</param>
/// <param name="getNewId">True if InsertAndGetID is called</param>
/// </summary>
OptionalValue<long?> ExecuteNonQuery(string commandText, IDictionary<string, object> parameters, ExpectedRows expectedRows, IQueryWithParams query, bool getNewId);

/// <summary>
/// Intercepts SqlHelper.ExecuteReader method.
/// </summary>
/// <param name="commandText">Command text</param>
/// <param name="parameters">The parameters.</param>
/// <param name="query">The query</param>
OptionalValue<IDataReader> ExecuteReader(string commandText, IDictionary<string, object> parameters, SqlQuery query);

/// <summary>
/// Intercepts SqlHelper.ExecuteReader method.
/// </summary>
/// <param name="commandText">Command text</param>
/// <param name="parameters">The parameters.</param>
/// <param name="query">The query</param>
OptionalValue<object> ExecuteScalar(string commandText, IDictionary<string, object> parameters, SqlQuery query);
}
7 changes: 0 additions & 7 deletions src/Serenity.Net.Services/Data/SqlHelpers/ReaderCallback.cs

This file was deleted.

231 changes: 80 additions & 151 deletions src/Serenity.Net.Services/Data/SqlHelpers/SqlHelper.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Serenity.Data;
namespace Serenity.Data;

/// <summary>
/// Contains extension methods to perform entity CRUD operations directly on connections.
Expand Down Expand Up @@ -38,6 +38,10 @@ public static TRow ById<TRow>(this IDbConnection connection, object id)
public static TRow TryById<TRow>(this IDbConnection connection, object id)
where TRow : class, IRow, IIdRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id, where: null, editQuery: null, byIdOrSingle: true) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
if (new SqlQuery().From(row)
.SelectTableFields()
Expand All @@ -62,7 +66,9 @@ public static TRow TryById<TRow>(this IDbConnection connection, object id)
public static TRow ById<TRow>(this IDbConnection connection, object id, Action<SqlQuery> editQuery)
where TRow : class, IRow, IIdRow, new()
{
var row = TryById<TRow>(connection, id, editQuery) ?? throw new ValidationError("RecordNotFound", string.Format("Can't locate '{0}' record with ID {1}!", new TRow().Table, id));
var row = TryById<TRow>(connection, id, editQuery)
?? throw new ValidationError("RecordNotFound", string.Format(
"Can't locate '{0}' record with ID {1}!", new TRow().Table, id));
return row;
}

Expand All @@ -79,6 +85,10 @@ public static TRow ById<TRow>(this IDbConnection connection, object id, Action<S
public static TRow TryById<TRow>(this IDbConnection connection, object id, Action<SqlQuery> editQuery)
where TRow : class, IRow, IIdRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id, where: null, editQuery, byIdOrSingle: true) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
var query = new SqlQuery().From(row)
.Where(new Criteria(row.IdField) == new ValueCriteria(id));
Expand Down Expand Up @@ -124,6 +134,10 @@ public static TRow Single<TRow>(this IDbConnection connection, ICriteria where)
public static TRow TrySingle<TRow>(this IDbConnection connection, ICriteria where)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id: default, where, editQuery: null, byIdOrSingle: true) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
if (new SqlQuery().From(row)
.SelectTableFields()
Expand Down Expand Up @@ -163,6 +177,10 @@ public static TRow Single<TRow>(this IDbConnection connection, Action<SqlQuery>
public static TRow TrySingle<TRow>(this IDbConnection connection, Action<SqlQuery> editQuery)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id: default, where: null, editQuery, byIdOrSingle: true) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
var query = new SqlQuery().From(row);

Expand Down Expand Up @@ -199,6 +217,10 @@ public static TRow First<TRow>(this IDbConnection connection, ICriteria where)
public static TRow TryFirst<TRow>(this IDbConnection connection, ICriteria where)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id: default, where, editQuery: null, byIdOrSingle: false) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
if (new SqlQuery().From(row)
.SelectTableFields()
Expand Down Expand Up @@ -236,6 +258,10 @@ public static TRow First<TRow>(this IDbConnection connection, Action<SqlQuery> e
public static TRow TryFirst<TRow>(this IDbConnection connection, Action<SqlQuery> editQuery)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id: default, where: null, editQuery, byIdOrSingle: false) is { HasValue: true } intres)
return (TRow)intres.Value;

var row = new TRow() { TrackWithChecks = true };
var query = new SqlQuery().From(row);

Expand Down Expand Up @@ -269,6 +295,10 @@ public static int Count<TRow>(this IDbConnection connection)
public static int Count<TRow>(this IDbConnection connection, ICriteria where)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ListRows(typeof(TRow), where, editQuery: null, countOnly: true) is { HasValue: true } intres)
return intres.Value?.Count() ?? 0;

var row = new TRow() { TrackWithChecks = true };

return Convert.ToInt32(SqlHelper.ExecuteScalar(connection,
Expand All @@ -287,6 +317,10 @@ public static int Count<TRow>(this IDbConnection connection, ICriteria where)
public static bool ExistsById<TRow>(this IDbConnection connection, object id)
where TRow : class, IRow, IIdRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id, where: null, editQuery: null, byIdOrSingle: false) is { HasValue: true } intres)
return intres.Value != null;

var row = new TRow();
return new SqlQuery()
.From(row)
Expand All @@ -305,6 +339,10 @@ public static bool ExistsById<TRow>(this IDbConnection connection, object id)
public static bool Exists<TRow>(this IDbConnection connection, ICriteria where)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.FindRow(typeof(TRow), id: default, where: null, editQuery: null, byIdOrSingle: false) is { HasValue: true } intres)
return intres.Value != null;

var row = new TRow() { TrackWithChecks = true };
return new SqlQuery().From(row)
.Select("1")
Expand Down Expand Up @@ -338,6 +376,10 @@ public static List<TRow> List<TRow>(this IDbConnection connection)
public static List<TRow> List<TRow>(this IDbConnection connection, ICriteria where)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ListRows(typeof(TRow), where, editQuery: null, countOnly: false) is { HasValue: true } intres)
return (List<TRow>)intres.Value;

var row = new TRow() { TrackWithChecks = true };
return new SqlQuery().From(row)
.SelectTableFields()
Expand All @@ -356,6 +398,10 @@ public static List<TRow> List<TRow>(this IDbConnection connection, ICriteria whe
public static List<TRow> List<TRow>(this IDbConnection connection, Action<SqlQuery> editQuery)
where TRow : class, IRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ListRows(typeof(TRow), where: null, editQuery, countOnly: false) is { HasValue: true } intres)
return (List<TRow>)intres.Value;

var row = new TRow() { TrackWithChecks = true };
var query = new SqlQuery().From(row);

Expand All @@ -372,8 +418,12 @@ public static List<TRow> List<TRow>(this IDbConnection connection, Action<SqlQue
/// <param name="connection">The connection.</param>
/// <param name="row">The row.</param>
public static void Insert<TRow>(this IDbConnection connection, TRow row)
where TRow : class, IRow
where TRow : IRow
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ManipulateRow(typeof(TRow), id: default, row, ExpectedRows.Ignore, getNewId: false) is { HasValue: true })
return;

ToSqlInsert(row).Execute(connection);
}

Expand All @@ -387,8 +437,12 @@ public static void Insert<TRow>(this IDbConnection connection, TRow row)
/// <param name="row">The row.</param>
/// <returns>The ID of the record inserted.</returns>
public static long? InsertAndGetID<TRow>(this IDbConnection connection, TRow row)
where TRow : IRow
where TRow: IRow
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ManipulateRow(typeof(TRow), id: default, row, ExpectedRows.Ignore, getNewId: true) is { HasValue: true } intres)
return intres.Value;

return ToSqlInsert(row).ExecuteAndGetID(connection);
}

Expand All @@ -402,7 +456,7 @@ public static void Insert<TRow>(this IDbConnection connection, TRow row)
/// <param name="expectedRows">The expected number of rows to be updated, by default 1.</param>
/// <exception cref="InvalidOperationException">ID field of row has null value!</exception>
/// <exception cref="InvalidOperationException">Expected rows and number of updated rows does not match!</exception>
public static void UpdateById<TRow>(this IDbConnection connection, TRow row, ExpectedRows expectedRows = ExpectedRows.One)
public static int UpdateById<TRow>(this IDbConnection connection, TRow row, ExpectedRows expectedRows = ExpectedRows.One)
where TRow : IIdRow
{
var idField = row.IdField;
Expand All @@ -411,7 +465,11 @@ public static void UpdateById<TRow>(this IDbConnection connection, TRow row, Exp
if (idField.IsNull(r))
throw new InvalidOperationException("ID field of row has null value!");

row.ToSqlUpdateById()
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ManipulateRow(typeof(TRow), id: idField.AsObject(row), row, expectedRows, getNewId: false) is { HasValue: true } intres)
return (int)intres.Value;

return row.ToSqlUpdateById()
.Execute(connection, expectedRows);
}

Expand All @@ -428,6 +486,10 @@ public static void UpdateById<TRow>(this IDbConnection connection, TRow row, Exp
public static int DeleteById<TRow>(this IDbConnection connection, object id, ExpectedRows expectedRows = ExpectedRows.One)
where TRow : class, IRow, IIdRow, new()
{
if (connection is IRowOperationInterceptor interceptor &&
interceptor.ManipulateRow(typeof(TRow), id, row: null, expectedRows, getNewId: false) is { HasValue: true } intres)
return (int)intres.Value;

var row = new TRow();
return new SqlDelete(row.Table)
.Where(row.IdField == new ValueCriteria(id))
Expand Down
42 changes: 18 additions & 24 deletions src/Serenity.Net.Services/Entity/Extensions/EntitySqlHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using Dictionary = System.Collections.Generic.Dictionary<string, object>;

namespace Serenity.Data;

/// <summary>
Expand All @@ -16,14 +14,12 @@ public static class EntitySqlHelper
/// <returns>True if any rows returned</returns>
public static bool GetFirst(this SqlQuery query, IDbConnection connection)
{
using IDataReader reader = SqlHelper.ExecuteReader(connection, query);
if (reader.Read())
{
query.GetFromReader(reader);
return true;
}
else
using var reader = query.ExecuteReader(connection);
if (!reader.Read())
return false;

query.GetFromReader(reader);
return true;
}

/// <summary>
Expand All @@ -36,18 +32,16 @@ public static bool GetFirst(this SqlQuery query, IDbConnection connection)
/// <exception cref="InvalidOperationException">Query returned more than one result!</exception>
public static bool GetSingle(this SqlQuery query, IDbConnection connection)
{
using IDataReader reader = SqlHelper.ExecuteReader(connection, query);
if (reader.Read())
{
query.GetFromReader(reader);
using IDataReader reader = query.ExecuteReader(connection);
if (!reader.Read())
return false;

if (reader.Read())
throw new InvalidOperationException("Query returned more than one result!");
query.GetFromReader(reader);

return true;
}
else
return false;
if (reader.Read())
throw new InvalidOperationException("Query returned more than one result!");

return true;
}

/// <summary>
Expand All @@ -68,20 +62,20 @@ public static int ForEach(this SqlQuery query, IDbConnection connection,
/// </summary>
/// <param name="query">The query.</param>
/// <param name="connection">The connection.</param>
/// <param name="callBack">The call back.</param>
/// <param name="callback">The call back.</param>
/// <returns>Number of returned results</returns>
public static int ForEach(this SqlQuery query, IDbConnection connection,
ReaderCallBack callBack)
Action<IDataReader> callback)
{
int count = 0;

if (connection.GetDialect().MultipleResultsets)
{
using IDataReader reader = SqlHelper.ExecuteReader(connection, query);
using IDataReader reader = query.ExecuteReader(connection);
while (reader.Read())
{
query.GetFromReader(reader);
callBack(reader);
callback(reader);
}

if (query.CountRecords && reader.NextResult() && reader.Read())
Expand All @@ -97,7 +91,7 @@ public static int ForEach(this SqlQuery query, IDbConnection connection,
while (reader.Read())
{
query.GetFromReader(reader);
callBack(reader);
callback(reader);
}
}

Expand Down
Loading

0 comments on commit 4058bd2

Please sign in to comment.