From e6a3794badeb73e15956be1300bb99d02e79d0d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Ko=C5=BEelj?= Date: Fri, 2 Dec 2022 15:09:39 +0100 Subject: [PATCH] Initial commit --- .../Connected.ServiceModel.Client.Data.csproj | 17 + .../CqlDataType.cs | 17 + .../CqlFormatter.cs | 771 ++++++++++++++++++ .../CqlLanguage.cs | 52 ++ .../CqlLinguist.cs | 41 + .../CqlTypeSystem.cs | 190 +++++ .../DataStartup.cs | 14 + .../RemoteTableService.cs | 13 + .../TableCommand.cs | 68 ++ .../TableConnection.cs | 77 ++ .../TableDataConnection.cs | 59 ++ .../TableReader.cs | 74 ++ .../TableSchemaMiddleware.cs | 57 ++ .../TableStorageProvider.cs | 114 +++ .../TableTransaction.cs | 59 ++ .../TableWriter.cs | 38 + .../Bootstrapper.cs | 15 + ...nnected.ServiceModel.Client.Storage.csproj | 16 + .../StorageOps.cs | 28 + .../StorageService.cs | 57 ++ Connected.ServiceModel.Client/Bootstrapper.cs | 16 + .../Connected.ServiceModel.Client.csproj | 14 + .../Net/ConnectedServer.cs | 15 + .../Net/ConnectedServerArgs.cs | 13 + .../Net/ConnectedServerOps.cs | 20 + .../Net/IConnectedServer.cs | 6 + .../Subscription/ISubscription.cs | 7 + .../Subscription/ISubscriptionService.cs | 6 + .../Subscription/Subscription.cs | 9 + .../Subscription/SubscriptionOps.cs | 16 + .../Subscription/SubscriptionService.cs | 15 + Framerwork.ServiceModel.Client.sln | 69 ++ 32 files changed, 1983 insertions(+) create mode 100644 Connected.ServiceModel.Client.Data/Connected.ServiceModel.Client.Data.csproj create mode 100644 Connected.ServiceModel.Client.Data/CqlDataType.cs create mode 100644 Connected.ServiceModel.Client.Data/CqlFormatter.cs create mode 100644 Connected.ServiceModel.Client.Data/CqlLanguage.cs create mode 100644 Connected.ServiceModel.Client.Data/CqlLinguist.cs create mode 100644 Connected.ServiceModel.Client.Data/CqlTypeSystem.cs create mode 100644 Connected.ServiceModel.Client.Data/DataStartup.cs create mode 100644 Connected.ServiceModel.Client.Data/RemoteTableService.cs create mode 100644 Connected.ServiceModel.Client.Data/TableCommand.cs create mode 100644 Connected.ServiceModel.Client.Data/TableConnection.cs create mode 100644 Connected.ServiceModel.Client.Data/TableDataConnection.cs create mode 100644 Connected.ServiceModel.Client.Data/TableReader.cs create mode 100644 Connected.ServiceModel.Client.Data/TableSchemaMiddleware.cs create mode 100644 Connected.ServiceModel.Client.Data/TableStorageProvider.cs create mode 100644 Connected.ServiceModel.Client.Data/TableTransaction.cs create mode 100644 Connected.ServiceModel.Client.Data/TableWriter.cs create mode 100644 Connected.ServiceModel.Client.Storage/Bootstrapper.cs create mode 100644 Connected.ServiceModel.Client.Storage/Connected.ServiceModel.Client.Storage.csproj create mode 100644 Connected.ServiceModel.Client.Storage/StorageOps.cs create mode 100644 Connected.ServiceModel.Client.Storage/StorageService.cs create mode 100644 Connected.ServiceModel.Client/Bootstrapper.cs create mode 100644 Connected.ServiceModel.Client/Connected.ServiceModel.Client.csproj create mode 100644 Connected.ServiceModel.Client/Net/ConnectedServer.cs create mode 100644 Connected.ServiceModel.Client/Net/ConnectedServerArgs.cs create mode 100644 Connected.ServiceModel.Client/Net/ConnectedServerOps.cs create mode 100644 Connected.ServiceModel.Client/Net/IConnectedServer.cs create mode 100644 Connected.ServiceModel.Client/Subscription/ISubscription.cs create mode 100644 Connected.ServiceModel.Client/Subscription/ISubscriptionService.cs create mode 100644 Connected.ServiceModel.Client/Subscription/Subscription.cs create mode 100644 Connected.ServiceModel.Client/Subscription/SubscriptionOps.cs create mode 100644 Connected.ServiceModel.Client/Subscription/SubscriptionService.cs create mode 100644 Framerwork.ServiceModel.Client.sln 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