public async Task <ActionResult> Create(string name, int projectId, string priority, string description) { var result = await _taskRepository.Create(name, projectId, priority, description); return(ControllerUtilities.CheckResult(result)); }
public static string GetSelectView(string controller) { ControllerUtilities c = new ControllerUtilities(); return c.GetActionView(controller, "editForm1", "Select"); }
/// <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" hash set, 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 hash set of strong related entity in the original result entities (excluding the result entities)</returns> protected Dictionary <string, IEnumerable <Entity> > FlattenAndTrim <T>(IEnumerable <T> resultEntities, CancellationToken cancellation) where T : Entity { return(ControllerUtilities.FlattenAndTrim(resultEntities, cancellation)); }
public static string GetDeleteView(string controller) { ControllerUtilities c = new ControllerUtilities(); return(c.GetActionView(controller, "editForm1", "Delete")); }
protected override async Task SaveValidateAsync(List <LineDefinitionForSave> entities) { var defs = _defCache.GetCurrentDefinitionsIfCached().Data; var settings = _settingsCache.GetCurrentSettingsIfCached().Data; // C# validation int lineDefinitionIndex = 0; entities.ForEach(lineDefinition => { // Columns int columnIndex = 0; lineDefinition.Columns.ForEach(column => { int index = column.EntryIndex.Value; if (index < 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Columns)}[{columnIndex}].{nameof(LineDefinitionColumn.EntryIndex)}"; string msg = _localizer["Error_IndexMustBeGreaterOrEqualZero"]; ModelState.AddModelError(path, msg); } else if (index > (lineDefinition.Entries?.Count ?? 0)) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Columns)}[{columnIndex}].{nameof(LineDefinitionColumn.EntryIndex)}"; string msg = _localizer["Error_NoEntryCorrespondsToIndex0", index]; ModelState.AddModelError(path, msg); } // Required state should always be <= ReadOnlyState if (column.RequiredState > column.ReadOnlyState) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Columns)}[{columnIndex}].{nameof(LineDefinitionColumn.ReadOnlyState)}"; string msg = _localizer["Error_ReadOnlyStateCannotBeBeforeRequiredState"];; ModelState.AddModelError(path, msg); } columnIndex++; }); // GenerateScript if (!string.IsNullOrWhiteSpace(lineDefinition.GenerateScript)) { // If auto-generate script is specified, DefaultsToForm must be false if (lineDefinition.ViewDefaultsToForm.Value) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.GenerateScript)}"; string msg = _localizer["Error_CannotHaveGenerateScriptWithDefaultsToForm"]; ModelState.AddModelError(path, msg); } } // Generate parameters int paramIndex = 0; lineDefinition.GenerateParameters.ForEach(parameter => { var errors = ControllerUtilities.ValidateControlOptions(parameter.Control, parameter.ControlOptions, _localizer, settings, defs); foreach (var msg in errors) { ModelState.AddModelError($"[{lineDefinitionIndex}].{nameof(lineDefinition.GenerateParameters)}[{paramIndex}].{nameof(parameter.ControlOptions)}", msg); } paramIndex++; }); // Workflows int workflowIndex = 0; lineDefinition.Workflows.ForEach(workflow => { int signatureIndex = 0; workflow.Signatures?.ForEach(signature => { // Role is required if (signature.RuleType == RuleTypes.ByRole && signature.RoleId == null) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.RoleId)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["WorkflowSignature_Role"]]; ModelState.AddModelError(path, msg); } // User is required if (signature.RuleType == RuleTypes.ByUser && signature.UserId == null) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.UserId)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["WorkflowSignature_User"]]; ModelState.AddModelError(path, msg); } if (signature.RuleType == RuleTypes.ByCustodian && signature.RuleTypeEntryIndex == null) { // Entry index is required if (signature.RuleTypeEntryIndex == null) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.RuleTypeEntryIndex)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["WorkflowSignature_RuleTypeEntryIndex"]]; ModelState.AddModelError(path, msg); } else { // Make sure Entry index is not out of bounds int index = signature.RuleTypeEntryIndex.Value; if (index < 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.RuleTypeEntryIndex)}"; string msg = _localizer["Error_IndexMustBeGreaterOrEqualZero"]; ModelState.AddModelError(path, msg); } else if (index > (lineDefinition.Entries?.Count ?? 0)) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.RuleTypeEntryIndex)}"; string msg = _localizer["Error_NoEntryCorrespondsToIndex0", index]; ModelState.AddModelError(path, msg); } } } if (signature.PredicateType == PredicateTypes.ValueGreaterOrEqual) { // Value is required if (signature.Value == null) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.Value)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["WorkflowSignature_Value"]]; ModelState.AddModelError(path, msg); } // Entry Index is required if (signature.PredicateTypeEntryIndex == null) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.PredicateTypeEntryIndex)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["WorkflowSignature_PredicateTypeEntryIndex"]]; ModelState.AddModelError(path, msg); } else { // Make sure Entry index is not out of bounds int index = signature.PredicateTypeEntryIndex.Value; if (index < 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.PredicateTypeEntryIndex)}"; string msg = _localizer["Error_IndexMustBeGreaterOrEqualZero"]; ModelState.AddModelError(path, msg); } else if (index > (lineDefinition.Entries?.Count ?? 0)) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.Workflows)}[{workflowIndex}].{nameof(Workflow.Signatures)}[{signatureIndex}].{nameof(WorkflowSignature.PredicateTypeEntryIndex)}"; string msg = _localizer["Error_NoEntryCorrespondsToIndex0", index]; ModelState.AddModelError(path, msg); } } } signatureIndex++; }); workflowIndex++; }); // Barcode if (lineDefinition.BarcodeColumnIndex != null) { // If barcode is enabled, BarcodeProperty must be specified if (string.IsNullOrWhiteSpace(lineDefinition.BarcodeProperty)) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeProperty)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["LineDefinition_BarcodeProperty"]]; ModelState.AddModelError(path, msg); } // If barcode is enabled, BarcodeExistingItemHandling must be specified if (string.IsNullOrWhiteSpace(lineDefinition.BarcodeExistingItemHandling)) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeExistingItemHandling)}"; string msg = _localizer[Constants.Error_Field0IsRequired, _localizer["LineDefinition_BarcodeExistingItemHandling"]]; ModelState.AddModelError(path, msg); } // If barcode is enabled, DefaultsToForm must be false if (lineDefinition.ViewDefaultsToForm.Value) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string msg = _localizer["Error_CannotHaveBarcodeWithDefaultsToForm"]; ModelState.AddModelError(path, msg); } // BarcodeColumnIndex must be within Columns range var colIndex = lineDefinition.BarcodeColumnIndex.Value; if (colIndex >= lineDefinition.Columns.Count) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string msg = _localizer["Error_BarcodeColumnIndexOutOfRange"]; ModelState.AddModelError(path, msg); } else { // Barcode Column cannot inherit from headers var colDef = lineDefinition.Columns[colIndex]; if (colDef.InheritsFromHeader > 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string msg = _localizer["Error_BarcodeColumnCannotInheritFromHeaders"]; ModelState.AddModelError(path, msg); } // Barcode Column must be visible from DRAFT if (colDef.VisibleState > 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string msg = _localizer["Error_BarcodeColumnMustBeVisibleFromDraft"]; ModelState.AddModelError(path, msg); } // Barcode Column must be editable from DRAFT if (colDef.ReadOnlyState == 0) { string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string msg = _localizer["Error_BarcodeColumnCannotBeReadOnlyFromDraft"]; ModelState.AddModelError(path, msg); } Dictionary <string, Type> acceptableColumnNames = new Dictionary <string, Type> { { "CustodianId", typeof(Relation) }, { "CustodyId", typeof(Custody) }, { "ParticipantId", typeof(Relation) }, { "ResourceId", typeof(Resource) } }; if (string.IsNullOrWhiteSpace(colDef.ColumnName)) { // Error handled earlier } else if (!acceptableColumnNames.TryGetValue(colDef.ColumnName, out Type colType)) { // Barcode Column must have on of the supported column names string path = $"[{lineDefinitionIndex}].{nameof(LineDefinition.BarcodeColumnIndex)}"; string names = string.Join(", ", acceptableColumnNames.Keys.Select(e => _localizer["Entry_" + e[0..^ 2]]));
public static void VerifyMvcPage <ControllerUnderTest>(Expression <Func <ControllerUnderTest, Func <ActionResult> > > actionName, Func <string, string> scrubber = null) where ControllerUnderTest : TestableControllerBase { VerifyMvcUrl(ReflectionUtility.GetControllerName <ControllerUnderTest>(), ControllerUtilities.GetMethodName(actionName.Body), GetFilePathasQueryString <ControllerUnderTest>(), scrubber); }
public async Task <ActionResult> Delete(int id) { var result = await _projectRepository.Delete(id); return(ControllerUtilities.CheckResult(result)); }
public async Task <(List <DocumentDefinition>, Extras)> UpdateState(List <int> ids, UpdateStateArguments args) { // Make sure int jvDefId = _defCache.GetCurrentDefinitionsIfCached()?.Data?.ManualJournalVouchersDefinitionId ?? throw new BadRequestException("The Manual Journal Voucher Id is not defined"); int index = 0; ids.ForEach(id => { if (id == jvDefId) { string path = $"[{index}]"; string msg = _localizer["Error_CannotModifySystemItem"]; ModelState.AddModelError(path, msg); } index++; }); // No point carrying on ModelState.ThrowIfInvalid(); // Check user permissions var action = "State"; var actionFilter = await UserPermissionsFilter(action, cancellation : default); ids = await CheckActionPermissionsBefore(actionFilter, ids); // C# Validation if (string.IsNullOrWhiteSpace(args.State)) { throw new BadRequestException(_localizer[Constants.Error_Field0IsRequired, _localizer["State"]]); } if (!DefStates.All.Contains(args.State)) { string validStates = string.Join(", ", DefStates.All); throw new BadRequestException($"'{args.State}' is not a valid definition state, valid states are: {validStates}"); } // Transaction using var trx = ControllerUtilities.CreateTransaction(); // Validate int remainingErrorCount = ModelState.MaxAllowedErrors - ModelState.ErrorCount; var errors = await _repo.DocumentDefinitions_Validate__UpdateState(ids, args.State, top : remainingErrorCount); ControllerUtilities.AddLocalizedErrors(ModelState, errors, _localizer); ModelState.ThrowIfInvalid(); // Execute await _repo.DocumentDefinitions__UpdateState(ids, args.State); // Prepare response List <DocumentDefinition> data = null; Extras extras = null; if (args.ReturnEntities ?? false) { (data, extras) = await GetByIds(ids, args, action, cancellation : default); } // Check user permissions again await CheckActionPermissionsAfter(actionFilter, ids, data); // Commit and return trx.Complete(); return(data, extras); }
public async Task <ActionResult <IEnumerable <Project> > > Get() { var result = await _projectRepository.Get(); return(ControllerUtilities.CheckResult(result)); }
public async Task <ActionResult> Create(string name, string priority) { var result = await _projectRepository.Create(name, priority); return(ControllerUtilities.CheckResult(result)); }
protected override async Task <(List <MeasurementUnitForQuery>, IQueryable <MeasurementUnitForQuery>)> PersistAsync(List <MeasurementUnitForSave> entities, SaveArguments args) { // Add created entities DataTable entitiesTable = ControllerUtilities.DataTable(entities, addIndex: true); var entitiesTvp = new SqlParameter("Entities", entitiesTable) { TypeName = $"dbo.{nameof(MeasurementUnitForSave)}List", SqlDbType = SqlDbType.Structured }; string saveSql = $@" -- Procedure: MeasurementUnitsSave SET NOCOUNT ON; DECLARE @IndexedIds [dbo].[IndexedIdList]; DECLARE @TenantId int = CONVERT(INT, SESSION_CONTEXT(N'TenantId')); DECLARE @Now DATETIMEOFFSET(7) = SYSDATETIMEOFFSET(); DECLARE @UserId INT = CONVERT(INT, SESSION_CONTEXT(N'UserId')); INSERT INTO @IndexedIds([Index], [Id]) SELECT x.[Index], x.[Id] FROM ( MERGE INTO [dbo].MeasurementUnits AS t USING ( SELECT [Index], [Id], [Code], [UnitType], [Name], [Name2], [UnitAmount], [BaseAmount] FROM @Entities WHERE [EntityState] IN (N'Inserted', N'Updated') ) AS s ON (t.Id = s.Id) WHEN MATCHED THEN UPDATE SET t.[UnitType] = s.[UnitType], t.[Name] = s.[Name], t.[Name2] = s.[Name2], t.[UnitAmount] = s.[UnitAmount], t.[BaseAmount] = s.[BaseAmount], t.[Code] = s.[Code], t.[ModifiedAt] = @Now, t.[ModifiedById] = @UserId WHEN NOT MATCHED THEN INSERT ([TenantId], [UnitType], [Name], [Name2], [UnitAmount], [BaseAmount], [Code], [CreatedAt], [CreatedById], [ModifiedAt], [ModifiedById]) VALUES (@TenantId, s.[UnitType], s.[Name], s.[Name2], s.[UnitAmount], s.[BaseAmount], s.[Code], @Now, @UserId, @Now, @UserId) OUTPUT s.[Index], inserted.[Id] ) As x OPTION(RECOMPILE) "; // Optimization if (!(args.ReturnEntities ?? false)) { // IF no returned items are expected, simply execute a non-Query and return an empty list; await _db.Database.ExecuteSqlCommandAsync(saveSql, entitiesTvp); return(new List <MeasurementUnitForQuery>(), null); } else { // If returned items are expected, append a select statement to the SQL command saveSql = saveSql += "SELECT * FROM @IndexedIds;"; // Retrieve the map from Indexes to Ids var indexedIds = await _db.Saving.FromSql(saveSql, entitiesTvp).ToListAsync(); // Load the entities using their Ids DataTable idsTable = ControllerUtilities.DataTable(indexedIds.Select(e => new { e.Id }), addIndex: false); var idsTvp = new SqlParameter("Ids", idsTable) { TypeName = $"dbo.IdList", SqlDbType = SqlDbType.Structured }; var q = _db.VW_MeasurementUnits.FromSql("SELECT * FROM dbo.[MeasurementUnits] WHERE Id IN (SELECT Id FROM @Ids)", idsTvp); q = Expand(q, args.Expand); var savedEntities = await q.ToListAsync(); // SQL Server does not guarantee order, so make sure the result is sorted according to the initial index Dictionary <int, int> indices = indexedIds.ToDictionary(e => e.Id, e => e.Index); var sortedSavedEntities = new MeasurementUnitForQuery[savedEntities.Count]; foreach (var item in savedEntities) { int index = indices[item.Id.Value]; sortedSavedEntities[index] = item; } // Return the sorted collection return(sortedSavedEntities.ToList(), q); } }
protected override async Task ValidateAsync(List <MeasurementUnitForSave> entities) { // Hash the indices for performance var indices = entities.ToIndexDictionary(); // Check that Ids make sense in relation to EntityState, and that no entity is DELETED // All these errors indicate a bug foreach (var entity in entities) { if (entity.EntityState == EntityStates.Deleted) { // Won't be supported for this API var index = indices[entity]; ModelState.AddModelError($"[{index}].{nameof(entity.EntityState)}", _localizer["Error_Deleting0IsNotSupportedFromThisAPI", _localizer["MeasurementUnits"]]); } } // Check that Ids are unique var duplicateIds = entities.Where(e => e.Id != null).GroupBy(e => e.Id.Value).Where(g => g.Count() > 1); foreach (var groupWithDuplicateIds in duplicateIds) { foreach (var entity in groupWithDuplicateIds) { // This error indicates a bug var index = indices[entity]; ModelState.AddModelError($"[{index}].{nameof(entity.Id)}", _localizer["Error_TheEntityWithId0IsSpecifiedMoreThanOnce", entity.Id]); } } // No need to invoke SQL if the model state is full of errors if (ModelState.HasReachedMaxErrors) { return; } // Perform SQL-side validation DataTable entitiesTable = ControllerUtilities.DataTable(entities, addIndex: true); var entitiesTvp = new SqlParameter("Entities", entitiesTable) { TypeName = $"dbo.{nameof(MeasurementUnitForSave)}List", SqlDbType = SqlDbType.Structured }; int remainingErrorCount = ModelState.MaxAllowedErrors - ModelState.ErrorCount; // Code, Name and Name2 must be unique var sqlErrors = await _db.Validation.FromSql($@" SET NOCOUNT ON; DECLARE @ValidationErrors dbo.ValidationErrorList; -- Non Null Ids must exist INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1]) SELECT '[' + CAST([Id] AS NVARCHAR(255)) + '].Id' As [Key], N'Error_TheId0WasNotFound' As [ErrorName], CAST([Id] As NVARCHAR(255)) As [Argument1] FROM @Entities WHERE Id Is NOT NULL AND Id NOT IN (SELECT Id from [dbo].[MeasurementUnits]) -- Code must be unique INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST(FE.[Index] AS NVARCHAR(255)) + '].Code' As [Key], N'Error_TheCode0IsUsed' As [ErrorName], FE.Code AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities FE JOIN [dbo].MeasurementUnits BE ON FE.Code = BE.Code WHERE FE.[Code] IS NOT NULL AND (FE.[EntityState] = N'Inserted') OR (FE.Id <> BE.Id) OPTION(HASH JOIN); -- Code must not be duplicated in the uploaded list INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST([Index] AS NVARCHAR(255)) + '].Code' As [Key], N'Error_TheCode0IsDuplicated' As [ErrorName], [Code] AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities WHERE [Code] IN ( SELECT [Code] FROM @Entities WHERE [Code] IS NOT NULL GROUP BY [Code] HAVING COUNT(*) > 1 ) OPTION(HASH JOIN); -- Name must not exist already INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST(FE.[Index] AS NVARCHAR(255)) + '].Name' As [Key], N'Error_TheName0IsUsed' As [ErrorName], FE.[Name] AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities FE JOIN [dbo].MeasurementUnits BE ON FE.[Name] = BE.[Name] WHERE (FE.[EntityState] = N'Inserted') OR (FE.Id <> BE.Id) OPTION(HASH JOIN); -- Name must be unique in the uploaded list INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST([Index] AS NVARCHAR(255)) + '].Name' As [Key], N'Error_TheName0IsDuplicated' As [ErrorName], [Name] AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities WHERE [Name] IN ( SELECT [Name] FROM @Entities GROUP BY [Name] HAVING COUNT(*) > 1 ) OPTION(HASH JOIN); -- Name2 must not exist already INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST(FE.[Index] AS NVARCHAR(255)) + '].Name2' As [Key], N'Error_TheName0IsUsed' As [ErrorName], FE.[Name2] AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities FE JOIN [dbo].MeasurementUnits BE ON FE.[Name2] = BE.[Name2] WHERE (FE.[EntityState] = N'Inserted') OR (FE.Id <> BE.Id) OPTION(HASH JOIN); -- Name2 must be unique in the uploaded list INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST([Index] AS NVARCHAR(255)) + '].Name2' As [Key], N'Error_TheName0IsDuplicated' As [ErrorName], [Name2] AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities WHERE [Name2] IN ( SELECT [Name2] FROM @Entities GROUP BY [Name2] HAVING COUNT(*) > 1 ) OPTION(HASH JOIN); -- Add further logic SELECT TOP {remainingErrorCount} * FROM @ValidationErrors; ", entitiesTvp).ToListAsync(); // Loop over the errors returned from SQL and add them to ModelState foreach (var sqlError in sqlErrors) { var formatArguments = sqlError.ToFormatArguments(); string key = sqlError.Key; string errorMessage = _localizer[sqlError.ErrorName, formatArguments]; ModelState.AddModelError(key: key, errorMessage: errorMessage); } }
protected override async Task <IEnumerable <AbstractPermission> > UserPermissions(PermissionLevel level) { return(await ControllerUtilities.GetPermissions(_db.AbstractPermissions, level, "measurement-units")); }
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); }
public async Task <ActionResult> UpdateName(int id, string name) { var result = await _projectRepository.UpdateName(id, name); return(ControllerUtilities.CheckResult(result)); }
/// <summary> /// Takes a list of <see cref="Entity"/>, 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" hash set, this has several advantages: <br/> /// 1 - JSON.NET will not have to deal with circular references <br/> /// 2 - Every strong entity is mentioned once in the JSON response (smaller response size) <br/> /// 3 - It makes it easier for clients to store and track entities in a central workspace <br/> /// </summary> /// <returns>A hash set of strong related entity in the original result entities (excluding the result entities).</returns> protected RelatedEntities Flatten <T>(IEnumerable <T> resultEntities, CancellationToken cancellation) where T : Entity { return(ControllerUtilities.Flatten(resultEntities, cancellation)); }
public async Task <ActionResult> UpdatePriority(int id, string priority) { var result = await _projectRepository.UpdatePriority(id, priority); return(ControllerUtilities.CheckResult(result)); }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var cancellation = context.HttpContext.RequestAborted; // (1) Make sure the requester has an active user AdminUserInfo userInfo = await _adminRepo.GetAdminUserInfoAsync(cancellation); if (userInfo.UserId == null) { // If there is no user cut the pipeline short and return a Forbidden 403 context.Result = new StatusCodeResult(StatusCodes.Status403Forbidden); // This indicates to the client to discard all cached information about this // company since the user is no longer a member of it context.HttpContext.Response.Headers.Add("x-admin-settings-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-admin-permissions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-admin-user-settings-version", Constants.Unauthorized); return; } var userId = userInfo.UserId.Value; var externalId = _externalUserAccessor.GetUserId(); var externalEmail = _externalUserAccessor.GetUserEmail(); // (3) If the user exists but new, set the External Id if (userInfo.ExternalId == null) { using var trx = ControllerUtilities.CreateTransaction(); await _adminRepo.AdminUsers__SetExternalIdByUserId(userId, externalId); await _adminRepo.DirectoryUsers__SetExternalIdByEmail(externalEmail, externalId); trx.Complete(); } else if (userInfo.ExternalId != externalId) { // Note: we will assume that no identity provider can provider the same email twice with // two different external Ids, i.e. that no provider allows email recycling, so we won't handle this case now // This can only happen if the application is re-configured to a new identity provider, or if someone messed with // the database directly context.Result = new BadRequestObjectResult("The sign-in email already exists but with a different external Id"); return; } // (4) If the user's email address has changed at the identity server, update it locally else if (userInfo.Email != externalEmail) { using var trx = ControllerUtilities.CreateTransaction(); await _adminRepo.AdminUsers__SetEmailByUserId(userId, externalEmail); await _adminRepo.DirectoryUsers__SetEmailByExternalId(externalId, externalEmail); trx.Complete(); } // (5) If any version headers are supplied: examine their freshness { // Permissions var clientVersion = context.HttpContext.Request.Headers["X-Admin-Permissions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.PermissionsVersion; context.HttpContext.Response.Headers.Add("x-admin-permissions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // User Settings var clientVersion = context.HttpContext.Request.Headers["X-Admin-User-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.UserSettingsVersion; context.HttpContext.Response.Headers.Add("x-admin-user-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Settings var clientVersion = context.HttpContext.Request.Headers["X-Admin-Settings-Version"].FirstOrDefault(); var adminInfo = new { SettingsVersion = clientVersion }; // await _adminRepo.GetAdminInfoAsync(); // TODO if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = adminInfo.SettingsVersion; context.HttpContext.Response.Headers.Add("x-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } // Finally call the Action itself await next(); }
protected override async Task <IQueryable <AgentForQuery> > ApplyReadPermissionsCriteria(IQueryable <AgentForQuery> query, IEnumerable <AbstractPermission> permissions) { if (ViewId() != ALL) { return(await base.ApplyReadPermissionsCriteria(query, permissions)); } else { // Get all permissions related to agents var allPermissions = await ControllerUtilities.GetPermissions(_db.AbstractPermissions, PermissionLevel.Read, _agentTypes); if (!allPermissions.Any()) { // User doesn't have access to any type of agent throw new ForbiddenException(); } else if (allPermissions.Any(e => e.ViewId == ALL)) { // Optimization return(query); } else if (_agentTypes.All(t => allPermissions.Any(e => e.ViewId == t && string.IsNullOrWhiteSpace(e.Criteria)))) { // this might be risky if the developer forgets to add an agent type in 'agentTypes' array return(query); } else { /* IF we reach here it means the user can only see a filtered list of agents * The purpose of the code below is to construct a dynamic linq query that looks like this: * * e => * (e.AgentType == "individuals" && <dynamic linq for individuals>) || * (e.AgentType == "organizations" && <dynamic linq for organizations>) || * */ // The parameter on which the dynamic LINQ expression is based var eParam = Expression.Parameter(typeof(AgentForQuery)); Expression fullExpression = null; foreach (var g in allPermissions.GroupBy(e => e.ViewId)) { string viewId = g.Key; Expression typePropAccess = Expression.Property(eParam, nameof(AgentForQuery.AgentType)); Expression viewIdConstant = Expression.Constant(viewId); Expression typePropEquality = Expression.Equal(typePropAccess, viewIdConstant); Expression viewIdExpression; if (g.Any(e => string.IsNullOrWhiteSpace(e.Criteria))) { // The user can read all records of this type viewIdExpression = typePropEquality; } else { // The user has access to part of the data set based on a list of filters that will // be ORed together in a dynamic linq query IEnumerable <string> criteriaList = g.Select(e => e.Criteria); // First criteria viewIdExpression = _filterParser.ParseFilterExpression <AgentForQuery>(criteriaList.First(), eParam); // The remaining criteria foreach (var criteria in criteriaList.Skip(1)) { var criteriaExpression = _filterParser.ParseFilterExpression <AgentForQuery>(criteria, eParam); viewIdExpression = Expression.OrElse(viewIdExpression, criteriaExpression); } viewIdExpression = Expression.AndAlso(typePropEquality, viewIdExpression); } // OR this viewId expression with the remaining viewId expressions fullExpression = fullExpression == null ? viewIdExpression : Expression.OrElse(fullExpression, viewIdExpression); } var lambda = Expression.Lambda <Func <AgentForQuery, bool> >(fullExpression, eParam); return(query.Where(lambda)); } } }
protected override Task <List <LineDefinitionForSave> > SavePreprocessAsync(List <LineDefinitionForSave> entities) { var settings = _settingsCache.GetCurrentSettingsIfCached().Data; entities.ForEach(lineDefinition => { lineDefinition.AllowSelectiveSigning ??= false; lineDefinition.ViewDefaultsToForm ??= false; lineDefinition.BarcodeBeepsEnabled ??= false; lineDefinition.Columns ??= new List <LineDefinitionColumnForSave>(); lineDefinition.Entries ??= new List <LineDefinitionEntryForSave>(); lineDefinition.GenerateParameters ??= new List <LineDefinitionGenerateParameterForSave>(); lineDefinition.StateReasons ??= new List <LineDefinitionStateReasonForSave>(); lineDefinition.Workflows ??= new List <WorkflowForSave>(); lineDefinition?.Columns.ForEach(column => { // Those two are required in the sql table, so they cannot be null if (column.ColumnName == nameof(Entry.CenterId)) { column.VisibleState = LineState.Draft; column.RequiredState = LineState.Draft; } if (column.ColumnName == nameof(Entry.CurrencyId)) { column.VisibleState = LineState.Draft; column.RequiredState = LineState.Draft; } // IMPORTANT: Keep in sync with line-definitions-details.component.ts switch (column.ColumnName) { case "PostingDate": case "Memo": case "CurrencyId": case "CenterId": case "CustodianId": case "CustodyId": case "ParticipantId": case "ResourceId": case "Quantity": case "UnitId": case "Time1": case "Time2": case "ExternalReference": case "InternalReference": break; default: column.InheritsFromHeader = 0; // Only listed columns can inherit break; } if (column.ColumnName == null || !column.ColumnName.EndsWith("Id")) { column.Filter = null; // Only listed columns can inherit } }); // Generate Parameters lineDefinition.GenerateParameters.ForEach(parameter => { parameter.ControlOptions = ControllerUtilities.PreprocessControlOptions(parameter.Control, parameter.ControlOptions, settings); }); // Workflows lineDefinition?.Workflows.ForEach(workflow => { workflow?.Signatures?.ForEach(signature => { if (signature != null) { if (signature.RuleType != RuleTypes.ByUser) { signature.UserId = null; } if (signature.RuleType != RuleTypes.ByRole) { signature.RoleId = null; } if (signature.RuleType != RuleTypes.ByCustodian) { signature.RuleTypeEntryIndex = null; } if (signature.RuleType == RuleTypes.Public) { signature.ProxyRoleId = null; } if (signature.PredicateType == null) { signature.PredicateTypeEntryIndex = null; signature.Value = null; } } }); }); }); return(Task.FromResult(entities)); }
protected override async Task ValidateAsync(List <AgentForSave> entities) { // Get the agent type from the context string agentType = ViewId(); // Hash the indices for performance var indices = entities.ToIndexDictionary(); // Check that Ids make sense in relation to EntityState, and that no entity is DELETED // All these errors indicate a bug foreach (var entity in entities) { if (entity.EntityState == EntityStates.Deleted) { // Won't be supported for this API var index = indices[entity]; ModelState.AddModelError($"[{index}].{nameof(entity.EntityState)}", _localizer["Error_Deleting0IsNotSupportedFromThisAPI", PluralName()]); } } // Check that Ids are unique var duplicateIds = entities.Where(e => e.Id != null).GroupBy(e => e.Id.Value).Where(g => g.Count() > 1); foreach (var groupWithDuplicateIds in duplicateIds) { foreach (var entity in groupWithDuplicateIds) { // This error indicates a bug var index = indices[entity]; ModelState.AddModelError($"[{index}].{nameof(entity.Id)}", _localizer["Error_TheEntityWithId0IsSpecifiedMoreThanOnce", entity.Id]); } } // No need to invoke SQL if the model state is full of errors if (ModelState.HasReachedMaxErrors) { return; } // Perform SQL-side validation DataTable entitiesTable = ControllerUtilities.DataTable(entities, addIndex: true); var entitiesTvp = new SqlParameter("Entities", entitiesTable) { TypeName = $"dbo.{nameof(AgentForSave)}List", SqlDbType = SqlDbType.Structured }; int remainingErrorCount = ModelState.MaxAllowedErrors - ModelState.ErrorCount; // (1) Code must be unique var sqlErrors = await _db.Validation.FromSql($@" SET NOCOUNT ON; DECLARE @ValidationErrors [dbo].[ValidationErrorList]; INSERT INTO @ValidationErrors([Key], [ErrorName]) SELECT '[' + CAST([Id] AS NVARCHAR(255)) + '].Id' As [Key], N'Error_CannotModifyInactiveItem' As [ErrorName] FROM @Entities WHERE Id IN (SELECT Id from [dbo].[Custodies] WHERE IsActive = 0) OPTION(HASH JOIN); -- Non Null Ids must exist INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1]) SELECT '[' + CAST([Id] AS NVARCHAR(255)) + '].Id' As [Key], N'Error_TheId0WasNotFound' As [ErrorName], CAST([Id] As NVARCHAR(255)) As [Argument1] FROM @Entities WHERE Id Is NOT NULL AND Id NOT IN (SELECT Id from [dbo].[Custodies] WHERE CustodyType = 'Agent' AND AgentType = '{agentType}') -- Code must be unique INSERT INTO @ValidationErrors([Key], [ErrorName], [Argument1], [Argument2], [Argument3], [Argument4], [Argument5]) SELECT '[' + CAST(FE.[Index] AS NVARCHAR(255)) + '].Code' As [Key], N'Error_TheCode0IsUsed' As [ErrorName], FE.Code AS Argument1, NULL AS Argument2, NULL AS Argument3, NULL AS Argument4, NULL AS Argument5 FROM @Entities FE JOIN [dbo].[Custodies] BE ON FE.Code = BE.Code WHERE (FE.Id IS NULL) OR (FE.Id <> BE.Id); SELECT TOP {remainingErrorCount} * FROM @ValidationErrors; ", entitiesTvp).ToListAsync(); // Loop over the errors returned from SQL and add them to ModelState foreach (var sqlError in sqlErrors) { var formatArguments = sqlError.ToFormatArguments(); string key = sqlError.Key; string errorMessage = _localizer[sqlError.ErrorName, formatArguments]; ModelState.AddModelError(key: key, errorMessage: errorMessage); } }
protected override Expression ParseSpecialFilterKeyword(string keyword, ParameterExpression param) { return(ControllerUtilities.CreatedByMeFilter <M.Role>(keyword, param, _tenantInfoAccessor.GetCurrentInfo().UserId.Value)); }
protected override async Task <(List <AgentForQuery>, IQueryable <AgentForQuery>)> PersistAsync(List <AgentForSave> entities, SaveArguments args) { // Some properties are always set to null for organizations string agentType = ViewId(); if (agentType == ORGANIZATION) { entities.ForEach(e => { e.Title = null; e.Title2 = null; e.Gender = null; }); } // Add created entities DataTable entitiesTable = ControllerUtilities.DataTable(entities, addIndex: true); var entitiesTvp = new SqlParameter("Entities", entitiesTable) { TypeName = $"dbo.{nameof(AgentForSave)}List", SqlDbType = SqlDbType.Structured }; // The agent type var agentTypeParameter = new SqlParameter("AgentType", agentType); string saveSql = $@" -- Procedure: AgentsForSave SET NOCOUNT ON; DECLARE @IndexedIds [dbo].[IndexedIdList]; DECLARE @TenantId int = CONVERT(INT, SESSION_CONTEXT(N'TenantId')); DECLARE @Now DATETIMEOFFSET(7) = SYSDATETIMEOFFSET(); DECLARE @UserId INT = CONVERT(INT, SESSION_CONTEXT(N'UserId')); -- Deletions DELETE FROM [dbo].[Custodies] WHERE [Id] IN (SELECT [Id] FROM @Entities WHERE [EntityState] = N'Deleted'); INSERT INTO @IndexedIds([Index], [Id]) SELECT x.[Index], x.[Id] FROM ( MERGE INTO [dbo].[Custodies] AS t USING ( SELECT [Index], [Id], [Name], [Name2], [Code], [Address], [BirthDateTime], [IsRelated], [TaxIdentificationNumber], [Title], [Title2], [Gender] FROM @Entities WHERE [EntityState] IN (N'Inserted', N'Updated') ) AS s ON (t.Id = s.Id) WHEN MATCHED THEN UPDATE SET t.[Name] = s.[Name], t.[Name2] = s.[Name2], t.[Code] = s.[Code], t.[Address] = s.[Address], t.[BirthDateTime] = s.[BirthDateTime], t.[IsRelated] = s.[IsRelated], t.[TaxIdentificationNumber] = s.[TaxIdentificationNumber], t.[Title] = s.[Title], t.[Title2] = s.[Title2], t.[Gender] = s.[Gender], t.[ModifiedAt] = @Now, t.[ModifiedById] = @UserId WHEN NOT MATCHED THEN INSERT ([TenantId], [CustodyType], [Name], [Name2], [Code], [Address], [BirthDateTime], [AgentType], [IsRelated], [TaxIdentificationNumber], [Title], [Title2], [Gender], [CreatedAt], [CreatedById], [ModifiedAt], [ModifiedById]) VALUES (@TenantId, 'Agent', s.[Name], s.[Name2], s.[Code], s.[Address], s.[BirthDateTime], @AgentType, s.[IsRelated], s.[TaxIdentificationNumber], s.[Title], [Title2], s.[Gender], @Now, @UserId, @Now, @UserId) OUTPUT s.[Index], inserted.[Id] ) AS x; "; // Optimization if (!(args.ReturnEntities ?? false)) { // IF no returned items are expected, simply execute a non-Query and return an empty list; await _db.Database.ExecuteSqlCommandAsync(saveSql, entitiesTvp, agentTypeParameter); return(new List <AgentForQuery>(), null); } else { // If returned items are expected, append a select statement to the SQL command saveSql = saveSql += "SELECT * FROM @IndexedIds;"; // Retrieve the map from Indexes to Ids var indexedIds = await _db.Saving.FromSql(saveSql, entitiesTvp, agentTypeParameter).ToListAsync(); //// Load the entities using their Ids //DataTable idsTable = DataTable(indexedIds.Select(e => new { e.Id }), addIndex: false); //var idsTvp = new SqlParameter("Ids", idsTable) //{ // TypeName = $"dbo.IdList", // SqlDbType = SqlDbType.Structured //}; // var q = _db.Agents.FromSql("SELECT * FROM dbo.[Custodies] WHERE Id IN (SELECT Id FROM @Ids)", idsTvp); var ids = indexedIds.Select(e => e.Id); var q = _db.VW_Agents.Where(e => ids.Contains(e.Id.Value)); q = Expand(q, args.Expand); var savedEntities = await q.ToListAsync(); // SQL Server does not guarantee order, so make sure the result is sorted according to the initial index Dictionary <int, int> indices = indexedIds.ToDictionary(e => e.Id, e => e.Index); var sortedSavedEntities = new AgentForQuery[savedEntities.Count]; foreach (var item in savedEntities) { int index = indices[item.Id.Value]; sortedSavedEntities[index] = item; } // Return the sorted collection return(sortedSavedEntities.ToList(), q); } }
public static string GetInsertView(string controller) { ControllerUtilities c = new ControllerUtilities(); return(c.GetActionView(controller, "createForm1", "Insert")); }
public async Task <ActionResult <EntitiesResponse <Agent> > > Deactivate([FromBody] List <int> ids, [FromQuery] DeactivateArguments <int> args) { return(await ControllerUtilities.ExecuteAndHandleErrorsAsync(() => ActivateDeactivate(ids, args.ReturnEntities ?? false, args.Expand, isActive : false) , _logger)); }
public async Task <ActionResult <ProjectTask> > Get(int id) { var result = await _taskRepository.Get(id); return(ControllerUtilities.CheckResult(result)); }
protected override async Task <(List <AgentForSave>, Func <string, int?>)> ToDtosForSave(AbstractDataGrid grid, ParseArguments args) { // Get the properties of the DTO for Save, excluding Id or EntityState string mode = args.Mode; var readType = typeof(Agent); var custodySaveType = typeof(CustodyForSave); var agentSaveType = typeof(AgentForSave); var readProps = readType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .ToDictionary(prop => _metadataProvider.GetMetadataForProperty(readType, prop.Name)?.DisplayName ?? prop.Name, StringComparer.InvariantCultureIgnoreCase); var orgExemptProperties = new string[] { nameof(Agent.Title), nameof(Agent.Title2), nameof(Agent.Gender) }; var saveProps = custodySaveType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) .Union(agentSaveType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) .Where(e => ViewId() == INDIVIDUAL || orgExemptProperties.Contains(e.Name)) // Take away .ToDictionary(prop => _metadataProvider.GetMetadataForProperty(agentSaveType, prop.Name)?.DisplayName ?? prop.Name, StringComparer.InvariantCultureIgnoreCase); // Maps the index of the grid column to a property on the DtoForSave var saveColumnMap = new List <(int Index, PropertyInfo Property)>(grid.RowSize); // Make sure all column header labels are recognizable // and construct the save column map var firstRow = grid[0]; for (int c = 0; c < firstRow.Length; c++) { var column = firstRow[c]; string headerLabel = column.Content?.ToString(); // So any thing after an empty column is ignored if (string.IsNullOrWhiteSpace(headerLabel)) { break; } if (saveProps.ContainsKey(headerLabel)) { var prop = saveProps[headerLabel]; saveColumnMap.Add((c, prop)); } else if (readProps.ContainsKey(headerLabel)) { // All good, just ignore } else { AddRowError(1, _localizer["Error_Column0NotRecognizable", headerLabel]); } } // Milestone 1: columns in the abstract grid mapped if (!ModelState.IsValid) { throw new UnprocessableEntityException(ModelState); } // Construct the result using the map generated earlier List <AgentForSave> result = new List <AgentForSave>(grid.Count - 1); for (int i = 1; i < grid.Count; i++) // Skip the header { var row = grid[i]; // Anything after an empty row is ignored if (saveColumnMap.All((p) => string.IsNullOrWhiteSpace(row[p.Index].Content?.ToString()))) { break; } var entity = new AgentForSave(); foreach (var(index, prop) in saveColumnMap) { var content = row[index].Content; var propName = _metadataProvider.GetMetadataForProperty(readType, prop.Name).DisplayName; // Special handling for choice lists if (content != null) { var choiceListAttr = prop.GetCustomAttribute <ChoiceListAttribute>(); if (choiceListAttr != null) { List <string> displayNames = choiceListAttr.DisplayNames.Select(e => _localizer[e].Value).ToList(); string stringContent = content.ToString(); var displayNameIndex = displayNames.IndexOf(stringContent); if (displayNameIndex == -1) { string seperator = _localizer[", "]; AddRowError(i + 1, _localizer["Error_Value0IsNotValidFor1AcceptableValuesAre2", stringContent, propName, string.Join(seperator, displayNames)]); } else { content = choiceListAttr.Choices[displayNameIndex]; } } } // Special handling for DateTime and DateTimeOffset if (prop.PropertyType.IsDateOrTime()) { try { var date = ParseImportedDateTime(content); content = date; if (prop.PropertyType.IsDateTimeOffset()) { content = AddUserTimeZone(date); } } catch (Exception) { AddRowError(i + 1, _localizer["Error_TheValue0IsNotValidFor1Field", content?.ToString(), propName]); } } // Try setting the value and return an error if it doesn't work try { prop.SetValue(entity, content); } catch (ArgumentException) { AddRowError(i + 1, _localizer["Error_TheValue0IsNotValidFor1Field", content?.ToString(), propName]); } } result.Add(entity); } // Milestone 2: DTOs created if (!ModelState.IsValid) { throw new UnprocessableEntityException(ModelState); } // Prepare a dictionary of indices in order to construct any validation errors performantly // "IndexOf" is O(n), this brings it down to O(1) Dictionary <AgentForSave, int> indicesDic = result.ToIndexDictionary(); // For each entity, set the Id and EntityState depending on import mode if (mode == "Insert") { // For Insert mode, all are marked inserted and all Ids are null // Any duplicate codes will be handled later in the validation result.ForEach(e => e.Id = null); result.ForEach(e => e.EntityState = EntityStates.Inserted); } else { // For all other modes besides Insert, we need to match the entity codes to Ids by querying the DB // Load the code Ids from the database var nonNullCodes = result.Where(e => !string.IsNullOrWhiteSpace(e.Code)); var codesDataTable = ControllerUtilities.DataTable(nonNullCodes.Select(e => new { e.Code })); var entitiesTvp = new SqlParameter("@Codes", codesDataTable) { TypeName = $"dbo.CodeList", SqlDbType = SqlDbType.Structured }; string agentType = ViewId(); var idCodesDic = await _db.CodeIds.FromSql( $@"SELECT c.Code, e.Id FROM @Codes c JOIN [dbo].[Custodies] e ON c.Code = e.Code WHERE e.CustodyType = 'Agent' && e.AgentType == {agentType};" , entitiesTvp).ToDictionaryAsync(e => e.Code, e => e.Id); result.ForEach(e => { if (!string.IsNullOrWhiteSpace(e.Code) && idCodesDic.ContainsKey(e.Code)) { e.Id = idCodesDic[e.Code]; } else { e.Id = null; } }); // Make sure no codes are mentioned twice, if we don't do it here, the save validation later will complain // about duplicated Id, but the error will not be clear since user deals with code while importing from Excel var duplicateIdGroups = result.Where(e => e.Id != null).GroupBy(e => e.Id.Value).Where(g => g.Count() > 1); foreach (var duplicateIdGroup in duplicateIdGroups) { foreach (var entity in duplicateIdGroup) { int index = indicesDic[entity]; AddRowError(index + 2, _localizer["Error_TheCode0IsDuplicated", entity.Code]); } } if (mode == "Merge") { // Merge simply inserts codes that are not found, and updates codes that are found result.ForEach(e => { if (e.Id != null) { e.EntityState = EntityStates.Updated; } else { e.EntityState = EntityStates.Inserted; } }); } else { // In the case of update: codes are required, and MUST match database Ids if (mode == "Update") { for (int index = 0; index < result.Count; index++) { var entity = result[index]; if (string.IsNullOrWhiteSpace(entity.Code)) { AddRowError(index + 2, _localizer["Error_CodeIsRequiredForImportModeUpdate"]); } else if (entity.Id == null) { AddRowError(index + 2, _localizer["Error_TheCode0DoesNotExist", entity.Code]); } } result.ForEach(e => e.EntityState = EntityStates.Updated); } else { throw new InvalidOperationException("Unknown save mode"); // Developer bug } } } // Milestone 3: Id and EntityState are set if (!ModelState.IsValid) { throw new UnprocessableEntityException(ModelState); } // Function that maps any future validation errors back to specific rows int?errorKeyMap(string key) { int?rowNumber = null; if (key != null && key.StartsWith("[")) { var indexStr = key.TrimStart('[').Split(']')[0]; if (int.TryParse(indexStr, out int index)) { // Add 2: // 1 for the header in the abstract grid // 1 for the difference between index and number rowNumber = index + 2; } } return(rowNumber); } return(result, errorKeyMap); }
public static string GetInsertView(string controller) { ControllerUtilities c = new ControllerUtilities(); return c.GetActionView(controller, "createForm1", "Insert"); }
private async Task <ActionResult <EntitiesResponse <Agent> > > ActivateDeactivate([FromBody] List <int> ids, bool returnEntities, string expand, bool isActive) { await CheckActionPermissions(ids.Cast <int?>()); var isActiveParam = new SqlParameter("@IsActive", isActive); DataTable idsTable = ControllerUtilities.DataTable(ids.Select(id => new { Id = id }), addIndex: false); var idsTvp = new SqlParameter("@Ids", idsTable) { TypeName = $"dbo.IdList", SqlDbType = SqlDbType.Structured }; string sql = @" DECLARE @Now DATETIMEOFFSET(7) = SYSDATETIMEOFFSET(); DECLARE @UserId INT = CONVERT(INT, SESSION_CONTEXT(N'UserId')); MERGE INTO [dbo].[Custodies] AS t USING ( SELECT [Id] FROM @Ids ) AS s ON (t.Id = s.Id) WHEN MATCHED AND (t.IsActive <> @IsActive) THEN UPDATE SET t.[IsActive] = @IsActive, t.[ModifiedAt] = @Now, t.[ModifiedById] = @UserId; "; using (var trx = await _db.Database.BeginTransactionAsync()) { try { // Update the entities await _db.Database.ExecuteSqlCommandAsync(sql, idsTvp, isActiveParam); trx.Commit(); } catch (Exception ex) { trx.Rollback(); throw ex; } } // Determine whether entities should be returned if (!returnEntities) { // IF no returned items are expected, simply return 200 OK return(Ok()); } else { // Load the entities using their Ids var affectedDbEntitiesQ = _db.VW_Agents.Where(e => ids.Contains(e.Id.Value)); //.FromSql("SELECT * FROM [dbo].[Custodies] WHERE Id IN (SELECT Id FROM @Ids)", idsTvp); var affectedDbEntitiesExpandedQ = Expand(affectedDbEntitiesQ, expand); var affectedDbEntities = await affectedDbEntitiesExpandedQ.ToListAsync(); // Add the metadata ApplySelectAndAddMetadata(affectedDbEntities, expand, null); // sort the entities the way their Ids came, as a good practice var affectedEntities = Mapper.Map <List <Agent> >(affectedDbEntities); Agent[] sortedAffectedEntities = new Agent[ids.Count]; Dictionary <int, Agent> affectedEntitiesDic = affectedEntities.ToDictionary(e => e.Id.Value); for (int i = 0; i < ids.Count; i++) { var id = ids[i]; Agent entity = null; if (affectedEntitiesDic.ContainsKey(id)) { entity = affectedEntitiesDic[id]; } sortedAffectedEntities[i] = entity; } // Apply the permission masks (setting restricted fields to null) and adjust the metadata accordingly await ApplyReadPermissionsMask(affectedDbEntities, affectedDbEntitiesExpandedQ, await UserPermissions(PermissionLevel.Read), GetDefaultMask()); // Flatten related entities and map each to its respective DTO var relatedEntities = FlattenRelatedEntitiesAndTrim(affectedDbEntities, expand); // Prepare a proper response var response = new EntitiesResponse <Agent> { Data = sortedAffectedEntities, CollectionName = GetCollectionName(typeof(Agent)), RelatedEntities = relatedEntities }; // Commit and return return(Ok(response)); } }
public static string GetUpdateView(string controller) { ControllerUtilities c = new ControllerUtilities(); return c.GetActionView(controller, "editForm1", "Update"); }
private async Task <GetByIdResponse <User> > SaveMyUserImpl([FromBody] MyUserForSave me) { int myId = _appRepo.GetUserInfo().UserId.Value; var user = await _appRepo.Users.Expand("Roles").FilterByIds(myId).FirstOrDefaultAsync(); // Create a user for save var userForSave = new UserForSave { Id = user.Id, Email = user.Email, Name = me.Name?.Trim(), Name2 = me.Name2?.Trim(), Name3 = me.Name3?.Trim(), PreferredLanguage = me.PreferredLanguage?.Trim(), Image = me.Image, EntityMetadata = new EntityMetadata { [nameof(UserForSave.Id)] = FieldMetadata.Loaded, [nameof(UserForSave.Email)] = FieldMetadata.Loaded, [nameof(UserForSave.Name)] = FieldMetadata.Loaded, [nameof(UserForSave.Name2)] = FieldMetadata.Loaded, [nameof(UserForSave.Name3)] = FieldMetadata.Loaded, [nameof(UserForSave.PreferredLanguage)] = FieldMetadata.Loaded, [nameof(UserForSave.Image)] = FieldMetadata.Loaded }, // The roles must remain the way they are Roles = user.Roles?.Select(e => new RoleMembershipForSave { Id = e.Id, Memo = e.Memo, RoleId = e.RoleId, UserId = e.UserId, EntityMetadata = new EntityMetadata { [nameof(RoleMembershipForSave.Id)] = FieldMetadata.Loaded, [nameof(RoleMembershipForSave.Memo)] = FieldMetadata.Loaded, [nameof(RoleMembershipForSave.RoleId)] = FieldMetadata.Loaded, [nameof(RoleMembershipForSave.UserId)] = FieldMetadata.Loaded }, }) .ToList() }; var entities = new List <UserForSave>() { userForSave }; // Start a transaction scope for save since it causes data modifications using var trx = ControllerUtilities.CreateTransaction(null, GetSaveTransactionOptions()); // Validation await SaveValidateAsync(entities); if (!ModelState.IsValid) { // TODO map the errors throw new UnprocessableEntityException(ModelState); } // Save and retrieve response await SaveExecuteAsync(entities, null, false); var response = await GetMyUserImpl(); // Commit and return trx.Complete(); return(response); }