/// <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); }
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)); }
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, }, }); }
/// <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); }