Example #1
0
        public async Task <ActionResult <EntitiesResponse <Document> > > Assign([FromBody] List <int> ids, [FromQuery] AssignArguments args)
        {
            return(await ControllerUtilities.InvokeActionImpl(async() =>
            {
                // Parse parameters
                var selectExp = SelectExpression.Parse(args.Select);
                var expandExp = ExpandExpression.Parse(args.Expand);
                var idsArray = ids.ToArray();

                // TODO: Check user permissions
                // await CheckActionPermissions("IsActive", idsArray);

                // Execute and return
                using var trx = ControllerUtilities.CreateTransaction();
                // TODO: Validate assign

                await _repo.Documents__Assign(ids, args.AssigneeId, args.Comment);

                if (args.ReturnEntities ?? false)
                {
                    var response = await GetByIdListAsync(idsArray, expandExp, selectExp);

                    trx.Complete();
                    return Ok(response);
                }
                else
                {
                    trx.Complete();
                    return Ok();
                }
            }
                                                              , _logger));
        }
        private async Task <ActionResult <EntitiesResponse <ResponsibilityCenter> > > Activate([FromBody] List <int> ids, bool returnEntities, string expand, bool isActive)
        {
            // Parse parameters
            var expandExp = ExpandExpression.Parse(expand);
            var idsArray  = ids.ToArray();

            // Check user permissions
            await CheckActionPermissions("IsActive", idsArray);

            // Execute and return
            using var trx = ControllerUtilities.CreateTransaction();
            await _repo.ResponsibilityCenters__Activate(ids, isActive);

            if (returnEntities)
            {
                var response = await GetByIdListAsync(idsArray, expandExp);

                trx.Complete();
                return(Ok(response));
            }
            else
            {
                trx.Complete();
                return(Ok());
            }
        }
Example #3
0
        /// <summary>
        /// Saves the entities (Insert or Update) into the database after authorization and validation.
        /// </summary>
        /// <returns>Optionally returns the same entities in their persisted READ form.</returns>
        protected virtual async Task <EntitiesResponse <TEntity> > SaveImplAsync(List <TEntityForSave> entities, SaveArguments args)
        {
            try
            {
                // Parse arguments
                var expand         = ExpandExpression.Parse(args?.Expand);
                var returnEntities = args?.ReturnEntities ?? false;

                // Trim all strings as a preprocessing step
                entities.ForEach(e => TrimStringProperties(e));

                // This implements field level security
                entities = await ApplyUpdatePermissionsMask(entities);

                // Start a transaction scope for save since it causes data modifications
                using var trx = ControllerUtilities.CreateTransaction(null, GetSaveTransactionOptions());

                // Optional preprocessing
                await SavePreprocessAsync(entities);

                // Validate
                // Basic validation that applies to all entities
                ControllerUtilities.ValidateUniqueIds(entities, ModelState, _localizer);

                // Actual Validation
                await SaveValidateAsync(entities);

                if (!ModelState.IsValid)
                {
                    throw new UnprocessableEntityException(ModelState);
                }

                // Save and retrieve Ids
                var ids = await SaveExecuteAsync(entities, expand, returnEntities);

                // Use the Ids to retrieve the items
                EntitiesResponse <TEntity> result = null;
                if (returnEntities && ids != null)
                {
                    result = await GetByIdListAsync(ids.ToArray(), expand);
                }

                await PostProcess(result);

                // Commit and return
                await OnSaveCompleted();

                trx.Complete();
                return(result);
            }
            catch (Exception ex)
            {
                await OnSaveError(ex);

                throw ex;
            }
        }
Example #4
0
        /// <summary>
        /// Returns a single entity as per the ID and specifications in the get request
        /// </summary>
        protected virtual async Task <EntitiesResponse <TEntity> > GetChildrenOfAsync(GetChildrenArguments <TKey> args)
        {
            // Parse the parameters
            var expand  = ExpandExpression.Parse(args.Expand);
            var select  = SelectExpression.Parse(args.Select);
            var filter  = FilterExpression.Parse(args.Filter);
            var orderby = OrderByExpression.Parse("Node");
            var ids     = args.I ?? new List <TKey>();

            return(await GetByCustomQuery(q => q.FilterByParentIds(ids, args.Roots).Filter(filter), expand, select, orderby));
        }
Example #5
0
        public void Visit(ExpandExpression expression, CommonTree tree)
        {
            Parent(tree).Children.Add(expression);
            SetLine(expression, tree);
            VisitChildren(tree);

            if (tree.ChildCount < 3) //if no expand expression we just add the default iteration variable
            {
                var iterationVariable = new ExpandIterationVariable();
                expression.Children.Add(iterationVariable);
            }
        }
Example #6
0
        internal Expression VisitExpand(ExpandExpression node)
        {
            string translatedMethodName = null;

            if (!this.methodNameTranslator.TryGetValue(node.MethodName, out translatedMethodName))
            {
                throw new NotSupportedException($"{node.MethodName} does not support expansion.");
            }

            this.Model.ExpandTerms.Add(new ExpandTerm()
            {
                PropertyName = translatedMethodName, Offset = node.Offset, Limit = node.Limit
            });

            return(node);
        }
        public void Visit(ExpandExpression expression)
        {
            var fromDomArgs    = VisitChild(expression.From);
            var toDomArgs      = VisitChild(expression.To);
            var expressionArgs = VisitChild(expression.Expression);

            CodeMemberMethod method = new CodeMemberMethod();

            method.Name       = "Expand_" + fromDomArgs.MethodIdentifier;
            method.Attributes = MemberAttributes.Private;
            method.ReturnType = new CodeTypeReference("Table", new CodeTypeReference("Expand"));

            method.Statements.Add(new CodeVariableDeclarationStatement(method.ReturnType, "expandTable",
                                                                       new CodeObjectCreateExpression(new CodeTypeReference("RuntimeTable", new CodeTypeReference("Expand")))));

            method.Statements.Add(new CodeVariableDeclarationStatement(typeof(int?), "x"));

            var loop = new CodeIterationStatement();

            loop.InitStatement  = new CodeAssignStatement(new CodeVariableReferenceExpression("x"), fromDomArgs.CodeExpression);
            loop.TestExpression = new CodeBinaryOperatorExpression(new CodeVariableReferenceExpression("x"),
                                                                   CodeBinaryOperatorType.LessThanOrEqual, toDomArgs.CodeExpression);
            loop.IncrementStatement = new CodeAssignStatement(new CodeVariableReferenceExpression("x"), new CodeBinaryOperatorExpression(
                                                                  new CodeVariableReferenceExpression("x"), CodeBinaryOperatorType.Add, new CodePrimitiveExpression(1)));

            loop.Statements.Add(new CodeVariableDeclarationStatement("Expand", "expand", new CodeObjectCreateExpression("Expand")));
            loop.Statements.Add(new CodeAssignStatement(new CodeFieldReferenceExpression(new CodeVariableReferenceExpression("expand"), "value"),
                                                        expressionArgs.CodeExpression));

            loop.Statements.Add(new CodeMethodInvokeExpression(new CodeVariableReferenceExpression("expandTable"), "Add",
                                                               new CodeVariableReferenceExpression("expand")));

            method.Statements.Add(loop);

            method.Statements.Add(new CodeMethodReturnStatement(new CodeVariableReferenceExpression("expandTable")));

            _mainType.Type.Members.Add(method);
            var methodcall = new CodeMethodInvokeExpression(
                new CodeMethodReferenceExpression(null, method.Name));

            _codeStack.Peek().CodeExpression = methodcall;
            _codeStack.Peek().Scope          = new ScopeData <TableDescriptor> {
                Type = Expand.Columns, CodeDomReference = method.ReturnType
            };
        }
Example #8
0
        public async Task <ActionResult <EntitiesResponse <Document> > > SignLines([FromBody] List <int> ids, [FromQuery] SignArguments args)
        {
            return(await ControllerUtilities.InvokeActionImpl(async() =>
            {
                // Parse parameters
                var selectExp = SelectExpression.Parse(args.Select);
                var expandExp = ExpandExpression.Parse(args.Expand);
                var idsArray = ids.ToArray();

                // TODO: Check user permissions
                // await CheckActionPermissions("IsActive", idsArray);

                // Execute and return
                using var trx = ControllerUtilities.CreateTransaction();
                // TODO: Validate sign

                var documentIds = await _repo.Lines__Sign(
                    ids,
                    args.ToState,
                    args.ReasonId,
                    args.ReasonDetails,
                    args.OnBehalfOfUserId,
                    args.RoleId,
                    args.SignedAt ?? DateTimeOffset.Now);

                if (args.ReturnEntities ?? false)
                {
                    var response = await GetByIdListAsync(documentIds.ToArray(), expandExp, selectExp);

                    trx.Complete();
                    return Ok(response);
                }
                else
                {
                    trx.Complete();
                    return Ok();
                }
            }
                                                              , _logger));
        }
Example #9
0
        /// <summary>
        /// Takes a list of <see cref="Entity"/>s, and for every entity it inspects the navigation properties, if a navigation property
        /// contains an <see cref="Entity"/> with a strong type, it sets that property to null, and moves the strong entity into a separate
        /// "relatedEntities" collection, this has several advantages:
        /// 1 - JSON.NET will not have to deal with circular references
        /// 2 - Every strong entity is mentioned once in the JSON response (smaller response size)
        /// 3 - It makes it easier for clients to store and track entities in a central workspace
        /// </summary>
        /// <returns>A dictionary mapping every strong type name to a collection of entities of that type</returns>
        protected virtual Dictionary <string, IEnumerable <Entity> > FlattenAndTrim(IEnumerable <Entity> resultEntities, ExpandExpression expand)
        {
            // If the result is empty, nothing to do
            if (resultEntities == null || !resultEntities.Any())
            {
                return(new Dictionary <string, IEnumerable <Entity> >());
            }

            var relatedEntities = new HashSet <Entity>();
            var resultHash      = resultEntities.ToHashSet();

            // Method for efficiently retrieving the nav and nav collection properties of any entity
            var cacheNavigationProperties = new Dictionary <Type, IEnumerable <PropertyInfo> >();

            IEnumerable <PropertyInfo> NavProps(Entity entity)
            {
                if (!cacheNavigationProperties.TryGetValue(entity.GetType(), out IEnumerable <PropertyInfo> properties))
                {
                    // Return all navigation properties that Entity or list types
                    properties = cacheNavigationProperties[entity.GetType()] =
                        entity.GetType().GetProperties().Where(e =>
                                                               e.PropertyType.IsEntity() || /* nav property */
                                                               e.PropertyType.IsList());    /* nav collection property */
                }

                return(properties);
            }

            // Recursively trims and flattens the entity and all entities reachable from it
            var alreadyFlattenedAndTrimmed = new HashSet <Entity>();

            void FlattenAndTrimInner(Entity entity)
            {
                if (entity == null || alreadyFlattenedAndTrimmed.Contains(entity))
                {
                    return;
                }

                // This ensures Flatten is executed on every entity only once
                alreadyFlattenedAndTrimmed.Add(entity);

                foreach (var navProp in NavProps(entity))
                {
                    if (navProp.PropertyType.IsList())
                    {
                        var collection = navProp.GetValue(entity);
                        if (collection != null)
                        {
                            foreach (var item in collection.Enumerate <Entity>())
                            {
                                FlattenAndTrimInner(item);
                            }
                        }
                    }
                    else if (navProp.GetValue(entity) is Entity relatedEntity) // Checks for null
                    {
                        // If the type is a strong one trim the property and add the entity to relatedEntities
                        if (navProp.PropertyType.IsStrongEntity())
                        {
                            // This property has a strong type, so we set it to null and put its value in the
                            // related entities collection (unless it is part of the main result)

                            // Set the property to null
                            navProp.SetValue(entity, null);
                            if (!resultHash.Contains(relatedEntity))
                            {
                                // Unless it is part of the main result, add it to relatedEntities
                                relatedEntities.Add(relatedEntity);
                            }
                        }

                        // Recursively call flatten on the related entity whether it's strong or weak
                        FlattenAndTrimInner(relatedEntity);
                    }
                }
            }

            // Flatten every entity
            foreach (var entity in resultEntities)
            {
                FlattenAndTrimInner(entity);
            }

            // Return the result
            return(relatedEntities
                   .GroupBy(e => e.GetType().GetRootType().Name)
                   .ToDictionary(g => g.Key, g => g.AsEnumerable()));
        }
Example #10
0
        protected override async Task <List <string> > SaveExecuteAsync(List <CurrencyForSave> entities, ExpandExpression expand, bool returnIds)
        {
            await _repo.Currencies__Save(entities);

            return(entities.Select(e => e.Id).ToList());
        }
Example #11
0
 protected override async Task <List <int> > SaveExecuteAsync(List <EntryTypeForSave> entities, ExpandExpression expand, bool returnIds)
 {
     return(await _repo.EntryTypes__Save(entities, returnIds : returnIds));
 }
Example #12
0
        protected override async Task <List <int> > SaveExecuteAsync(List <AgentForSave> entities, ExpandExpression expand, bool returnIds)
        {
            var(blobsToDelete, blobsToSave, imageIds) = await ImageUtilities.ExtractImages <Agent, AgentForSave>(_repo, entities, BlobName);

            // Save the agents
            var ids = await _repo.Agents__Save(
                DefinitionId,
                entities : entities,
                imageIds : imageIds,
                returnIds : returnIds);

            // Delete the blobs retrieved earlier
            if (blobsToDelete.Any())
            {
                await _blobService.DeleteBlobsAsync(blobsToDelete);
            }

            // Save new blobs if any
            if (blobsToSave.Any())
            {
                await _blobService.SaveBlobsAsync(blobsToSave);
            }

            return(ids);
        }
Example #13
0
        /// <summary>
        /// Returns a single entity as per the ID and specifications in the get request
        /// </summary>
        protected virtual async Task <GetByIdResponse <TEntity> > GetByIdImplAsync(TKey id, [FromQuery] GetByIdArguments args)
        {
            // Parse the parameters
            var expand = ExpandExpression.Parse(args?.Expand);
            var select = SelectExpression.Parse(args?.Select);

            // Prepare the odata query
            var repo  = GetRepository();
            var query = repo.Query <TEntity>();

            // Add the filter by Id
            query = query.FilterByIds(id);

            // Check that the entity exists
            int count = await query.CountAsync();

            if (count == 0)
            {
                throw new NotFoundException <TKey>(id);
            }

            // Apply read permissions
            var permissions = await UserPermissions(Constants.Read);

            var permissionsFilter = GetReadPermissionsCriteria(permissions);

            query = query.Filter(permissionsFilter);

            // Apply the expand, which has the general format 'Expand=A,B/C,D'
            var expandedQuery = query.Expand(expand);

            // Apply the select, which has the general format 'Select=A,B/C,D'
            expandedQuery = expandedQuery.Select(select);

            // Load
            var result = await expandedQuery.FirstOrDefaultAsync();

            if (result == null)
            {
                // We already checked for not found earlier,
                // This can only mean lack of permissions
                throw new ForbiddenException();
            }

            // Apply the permission masks (setting restricted fields to null) and adjust the metadata accordingly
            var singleton = new List <TEntity> {
                result
            };

            await ApplyReadPermissionsMask(singleton, query, permissions, GetDefaultMask());

            // Flatten and Trim
            var relatedEntities = FlattenAndTrim(singleton, expand);

            // Return
            return(new GetByIdResponse <TEntity>
            {
                Result = result,
                CollectionName = GetCollectionName(typeof(TEntity)),
                RelatedEntities = relatedEntities
            });
        }
Example #14
0
        /// <summary>
        /// Returns an entities response based on custom filtering function applied to the query, as well as
        /// optional select and expand arguments, checking the user permissions along the way
        /// </summary>
        /// <param name="filterFunc">Allows you to apply any filteration you like to the query,</param>
        /// <param name="expand">Optional expand argument</param>
        /// <param name="select">Optional select argument</param>
        protected async Task <EntitiesResponse <TEntity> > GetByCustomQuery(Func <Query <TEntity>, Query <TEntity> > filterFunc, ExpandExpression expand, SelectExpression select, OrderByExpression orderby = null)
        {
            // Prepare a query of the result, and clone it
            var repo  = GetRepository();
            var query = repo.Query <TEntity>();

            // Apply custom filter function
            query = filterFunc(query);

            // Expand the result as specified in the OData agruments and load into memory
            var expandedQuery = query.Expand(expand);

            expandedQuery = expandedQuery.Select(select);
            expandedQuery = expandedQuery.OrderBy(orderby ?? OrderByExpression.Parse("Id")); // Required
            var result = await expandedQuery.ToListAsync();                                  // this is potentially unordered, should that be a concern?

            // Apply the permissions on the result
            var permissions = await UserPermissions(Constants.Read);

            var defaultMask = GetDefaultMask();

            await ApplyReadPermissionsMask(result, query, permissions, defaultMask);

            // Flatten and Trim
            var relatedEntities = FlattenAndTrim(result, expand);

            // Prepare the result in a response object
            return(new EntitiesResponse <TEntity>
            {
                Result = result,
                RelatedEntities = relatedEntities,
                CollectionName = GetCollectionName(typeof(TEntity))
            });
        }
Example #15
0
        protected async Task <EntitiesResponse <TEntity> > GetByIdListAsync(TKey[] ids, ExpandExpression expand = null, SelectExpression select = null)
        {
            var result = await GetByCustomQuery(q => q.FilterByIds(ids), expand, select);

            // Sort the entities according to the original Ids, as a good practice
            TEntity[] sortedResult = new TEntity[ids.Length];
            Dictionary <TKey, TEntity> resultDic = result.Result.ToDictionary(e => e.Id);

            for (int i = 0; i < ids.Length; i++)
            {
                var     id     = ids[i];
                TEntity entity = null;
                if (resultDic.ContainsKey(id))
                {
                    entity = resultDic[id];
                }

                sortedResult[i] = entity;
            }

            result.Result = sortedResult;

            // Return the sorted result
            return(result);
        }
Example #16
0
 protected override async Task <List <int> > SaveExecuteAsync(List <MeasurementUnitForSave> entities, ExpandExpression expand, bool returnIds)
 {
     return(await _repo.MeasurementUnits__Save(entities, returnIds : returnIds));
 }
 protected override async Task <List <int> > SaveExecuteAsync(List <ResponsibilityCenterForSave> entities, ExpandExpression expand, bool returnIds)
 {
     return(await _repo.ResponsibilityCenters__Save(entities, returnIds : returnIds));
 }
Example #18
0
 protected override async Task <List <int> > SaveExecuteAsync(List <ResourceForSave> entities, ExpandExpression expand, bool returnIds)
 {
     return(await _repo.Resources__Save(DefinitionId, entities, returnIds : returnIds));
 }
Example #19
0
 protected override async Task <List <int> > SaveExecuteAsync(List <LegacyClassificationForSave> entities, ExpandExpression expand, bool returnIds)
 {
     return(await _repo.LegacyClassifications__Save(entities, returnIds));
 }
Example #20
0
        protected override async Task <List <int> > SaveExecuteAsync(List <DocumentForSave> entities, ExpandExpression expand, bool returnIds)
        {
            var blobsToSave = new List <(string, byte[])>();

            // Prepare the list of attachments with extras
            var attachments = new List <AttachmentWithExtras>();

            foreach (var(doc, docIndex) in entities.Select((d, i) => (d, i)))
            {
                if (doc.Attachments != null)
                {
                    doc.Attachments.ForEach(att =>
                    {
                        var attWithExtras = new AttachmentWithExtras
                        {
                            Id            = att.Id,
                            FileName      = att.FileName,
                            FileExtension = att.FileExtension,
                            DocumentIndex = docIndex,
                        };

                        // If new attachment
                        if (att.Id == 0)
                        {
                            // Add extras: file Id and size
                            byte[] file          = att.File;
                            string fileId        = Guid.NewGuid().ToString();
                            attWithExtras.FileId = fileId;
                            attWithExtras.Size   = file.LongLength;

                            // Also add to blobsToCreate
                            string blobName = BlobName(fileId);
                            blobsToSave.Add((blobName, file));
                        }

                        attachments.Add(attWithExtras);
                    });
                }
            }

            // Save the documents
            var(ids, fileIdsToDelete) = await _repo.Documents__Save(
                DefinitionId,
                documents : entities,
                attachments : attachments,
                returnIds : returnIds);

            // Assign new documents to the current user
            var userInfo = await _repo.GetUserInfoAsync();

            var currentUserId = userInfo.UserId.Value;
            var newDocIds     = entities.Select((doc, index) => (doc, index)).Where(e => e.doc.Id == 0).Select(e => ids[e.index]);
            await _repo.Documents__Assign(newDocIds, currentUserId, null);

            // Delete the file Ids retrieved earlier if any
            if (fileIdsToDelete.Any())
            {
                var blobsToDelete = fileIdsToDelete.Select(fileId => BlobName(fileId));
                await _blobService.DeleteBlobsAsync(blobsToDelete);
            }

            // Save new blobs if any
            if (blobsToSave.Any())
            {
                await _blobService.SaveBlobsAsync(blobsToSave);
            }

            // Return the new Ids
            return(ids);
        }
Example #21
0
 /// <summary>
 /// Persists the entities in the database, either creating them or updating, the call to this method is already wrapped inside a transaction
 /// </summary>
 protected abstract Task <List <TKey> > SaveExecuteAsync(List <TEntityForSave> entities, ExpandExpression expand, bool returnIds);
Example #22
0
        protected override async Task <List <string> > SaveExecuteAsync(List <ReportDefinitionForSave> entities, ExpandExpression expand, bool returnIds)
        {
            await _repo.ReportDefinitions__Save(entities);

            Response.Headers.Set("x-definitions-version", Constants.Stale);
            return(entities.Select(e => e.Id).ToList());
        }
Example #23
0
        // Endpoint implementations

        /// <summary>
        /// Returns the entities as per the specifications in the get request
        /// </summary>
        protected virtual async Task <GetResponse <TEntity> > GetImplAsync(GetArguments args, Query <TEntity> queryOverride = null)
        {
            // Parse the parameters
            var filter  = FilterExpression.Parse(args.Filter);
            var orderby = OrderByExpression.Parse(args.OrderBy);
            var expand  = ExpandExpression.Parse(args.Expand);
            var select  = SelectExpression.Parse(args.Select);

            // Prepare the query
            var query = queryOverride ?? GetRepository().Query <TEntity>();

            // Retrieve the user permissions for the current view
            var permissions = await UserPermissions(Constants.Read);

            // Filter out permissions with masks that would be violated by the filter or order by arguments
            var defaultMask = GetDefaultMask() ?? new MaskTree();

            permissions = FilterViolatedPermissionsForFlatQuery(permissions, defaultMask, filter, orderby);

            // Apply read permissions
            var permissionsFilter = GetReadPermissionsCriteria(permissions);

            query = query.Filter(permissionsFilter);

            // Search
            query = Search(query, args, permissions);

            // Filter
            query = query.Filter(filter);

            // Before ordering or paging, retrieve the total count
            int totalCount = await query.CountAsync();

            // OrderBy
            query = OrderBy(query, orderby);

            // Apply the paging (Protect against DOS attacks by enforcing a maximum page size)
            var top  = args.Top;
            var skip = args.Skip;

            top   = Math.Min(top, MaximumPageSize());
            query = query.Skip(skip).Top(top);

            // Apply the expand, which has the general format 'Expand=A,B/C,D'
            var expandedQuery = query.Expand(expand);

            // Apply the select, which has the general format 'Select=A,B/C,D'
            expandedQuery = expandedQuery.Select(select);

            // Load the data in memory
            var result = await expandedQuery.ToListAsync();

            // Apply the permission masks (setting restricted fields to null) and adjust the metadata accordingly
            await ApplyReadPermissionsMask(result, query, permissions, defaultMask);

            // Flatten and Trim
            var relatedEntities = FlattenAndTrim(result, expand);

            // Prepare the result in a response object
            return(new GetResponse <TEntity>
            {
                Skip = skip,
                Top = result.Count(),
                OrderBy = args.OrderBy,
                TotalCount = totalCount,

                Result = result,
                RelatedEntities = relatedEntities,
                CollectionName = GetCollectionName(typeof(TEntity))
            });
        }
Example #24
0
        protected override async Task <List <int> > SaveExecuteAsync(List <UserForSave> entities, ExpandExpression expand, bool returnIds)
        {
            // NOTE: this method is not optimized for massive bulk (e.g. 1,000+ users), since it relies
            // on querying identity through UserManager one email at a time but it should be acceptable
            // with the usual workloads, customers with more than 200 users are rare anyways

            // Step (1) enlist the app repo
            _appRepo.EnlistTransaction(Transaction.Current); // So that it is not affected by admin trx scope later

            // Step (2): If Embedded Identity Server is enabled, create any emails that don't already exist there
            var usersToInvite = new List <(EmbeddedIdentityServerUser IdUser, UserForSave User)>();

            if (_options.EmbeddedIdentityServerEnabled)
            {
                _identityTrxScope = ControllerUtilities.CreateTransaction(TransactionScopeOption.RequiresNew);

                foreach (var entity in entities)
                {
                    var email = entity.Email;

                    // In case the user was added in a previous failed transaction
                    // or something, we always try to be forgiving in the code
                    var identityUser = await _userManager.FindByNameAsync(email) ??
                                       await _userManager.FindByEmailAsync(email);

                    // This is truly a new user, create it
                    if (identityUser == null)
                    {
                        // Create the identity user
                        identityUser = new EmbeddedIdentityServerUser
                        {
                            UserName       = email,
                            Email          = email,
                            EmailConfirmed = !_options.EmailEnabled

                                             // Note: If the system is integrated with an email service, user emails
                                             // are automatically confirmed, otherwise users must confirm their
                        };

                        var result = await _userManager.CreateAsync(identityUser);

                        if (!result.Succeeded)
                        {
                            string msg = string.Join(", ", result.Errors.Select(e => e.Description));
                            _logger.LogError(msg);

                            throw new BadRequestException($"An unexpected error occurred while creating an account for '{email}'");
                        }
                    }

                    // Mark for invitation later
                    if (!identityUser.EmailConfirmed)
                    {
                        usersToInvite.Add((identityUser, entity));
                    }
                }
            }

            // Step (3): Extract the images
            var(blobsToDelete, blobsToSave, imageIds) = await ImageUtilities.ExtractImages <User, UserForSave>(_appRepo, entities, BlobName);

            // Step (4): Save the users in the app database
            var ids = await _appRepo.Users__Save(entities, imageIds, returnIds);

            // Step (5): Delete old images from the blob storage
            if (blobsToDelete.Any())
            {
                await _blobService.DeleteBlobsAsync(blobsToDelete);
            }

            // Step (6): Save new images to the blob storage
            if (blobsToSave.Any())
            {
                await _blobService.SaveBlobsAsync(blobsToSave);
            }

            // Step (7) Same the emails in the admin database
            var tenantId = _tenantIdAccessor.GetTenantId();

            _adminTrxScope = ControllerUtilities.CreateTransaction(TransactionScopeOption.RequiresNew);
            _adminRepo.EnlistTransaction(Transaction.Current);
            var oldEmails = new List <string>(); // Emails are readonly after the first save
            var newEmails = entities.Where(e => e.Id == 0).Select(e => e.Email);
            await _adminRepo.GlobalUsers__Save(newEmails, oldEmails, tenantId);

            // Step (8): Send the invitation emails
            if (usersToInvite.Any()) // This will be empty if embedded identity is disabled or if email is disabled
            {
                var userIds       = usersToInvite.Select(e => e.User.Id).ToArray();
                var tos           = new List <string>();
                var subjects      = new List <string>();
                var substitutions = new List <Dictionary <string, string> >();
                foreach (var(idUser, user) in usersToInvite)
                {
                    // Add the email sender parameters
                    var(subject, body) = await MakeInvitationEmailAsync(idUser, user.Name, user.Name2, user.Name3, user.PreferredLanguage);

                    tos.Add(idUser.Email);
                    subjects.Add(subject);
                    substitutions.Add(new Dictionary <string, string> {
                        { "-message-", body }
                    });
                }

                await _emailSender.SendEmailBulkAsync(
                    tos : tos,
                    subjects : subjects,
                    htmlMessage : $"-message-",
                    substitutions : substitutions.ToList()
                    );
            }

            // Return the new Ids
            return(ids);
        }