Beispiel #1
0
        /// <summary>
        /// Initializes the connection if it is not already initialized
        /// </summary>
        /// <returns>The connection string that was initialized</returns>
        private async Task <SqlConnection> GetConnectionAsync(CancellationToken cancellation = default)
        {
            using var _ = _instrumentation.Block("AdminRepo." + nameof(GetConnectionAsync));
            if (_conn == null)
            {
                _conn = new SqlConnection(_connectionString);
                await _conn.OpenAsync(cancellation);
            }

            if (_userInfo == null)
            {
                // Always call OnConnect SP as soon as you create the connection
                var externalUserId = _externalUserAccessor.GetUserId();
                var externalEmail  = _externalUserAccessor.GetUserEmail();
                var culture        = CultureInfo.CurrentUICulture.Name;
                var neutralCulture = CultureInfo.CurrentUICulture.IsNeutralCulture ? CultureInfo.CurrentUICulture.Name : CultureInfo.CurrentUICulture.Parent.Name;

                _userInfo = await OnConnect(externalUserId, externalEmail, culture, neutralCulture, cancellation);
            }

            // Since we opened the connection once, we need to explicitly enlist it in any ambient transaction
            // every time it is requested, otherwise commands will be executed outside the boundaries of the transaction
            _conn.EnlistInTransaction(transactionOverride: _transactionOverride);
            return(_conn);
        }
Beispiel #2
0
        public virtual async Task <ActionResult <GetResponse <TEntity> > > GetEntities([FromQuery] GetArguments args, CancellationToken cancellation)
        {
            return(await ControllerUtilities.InvokeActionImpl(async() =>
            {
                using var _ = _instrumentation.Block("Controller GetEntities");
                IDisposable block;

                // Calculate server time at the very beginning for consistency
                var serverTime = DateTimeOffset.UtcNow;

                // Retrieves the raw data from the database, unflattend, untrimmed
                var service = GetFactService();
                var(data, extras, isPartial, totalCount) = await service.GetEntities(args, cancellation);

                block = _instrumentation.Block("Flatten and trim");

                // Flatten and Trim
                var relatedEntities = FlattenAndTrim(data, cancellation);

                block.Dispose();
                block = _instrumentation.Block("Transform Extras");

                var transformedExtras = TransformExtras(extras, cancellation);

                block.Dispose();

                // Prepare the result in a response object
                var result = new GetResponse <TEntity>
                {
                    Skip = args.Skip,
                    Top = data.Count,
                    OrderBy = args.OrderBy,
                    TotalCount = totalCount,
                    IsPartial = isPartial,
                    Result = data,
                    RelatedEntities = relatedEntities,
                    CollectionName = ControllerUtilities.GetCollectionName(typeof(TEntity)),
                    Extras = TransformExtras(extras, cancellation),
                    ServerTime = serverTime
                };

                return Ok(result);
            }, _logger));
        }
Beispiel #3
0
        public async Task <ServerNotificationSummary> Recap(CancellationToken cancellation)
        {
            IDisposable block;

            using var _ = _instrumentation.Block("Recap");

            block = _instrumentation.Block("Get User Info Async");

            var serverTime = DateTimeOffset.UtcNow;
            var userInfo   = await _repo.GetUserInfoAsync(cancellation);

            block.Dispose();


            var userIdSingleton = new List <int> {
                userInfo.UserId.Value
            };
            var info = (await _repo.InboxCounts__Load(userIdSingleton, cancellation)).FirstOrDefault();

            var tenantId = _tenantIdAccessor.GetTenantId();

            return(new ServerNotificationSummary
            {
                Inbox = new InboxNotification
                {
                    Count = info?.Count ?? 0,
                    UnknownCount = info?.UnknownCount ?? 0,
                    UpdateInboxList = true,
                    ServerTime = serverTime,
                    TenantId = tenantId,
                },
                Notifications = new NotificationsNotification
                {
                    // TODO
                    Count = 0,
                    UnknownCount = 0,
                    ServerTime = serverTime,
                    TenantId = tenantId,
                },
            });
        }
Beispiel #4
0
        /// <summary>
        /// Inspects the data and mapping and loads all related entities from the API, that are referenced by custom use keys like Code and Name.
        /// The purpose is to use the Ids of these entities in the constructed Entity objects that are being imported
        /// </summary>
        private async Task <RelatedEntities> LoadRelatedEntities(IEnumerable <string[]> dataWithoutHeader, MappingInfo mapping, ImportErrors errors)
        {
            if (errors is null)
            {
                throw new ArgumentNullException(nameof(errors));
            }

            // Each set of foreign keys will result in an API query to retrieve the corresponding Ids for FK hydration
            // So we group foreign keys by the target type, the target definition Id and the key property (e.g. Code or Name)
            var queryInfos = new List <(Type navType, int?navDefId, PropertyMetadata keyPropMeta, HashSet <object> keysSet)>();

            foreach (var g in mapping.GetForeignKeys().Where(p => p.NotUsingIdAsKey)
                     .GroupBy(fk => (fk.TargetType, fk.TargetDefId, fk.KeyPropertyMetadata)))
            {
                var(navType, navDefId, keyPropMetadata) = g.Key;
                HashSet <object> keysSet = new HashSet <object>();

                foreach (var fkMapping in g)
                {
                    int rowNumber = 2;

                    foreach (var row in dataWithoutHeader)
                    {
                        string stringKey = row[fkMapping.Index];
                        if (string.IsNullOrEmpty(stringKey))
                        {
                            continue;
                        }

                        switch (fkMapping.KeyType)
                        {
                        case KeyType.String:
                            keysSet.Add(stringKey);

                            break;

                        case KeyType.Int:
                            if (int.TryParse(stringKey, out int intKey))
                            {
                                keysSet.Add(intKey);
                            }
                            else if (!errors.AddImportError(rowNumber, fkMapping.ColumnNumber, _localizer[$"Error_TheValue0IsNotAValidInteger", stringKey]))
                            {
                                // This means the validation errors are at maximum capacity, pointless to keep going.
                                return(null);
                            }

                            break;

                        default:
                            throw new InvalidOperationException("Bug: Only int and string IDs are supported");
                        }

                        rowNumber++;
                    }
                }

                if (keysSet.Any())
                {
                    // Actual API calls are delayed till the end in case there are any errors
                    queryInfos.Add((navType, navDefId, keyPropMetadata, keysSet));
                }
            }

            if (!errors.IsValid)
            {
                return(null);
            }
            using var _1 = _instrumentation.Block("Loading Related Entities");
            using var _2 = _instrumentation.Disable();

            var result = new RelatedEntities();

            if (queryInfos.Any())
            {
                // Load all related entities in parallel
                // If they're the same type we load them in sequence because it will be the same controller instance and it might cause issues
                await Task.WhenAll(queryInfos.GroupBy(e => e.navType).Select(async g =>
                {
                    var navType = g.Key;

                    foreach (var queryInfo in g)
                    {
                        var(_, navDefId, keyPropMeta, keysSet) = queryInfo;  // Deconstruct the queryInfo

                        var service     = _sp.FactWithIdServiceByEntityType(navType.Name, navDefId);
                        var keyPropDesc = keyPropMeta.Descriptor;
                        var keyPropName = keyPropDesc.Name;
                        var args        = new SelectExpandArguments {
                            Select = keyPropName
                        };

                        var(data, _) = await service.GetByPropertyValues(keyPropName, keysSet, args, cancellation: default);

                        var grouped = data.GroupBy(e => keyPropDesc.GetValue(e)).ToDictionary(g => g.Key, g => (IEnumerable <EntityWithKey>)g);
                        result.TryAdd((navType, navDefId, keyPropName), grouped);
                    }

                    // The code above might override the definition Id of the service calling it, here we fix that
                    _sp.FactWithIdServiceByEntityType(navType.Name, mapping.MetadataForSave.DefinitionId);
                }));
            }

            return(result);
        }