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()); } }
/// <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; } }
/// <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)); }
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); } }
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 }; }
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)); }
/// <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())); }
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()); }
protected override async Task <List <int> > SaveExecuteAsync(List <EntryTypeForSave> entities, ExpandExpression expand, bool returnIds) { return(await _repo.EntryTypes__Save(entities, returnIds : returnIds)); }
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); }
/// <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 }); }
/// <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)) }); }
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); }
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)); }
protected override async Task <List <int> > SaveExecuteAsync(List <ResourceForSave> entities, ExpandExpression expand, bool returnIds) { return(await _repo.Resources__Save(DefinitionId, entities, returnIds : returnIds)); }
protected override async Task <List <int> > SaveExecuteAsync(List <LegacyClassificationForSave> entities, ExpandExpression expand, bool returnIds) { return(await _repo.LegacyClassifications__Save(entities, returnIds)); }
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); }
/// <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);
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()); }
// 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)) }); }
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); }