diff --git a/Connected.ServiceModel.Client.Data/Connected.ServiceModel.Client.Data.csproj b/Connected.ServiceModel.Client.Data/Connected.ServiceModel.Client.Data.csproj
new file mode 100644
index 0000000..2e033fa
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/Connected.ServiceModel.Client.Data.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Connected.ServiceModel.Client.Data/CqlDataType.cs b/Connected.ServiceModel.Client.Data/CqlDataType.cs
new file mode 100644
index 0000000..340be8c
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/CqlDataType.cs
@@ -0,0 +1,17 @@
+using Connected.Expressions.Languages;
+using System.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+internal sealed class CqlDataType : DataType
+{
+ public CqlDataType(SqlDbType dbType, bool notNull, int length, short precision, short scale)
+ {
+ DbType = dbType;
+ NotNull = notNull;
+ Length = length;
+ Precision = precision;
+ Scale = scale;
+ }
+
+ public SqlDbType DbType { get; }
+}
diff --git a/Connected.ServiceModel.Client.Data/CqlFormatter.cs b/Connected.ServiceModel.Client.Data/CqlFormatter.cs
new file mode 100644
index 0000000..82e82bf
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/CqlFormatter.cs
@@ -0,0 +1,771 @@
+using Connected.Expressions;
+using Connected.Expressions.Formatters;
+using Connected.Expressions.Languages;
+using Connected.Interop;
+using System.Linq.Expressions;
+
+namespace Connected.ServiceModel.Client.Data;
+///
+/// This is currently the same implementation as the default (TSql) formatter.
+///
+internal sealed class CqlFormatter : SqlFormatter
+{
+ public CqlFormatter(ExpressionCompilationContext context, QueryLanguage? language)
+ : base(language)
+ {
+ Context = context;
+ }
+ public ExpressionCompilationContext Context { get; }
+
+ public static new string Format(ExpressionCompilationContext context, Expression expression)
+ {
+ return Format(context, expression, new CqlLanguage());
+ }
+ public static string Format(ExpressionCompilationContext context, Expression expression, QueryLanguage language)
+ {
+ var formatter = new CqlFormatter(context, language);
+
+ formatter.Visit(expression);
+
+ return formatter.ToString();
+ }
+ protected override void WriteAggregateName(string aggregateName)
+ {
+ if (string.Equals(aggregateName, "LongCount", StringComparison.Ordinal))
+ Write("COUNT_BIG");
+ else
+ base.WriteAggregateName(aggregateName);
+ }
+ protected override Expression VisitMemberAccess(MemberExpression m)
+ {
+ if (m.Member.DeclaringType == typeof(string))
+ {
+ switch (m.Member.Name)
+ {
+ case "Length":
+ Write("LEN(");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ }
+ }
+ else if (m.Member.DeclaringType == typeof(DateTime) || m.Member.DeclaringType == typeof(DateTimeOffset))
+ {
+ switch (m.Member.Name)
+ {
+ case "Day":
+ Write("DAY(");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Month":
+ Write("MONTH(");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Year":
+ Write("YEAR(");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Hour":
+ Write("DATEPART(hour, ");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Minute":
+ Write("DATEPART(minute, ");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Second":
+ Write("DATEPART(second, ");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "Millisecond":
+ Write("DATEPART(millisecond, ");
+ Visit(m.Expression);
+ Write(")");
+ return m;
+ case "DayOfWeek":
+ Write("(DATEPART(weekday, ");
+ Visit(m.Expression);
+ Write(") - 1)");
+ return m;
+ case "DayOfYear":
+ Write("(DATEPART(dayofyear, ");
+ Visit(m.Expression);
+ Write(") - 1)");
+ return m;
+ }
+ }
+
+ return base.VisitMemberAccess(m);
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression m)
+ {
+ if (m.Method.DeclaringType == typeof(string))
+ {
+ switch (m.Method.Name)
+ {
+ case "StartsWith":
+ Write("(");
+ Visit(m.Object);
+ Write(" LIKE ");
+ Visit(m.Arguments[0]);
+ Write(" + '%')");
+ return m;
+ case "EndsWith":
+ Write("(");
+ Visit(m.Object);
+ Write(" LIKE '%' + ");
+ Visit(m.Arguments[0]);
+ Write(")");
+ return m;
+ case "Contains":
+ Write("(");
+ Visit(m.Object);
+ Write(" LIKE '%' + ");
+ Visit(m.Arguments[0]);
+ Write(" + '%')");
+ return m;
+ case "Concat":
+ var args = m.Arguments;
+
+ if (args.Count == 1 && args[0].NodeType == ExpressionType.NewArrayInit)
+ args = ((NewArrayExpression)args[0]).Expressions;
+ for (var i = 0; i < args.Count; i++)
+ {
+ if (i > 0)
+ Write(" + ");
+
+ Visit(args[i]);
+ }
+ return m;
+ case "IsNullOrEmpty":
+ Write("(");
+ Visit(m.Arguments[0]);
+ Write(" IS NULL OR ");
+ Visit(m.Arguments[0]);
+ Write(" = '')");
+ return m;
+ case "ToUpper":
+ Write("UPPER(");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "ToLower":
+ Write("LOWER(");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "Replace":
+ Write("REPLACE(");
+ Visit(m.Object);
+ Write(", ");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ case "Substring":
+ Write("SUBSTRING(");
+ Visit(m.Object);
+ Write(", ");
+ Visit(m.Arguments[0]);
+ Write(" + 1, ");
+
+ if (m.Arguments.Count == 2)
+ Visit(m.Arguments[1]);
+ else
+ Write("8000");
+
+ Write(")");
+ return m;
+ case "Remove":
+ Write("STUFF(");
+ Visit(m.Object);
+ Write(", ");
+ Visit(m.Arguments[0]);
+ Write(" + 1, ");
+
+ if (m.Arguments.Count == 2)
+ Visit(m.Arguments[1]);
+ else
+ Write("8000");
+
+ Write(", '')");
+ return m;
+ case "IndexOf":
+ Write("(CHARINDEX(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Object);
+
+ if (m.Arguments.Count == 2 && m.Arguments[1].Type == typeof(int))
+ {
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(" + 1");
+ }
+
+ Write(") - 1)");
+ return m;
+ case "Trim":
+ Write("RTRIM(LTRIM(");
+ Visit(m.Object);
+ Write("))");
+ return m;
+ }
+ }
+ else if (m.Method.DeclaringType == typeof(DateTime))
+ {
+ switch (m.Method.Name)
+ {
+ case "op_Subtract":
+ if (m.Arguments[1].Type == typeof(DateTime))
+ {
+ Write("DATEDIFF(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ }
+ break;
+ case "AddYears":
+ Write("DATEADD(YYYY,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddMonths":
+ Write("DATEADD(MM,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddDays":
+ Write("DATEADD(DAY,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddHours":
+ Write("DATEADD(HH,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddMinutes":
+ Write("DATEADD(MI,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddSeconds":
+ Write("DATEADD(SS,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ case "AddMilliseconds":
+ Write("DATEADD(MS,");
+ Visit(m.Arguments[0]);
+ Write(",");
+ Visit(m.Object);
+ Write(")");
+ return m;
+ }
+ }
+ else if (m.Method.DeclaringType == typeof(Decimal))
+ {
+ switch (m.Method.Name)
+ {
+ case "Add":
+ case "Subtract":
+ case "Multiply":
+ case "Divide":
+ case "Remainder":
+ Write("(");
+ VisitValue(m.Arguments[0]);
+ Write(" ");
+ Write(GetOperator(m.Method.Name));
+ Write(" ");
+ VisitValue(m.Arguments[1]);
+ Write(")");
+ return m;
+ case "Negate":
+ Write("-");
+ Visit(m.Arguments[0]);
+ Write("");
+ return m;
+ case "Ceiling":
+ case "Floor":
+ Write(m.Method.Name.ToUpper());
+ Write("(");
+ Visit(m.Arguments[0]);
+ Write(")");
+ return m;
+ case "Round":
+ if (m.Arguments.Count == 1)
+ {
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", 0)");
+ return m;
+ }
+ else if (m.Arguments.Count == 2 && m.Arguments[1].Type == typeof(int))
+ {
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ }
+ break;
+ case "Truncate":
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", 0, 1)");
+ return m;
+ }
+ }
+ else if (m.Method.DeclaringType == typeof(Math))
+ {
+ switch (m.Method.Name)
+ {
+ case "Abs":
+ case "Acos":
+ case "Asin":
+ case "Atan":
+ case "Cos":
+ case "Exp":
+ case "Log10":
+ case "Sin":
+ case "Tan":
+ case "Sqrt":
+ case "Sign":
+ case "Ceiling":
+ case "Floor":
+ Write(m.Method.Name.ToUpper());
+ Write("(");
+ Visit(m.Arguments[0]);
+ Write(")");
+ return m;
+ case "Atan2":
+ Write("ATN2(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ case "Log":
+ if (m.Arguments.Count == 1)
+ goto case "Log10";
+
+ break;
+ case "Pow":
+ Write("POWER(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ case "Round":
+ if (m.Arguments.Count == 1)
+ {
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", 0)");
+ return m;
+ }
+ else if (m.Arguments.Count == 2 && m.Arguments[1].Type == typeof(int))
+ {
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", ");
+ Visit(m.Arguments[1]);
+ Write(")");
+ return m;
+ }
+ break;
+ case "Truncate":
+ Write("ROUND(");
+ Visit(m.Arguments[0]);
+ Write(", 0, 1)");
+ return m;
+ }
+ }
+ if (m.Method.Name == "ToString")
+ {
+ if (m.Object.Type != typeof(string))
+ {
+ Write("CONVERT(NVARCHAR, ");
+ Visit(m.Object);
+ Write(")");
+ }
+ else
+ Visit(m.Object);
+
+ return m;
+ }
+ else if (!m.Method.IsStatic && string.Equals(m.Method.Name, "CompareTo", StringComparison.Ordinal) && m.Method.ReturnType == typeof(int) && m.Arguments.Count == 1)
+ {
+ Write("(CASE WHEN ");
+ Visit(m.Object);
+ Write(" = ");
+ Visit(m.Arguments[0]);
+ Write(" THEN 0 WHEN ");
+ Visit(m.Object);
+ Write(" < ");
+ Visit(m.Arguments[0]);
+ Write(" THEN -1 ELSE 1 END)");
+ return m;
+ }
+ else if (m.Method.IsStatic && string.Equals(m.Method.Name, "Compare", StringComparison.Ordinal) && m.Method.ReturnType == typeof(int) && m.Arguments.Count == 2)
+ {
+ Write("(CASE WHEN ");
+ Visit(m.Arguments[0]);
+ Write(" = ");
+ Visit(m.Arguments[1]);
+ Write(" THEN 0 WHEN ");
+ Visit(m.Arguments[0]);
+ Write(" < ");
+ Visit(m.Arguments[1]);
+ Write(" THEN -1 ELSE 1 END)");
+ return m;
+ }
+ else if (m.Method.DeclaringType == typeof(TypeComparer) && m.Method.IsStatic && string.Equals(m.Method.Name, nameof(TypeComparer.Compare), StringComparison.Ordinal) && m.Method.ReturnType == typeof(bool) && m.Arguments.Count == 2)
+ {
+ Visit(m.Arguments[0]);
+ Write(" = ");
+ Visit(m.Arguments[1]);
+ return m;
+ }
+ return base.VisitMethodCall(m);
+ }
+
+ protected override NewExpression VisitNew(NewExpression nex)
+ {
+ if (nex.Constructor.DeclaringType == typeof(DateTime))
+ {
+ if (nex.Arguments.Count == 3)
+ {
+ Write("Convert(DateTime, ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[0]);
+ Write(") + '/' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[1]);
+ Write(") + '/' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[2]);
+ Write("))");
+ return nex;
+ }
+ else if (nex.Arguments.Count == 6)
+ {
+ Write("Convert(DateTime, ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[0]);
+ Write(") + '/' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[1]);
+ Write(") + '/' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[2]);
+ Write(") + ' ' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[3]);
+ Write(") + ':' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[4]);
+ Write(") + ':' + ");
+ Write("Convert(nvarchar, ");
+ Visit(nex.Arguments[5]);
+ Write("))");
+ return nex;
+ }
+ }
+
+ return base.VisitNew(nex);
+ }
+
+ protected override Expression VisitBinary(BinaryExpression b)
+ {
+ if (b.NodeType == ExpressionType.Power)
+ {
+ Write("POWER(");
+ VisitValue(b.Left);
+ Write(", ");
+ VisitValue(b.Right);
+ Write(")");
+ return b;
+ }
+ else if (b.NodeType == ExpressionType.Coalesce)
+ {
+ Write("COALESCE(");
+ VisitValue(b.Left);
+ Write(", ");
+
+ var right = b.Right;
+
+ while (right.NodeType == ExpressionType.Coalesce)
+ {
+ var rb = (BinaryExpression)right;
+
+ VisitValue(rb.Left);
+ Write(", ");
+
+ right = rb.Right;
+ }
+
+ VisitValue(right);
+ Write(")");
+
+ return b;
+ }
+ else if (b.NodeType == ExpressionType.LeftShift)
+ {
+ Write("(");
+ VisitValue(b.Left);
+ Write(" * POWER(2, ");
+ VisitValue(b.Right);
+ Write("))");
+ return b;
+ }
+ else if (b.NodeType == ExpressionType.RightShift)
+ {
+ Write("(");
+ VisitValue(b.Left);
+ Write(" / POWER(2, ");
+ VisitValue(b.Right);
+ Write("))");
+ return b;
+ }
+
+ return base.VisitBinary(b);
+ }
+
+ protected override Expression VisitConstant(ConstantExpression c)
+ {
+ var parameter = Context.Parameters.FirstOrDefault(f => f.Value == c);
+
+ if (parameter.Value is not null)
+ {
+ Write($"@{parameter.Key}");
+
+ return c;
+ }
+
+ return base.VisitConstant(c);
+ }
+
+ protected override Expression VisitValue(Expression expr)
+ {
+ if (IsPredicate(expr))
+ {
+ Write("CASE WHEN (");
+ Visit(expr);
+ Write(") THEN 1 ELSE 0 END");
+
+ return expr;
+ }
+
+ return base.VisitValue(expr);
+ }
+
+ protected override Expression VisitConditional(ConditionalExpression c)
+ {
+ if (IsPredicate(c.Test))
+ {
+ Write("(CASE WHEN ");
+ VisitPredicate(c.Test);
+ Write(" THEN ");
+ VisitValue(c.IfTrue);
+
+ var ifFalse = c.IfFalse;
+
+ while (ifFalse is not null && ifFalse.NodeType == ExpressionType.Conditional)
+ {
+ var fc = (ConditionalExpression)ifFalse;
+
+ Write(" WHEN ");
+ VisitPredicate(fc.Test);
+ Write(" THEN ");
+ VisitValue(fc.IfTrue);
+
+ ifFalse = fc.IfFalse;
+ }
+ if (ifFalse is not null)
+ {
+ Write(" ELSE ");
+ VisitValue(ifFalse);
+ }
+
+ Write(" END)");
+ }
+ else
+ {
+ Write("(CASE ");
+ VisitValue(c.Test);
+ Write(" WHEN 0 THEN ");
+ VisitValue(c.IfFalse);
+ Write(" ELSE ");
+ VisitValue(c.IfTrue);
+ Write(" END)");
+ }
+
+ return c;
+ }
+
+ protected override Expression VisitRowNumber(RowNumberExpression rowNumber)
+ {
+ Write("ROW_NUMBER() OVER(");
+
+ if (rowNumber.OrderBy is not null && rowNumber.OrderBy.Any())
+ {
+ Write("ORDER BY ");
+
+ for (var i = 0; i < rowNumber.OrderBy.Count; i++)
+ {
+ var exp = rowNumber.OrderBy[i];
+
+ if (i > 0)
+ Write(", ");
+
+ VisitValue(exp.Expression);
+
+ if (exp.OrderType != OrderType.Ascending)
+ Write(" DESC");
+ }
+ }
+
+ Write(")");
+
+ return rowNumber;
+ }
+
+ protected override Expression VisitIf(IfCommandExpression ifx)
+ {
+ if (!Language.AllowsMultipleCommands)
+ return base.VisitIf(ifx);
+
+ Write("IF ");
+ Visit(ifx.Check);
+ WriteLine(Indentation.Same);
+ Write("BEGIN");
+ WriteLine(Indentation.Inner);
+ VisitStatement(ifx.IfTrue);
+ WriteLine(Indentation.Outer);
+
+ if (ifx.IfFalse is not null)
+ {
+ Write("END ELSE BEGIN");
+ WriteLine(Indentation.Inner);
+ VisitStatement(ifx.IfFalse);
+ WriteLine(Indentation.Outer);
+ }
+
+ Write("END");
+
+ return ifx;
+ }
+
+ protected override Expression VisitBlock(Expressions.BlockExpression block)
+ {
+ if (!Language.AllowsMultipleCommands)
+ return base.VisitBlock(block);
+
+ for (var i = 0; i < block.Commands.Count; i++)
+ {
+ if (i > 0)
+ {
+ WriteLine(Indentation.Same);
+ WriteLine(Indentation.Same);
+ }
+
+ VisitStatement(block.Commands[i]);
+ }
+
+ return block;
+ }
+
+ protected override Expression VisitDeclaration(DeclarationExpression decl)
+ {
+ if (!Language.AllowsMultipleCommands)
+ return base.VisitDeclaration(decl);
+
+ for (var i = 0; i < decl.Variables.Count; i++)
+ {
+ var v = decl.Variables[i];
+
+ if (i > 0)
+ WriteLine(Indentation.Same);
+
+ Write("DECLARE @");
+ Write(v.Name);
+ Write(" ");
+ Write(Language.TypeSystem.Format(v.DataType, false));
+ }
+
+ if (decl.Source is not null)
+ {
+ WriteLine(Indentation.Same);
+ Write("SELECT ");
+
+ for (var i = 0; i < decl.Variables.Count; i++)
+ {
+ if (i > 0)
+ Write(", ");
+
+ Write("@");
+ Write(decl.Variables[i].Name);
+ Write(" = ");
+ Visit(decl.Source.Columns[i].Expression);
+ }
+
+ if (decl.Source.From is not null)
+ {
+ WriteLine(Indentation.Same);
+ Write("FROM ");
+ VisitSource(decl.Source.From);
+ }
+
+ if (decl.Source.Where is not null)
+ {
+ WriteLine(Indentation.Same);
+ Write("WHERE ");
+ Visit(decl.Source.Where);
+ }
+ }
+ else
+ {
+ for (var i = 0; i < decl.Variables.Count; i++)
+ {
+ var v = decl.Variables[i];
+
+ if (v.Expression is not null)
+ {
+ WriteLine(Indentation.Same);
+ Write("SET @");
+ Write(v.Name);
+ Write(" = ");
+ Visit(v.Expression);
+ }
+ }
+ }
+
+ return decl;
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/CqlLanguage.cs b/Connected.ServiceModel.Client.Data/CqlLanguage.cs
new file mode 100644
index 0000000..874d5c3
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/CqlLanguage.cs
@@ -0,0 +1,52 @@
+using Connected.Expressions;
+using Connected.Expressions.Languages;
+using Connected.Expressions.Translation;
+using Connected.Expressions.TypeSystem;
+
+namespace Connected.ServiceModel.Client.Data;
+internal sealed class CqlLanguage : QueryLanguage
+{
+ private static CqlLanguage? _default;
+
+ static CqlLanguage()
+ {
+ SplitChars = new char[] { '.' };
+ }
+
+ public CqlLanguage()
+ {
+ TypeSystem = new CqlTypeSystem();
+ }
+
+ public override QueryTypeSystem TypeSystem { get; }
+ private static char[] SplitChars { get; }
+ public override bool AllowsMultipleCommands => true;
+ public override bool AllowSubqueryInSelectWithoutFrom => true;
+ public override bool AllowDistinctInAggregates => true;
+
+ public static CqlLanguage Default
+ {
+ get
+ {
+ if (_default is null)
+ Interlocked.CompareExchange(ref _default, new CqlLanguage(), null);
+
+ return _default;
+ }
+ }
+
+ public override string Quote(string name)
+ {
+ if (name.StartsWith("[") && name.EndsWith("]"))
+ return name;
+ else if (name.Contains('.'))
+ return $"[{string.Join("].[", name.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries))}]";
+ else
+ return $"[{name}]";
+ }
+
+ public override Linguist CreateLinguist(ExpressionCompilationContext context, Translator translator)
+ {
+ return new CqlLinguist(context, this, translator);
+ }
+}
\ No newline at end of file
diff --git a/Connected.ServiceModel.Client.Data/CqlLinguist.cs b/Connected.ServiceModel.Client.Data/CqlLinguist.cs
new file mode 100644
index 0000000..defd5d4
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/CqlLinguist.cs
@@ -0,0 +1,41 @@
+using Connected.Expressions;
+using Connected.Expressions.Languages;
+using Connected.Expressions.Translation;
+using Connected.Expressions.Translation.Rewriters;
+using System.Linq.Expressions;
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal class CqlLinguist : Linguist
+{
+
+ public CqlLinguist(ExpressionCompilationContext context, CqlLanguage language, Translator translator)
+ : base(context, language, translator)
+ {
+ }
+
+ public override Expression Translate(Expression expression)
+ {
+ /*
+ * fix up any order-by's
+ */
+ expression = OrderByRewriter.Rewrite(Language, expression);
+
+ expression = base.Translate(expression);
+ /*
+ * convert skip/take info into RowNumber pattern
+ */
+ expression = SkipToRowNumberRewriter.Rewrite(Language, expression);
+ /*
+ * fix up any order-by's we may have changed
+ */
+ expression = OrderByRewriter.Rewrite(Language, expression);
+
+ return expression;
+ }
+
+ public override string Format(Expression expression)
+ {
+ return CqlFormatter.Format(Context, expression, Language);
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/CqlTypeSystem.cs b/Connected.ServiceModel.Client.Data/CqlTypeSystem.cs
new file mode 100644
index 0000000..30d042b
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/CqlTypeSystem.cs
@@ -0,0 +1,190 @@
+using Connected.Expressions.Languages;
+using Connected.Expressions.TypeSystem;
+using Connected.Interop;
+using System.Data;
+using System.Globalization;
+using System.Reflection;
+using System.Text;
+
+namespace Connected.ServiceModel.Client.Data;
+internal sealed class CqlTypeSystem : QueryTypeSystem
+{
+ public static int StringDefaultSize => int.MaxValue;
+ public static int BinaryDefaultSize => int.MaxValue;
+
+ public override DataType Parse(string typeDeclaration)
+ {
+ string[]? args = null;
+ string typeName;
+ string? remainder = null;
+ var openParen = typeDeclaration.IndexOf('(');
+
+ if (openParen >= 0)
+ {
+ typeName = typeDeclaration[..openParen].Trim();
+
+ var closeParen = typeDeclaration.IndexOf(')', openParen);
+
+ if (closeParen < openParen)
+ closeParen = typeDeclaration.Length;
+
+ var argstr = typeDeclaration[(openParen + 1)..closeParen];
+
+ args = argstr.Split(',');
+
+ remainder = typeDeclaration[(closeParen + 1)..];
+ }
+ else
+ {
+ var space = typeDeclaration.IndexOf(' ');
+
+ if (space >= 0)
+ {
+ typeName = typeDeclaration[..space];
+ remainder = typeDeclaration[(space + 1)..].Trim();
+ }
+ else
+ typeName = typeDeclaration;
+ }
+
+ var isNotNull = (remainder is not null) && remainder.ToUpper().Contains("NOT NULL");
+
+ return ResolveDataType(typeName, args, isNotNull);
+ }
+
+ public DataType ResolveDataType(string typeName, string[] args, bool isNotNull)
+ {
+ if (string.Equals(typeName, "rowversion", StringComparison.OrdinalIgnoreCase))
+ typeName = "Timestamp";
+
+ if (string.Equals(typeName, "numeric", StringComparison.OrdinalIgnoreCase))
+ typeName = "Decimal";
+
+ if (string.Equals(typeName, "sql_variant", StringComparison.OrdinalIgnoreCase))
+ typeName = "Variant";
+
+ var dbType = ResolveSqlType(typeName);
+ var length = 0;
+ short precision = 0;
+ short scale = 0;
+
+ switch (dbType)
+ {
+ case SqlDbType.Binary:
+ case SqlDbType.Char:
+ case SqlDbType.Image:
+ case SqlDbType.NChar:
+ case SqlDbType.NVarChar:
+ case SqlDbType.VarBinary:
+ case SqlDbType.VarChar:
+ length = args is null || !args.Any() ? 32 : string.Equals(args[0], "max", StringComparison.OrdinalIgnoreCase) ? int.MaxValue : int.Parse(args[0]);
+ break;
+ case SqlDbType.Money:
+ precision = args is null || !args.Any() ? (short)29 : short.Parse(args[0], NumberFormatInfo.InvariantInfo);
+ scale = args is null || args.Length < 2 ? (short)4 : short.Parse(args[1], NumberFormatInfo.InvariantInfo);
+ break;
+ case SqlDbType.Decimal:
+ precision = args is null || !args.Any() ? (short)29 : short.Parse(args[0], NumberFormatInfo.InvariantInfo);
+ scale = args is null || args.Length < 2 ? (short)0 : short.Parse(args[1], NumberFormatInfo.InvariantInfo);
+ break;
+ case SqlDbType.Float:
+ case SqlDbType.Real:
+ precision = args is null || !args.Any() ? (short)29 : short.Parse(args[0], NumberFormatInfo.InvariantInfo);
+ break;
+ }
+
+ return NewType(dbType, isNotNull, length, precision, scale);
+ }
+
+ private static DataType NewType(SqlDbType type, bool isNotNull, int length, short precision, short scale)
+ {
+ return new CqlDataType(type, isNotNull, length, precision, scale);
+ }
+
+ public static SqlDbType ResolveSqlType(string typeName)
+ {
+ return (SqlDbType)Enum.Parse(typeof(SqlDbType), typeName, true);
+ }
+
+ public override DataType ResolveColumnType(Type type)
+ {
+ var isNotNull = type.GetTypeInfo().IsValueType && !Nullables.IsNullableType(type);
+ type = Nullables.GetNonNullableType(type);
+
+ switch (Interop.TypeSystem.GetTypeCode(type))
+ {
+ case TypeCode.Boolean:
+ return NewType(SqlDbType.Bit, isNotNull, 0, 0, 0);
+ case TypeCode.SByte:
+ case TypeCode.Byte:
+ return NewType(SqlDbType.TinyInt, isNotNull, 0, 0, 0);
+ case TypeCode.Int16:
+ case TypeCode.UInt16:
+ return NewType(SqlDbType.SmallInt, isNotNull, 0, 0, 0);
+ case TypeCode.Int32:
+ case TypeCode.UInt32:
+ return NewType(SqlDbType.Int, isNotNull, 0, 0, 0);
+ case TypeCode.Int64:
+ case TypeCode.UInt64:
+ return NewType(SqlDbType.BigInt, isNotNull, 0, 0, 0);
+ case TypeCode.Single:
+ case TypeCode.Double:
+ return NewType(SqlDbType.Float, isNotNull, 0, 0, 0);
+ case TypeCode.String:
+ return NewType(SqlDbType.NVarChar, isNotNull, StringDefaultSize, 0, 0);
+ case TypeCode.Char:
+ return NewType(SqlDbType.NChar, isNotNull, 1, 0, 0);
+ case TypeCode.DateTime:
+ return NewType(SqlDbType.DateTime, isNotNull, 0, 0, 0);
+ case TypeCode.Decimal:
+ return NewType(SqlDbType.Decimal, isNotNull, 0, 29, 4);
+ default:
+ if (type == typeof(byte[]))
+ return NewType(SqlDbType.VarBinary, isNotNull, BinaryDefaultSize, 0, 0);
+ else if (type == typeof(Guid))
+ return NewType(SqlDbType.UniqueIdentifier, isNotNull, 0, 0, 0);
+ else if (type == typeof(DateTimeOffset))
+ return NewType(SqlDbType.DateTimeOffset, isNotNull, 0, 0, 0);
+ else if (type == typeof(TimeSpan))
+ return NewType(SqlDbType.Time, isNotNull, 0, 0, 0);
+ else if (type.GetTypeInfo().IsEnum)
+ return NewType(SqlDbType.Int, isNotNull, 0, 0, 0);
+ else
+ throw new NotSupportedException(nameof(ResolveColumnType));
+ }
+ }
+
+ public static bool IsVariableLength(SqlDbType dbType)
+ {
+ return dbType switch
+ {
+ SqlDbType.Image or SqlDbType.NText or SqlDbType.NVarChar or SqlDbType.Text or SqlDbType.VarBinary or SqlDbType.VarChar or SqlDbType.Xml => true,
+ _ => false,
+ };
+ }
+
+ public override string Format(DataType type, bool suppressSize)
+ {
+ var sqlType = (CqlDataType)type;
+ var sb = new StringBuilder();
+
+ sb.Append(sqlType.DbType.ToString().ToUpper());
+
+ if (sqlType.Length > 0 && !suppressSize)
+ {
+ if (sqlType.Length == int.MaxValue)
+ sb.Append("(max)");
+ else
+ sb.AppendFormat(NumberFormatInfo.InvariantInfo, "({0})", sqlType.Length);
+ }
+ else if (sqlType.Precision != 0)
+ {
+ if (sqlType.Scale != 0)
+ sb.AppendFormat(NumberFormatInfo.InvariantInfo, "({0},{1})", sqlType.Precision, sqlType.Scale);
+ else
+ sb.AppendFormat(NumberFormatInfo.InvariantInfo, "({0})", sqlType.Precision);
+ }
+
+ return sb.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Connected.ServiceModel.Client.Data/DataStartup.cs b/Connected.ServiceModel.Client.Data/DataStartup.cs
new file mode 100644
index 0000000..19c6725
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/DataStartup.cs
@@ -0,0 +1,14 @@
+using Connected.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: MicroService(MicroServiceType.Provider)]
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal sealed class DataStartup : Startup
+{
+ protected override void OnConfigureServices(IServiceCollection services)
+ {
+ services.AddScoped();
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/RemoteTableService.cs b/Connected.ServiceModel.Client.Data/RemoteTableService.cs
new file mode 100644
index 0000000..fe46b88
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/RemoteTableService.cs
@@ -0,0 +1,13 @@
+using Connected.Net;
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal sealed class RemoteTableService
+{
+ public RemoteTableService(IHttpService http)
+ {
+ Http = http;
+ }
+
+ private IHttpService Http { get; }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableCommand.cs b/Connected.ServiceModel.Client.Data/TableCommand.cs
new file mode 100644
index 0000000..a39056f
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableCommand.cs
@@ -0,0 +1,68 @@
+using Connected.Data.Storage;
+using Connected.Entities.Storage;
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal abstract class TableCommand : IStorageCommand
+{
+ protected TableCommand(IStorageOperation operation, IStorageConnection connection)
+ {
+ Connection = connection;
+ Operation = operation;
+ }
+
+ protected bool IsDisposed { get; private set; }
+ public IStorageOperation Operation { get; }
+ public IStorageConnection? Connection { get; protected set; }
+
+ protected virtual async ValueTask DisposeAsync(bool disposing)
+ {
+ if (!IsDisposed)
+ {
+ if (disposing)
+ {
+ Connection = null;
+
+ await OnDisposingAsync();
+ }
+
+ IsDisposed = true;
+ }
+ }
+
+ protected virtual async ValueTask OnDisposingAsync()
+ {
+ await ValueTask.CompletedTask;
+ }
+
+ protected virtual void OnDisposing()
+ {
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await DisposeAsync(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!IsDisposed)
+ {
+ if (disposing)
+ {
+ Connection = null;
+
+ OnDisposing();
+ }
+
+ IsDisposed = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableConnection.cs b/Connected.ServiceModel.Client.Data/TableConnection.cs
new file mode 100644
index 0000000..02f76a6
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableConnection.cs
@@ -0,0 +1,77 @@
+using System.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+///
+/// Represents the REST connection to the TomPIT.connected Table Service.
+///
+internal sealed class TableConnection : IDbConnection
+{
+ ///
+ /// The connection string (URL) used when performing the requests.
+ ///
+ public string ConnectionString { get; set; }
+ ///
+ /// The request timeout in seconds.
+ ///
+ public int ConnectionTimeout => 120;
+ ///
+ /// The database (partition) on the server to be used.
+ ///
+ public string Database { get; private set; }
+ ///
+ /// The connection state. It's always open since it's a stateless connection.
+ ///
+ public ConnectionState State => ConnectionState.Open;
+ ///
+ /// Starts the new storage transaction.
+ ///
+ /// A new .
+ public IDbTransaction BeginTransaction()
+ {
+ return new TableTransaction(this);
+ }
+ ///
+ /// Starts the new storage transaction.
+ ///
+ /// The isolation level. This property is ignored on this connection since
+ /// it's always .
+ /// A new .
+ public IDbTransaction BeginTransaction(IsolationLevel il)
+ {
+ return BeginTransaction();
+ }
+ ///
+ /// The database is actually the Partition on the remote service. Calling this method
+ /// changes the partition to be used on the remote.
+ ///
+ /// The partition name. It is created if it doesn't exist.
+ public void ChangeDatabase(string databaseName)
+ {
+ Database = databaseName;
+ }
+ ///
+ /// This method completes the connection by calling
+ /// if has not been called.
+ ///
+ public void Close()
+ {
+
+ }
+
+ public IDbCommand CreateCommand()
+ {
+ throw new NotImplementedException();
+ }
+
+ public void Dispose()
+ {
+ throw new NotImplementedException();
+ }
+ ///
+ /// This method doesn't do enything since the connection is stateless any is theoretically
+ /// always in open state.
+ ///
+ public void Open()
+ {
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableDataConnection.cs b/Connected.ServiceModel.Client.Data/TableDataConnection.cs
new file mode 100644
index 0000000..238c2ad
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableDataConnection.cs
@@ -0,0 +1,59 @@
+using Connected.Annotations;
+using Connected.Data.Storage;
+using Microsoft.Data.SqlClient;
+using System.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+
+[ServiceRegistration(ServiceRegistrationMode.Auto, ServiceRegistrationScope.Transient)]
+internal sealed class TableDataConnection : DatabaseConnection
+{
+ public TableDataConnection(ICancellationContext context) : base(context)
+ {
+ }
+
+ protected override void SetupParameters(IStorageCommand command, IDbCommand cmd)
+ {
+ if (cmd.Parameters.Count > 0)
+ {
+ foreach (SqlParameter i in cmd.Parameters)
+ i.Value = DBNull.Value;
+
+ return;
+ }
+
+ if (command.Operation.Parameters is null)
+ return;
+
+ foreach (var i in command.Operation.Parameters)
+ {
+ cmd.Parameters.Add(new SqlParameter
+ {
+ ParameterName = i.Name,
+ DbType = i.Type,
+ Direction = i.Direction
+ });
+ }
+ }
+
+ protected override object GetParameterValue(IDbCommand command, string parameterName)
+ {
+ if (command is SqlCommand cmd)
+ return cmd.Parameters[parameterName].Value;
+
+ return null;
+ }
+
+ protected override void SetParameterValue(IDbCommand command, string parameterName, object value)
+ {
+ if (command is SqlCommand cmd)
+ cmd.Parameters[parameterName].Value = value;
+ }
+
+ protected override async Task OnCreateConnection()
+ {
+ await Task.CompletedTask;
+
+ return new SqlConnection(ConnectionString);
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableReader.cs b/Connected.ServiceModel.Client.Data/TableReader.cs
new file mode 100644
index 0000000..e395ca6
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableReader.cs
@@ -0,0 +1,74 @@
+using System.Collections.Immutable;
+using System.Data;
+using Connected.Data.Storage;
+using Connected.Entities.Storage;
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal sealed class TableReader : TableCommand, IStorageReader
+{
+ public TableReader(IStorageOperation operation, IStorageConnection connection)
+ : base(operation, connection)
+ {
+ }
+
+ public async Task?> Query()
+ {
+ if (Connection is null)
+ return default;
+
+ try
+ {
+ var result = await Connection.Query(this);
+
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ await Connection.Commit();
+
+ return result;
+ }
+ finally
+ {
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ {
+ await Connection.Close();
+ await Connection.DisposeAsync();
+
+ Connection = null;
+ }
+ }
+ }
+
+ public async Task Select()
+ {
+ try
+ {
+ if (Connection is null)
+ return default;
+
+ var result = await Connection.Select(this);
+
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ await Connection.Commit();
+
+ return result;
+ }
+ finally
+ {
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ {
+ await Connection.Close();
+ await Connection.DisposeAsync();
+
+ Connection = null;
+ }
+ }
+ }
+
+ public async Task OpenReader()
+ {
+ if (Connection is null)
+ return default;
+
+ return await Connection.OpenReader(this);
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableSchemaMiddleware.cs b/Connected.ServiceModel.Client.Data/TableSchemaMiddleware.cs
new file mode 100644
index 0000000..04ee29b
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableSchemaMiddleware.cs
@@ -0,0 +1,57 @@
+using Connected.Annotations;
+using Connected.Data.Schema;
+using Connected.Entities.Storage;
+using Connected.Middleware;
+using Connected.ServiceModel.Client.Net;
+using Connected.ServiceModel.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+
+[Priority(2)]
+internal sealed class TableSchemaMiddleware : MiddlewareComponent, ISchemaMiddleware
+{
+ public TableSchemaMiddleware(IMiddlewareService middleware, IStorageProvider storage, IConnectedServer connected)
+ {
+ Storage = storage;
+ Connected = connected;
+ }
+ private IStorageProvider Storage { get; }
+ public IConnectedServer Connected { get; }
+ public Type ConnectionType => typeof(TableDataConnection);
+ public string DefaultConnectionString { get; private set; } = default!;
+
+ protected override async Task OnInitialize()
+ {
+ DefaultConnectionString = await Connected.SelectUrl(new ConnectedServerUrlArgs { Kind = ConnectedUrlKind.TableStorage });
+ }
+
+ public async Task IsEntitySupported(Type entityType)
+ {
+ await Task.CompletedTask;
+ /*
+ * This middleware supports all ITableEntity<> entities.
+ */
+ return entityType.IsAssignableTo(typeof(ITableEntity<,>));
+ }
+
+ public async Task Synchronize(Type entity, ISchema schema)
+ {
+ await Synchronize(schema, DefaultConnectionString);
+ }
+
+ private async Task Synchronize(ISchema schema, string connectionString)
+ {
+ //var args = new SchemaExecutionContext(Storage, schema, connectionString);
+ ///*
+ // * Sinchronize schema object first.
+ // */
+ //await new SchemaSynchronize().Execute(args);
+ ///*
+ // * Only tables are supported
+ // */
+ //if (string.IsNullOrWhiteSpace(schema.Type) || string.Equals(schema.Type, SchemaAttribute.SchemaTypeTable, StringComparison.OrdinalIgnoreCase))
+ // await new TableSynchronize().Execute(args);
+
+ await Task.CompletedTask;
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableStorageProvider.cs b/Connected.ServiceModel.Client.Data/TableStorageProvider.cs
new file mode 100644
index 0000000..3dee0af
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableStorageProvider.cs
@@ -0,0 +1,114 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using Connected.Annotations;
+using Connected.Data;
+using Connected.Data.Storage;
+using Connected.Entities;
+using Connected.Entities.Storage;
+using Connected.Expressions;
+using Connected.Expressions.Evaluation;
+using Connected.Expressions.Query;
+using Connected.Expressions.Translation;
+using Connected.ServiceModel.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+
+[Priority(2)]
+internal class TableStorageProvider : QueryProvider, IStorageExecutor, IStorageMiddleware
+{
+ public TableStorageProvider(IStorageProvider storage)
+ {
+ Storage = storage;
+ }
+
+ private IStorageProvider Storage { get; }
+
+ protected override object? OnExecute(Expression expression)
+ {
+ return CreateExecutionPlan(expression).Compile()(this);
+ }
+
+ private static Expression> CreateExecutionPlan(Expression expression)
+ {
+ var lambda = expression as LambdaExpression;
+
+ if (lambda is not null)
+ expression = lambda.Body;
+
+ var context = new ExpressionCompilationContext(new CqlLanguage());
+ var translator = new Translator(context);
+ var translation = translator.Translate(expression);
+ var parameters = lambda?.Parameters;
+ var provider = Resolve(expression, parameters, typeof(IStorage<>));
+
+ if (provider is null)
+ {
+ var rootQueryable = Resolve(expression, parameters, typeof(IQueryable));
+
+ provider = Expression.Property(rootQueryable, typeof(IQueryable).GetTypeInfo().GetDeclaredProperty(nameof(IQueryable.Provider)));
+ }
+
+ return ExecutionBuilder.Build(context, new CqlLinguist(context, CqlLanguage.Default, translator), translation, provider);
+ }
+
+ ///
+ /// Find the expression of the specified type, either in the specified expression or parameters.
+ ///
+ private static Expression Resolve(Expression expression, IList parameters, Type type)
+ {
+ if (parameters is not null)
+ {
+ var found = parameters.FirstOrDefault(p => type.IsAssignableFrom(p.Type));
+
+ if (found is not null)
+ return found;
+ }
+
+ return SubtreeResolver.Resolve(expression, type);
+ }
+
+ public IEnumerable Execute(IStorageOperation operation)
+ where TResult : IEntity
+ {
+ var result = Storage.Open().Query(new StorageContextArgs(operation));
+
+ if (result.IsCompleted)
+ return result.Result;
+
+ var r = result.GetAwaiter().GetResult();
+
+ return r;
+ }
+
+ public bool SupportsEntity(Type entityType)
+ {
+ return entityType.IsAssignableTo(typeof(ITableEntity<,>));
+ }
+
+ public IStorageOperation CreateOperation(TEntity entity)
+ where TEntity : IEntity
+ {
+ //var builder = new AggregatedCommandBuilder();
+
+ //if (builder.Build(entity) is not StorageOperation operation)
+ // throw new NullReferenceException(nameof(StorageOperation));
+
+ //return operation;
+ return null;
+ }
+
+ public IStorageReader OpenReader(IStorageOperation operation, IStorageConnection connection)
+ {
+ return new TableReader(operation, connection);
+ }
+
+ public IStorageWriter OpenWriter(IStorageOperation operation, IStorageConnection connection)
+ {
+ return new TableWriter(operation, connection);
+ }
+
+ public async Task Initialize()
+ {
+ await Task.CompletedTask;
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableTransaction.cs b/Connected.ServiceModel.Client.Data/TableTransaction.cs
new file mode 100644
index 0000000..8bec83b
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableTransaction.cs
@@ -0,0 +1,59 @@
+using System.Data;
+
+namespace Connected.ServiceModel.Client.Data;
+///
+/// Defines the transaction on the remote table storage.
+///
+///
+/// Connection write operations using this transaction are queued until
+/// the is called.
+///
+internal class TableTransaction : IDbTransaction
+{
+ ///
+ /// Creates new object.
+ ///
+ /// The storage connection associated with the transaction.
+ public TableTransaction(IDbConnection connection)
+ {
+ Connection = connection;
+ }
+ ///
+ /// The storage connection that created the transaction.
+ ///
+ ///
+ /// Multiple connections can use the transaction, but only one is directly associated with the transaction.
+ ///
+ public IDbConnection? Connection { get; }
+ ///
+ /// The isolation level of the transaction. It's always on this transaction.
+ ///
+ public IsolationLevel IsolationLevel { get; } = IsolationLevel.Snapshot;
+ ///
+ /// Commits this transaction. This call actually performs a REST call to the remote service.
+ ///
+ ///
+ public void Commit()
+ {
+ throw new NotImplementedException();
+ }
+ ///
+ /// Disposes the object.
+ ///
+ public void Dispose()
+ {
+ /*
+ * Nothing really to do here.
+ */
+ }
+ ///
+ /// Rolls back any changes made in this transaction.
+ ///
+ public void Rollback()
+ {
+ /*
+ * This call doesm't have to do enything since we weren't perform
+ * any calls to the remote service. We just discard the operations.
+ */
+ }
+}
diff --git a/Connected.ServiceModel.Client.Data/TableWriter.cs b/Connected.ServiceModel.Client.Data/TableWriter.cs
new file mode 100644
index 0000000..3cb8cee
--- /dev/null
+++ b/Connected.ServiceModel.Client.Data/TableWriter.cs
@@ -0,0 +1,38 @@
+using Connected.Data.Storage;
+using Connected.Entities.Storage;
+
+namespace Connected.ServiceModel.Client.Data;
+
+internal sealed class TableWriter : TableCommand, IStorageWriter
+{
+ public TableWriter(IStorageOperation operation, IStorageConnection connection)
+ : base(operation, connection)
+ {
+ }
+
+ public async Task Execute()
+ {
+ if (Connection is null)
+ return -1;
+
+ try
+ {
+ var recordsAffected = await Connection.Execute(this);
+
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ await Connection.Commit();
+
+ return recordsAffected;
+ }
+ finally
+ {
+ if (Connection.Behavior == StorageConnectionMode.Isolated)
+ {
+ await Connection.Close();
+ await Connection.DisposeAsync();
+
+ Connection = null;
+ }
+ }
+ }
+}
diff --git a/Connected.ServiceModel.Client.Storage/Bootstrapper.cs b/Connected.ServiceModel.Client.Storage/Bootstrapper.cs
new file mode 100644
index 0000000..c9f4f1a
--- /dev/null
+++ b/Connected.ServiceModel.Client.Storage/Bootstrapper.cs
@@ -0,0 +1,15 @@
+using Connected.Annotations;
+using Connected.ServiceModel.Storage;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: MicroService(MicroServiceType.Provider)]
+
+namespace Connected.ServiceModel.Client.Storage;
+
+internal class Bootstrapper : Startup
+{
+ protected override void OnConfigureServices(IServiceCollection services)
+ {
+ services.AddScoped(typeof(IStorageService), typeof(StorageService));
+ }
+}
diff --git a/Connected.ServiceModel.Client.Storage/Connected.ServiceModel.Client.Storage.csproj b/Connected.ServiceModel.Client.Storage/Connected.ServiceModel.Client.Storage.csproj
new file mode 100644
index 0000000..efbb5ce
--- /dev/null
+++ b/Connected.ServiceModel.Client.Storage/Connected.ServiceModel.Client.Storage.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/Connected.ServiceModel.Client.Storage/StorageOps.cs b/Connected.ServiceModel.Client.Storage/StorageOps.cs
new file mode 100644
index 0000000..ea6fefa
--- /dev/null
+++ b/Connected.ServiceModel.Client.Storage/StorageOps.cs
@@ -0,0 +1,28 @@
+using Connected.Net;
+using Connected.ServiceModel.Client.Net;
+using Connected.ServiceModel.Client.Subscription;
+using Connected.ServiceModel.Storage;
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Storage;
+
+internal sealed class DeleteDirectory : ServiceAction
+{
+ public DeleteDirectory(ISubscriptionService subscription, IHttpService http, IConnectedServer server, ICancellationContext cancel)
+ {
+ Subscription = subscription;
+ Http = http;
+ Server = server;
+ Cancel = cancel;
+ }
+
+ public ISubscriptionService Subscription { get; }
+ public IHttpService Http { get; }
+ public IConnectedServer Server { get; }
+ public ICancellationContext Cancel { get; }
+
+ protected override async Task OnInvoke()
+ {
+ await Http.Post(await Server.SelectUrl(new ConnectedServerUrlArgs { Kind = ConnectedUrlKind.FileSystem }), Arguments, Cancel.CancellationToken);
+ }
+}
diff --git a/Connected.ServiceModel.Client.Storage/StorageService.cs b/Connected.ServiceModel.Client.Storage/StorageService.cs
new file mode 100644
index 0000000..bf29f3b
--- /dev/null
+++ b/Connected.ServiceModel.Client.Storage/StorageService.cs
@@ -0,0 +1,57 @@
+using System.Collections.Immutable;
+using Connected.ServiceModel.Storage;
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Storage;
+
+internal class StorageService : Service, IStorageService
+{
+ public StorageService(IContext context) : base(context)
+ {
+ }
+
+ public async Task DeleteDirectory(DeleteDirectoryArgs args)
+ {
+ await Invoke(GetOperation(), args);
+ }
+
+ public Task DeleteFile(DeleteFileArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task InsertDirectory(InsertDirectoryArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task MoveFile(MoveFileArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task?> QueryDirectories(DirectoryArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task?> QueryFiles(DirectoryArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task SelectFile(FileArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateDirectory(UpdateDirectoryArgs args)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task UpdateFile(UpdateFileArgs args)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Connected.ServiceModel.Client/Bootstrapper.cs b/Connected.ServiceModel.Client/Bootstrapper.cs
new file mode 100644
index 0000000..2b9f35d
--- /dev/null
+++ b/Connected.ServiceModel.Client/Bootstrapper.cs
@@ -0,0 +1,16 @@
+using Connected.Annotations;
+using Connected.ServiceModel.Client.Subscription;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: MicroService(MicroServiceType.Provider)]
+
+namespace Connected.ServiceModel.Client.Net;
+
+internal class Bootstrapper : Startup
+{
+ protected override void OnConfigureServices(IServiceCollection services)
+ {
+ services.AddScoped(typeof(IConnectedServer), typeof(ConnectedServer));
+ services.AddScoped(typeof(ISubscriptionService), typeof(SubscriptionService));
+ }
+}
diff --git a/Connected.ServiceModel.Client/Connected.ServiceModel.Client.csproj b/Connected.ServiceModel.Client/Connected.ServiceModel.Client.csproj
new file mode 100644
index 0000000..a1df388
--- /dev/null
+++ b/Connected.ServiceModel.Client/Connected.ServiceModel.Client.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/Connected.ServiceModel.Client/Net/ConnectedServer.cs b/Connected.ServiceModel.Client/Net/ConnectedServer.cs
new file mode 100644
index 0000000..f0c1d36
--- /dev/null
+++ b/Connected.ServiceModel.Client/Net/ConnectedServer.cs
@@ -0,0 +1,15 @@
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Net;
+
+internal class ConnectedServer : Service, IConnectedServer
+{
+ public ConnectedServer(IContext context) : base(context)
+ {
+ }
+
+ public async Task SelectUrl(ConnectedServerUrlArgs args)
+ {
+ return await Invoke(GetOperation(), args);
+ }
+}
diff --git a/Connected.ServiceModel.Client/Net/ConnectedServerArgs.cs b/Connected.ServiceModel.Client/Net/ConnectedServerArgs.cs
new file mode 100644
index 0000000..eebafed
--- /dev/null
+++ b/Connected.ServiceModel.Client/Net/ConnectedServerArgs.cs
@@ -0,0 +1,13 @@
+namespace Connected.ServiceModel.Client.Net;
+
+public enum ConnectedUrlKind
+{
+ Root = 0,
+ FileSystem = 1,
+ TableStorage = 2
+}
+
+public sealed class ConnectedServerUrlArgs : Dto
+{
+ public ConnectedUrlKind Kind { get; set; } = ConnectedUrlKind.Root;
+}
diff --git a/Connected.ServiceModel.Client/Net/ConnectedServerOps.cs b/Connected.ServiceModel.Client/Net/ConnectedServerOps.cs
new file mode 100644
index 0000000..b971983
--- /dev/null
+++ b/Connected.ServiceModel.Client/Net/ConnectedServerOps.cs
@@ -0,0 +1,20 @@
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Net;
+
+internal sealed class SelectUrl : ServiceFunction
+{
+ private const string Root = "https://connected.tompit.com";
+ protected override async Task OnInvoke()
+ {
+ await Task.CompletedTask;
+
+ return Arguments.Kind switch
+ {
+ ConnectedUrlKind.Root => Root,
+ ConnectedUrlKind.FileSystem => $"{Root}/services/fileSystem",
+ ConnectedUrlKind.TableStorage => $"{Root}/services/tables",
+ _ => throw new NotImplementedException(),
+ };
+ }
+}
diff --git a/Connected.ServiceModel.Client/Net/IConnectedServer.cs b/Connected.ServiceModel.Client/Net/IConnectedServer.cs
new file mode 100644
index 0000000..28186d3
--- /dev/null
+++ b/Connected.ServiceModel.Client/Net/IConnectedServer.cs
@@ -0,0 +1,6 @@
+namespace Connected.ServiceModel.Client.Net;
+
+public interface IConnectedServer
+{
+ Task SelectUrl(ConnectedServerUrlArgs args);
+}
diff --git a/Connected.ServiceModel.Client/Subscription/ISubscription.cs b/Connected.ServiceModel.Client/Subscription/ISubscription.cs
new file mode 100644
index 0000000..0ede1f3
--- /dev/null
+++ b/Connected.ServiceModel.Client/Subscription/ISubscription.cs
@@ -0,0 +1,7 @@
+using Connected.Data;
+
+namespace Connected.ServiceModel.Client.Subscription;
+
+public interface ISubscription : IPrimaryKey
+{
+}
diff --git a/Connected.ServiceModel.Client/Subscription/ISubscriptionService.cs b/Connected.ServiceModel.Client/Subscription/ISubscriptionService.cs
new file mode 100644
index 0000000..49dcf57
--- /dev/null
+++ b/Connected.ServiceModel.Client/Subscription/ISubscriptionService.cs
@@ -0,0 +1,6 @@
+namespace Connected.ServiceModel.Client.Subscription;
+
+public interface ISubscriptionService
+{
+ Task Select();
+}
diff --git a/Connected.ServiceModel.Client/Subscription/Subscription.cs b/Connected.ServiceModel.Client/Subscription/Subscription.cs
new file mode 100644
index 0000000..9d46ed2
--- /dev/null
+++ b/Connected.ServiceModel.Client/Subscription/Subscription.cs
@@ -0,0 +1,9 @@
+using Connected.Entities;
+using Connected.Entities.Annotations;
+
+namespace Connected.ServiceModel.Client.Subscription;
+
+[Persistence(Persistence = ColumnPersistence.InMemory)]
+internal record Subscription : Entity, ISubscription
+{
+}
diff --git a/Connected.ServiceModel.Client/Subscription/SubscriptionOps.cs b/Connected.ServiceModel.Client/Subscription/SubscriptionOps.cs
new file mode 100644
index 0000000..56d55ba
--- /dev/null
+++ b/Connected.ServiceModel.Client/Subscription/SubscriptionOps.cs
@@ -0,0 +1,16 @@
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Subscription;
+
+internal sealed class SelectSubscription : ServiceFunction
+{
+ protected override async Task OnInvoke()
+ {
+ await Task.CompletedTask;
+
+ return new Subscription
+ {
+ Id = 1
+ };
+ }
+}
diff --git a/Connected.ServiceModel.Client/Subscription/SubscriptionService.cs b/Connected.ServiceModel.Client/Subscription/SubscriptionService.cs
new file mode 100644
index 0000000..1e734e1
--- /dev/null
+++ b/Connected.ServiceModel.Client/Subscription/SubscriptionService.cs
@@ -0,0 +1,15 @@
+using Connected.Services;
+
+namespace Connected.ServiceModel.Client.Subscription;
+
+internal class SubscriptionService : Service, ISubscriptionService
+{
+ public SubscriptionService(IContext context) : base(context)
+ {
+ }
+
+ public async Task Select()
+ {
+ return await Invoke(GetOperation(), Dto.Empty);
+ }
+}
diff --git a/Framerwork.ServiceModel.Client.sln b/Framerwork.ServiceModel.Client.sln
new file mode 100644
index 0000000..4321ae1
--- /dev/null
+++ b/Framerwork.ServiceModel.Client.sln
@@ -0,0 +1,69 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33027.239
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{CF4933B0-B3E2-49C9-8FB3-D3FA3E2DBA3B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel.Client", "Connected.ServiceModel.Client\Connected.ServiceModel.Client.csproj", "{679249A7-58C5-42D9-A764-4EBDC7DE7F9D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel.Client.Storage", "Connected.ServiceModel.Client.Storage\Connected.ServiceModel.Client.Storage.csproj", "{1F7F8DB9-3545-4B06-BAB8-3A15611E39EA}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Entities", "..\Framework\Connected.Entities\Connected.Entities.csproj", "{19B41DDB-EBD0-4B5F-86E2-2080DC660EE0}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Services", "..\Framework\Connected.Services\Connected.Services.csproj", "{9ADD797F-CBE8-402E-BF15-2EEF5C4D035D}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel", "..\Framework.ServiceModel\Connected.ServiceModel\Connected.ServiceModel.csproj", "{FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.ServiceModel.Client.Data", "Connected.ServiceModel.Client.Data\Connected.ServiceModel.Client.Data.csproj", "{D9F21D24-E114-4901-8245-720A1094EA24}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connected.Data", "..\Framework\Connected.Data\Connected.Data.csproj", "{F4764705-1857-4BF0-B4D8-1EE08A6958CA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {679249A7-58C5-42D9-A764-4EBDC7DE7F9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {679249A7-58C5-42D9-A764-4EBDC7DE7F9D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {679249A7-58C5-42D9-A764-4EBDC7DE7F9D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {679249A7-58C5-42D9-A764-4EBDC7DE7F9D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1F7F8DB9-3545-4B06-BAB8-3A15611E39EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1F7F8DB9-3545-4B06-BAB8-3A15611E39EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1F7F8DB9-3545-4B06-BAB8-3A15611E39EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1F7F8DB9-3545-4B06-BAB8-3A15611E39EA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19B41DDB-EBD0-4B5F-86E2-2080DC660EE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19B41DDB-EBD0-4B5F-86E2-2080DC660EE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19B41DDB-EBD0-4B5F-86E2-2080DC660EE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19B41DDB-EBD0-4B5F-86E2-2080DC660EE0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9ADD797F-CBE8-402E-BF15-2EEF5C4D035D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9ADD797F-CBE8-402E-BF15-2EEF5C4D035D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9ADD797F-CBE8-402E-BF15-2EEF5C4D035D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9ADD797F-CBE8-402E-BF15-2EEF5C4D035D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D9F21D24-E114-4901-8245-720A1094EA24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D9F21D24-E114-4901-8245-720A1094EA24}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D9F21D24-E114-4901-8245-720A1094EA24}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D9F21D24-E114-4901-8245-720A1094EA24}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F4764705-1857-4BF0-B4D8-1EE08A6958CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F4764705-1857-4BF0-B4D8-1EE08A6958CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F4764705-1857-4BF0-B4D8-1EE08A6958CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F4764705-1857-4BF0-B4D8-1EE08A6958CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {19B41DDB-EBD0-4B5F-86E2-2080DC660EE0} = {CF4933B0-B3E2-49C9-8FB3-D3FA3E2DBA3B}
+ {9ADD797F-CBE8-402E-BF15-2EEF5C4D035D} = {CF4933B0-B3E2-49C9-8FB3-D3FA3E2DBA3B}
+ {FBB12EF1-94A8-4EBF-BA5D-2287F3A1032A} = {CF4933B0-B3E2-49C9-8FB3-D3FA3E2DBA3B}
+ {F4764705-1857-4BF0-B4D8-1EE08A6958CA} = {CF4933B0-B3E2-49C9-8FB3-D3FA3E2DBA3B}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {62DF7178-0DD2-461C-999E-51A5E1BB8CAD}
+ EndGlobalSection
+EndGlobal