protected override DefaultMetadataDetails[] CreatePropertyDetails(ModelMetadataIdentity key) { // Call the base implementation var propsDetails = base.CreatePropertyDetails(key); // Customize the label of Resource properties if (key.ModelType.IsSameOrSubClassOf <ResourceForSave>()) { // Get the route data from http context // Loop over the properties and special treatment to the dynamic ones foreach (var propDetails in propsDetails) { var defaultName = propDetails.ModelAttributes.PropertyAttributes .OfType <DisplayAttribute>().FirstOrDefault()?.Name ?? propDetails.Key.Name; DisplayMetadata displayMetadata; switch (propDetails.Key.Name) { case nameof(Resource.Identifier): displayMetadata = LocalizeResourceProperty( e => e.IdentifierVisibility, e => e.IdentifierLabel, e => e.IdentifierLabel2, e => e.IdentifierLabel3, defaultName); break; // All dynamically labelled properties case nameof(Resource.Currency): case nameof(Resource.CurrencyId): displayMetadata = LocalizeResourceProperty( e => e.CurrencyVisibility, e => e.CurrencyLabel, e => e.CurrencyLabel2, e => e.CurrencyLabel3, defaultName); break; case nameof(Resource.MonetaryValue): displayMetadata = LocalizeResourceProperty( e => e.MonetaryValueVisibility, e => e.MonetaryValueLabel, e => e.MonetaryValueLabel2, e => e.MonetaryValueLabel3, defaultName); break; case nameof(Resource.CountUnit): case nameof(Resource.CountUnitId): displayMetadata = LocalizeResourceProperty( e => e.CountUnitVisibility, e => e.CountUnitLabel, e => e.CountUnitLabel2, e => e.CountUnitLabel3, defaultName); break; case nameof(Resource.Count): displayMetadata = LocalizeResourceProperty( e => e.CountVisibility, e => e.CountLabel, e => e.CountLabel2, e => e.CountLabel3, defaultName); break; case nameof(Resource.MassUnit): case nameof(Resource.MassUnitId): displayMetadata = LocalizeResourceProperty( e => e.MassUnitVisibility, e => e.MassUnitLabel, e => e.MassUnitLabel2, e => e.MassUnitLabel3, defaultName); break; case nameof(Resource.Mass): displayMetadata = LocalizeResourceProperty( e => e.MassVisibility, e => e.MassLabel, e => e.MassLabel2, e => e.MassLabel3, defaultName); break; case nameof(Resource.VolumeUnit): case nameof(Resource.VolumeUnitId): displayMetadata = LocalizeResourceProperty( e => e.VolumeUnitVisibility, e => e.VolumeUnitLabel, e => e.VolumeUnitLabel2, e => e.VolumeUnitLabel3, defaultName); break; case nameof(Resource.Volume): displayMetadata = LocalizeResourceProperty( e => e.VolumeVisibility, e => e.VolumeLabel, e => e.VolumeLabel2, e => e.VolumeLabel3, defaultName); break; case nameof(Resource.TimeUnit): case nameof(Resource.TimeUnitId): displayMetadata = LocalizeResourceProperty (e => e.TimeUnitVisibility, e => e.TimeUnitLabel, e => e.TimeUnitLabel2, e => e.TimeUnitLabel3, defaultName); break; case nameof(Resource.Time): displayMetadata = LocalizeResourceProperty( e => e.TimeVisibility, e => e.TimeLabel, e => e.TimeLabel2, e => e.TimeLabel3, defaultName); break; case nameof(Resource.AvailableSince): displayMetadata = LocalizeResourceProperty( e => e.AvailableSinceVisibility, e => e.AvailableSinceLabel, e => e.AvailableSinceLabel2, e => e.AvailableSinceLabel3, defaultName); break; case nameof(Resource.AvailableTill): displayMetadata = LocalizeResourceProperty( e => e.AvailableTillVisibility, e => e.AvailableTillLabel, e => e.AvailableTillLabel2, e => e.AvailableTillLabel3, defaultName); break; case nameof(Resource.Lookup1): case nameof(Resource.Lookup1Id): displayMetadata = LocalizeResourceProperty( e => e.Lookup1Visibility, e => e.Lookup1Label, e => e.Lookup1Label2, e => e.Lookup1Label3, defaultName); break; case nameof(Resource.Lookup2): case nameof(Resource.Lookup2Id): displayMetadata = LocalizeResourceProperty( e => e.Lookup2Visibility, e => e.Lookup2Label, e => e.Lookup2Label2, e => e.Lookup2Label3, defaultName); break; //case nameof(Resource.Lookup3): //case nameof(Resource.Lookup3Id): // displayMetadata = LocalizeResourceProperty(e => e.Lookup3Visibility, e => e.Lookup3Label, e => e.Lookup3Label2, e => e.Lookup3Label3, defaultName); // break; //case nameof(Resource.Lookup4): //case nameof(Resource.Lookup4Id): // displayMetadata = LocalizeResourceProperty(e => e.Lookup4Visibility, e => e.Lookup4Label, e => e.Lookup4Label2, e => e.Lookup4Label3, defaultName); // break; //case nameof(Resource.Lookup5): //case nameof(Resource.Lookup5Id): // displayMetadata = LocalizeResourceProperty(e => e.Lookup5Visibility, e => e.Lookup5Label, e => e.Lookup5Label2, e => e.Lookup5Label3, defaultName); // break; default: displayMetadata = null; break; } propDetails.DisplayMetadata = displayMetadata; } } // Customize the label of Agent properties if (key.ModelType.IsSameOrSubClassOf <AgentForSave>()) { // Get the route data from http context // Loop over the properties and special treatment to the dynamic ones foreach (var propDetails in propsDetails) { var defaultName = propDetails.ModelAttributes.PropertyAttributes .OfType <DisplayAttribute>().FirstOrDefault()?.Name ?? propDetails.Key.Name; var displayMetadata = propDetails.Key.Name switch { // All dynamically labelled properties nameof(Agent.TaxIdentificationNumber) => LocalizeAgentSpecificProperty(e => e.TaxIdentificationNumberVisibility, defaultName), nameof(Agent.StartDate) => LocalizeAgentProperty(e => e.StartDateVisibility, e => e.StartDateLabel, e => e.StartDateLabel2, e => e.StartDateLabel3, defaultName), nameof(Agent.JobId) => LocalizeAgentSpecificProperty(e => e.JobVisibility, defaultName),// case nameof(Agent.Job): TODO nameof(Agent.BasicSalary) => LocalizeAgentSpecificProperty(e => e.BasicSalaryVisibility, defaultName), nameof(Agent.TransportationAllowance) => LocalizeAgentSpecificProperty(e => e.TransportationAllowanceVisibility, defaultName), nameof(Agent.OvertimeRate) => LocalizeAgentSpecificProperty(e => e.OvertimeRateVisibility, defaultName), nameof(Agent.BankAccountNumber) => LocalizeAgentSpecificProperty(e => e.BankAccountNumberVisibility, defaultName), _ => null, }; propDetails.DisplayMetadata = displayMetadata; } } // In general: append the language name to the labels of multilingual foreach (var propDetails in propsDetails) { var att = propDetails.ModelAttributes.PropertyAttributes .OfType <MultilingualDisplayAttribute>().FirstOrDefault(); if (att != null) { var name = att.Name ?? ""; var lang = att.Language; propDetails.DisplayMetadata = new DisplayMetadata { DisplayName = () => { var info = _tenantInfoAccessor.GetCurrentInfo(); if (info == null) { // Developer mistake throw new InvalidOperationException("TenantInfo is not set"); } var result = lang switch { Language.Primary => _localizer[name] + PrimaryPostfix(info), Language.Secondary => string.IsNullOrWhiteSpace(info.SecondaryLanguageId) ? Constants.HIDDEN_FIELD : _localizer[name] + SecondaryPostfix(info), Language.Ternary => string.IsNullOrWhiteSpace(info.TernaryLanguageId) ? Constants.HIDDEN_FIELD : _localizer[name] + TernaryPostfix(info), _ => _localizer[name], }; ; return(result); } }; } } return(propsDetails); } DisplayMetadata LocalizeResourceProperty( Func <ResourceDefinitionForClient, string> visibilityFunc, Func <ResourceDefinitionForClient, string> s1Func, Func <ResourceDefinitionForClient, string> s2Func, Func <ResourceDefinitionForClient, string> s3Func, string defaultDisplayName) { return(LocalizeProperty( (tenantId, definitionId) => _definitionsCache.GetDefinitionsIfCached(tenantId)?.Data?.Resources?.GetValueOrDefault(definitionId), visibilityFunc, s1Func, s2Func, s3Func, defaultDisplayName)); } DisplayMetadata LocalizeAgentProperty( Func <AgentDefinitionForClient, string> visibilityFunc, Func <AgentDefinitionForClient, string> s1Func, Func <AgentDefinitionForClient, string> s2Func, Func <AgentDefinitionForClient, string> s3Func, string defaultDisplayName) { return(LocalizeProperty( (tenantId, definitionId) => _definitionsCache.GetDefinitionsIfCached(tenantId)?.Data?.Agents?.GetValueOrDefault(definitionId), visibilityFunc, s1Func, s2Func, s3Func, defaultDisplayName)); } DisplayMetadata LocalizeAgentSpecificProperty( Func <AgentDefinitionForClient, string> visibilityFunc, string defaultDisplayName) { return(LocalizeProperty( (tenantId, definitionId) => _definitionsCache.GetDefinitionsIfCached(tenantId)?.Data?.Agents?.GetValueOrDefault(definitionId), visibilityFunc, e => null, e => null, e => null, defaultDisplayName)); } DisplayMetadata LocalizeProperty <TDefinitionForClient>( Func <int, string, TDefinitionForClient> definitionFunc, Func <TDefinitionForClient, string> visibilityFunc, Func <TDefinitionForClient, string> s1Func, Func <TDefinitionForClient, string> s2Func, Func <TDefinitionForClient, string> s3Func, string defaultDisplayName) { return(new DisplayMetadata { // Return a dynamic display name from the definitions, and fall back to // the default if non are available. Be as forgiving as possible DisplayName = () => { string result = _localizer[defaultDisplayName]; var routeData = _httpContextAccessor.HttpContext.GetRouteData(); var definitionId = routeData.Values["definitionId"]?.ToString(); if (!string.IsNullOrWhiteSpace(definitionId)) { var tenantId = _tenantIdAccessor.GetTenantId(); var definition = definitionFunc(tenantId, definitionId); if (definition != null) { if (visibilityFunc(definition) == null) { result = Constants.HIDDEN_FIELD; } else { result = _tenantInfoAccessor.GetCurrentInfo().Localize( s1Func(definition), s2Func(definition), s3Func(definition)) ?? result; } } } return result; } }); }
public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { // (1) Make sure the API caller have provided a tenantId, and extract it try { var cancellation = context.HttpContext.RequestAborted; int tenantId = _tenantIdAccessor.GetTenantId(); // Init the database connection... // The client sometimes makes ambient API calls, not in response to user interaction // Such calls should not update LastAccess of that user bool silent = context.HttpContext.Request.Query["silent"].FirstOrDefault()?.ToString()?.ToLower() == "true"; await _appRepo.InitConnectionAsync(tenantId, setLastActive : !silent, cancellation); // (2) Make sure the user is a member of this tenant UserInfo userInfo = await _appRepo.GetUserInfoAsync(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-settings-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-definitions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-permissions-version", Constants.Unauthorized); context.HttpContext.Response.Headers.Add("x-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) { // Update external Id in this tenant database await _appRepo.Users__SetExternalIdByUserId(userId, externalId); // Update external Id in the central Admin database too (To avoid an awkward situation // where a user exists on the tenant but not on the Admin db, if they change their email in between) var adminRepo = _serviceProvider.GetRequiredService <AdminRepository>(); await adminRepo.DirectoryUsers__SetExternalIdByEmail(externalEmail, externalId); } else if (userInfo.ExternalId != externalId) { // Note: there is the edge case of identity providers who allow email recycling. I.e. we can get the same email twice with // two different external Ids. This issue is so unlikely to naturally occur and cause problems here that we are not going // to handle it for now. It can however happen artificually if the application is re-configured to a new identity provider, // or if someone messed with the identity database directly, but again out of scope for now. 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) { await _appRepo.Users__SetEmailByUserId(userId, externalEmail); } // (5) Set the tenant info in the context, to make it accessible for model metadata providers var tenantInfo = await _appRepo.GetTenantInfoAsync(cancellation); _tenantInfoAccessor.SetInfo(tenantId, tenantInfo); // (6) Ensure the freshness of the definitions and settings caches { var databaseVersion = tenantInfo.DefinitionsVersion; var serverVersion = _definitionsCache.GetDefinitionsIfCached(tenantId)?.Version; if (serverVersion == null || serverVersion != databaseVersion) { // Update the cache var definitions = await DefinitionsService.LoadDefinitionsForClient(_appRepo, cancellation); if (!cancellation.IsCancellationRequested) { _definitionsCache.SetDefinitions(tenantId, definitions); } } } { var databaseVersion = tenantInfo.SettingsVersion; var serverVersion = _settingsCache.GetSettingsIfCached(tenantId)?.Version; if (serverVersion == null || serverVersion != databaseVersion) { // Update the cache var settings = await GeneralSettingsService.LoadSettingsForClient(_appRepo, cancellation); if (!cancellation.IsCancellationRequested) { _settingsCache.SetSettings(tenantId, settings); } } } // (7) If any version headers are supplied: examine their freshness { // Permissions var clientVersion = context.HttpContext.Request.Headers["X-Permissions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.PermissionsVersion; context.HttpContext.Response.Headers.Add("x-permissions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // User Settings var clientVersion = context.HttpContext.Request.Headers["X-User-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = userInfo.UserSettingsVersion; context.HttpContext.Response.Headers.Add("x-user-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Definitions var clientVersion = context.HttpContext.Request.Headers["X-Definitions-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = tenantInfo.DefinitionsVersion; context.HttpContext.Response.Headers.Add("x-definitions-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } { // Settings var clientVersion = context.HttpContext.Request.Headers["X-Settings-Version"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(clientVersion)) { var databaseVersion = tenantInfo.SettingsVersion; context.HttpContext.Response.Headers.Add("x-settings-version", clientVersion == databaseVersion ? Constants.Fresh : Constants.Stale); } } // Call the Action itself await next(); } catch (TaskCanceledException) { context.Result = new OkResult(); return; } catch (MultitenancyException ex) { // If the tenant Id is not provided cut the pipeline short and return a Bad Request 400 context.Result = new BadRequestObjectResult(ex.Message); return; } catch (BadRequestException ex) { // If the tenant Id is not provided cut the pipeline short and return a Bad Request 400 context.Result = new BadRequestObjectResult(ex.Message); return; } catch (Exception ex) { // TODO: Return to logging and 500 status code context.Result = new BadRequestObjectResult(ex.GetType().Name + ": " + ex.Message); //_logger.LogError(ex.Message); //context.Result = new StatusCodeResult(StatusCodes.Status500InternalServerError); return; } }