Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 55 additions & 7 deletions src/FlexQuery.NET.Dapper/Configuration/FlexQueryDapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,77 @@ namespace FlexQuery.NET.Dapper.Configuration;
/// </summary>
public static class FlexQueryDapper
{
private static FlexQueryModel? _defaultModel;
private static DapperQueryOptions _defaultOptions = new();
private static bool _configured;
private static readonly object _lock = new();

/// <summary>
/// Gets the global entity-mapping model configured during application startup.
/// Used internally by <c>FlexQueryAsync</c> when a per-request model is not supplied.
/// Returns null if <see cref="Configure"/> has not been called.
/// </summary>
internal static FlexQueryModel? DefaultModel { get; set; }
internal static FlexQueryModel? DefaultModel
{
get
{
lock (_lock)
{
return _defaultModel;
}
}
}

internal static DapperQueryOptions DefaultOptions { get; set; } = new();
/// <summary>
/// Gets the global Dapper query options configured during application startup.
/// </summary>
internal static DapperQueryOptions DefaultOptions
{
get
{
lock (_lock)
{
return _defaultOptions;
}
}
}

/// <summary>
/// Configures the Dapper entity-mapping model used by FlexQuery at runtime.
/// Must be called once during application startup, before executing any queries.
/// After the first call, the configuration becomes immutable.
/// </summary>
/// <param name="configure">
/// An optional delegate used to configure FlexQuery Dapper entity mappings.
/// </param>
/// <exception cref="InvalidOperationException">Thrown when configuration is attempted after queries have already been executed.</exception>
public static void Configure(Action<FlexQueryDapperOptions>? configure = null)
{
lock (_lock)
{
if (_configured)
throw new InvalidOperationException("FlexQueryDapper has already been configured and is now immutable. Configure must be called before any queries are executed.");

var options = new FlexQueryDapperOptions();
configure?.Invoke(options);

var options = new FlexQueryDapperOptions();
configure?.Invoke(options);
_defaultOptions.UseModel(options.Model.Build());
_defaultOptions.CommandTimeout = options.CommandTimeout;
_defaultModel = options.Model.Build();
_configured = true;
}
}

DefaultOptions.UseModel(options.Model.Build());
DefaultOptions.CommandTimeout = options.CommandTimeout;
DefaultModel = options.Model.Build();
/// <summary>
/// Resets the configuration state. For testing only.
/// </summary>
internal static void Reset()
{
lock (_lock)
{
_defaultModel = null;
_defaultOptions = new DapperQueryOptions();
_configured = false;
}
}
}
45 changes: 40 additions & 5 deletions src/FlexQuery.NET.EntityFrameworkCore/FlexQueryEFCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ namespace FlexQuery.NET.EntityFrameworkCore;
/// </summary>
public static class FlexQueryEFCore
{
private static FlexQueryEfCoreOptions? _defaultOptions;
private static bool _configured;
private static readonly object _lock = new();

/// <summary>
/// Gets the global EF Core execution options configured during application startup.
/// Used by <c>FlexQueryAsync</c> when no per-execution options are supplied.
/// Returns null if <see cref="Configure"/> has not been called.
/// </summary>
internal static FlexQueryEfCoreOptions? DefaultOptions { get; private set; }
internal static FlexQueryEfCoreOptions? DefaultOptions
{
get
{
lock (_lock)
{
return _defaultOptions;
}
}
}

/// <summary>
/// Ensures EF Core-specific query operators are registered.
Expand All @@ -27,15 +41,36 @@ public static void Setup()
/// Configures the global EF Core execution options used by FlexQuery at runtime and
/// ensures EF Core-specific query operators are registered. Must be called once during
/// application startup, before executing any queries.
/// After the first call, the configuration becomes immutable.
/// </summary>
/// <param name="configure">
/// A delegate used to configure the global <see cref="FlexQueryEfCoreOptions"/>.
/// </param>
/// <exception cref="InvalidOperationException">Thrown when configuration is attempted after queries have already been executed.</exception>
public static void Configure(Action<FlexQueryEfCoreOptions>? configure = null)
{
var options = new FlexQueryEfCoreOptions();
configure?.Invoke(options);
DefaultOptions = options;
Setup();
lock (_lock)
{
if (_configured)
throw new InvalidOperationException("FlexQueryEFCore has already been configured and is now immutable. Configure must be called before any queries are executed.");

var options = new FlexQueryEfCoreOptions();
configure?.Invoke(options);
_defaultOptions = options;
_configured = true;
Setup();
}
}

/// <summary>
/// Resets the configuration state. For testing only.
/// </summary>
internal static void Reset()
{
lock (_lock)
{
_defaultOptions = null;
_configured = false;
}
}
}
1 change: 1 addition & 0 deletions src/FlexQuery.NET.OpenApi/FlexQuery.NET.OpenApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

<ItemGroup>
<InternalsVisibleTo Include="FlexQuery.NET.Tests" />
<InternalsVisibleTo Include="FlexQuery.NET.OpenApi.Tests" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 3 additions & 1 deletion src/FlexQuery.NET.Parsers.Fql/Parsing/FqlParseException.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using FlexQuery.NET.Exceptions;

namespace FlexQuery.NET.Parsers.Fql;

/// <summary>
Expand All @@ -13,7 +15,7 @@ namespace FlexQuery.NET.Parsers.Fql;
///
/// Consumers should catch <see cref="Exceptions.QueryParseException"/> rather than this type.
/// </remarks>
public sealed class FqlParseException : Exception
public sealed class FqlParseException : FlexQueryException
{
/// <summary>Creates a <see cref="FqlParseException"/> with the specified error message.</summary>
/// <param name="message">A description of the Fql grammar violation.</param>
Expand Down
12 changes: 12 additions & 0 deletions src/FlexQuery.NET.Parsers.Fql/Parsing/FqlQueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ public QueryOptions Parse(FlexQueryParameters parameters)
{
var options = QueryOptionsFactory.Create(parameters);

if (!string.IsNullOrWhiteSpace(parameters.Select))
{
try
{
FqlSelectParser.Parse(options, parameters.Select);
}
catch (FqlParseException ex)
{
throw new QueryParseException(QueryOptionKeys.Select, QuerySyntax.Fql, parameters.Select, ex);
}
}

if (!string.IsNullOrWhiteSpace(parameters.Filter))
{
try
Expand Down
59 changes: 59 additions & 0 deletions src/FlexQuery.NET.Parsers.Fql/Parsing/FqlSelectParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using FlexQuery.NET.Models;
using FlexQuery.NET.Parsers;

namespace FlexQuery.NET.Parsers.Fql;

/// <summary>
/// Parses FQL SELECT expressions into a list of scalar field paths and expressions.
/// </summary>
internal static class FqlSelectParser
{
/// <summary>
/// Parses an FQL select string into field paths on options.Select.
/// Accepts property paths, "AS" aliases, and expressions with parentheses.
/// </summary>
public static void Parse(QueryOptions options, string? rawSelect)
{
if (rawSelect is not null && string.IsNullOrWhiteSpace(rawSelect))
throw new FqlParseException(
"The 'select' parameter value is empty. Expected comma-separated field paths or expressions.");

var fields = ParserUtilities.SplitCsv(rawSelect);
var selectedFields = new List<string>(fields.Count);
foreach (var field in fields)
{
if (field.Contains('(') || field.Contains(')'))
{
selectedFields.Add(field);
continue;
}

var asIndex = field.IndexOf(" AS ", StringComparison.OrdinalIgnoreCase);
if (asIndex >= 0)
{
var pathPart = field[..asIndex].Trim();
if (pathPart.Length > 0 && !ParserUtilities.IsValidPropertyPath(pathPart.AsSpan()))
throw new FqlParseException(
$"Invalid property path '{pathPart}' in 'select' parameter. " +
"Property paths must be dot-separated identifiers (e.g. 'Id' or 'Customer.Name').");

selectedFields.Add(field);
continue;
}

if (field.Contains(':'))
throw new FqlParseException(
$"Invalid alias in 'select' parameter. " +
"FQL does not support colon-separated aliases. Use 'AS' syntax instead (e.g. 'Name AS FullName').");

if (!ParserUtilities.IsValidPropertyPath(field.AsSpan()))
throw new FqlParseException(
$"Invalid property path '{field}' in 'select' parameter. " +
"Property paths must be dot-separated identifiers (e.g. 'Id' or 'Customer.Name').");

selectedFields.Add(field);
}

options.Select = selectedFields;
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using FlexQuery.NET.Exceptions;

namespace FlexQuery.NET.Parsers.MiniOData;

/// <summary>
/// Exception thrown when the Mini OData parser encounters invalid syntax.
/// </summary>
public sealed class MiniODataParseException : Exception
public sealed class MiniODataParseException : FlexQueryException
{
/// <summary>Creates a new parse exception with the specified message.</summary>
public MiniODataParseException(string message) : base(message) { }
Expand Down
58 changes: 50 additions & 8 deletions src/FlexQuery.NET/FlexQueryCore.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,70 @@
using System.Threading;
using System.Threading;
using FlexQuery.NET.Configuration;
using FlexQuery.NET.Parsers;

namespace FlexQuery.NET;

/// <summary>
///
/// Global FlexQuery configuration entry point.
/// </summary>
public static class FlexQueryCore
{
internal static FlexQueryOptions DefaultOptions { get; set; } = new();
private static FlexQueryOptions? _defaultOptions;
private static bool _configured;
private static readonly object _lock = new();

/// <summary>
/// Gets the global FlexQuery options configured during application startup.
/// Returns an empty options instance if <see cref="Configure"/> has not been called.
/// </summary>
internal static FlexQueryOptions DefaultOptions
{
get
{
lock (_lock)
{
if (_defaultOptions is null)
_defaultOptions = new FlexQueryOptions();

return _defaultOptions;
}
}
}

/// <summary>
/// Configures the global FlexQuery options that serve as defaults for all queries.
/// Must be called once during application startup, before any queries are executed.
/// Must be called once during application startup, before executing any queries.
/// After the first call, the configuration becomes immutable.
/// </summary>
/// <param name="configure">An optional delegate used to configure the global <see cref="FlexQueryOptions"/>.</param>
/// <returns>The configured <see cref="FlexQueryOptions"/> instance.</returns>
/// <exception cref="InvalidOperationException">Thrown when configuration is attempted after queries have already been executed.</exception>
public static void Configure(Action<FlexQueryOptions>? configure = null)
{
var options = new FlexQueryOptions();
configure?.Invoke(options);
lock (_lock)
{
if (_configured)
throw new InvalidOperationException("FlexQueryCore has already been configured and is now immutable. Configure must be called before any queries are executed.");

QueryOptionsParser.SetGlobalSyntax(options.QuerySyntax);
var options = new FlexQueryOptions();
configure?.Invoke(options);

DefaultOptions = options;
QueryOptionsParser.SetGlobalSyntax(options.QuerySyntax);

_defaultOptions = options;
_configured = true;
}
}

/// <summary>
/// Resets the configuration state. For testing only.
/// </summary>
internal static void Reset()
{
lock (_lock)
{
_defaultOptions = null;
_configured = false;
}
}
}
4 changes: 3 additions & 1 deletion src/FlexQuery.NET/Parsers/Dsl/Ast/DslParseException.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using FlexQuery.NET.Exceptions;

namespace FlexQuery.NET.Parsers.Dsl;

/// <summary>
Expand All @@ -10,7 +12,7 @@ namespace FlexQuery.NET.Parsers.Dsl;
///
/// Consumers should catch <see cref="Exceptions.QueryParseException"/> rather than this type.
/// </remarks>
public sealed class DslParseException : Exception
public sealed class DslParseException : FlexQueryException
{
/// <summary>Creates a <see cref="DslParseException"/> with the specified error message.</summary>
/// <param name="message">A description of the DSL grammar violation.</param>
Expand Down
Loading
Loading