Botframework V4: Cosmos DB config - c#

Hello i am having a hard time configuring my cosmos db to botframework.Before when using memory storage it is working fine. I am reading this and this as a guide. I included the errors with comments within the codes. Can anyone help me with this. I would greatly appreciate the help. I have been researching this for 3 days already. Thank you!
public class Startup
{
private const string CosmosServiceEndpoint = "xxxxxxxxxxx";
private const string CosmosDBKey = "xxxxxxxxxxx";
private const string CosmosDBDatabaseName = "xxxxxxxxxxx";
private const string CosmosDBCollectionNameConState = "conversationState";
private const string CosmosDBCollectionNameUserState = "userState";
private ILoggerFactory _loggerFactory;
private bool _isProduction = false;
public Startup(IHostingEnvironment env)
{
_isProduction = env.IsProduction();
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<BasicBot>(options =>
{
ILogger logger = _loggerFactory.CreateLogger<BasicBot>();
var secretKey = Configuration.GetSection("botFileSecret")?.Value;
var botFilePath = Configuration.GetSection("botFilePath")?.Value;
if (!File.Exists(botFilePath))
{
throw new FileNotFoundException($"The .bot configuration file was not found. botFilePath: {botFilePath}");
}
BotConfiguration botConfig = null;
try
{
botConfig = BotConfiguration.Load(botFilePath ?? #".\echo-with-counter.bot", secretKey);
}
catch
{
var msg = #"Error reading bot file. Please ensure you have valid botFilePath and botFileSecret set for your environment.
- You can find the botFilePath and botFileSecret in the Azure App Service application settings.
- If you are running this bot locally, consider adding a appsettings.json file with botFilePath and botFileSecret.
- See https://aka.ms/about-bot-file to learn more about .bot file its use and bot configuration.
";
logger.LogError(msg);
throw new InvalidOperationException(msg);
}
services.AddSingleton(sp => botConfig);
var environment = _isProduction ? "production" : "development";
var service = botConfig.Services.FirstOrDefault(s => s.Type == "endpoint" && s.Name == environment);
if (service == null && _isProduction)
{
service = botConfig.Services.Where(s => s.Type == "endpoint" && s.Name == "development").FirstOrDefault();
logger.LogWarning("Attempting to load development endpoint in production environment.");
}
if (!(service is EndpointService endpointService))
{
throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
}
options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};
// The Memory Storage used here is for local bot debugging only. When the bot
// is restarted, everything stored in memory will be gone.
// IStorage dataStore = new MemoryStorage();
// error : COSMOSDBSTORAGE DOES NOT CONTAIN CONSTRUCTOR TAKES 4 ARGUMENTS
//IStorage dataStoreConversationState =
// new CosmosDbStorage(
// uri,
// "** auth key **",
// "helloworldbot",
// "conversationstate");
var uri = new Uri(CosmosServiceEndpoint);
IStorage dataStoreConversationState =
new CosmosDbStorage(new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBCollectionNameConState,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
});
IStorage dataStoreUserState =
new CosmosDbStorage(new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBCollectionNameUserState,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
});
//error : THE NON GENERIC TYPE "CONVERSATIONsTATE" CANNOT BE USED WITH TYPED ARGUMENTS
options.Middleware.Add(new ConversationState<BasicState>(dataStoreConversationState));
options.Middleware.Add(new UserState<BasicUserState>(dataStoreUserState));
}

There's a good chance that the reason this isn't working for you is because both of those links mention you need to create a New Collection in your CosmosDB resource in Azure. Microsoft recently updated the CosmosDB resource to require that new collections are made with Partition Keys, which aren't yet supported in Bot Framework. There's currently a Design Change Request to add this ability, but it's being stalled by the C# Cosmos SDK.
In the meantime, start by making the Cosmos resource in Azure and DO NOT make a database or collection. ONLY make the Cosmos resource. The bot framework SDK is set up to make a new DB and collection if the one you specify doesn't exist, and it can make one without partitions...so let the bot do the work here.
I used the second link you posted to change the Simple Prompt bot sample to work with Cosmos. Note: The endpoint and key are the default ones for the CosmosDB Emulator, which you can use to test locally, if you prefer.
Here is my startup.cs:
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Azure;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Builder.Integration;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Configuration;
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.BotBuilderSamples
{
/// <summary>
/// The Startup class configures services and the app's request pipeline.
/// </summary>
public class Startup
{
private const string CosmosServiceEndpoint = "https://localhost:8081";
private const string CosmosDBKey = "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";
private const string CosmosDBDatabaseName = "bot-cosmos-sql-db";
private const string CosmosDBCollectionName = "bot-storage";
private static readonly CosmosDbStorage _myStorage = new CosmosDbStorage(new CosmosDbStorageOptions
{
AuthKey = CosmosDBKey,
CollectionId = CosmosDBCollectionName,
CosmosDBEndpoint = new Uri(CosmosServiceEndpoint),
DatabaseId = CosmosDBDatabaseName,
});
private ILoggerFactory _loggerFactory;
private bool _isProduction = false;
public Startup(IHostingEnvironment env)
{
_isProduction = env.IsProduction();
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
}
/// <summary>
/// Gets the configuration that represents a set of key/value application configuration properties.
/// </summary>
/// <value>
/// The <see cref="IConfiguration"/> that represents a set of key/value application configuration properties.
/// </value>
public IConfiguration Configuration { get; }
/// <summary>
/// This method gets called by the runtime. Use this method to add services to the container.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> specifies the contract for a collection of service descriptors.</param>
/// <seealso cref="IStatePropertyAccessor{T}"/>
/// <seealso cref="https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/dependency-injection"/>
/// <seealso cref="https://learn.microsoft.com/en-us/azure/bot-service/bot-service-manage-channels?view=azure-bot-service-4.0"/>
public void ConfigureServices(IServiceCollection services)
{
services.AddBot<SimplePromptBot>(options =>
{
var secretKey = Configuration.GetSection("botFileSecret")?.Value;
var botFilePath = Configuration.GetSection("botFilePath")?.Value;
if (!File.Exists(botFilePath))
{
throw new FileNotFoundException($"The .bot configuration file was not found. botFilePath: {botFilePath}");
}
// Loads .bot configuration file and adds a singleton that your Bot can access through dependency injection.
var botConfig = BotConfiguration.Load(botFilePath ?? #".\simple-prompt.bot", secretKey);
services.AddSingleton(sp => botConfig ?? throw new InvalidOperationException($"The .bot configuration file could not be loaded. botFilePath: {botFilePath}"));
// Retrieve current endpoint.
var environment = _isProduction ? "production" : "development";
var service = botConfig.Services.FirstOrDefault(s => s.Type == "endpoint" && s.Name == environment);
if (!(service is EndpointService endpointService))
{
throw new InvalidOperationException($"The .bot file does not contain an endpoint with name '{environment}'.");
}
options.CredentialProvider = new SimpleCredentialProvider(endpointService.AppId, endpointService.AppPassword);
// Creates a logger for the application to use.
ILogger logger = _loggerFactory.CreateLogger<SimplePromptBot>();
// Catches any errors that occur during a conversation turn and logs them.
options.OnTurnError = async (context, exception) =>
{
logger.LogError($"Exception caught : {exception}");
await context.SendActivityAsync("Sorry, it looks like something went wrong.");
};
// Memory Storage is for local bot debugging only. When the bot
// is restarted, everything stored in memory will be gone.
//IStorage dataStore = new MemoryStorage();
// For production bots use the Azure Blob or
// Azure CosmosDB storage providers. For the Azure
// based storage providers, add the Microsoft.Bot.Builder.Azure
// Nuget package to your solution. That package is found at:
// https://www.nuget.org/packages/Microsoft.Bot.Builder.Azure/
// Uncomment the following lines to use Azure Blob Storage
// //Storage configuration name or ID from the .bot file.
// const string StorageConfigurationId = "<STORAGE-NAME-OR-ID-FROM-BOT-FILE>";
// var blobConfig = botConfig.FindServiceByNameOrId(StorageConfigurationId);
// if (!(blobConfig is BlobStorageService blobStorageConfig))
// {
// throw new InvalidOperationException($"The .bot file does not contain an blob storage with name '{StorageConfigurationId}'.");
// }
// // Default container name.
// const string DefaultBotContainer = "<DEFAULT-CONTAINER>";
// var storageContainer = string.IsNullOrWhiteSpace(blobStorageConfig.Container) ? DefaultBotContainer : blobStorageConfig.Container;
// IStorage dataStore = new Microsoft.Bot.Builder.Azure.AzureBlobStorage(blobStorageConfig.ConnectionString, storageContainer);
// Create Conversation State object.
// The Conversation State object is where we persist anything at the conversation-scope.
var conversationState = new ConversationState(_myStorage);
options.State.Add(conversationState);
});
services.AddSingleton(sp =>
{
// We need to grab the conversationState we added on the options in the previous step.
var options = sp.GetRequiredService<IOptions<BotFrameworkOptions>>().Value;
if (options == null)
{
throw new InvalidOperationException("BotFrameworkOptions must be configured prior to setting up the State Accessors");
}
var conversationState = options.State.OfType<ConversationState>().FirstOrDefault();
if (conversationState == null)
{
throw new InvalidOperationException("ConversationState must be defined and added before adding conversation-scoped state accessors.");
}
// The dialogs will need a state store accessor. Creating it here once (on-demand) allows the dependency injection
// to hand it to our IBot class that is create per-request.
var accessors = new SimplePromptBotAccessors(conversationState)
{
ConversationDialogState = conversationState.CreateProperty<DialogState>("DialogState"),
};
return accessors;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
app.UseDefaultFiles()
.UseStaticFiles()
.UseBotFramework();
}
}
}
Here's a diff, so you can easily see the code differences.
Here's a screenshot of it working:
It looks like your code also stores the userState and conversationState in separate collections. I think that works...but the "conventional" method is to only create one instance of CosmosDbStorage. The bot will store userState and conversationState in separate documents within the collection. Note that in addition to the above code, you'll likely need something like, var userState = new UserState(_myStorage), since your code also uses userState and the above code does not.
Additionally, and in line with Drew's answer, I think the code from that tutorial you linked might be causing some issues, simply because it's out of date. The best thing to do, would be finding a relevant sample from the GitHub Repo and using that as a guide. Basic Bot is a good one with conversationState and userState functionality.

Your code makes it seem like maybe you were writing some code using the pre-release bits at one point or maybe copied from someone who was. ConversationState itself is no longer a piece of middleware, nor is it a generic class any longer.
So you no longer create a ConversationState<T> for each piece of state you want to maintain. Instead you create a single ConversationState, which acts as a sort of scoping "bucket" if you will, and then you create many properties within that "bucket" using the CreateProperty<T> API.
Like this:
var conversationState = new ConversationState(myStorage);
var myBasicStateProperty conversationState.CreateProperty<BasicState>("MyBasicState");
Now, as I also said, it is no longer middleware. Instead, what is returned from CreateProperty<T> is an IStatePropertyAccessor<T> which you can then pass into whatever needs to use it (e.g. your bot). Likewise you would also pass the ConversationState itself into the bot so it can ultimately call SaveChangesAsync on it at the end of the turn. Alternatively you can configure the AutoSaveStateMiddleware which will take care of saving the state for you at the end of every turn, but you lose control over the ability to deal with exceptions that arise during the call to SaveChangesAsync (e.g. network partition, data concurrency, etc).

Related

ASP.NET IConfiguration, how to iterate or enumerate Providers

I am trying to write a test to verify that X providers are loaded in a certain scenario.
I can resolve the IConfiguration object, how can I tell how many IConfigurationProviders are in it?
In my startup, I use totally bog standard Configuration in my Program
IConfiguration configuration = null;
var builder = Host.CreateDefaultBuilder()
.ConfigureServices((hostContext, services) =>
{
configuration = hostContext.Configuration;
var startup = new Startup(hostContext.Configuration, hostContext.HostingEnvironment);
//startup.ConfigureServices(services);
})
.ConfigureAppConfiguration((hostContext, config) =>
{
config.AddAzureKeyVaultsFromConfig();
});
builder.Build();
return configuration;
When I'm debugging, I can see my list of Providers when I hover over configuration.
What I would want to do is see that there are 4 providers. I would also want to check and see what type the providers are.
If I use Enumerate() it just gives me a flattened list of all of the values and discards which provider it came from.
The object you have here is actually an IConfigurationRoot, and that happens to also implement IConfiguration. So if you treat it as such, you can access the Providers property. For example:
IConfigurationRoot configuration = null;
// snip the rest of your code
var providerCount = configuration.Providers.Count();

How to connect to a database using Active directory Login and MultiFactor Authentication (MFA)

I have already configured my Azure SQL Server so that I am Server admin, my account also has MFA enabled. I was trying to follow this documentation but it doesn't mention anything about Active directory with MFA.
I can use my account and MFA to sign into the server fine using SQL Management studio
Initially I tried (based on the new SqlAuthenticationMethod Enum):
SqlConnection con = new SqlConnection("Server=tcp:myapp.database.windows.net;Database=CustomerDB;Authentication=Active Directory Interactive;Encrypt=True;UID=User#User.co.uk"))
Error:
'Cannot find an authentication provider for
'ActiveDirectoryInteractive'.'
I then saw this about accessing SQL via an Azure application But this is not what I want to do.
This SO question talks about connecting without the provider and setting the Driver in the connection string
SqlConnection con = new SqlConnection("DRIVER={ODBC Driver 17 for SQL Server};Server=tcp:myapp.database.windows.net;Database=CustomerDB;Authentication=Active Directory Interactive;Encrypt=True;UID=User#User.co.uk"))
but I get the error:
'Keyword not supported: 'driver'.'
Is there anyway to write a connection string so that when it tries to connect the Microsoft authentication box pops up to walk the user through Multi factor authentication?
To use Azure AD authentication, your C# program has to register as an Azure AD application. Completing an app registration generates and displays an application ID. Your program has to include this ID to connect. To register and set necessary permissions for your application, go to the Azure portal, select Azure Active Directory > App registrations > New registration.
After the app registration is created, the application ID value is generated and displayed.
Select API permissions > Add a permission.
Select APIs my organization uses > type Azure SQL Database into the search > and select Azure SQL Database.
Select Delegated permissions > user_impersonation > Add permissions.
It seems you have already set an Azure AD admin for your Azure SQL Database.
You can also add a user to the database with the SQL Create User command. An example is CREATE USER [] FROM EXTERNAL PROVIDER. For more information, see here.
Below an example on C#.
using System;
// Reference to Azure AD authentication assembly
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using DA = System.Data;
using SC = System.Data.SqlClient;
using AD = Microsoft.IdentityModel.Clients.ActiveDirectory;
using TX = System.Text;
using TT = System.Threading.Tasks;
namespace ADInteractive5
{
class Program
{
// ASSIGN YOUR VALUES TO THESE STATIC FIELDS !!
static public string Az_SQLDB_svrName = "<Your SQL DB server>";
static public string AzureAD_UserID = "<Your User ID>";
static public string Initial_DatabaseName = "<Your Database>";
// Some scenarios do not need values for the following two fields:
static public readonly string ClientApplicationID = "<Your App ID>";
static public readonly Uri RedirectUri = new Uri("<Your URI>");
public static void Main(string[] args)
{
var provider = new ActiveDirectoryAuthProvider();
SC.SqlAuthenticationProvider.SetProvider(
SC.SqlAuthenticationMethod.ActiveDirectoryInteractive,
//SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated, // Alternatives.
//SC.SqlAuthenticationMethod.ActiveDirectoryPassword,
provider);
Program.Connection();
}
public static void Connection()
{
SC.SqlConnectionStringBuilder builder = new SC.SqlConnectionStringBuilder();
// Program._ static values that you set earlier.
builder["Data Source"] = Program.Az_SQLDB_svrName;
builder.UserID = Program.AzureAD_UserID;
builder["Initial Catalog"] = Program.Initial_DatabaseName;
// This "Password" is not used with .ActiveDirectoryInteractive.
//builder["Password"] = "<YOUR PASSWORD HERE>";
builder["Connect Timeout"] = 15;
builder["TrustServerCertificate"] = true;
builder.Pooling = false;
// Assigned enum value must match the enum given to .SetProvider().
builder.Authentication = SC.SqlAuthenticationMethod.ActiveDirectoryInteractive;
SC.SqlConnection sqlConnection = new SC.SqlConnection(builder.ConnectionString);
SC.SqlCommand cmd = new SC.SqlCommand(
"SELECT '******** MY QUERY RAN SUCCESSFULLY!! ********';",
sqlConnection);
try
{
sqlConnection.Open();
if (sqlConnection.State == DA.ConnectionState.Open)
{
var rdr = cmd.ExecuteReader();
var msg = new TX.StringBuilder();
while (rdr.Read())
{
msg.AppendLine(rdr.GetString(0));
}
Console.WriteLine(msg.ToString());
Console.WriteLine(":Success");
}
else
{
Console.WriteLine(":Failed");
}
sqlConnection.Close();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Connection failed with the following exception...");
Console.WriteLine(ex.ToString());
Console.ResetColor();
}
}
} // EOClass Program.
/// <summary>
/// SqlAuthenticationProvider - Is a public class that defines 3 different Azure AD
/// authentication methods. The methods are supported in the new .NET 4.7.2.
/// .
/// 1. Interactive, 2. Integrated, 3. Password
/// .
/// All 3 authentication methods are based on the Azure
/// Active Directory Authentication Library (ADAL) managed library.
/// </summary>
public class ActiveDirectoryAuthProvider : SC.SqlAuthenticationProvider
{
// Program._ more static values that you set!
private readonly string _clientId = Program.ClientApplicationID;
private readonly Uri _redirectUri = Program.RedirectUri;
public override async TT.Task<SC.SqlAuthenticationToken>
AcquireTokenAsync(SC.SqlAuthenticationParameters parameters)
{
AD.AuthenticationContext authContext =
new AD.AuthenticationContext(parameters.Authority);
authContext.CorrelationId = parameters.ConnectionId;
AD.AuthenticationResult result;
switch (parameters.AuthenticationMethod)
{
case SC.SqlAuthenticationMethod.ActiveDirectoryInteractive:
Console.WriteLine("In method 'AcquireTokenAsync', case_0 == '.ActiveDirectoryInteractive'.");
result = await authContext.AcquireTokenAsync(
parameters.Resource, // "https://database.windows.net/"
_clientId,
_redirectUri,
new AD.PlatformParameters(AD.PromptBehavior.Auto),
new AD.UserIdentifier(
parameters.UserId,
AD.UserIdentifierType.RequiredDisplayableId));
break;
case SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated:
Console.WriteLine("In method 'AcquireTokenAsync', case_1 == '.ActiveDirectoryIntegrated'.");
result = await authContext.AcquireTokenAsync(
parameters.Resource,
_clientId,
new AD.UserCredential());
break;
case SC.SqlAuthenticationMethod.ActiveDirectoryPassword:
Console.WriteLine("In method 'AcquireTokenAsync', case_2 == '.ActiveDirectoryPassword'.");
result = await authContext.AcquireTokenAsync(
parameters.Resource,
_clientId,
new AD.UserPasswordCredential(
parameters.UserId,
parameters.Password));
break;
default: throw new InvalidOperationException();
}
return new SC.SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);
}
public override bool IsSupported(SC.SqlAuthenticationMethod authenticationMethod)
{
return authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryIntegrated
|| authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryInteractive
|| authenticationMethod == SC.SqlAuthenticationMethod.ActiveDirectoryPassword;
}
} // EOClass ActiveDirectoryAuthProvider.
} // EONamespace. End of entire program source code.
The example above relies on the Microsoft.IdentityModel.Clients.ActiveDirectory DLL assembly.
To install this package, in Visual Studio, select Project > Manage NuGet Packages. Search for and install Microsoft.IdentityModel.Clients.ActiveDirectory.
Starting in .NET Framework version 4.7.2, the enum SqlAuthenticationMethod has a new value: ActiveDirectoryInteractive.
The only way I have found to login using Active Directory and MFA and cache the token is to use #Alberto's method
I did also find another way which would ask for login credentials every time which is to use this connection string:
OdbcConnection con = new OdbcConnection("Driver={ODBC Driver 17 for SQL Server};SERVER=tcp:myserver.database.windows.net;DATABASE=MyDb;Authentication=ActiveDirectoryInteractive;UID=User#Userco.uk")
Improving the code posted by #alberto. I must say for something so fundamental in the modern world this is unbelievably undocumented. Anyway here's the improved Provider code.
This code also requires you to target .Net Framework 4.7.2 or greater
Firstly follow #alberto's code.. I did find one extra unmentioned step is that you need to also configure a Platform for your app in azure on the authentication tab to look like:
Add these two classes to your project:
ActiveDirectoryAuthProvider
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.Data.SqlClient;
namespace SQLAzureConnectivity
{
public class ActiveDirectoryAuthProvider : SqlAuthenticationProvider
{
private string _clientId { get; set; }
private Uri _redirectURL { get; set; } = new Uri("https://login.microsoftonline.com/common/oauth2/nativeclient");
public ActiveDirectoryAuthProvider(string clientId)
{
_clientId = clientId;
}
//https://learn.microsoft.com/en-us/azure/sql-database/active-directory-interactive-connect-azure-sql-db#c-code-example
public override async Task<System.Data.SqlClient.SqlAuthenticationToken> AcquireTokenAsync(System.Data.SqlClient.SqlAuthenticationParameters parameters)
{
AuthenticationContext authContext = new AuthenticationContext(parameters.Authority, new FilesBasedAdalV3TokenCache(".\\Token.dat"));
authContext.CorrelationId = parameters.ConnectionId;
AuthenticationResult result = null;
switch (parameters.AuthenticationMethod)
{
case System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryInteractive:
Console.WriteLine("In method 'AcquireTokenAsync', case_0 == '.ActiveDirectoryInteractive'.");
try
{
result = await authContext.AcquireTokenSilentAsync(parameters.Resource, _clientId);
}
catch (AdalException adalException)
{
if (adalException.ErrorCode == AdalError.FailedToAcquireTokenSilently || adalException.ErrorCode == AdalError.InteractionRequired)
{
result = await authContext.AcquireTokenAsync(parameters.Resource, _clientId, _redirectURL, new PlatformParameters(PromptBehavior.Auto));
//result = await authContext.AcquireTokenAsync(parameters.Resource, _clientId, _redirectURL, new PlatformParameters(PromptBehavior.Auto), new UserIdentifier(parameters.UserId, UserIdentifierType.RequiredDisplayableId));
}
}
break;
case System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryIntegrated:
Console.WriteLine("In method 'AcquireTokenAsync', case_1 == '.ActiveDirectoryIntegrated'.");
result = await authContext.AcquireTokenAsync(parameters.Resource, _clientId, new UserCredential());
break;
case System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryPassword:
Console.WriteLine("In method 'AcquireTokenAsync', case_2 == '.ActiveDirectoryPassword'.");
result = await authContext.AcquireTokenAsync(parameters.Resource, _clientId, new UserPasswordCredential(parameters.UserId, parameters.Password));
break;
default:
throw new InvalidOperationException();
}
return new System.Data.SqlClient.SqlAuthenticationToken(result.AccessToken, result.ExpiresOn);
}
public override bool IsSupported(System.Data.SqlClient.SqlAuthenticationMethod authenticationMethod)
{
return authenticationMethod == System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryIntegrated
|| authenticationMethod == System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryInteractive
|| authenticationMethod == System.Data.SqlClient.SqlAuthenticationMethod.ActiveDirectoryPassword;
}
}
}
FilesBasedAdalV3TokenCache
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System.IO;
using System.Security.Cryptography;
namespace SQLAzureConnectivity
{
// This is a simple persistent cache implementation for an ADAL V3 desktop application
public class FilesBasedAdalV3TokenCache : TokenCache
{
public string CacheFilePath { get; }
private static readonly object FileLock = new object();
// Initializes the cache against a local file.
// If the file is already present, it loads its content in the ADAL cache
public FilesBasedAdalV3TokenCache(string filePath)
{
CacheFilePath = filePath;
this.AfterAccess = AfterAccessNotification;
this.BeforeAccess = BeforeAccessNotification;
lock (FileLock)
{
this.DeserializeAdalV3(ReadFromFileIfExists(CacheFilePath));
}
}
// Empties the persistent store.
public override void Clear()
{
base.Clear();
File.Delete(CacheFilePath);
}
// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
lock (FileLock)
{
this.DeserializeAdalV3(ReadFromFileIfExists(CacheFilePath));
}
}
// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (this.HasStateChanged)
{
lock (FileLock)
{
// reflect changes in the persistent store
WriteToFileIfNotNull(CacheFilePath, this.SerializeAdalV3());
// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
}
}
/// <summary>
/// Read the content of a file if it exists
/// </summary>
/// <param name="path">File path</param>
/// <returns>Content of the file (in bytes)</returns>
private byte[] ReadFromFileIfExists(string path)
{
byte[] protectedBytes = (!string.IsNullOrEmpty(path) && File.Exists(path))
? File.ReadAllBytes(path) : null;
byte[] unprotectedBytes = (protectedBytes != null)
? ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser) : null;
return unprotectedBytes;
}
/// <summary>
/// Writes a blob of bytes to a file. If the blob is <c>null</c>, deletes the file
/// </summary>
/// <param name="path">path to the file to write</param>
/// <param name="blob">Blob of bytes to write</param>
private static void WriteToFileIfNotNull(string path, byte[] blob)
{
if (blob != null)
{
byte[] protectedBytes = ProtectedData.Protect(blob, null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(path, protectedBytes);
}
else
{
File.Delete(path);
}
}
}
}
Then before using a SQLConnection write these two lines:
var provider = new ActiveDirectoryAuthProvider("ClientID from the Azure app you set up earlier");
SqlAuthenticationProvider.SetProvider(SqlAuthenticationMethod.ActiveDirectoryInteractive, provider);
References:
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/wiki/Token-cache-serialization
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/wiki/Acquiring-tokens-interactively---Public-client-application-flows#properties-or-platformparameters-constructors-parameters-common-to-most-platforms
https://github.com/AzureAD/azure-activedirectory-library-for-dotnet/wiki/AcquireTokenSilentAsync-using-a-cached-token#recommended-pattern-to-acquire-a-token
https://learn.microsoft.com/en-us/azure/sql-database/active-directory-interactive-connect-azure-sql-db
As mentioned elsewhere, you can use ODBC to connect, without registering your app in the Azure Portal. The interactive prompt will be shown whenever a new connection is added to the pool. Thus, even if you open multiple ODBC connections using the same connection string, you will only see the prompt once within your application lifecycle (or until the connection pool is recycled).
If you don't want to use ODBC, you may also use OLE DB with the MSOLEDBSQL driver, which has similar (or better) performance than the native SQL Client provider (which is deprecated and shouldn't be used anyway):
using System.Data.OleDb;
...
OleDbConnection con = new OleDbConnection("Provider=MSOLEDBSQL;Data Source=sqlserver.database.windows.net;User ID=user#domain.com;Initial Catalog=database;Authentication=ActiveDirectoryInteractive");
This may not be the best place to put this answer, as is it is specific to unit testing sql server and visual studio (community,prof,ent) -- https://youtu.be/OZiTKfNSXh4 # 1:10 -- via mfa interactive using #Dan answer.
The problem is that generating a c#/sql unit test project can be done using interactive connection. But running any unit test will fail because mfa interactive is not supported by SqlClient provider. Below is a work-around.
New file OleDatabaseTestService.cs
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using System.Data.OleDb;
namespace [YourNamespace]Tests
{
public class OleDatabaseTestService : SqlDatabaseTestService
{
static OleDatabaseTestService()
{
SetupConext();
}
private static ConnectionContext _contextExecution = null;
private static ConnectionContext _contextPrivileged = null;
public static ConnectionContext ContextExecution { get; set; } = null;
public static ConnectionContext ContextPrivileged { get; set; } = null;
public override ConnectionContext OpenExecutionContext()
{
return ContextExecution;
}
public override ConnectionContext OpenPrivilegedContext()
{
return ContextPrivileged == null ? ContextExecution : ContextPrivileged;
}
// TODO: This can be a written a lot better - please edit this SO if you wish to help
protected static ConnectionContext SetupConext()
{
var context = new ConnectionContext();
context.Provider = OleDbFactory.Instance;
var connection = context.Provider.CreateConnection();
// TODO: Drive the connection string from app.config interactive connection string (wizard creates interactive correctly, but not supported by SqlClient provider)
// var connectionSection = (SqlUnitTestingSection)ConfigurationManager.GetSection("SqlUnitTesting"); // DbConnection connection = new OleDbConnection(
connection.ConnectionString = "Provider=MSOLEDBSQL;Data Source=[azure_database_name].database.windows.net;Initial Catalog=[initial db];User ID=[email];Authentication=ActiveDirectoryInteractive"; // + connectionSection.ExecutionContext.ConnectionString;
connection.Open();
context.Connection = connection;
ContextExecution = context;
ContextPrivileged = context;
return context;
}
}
}
Change to SqlDatabaseSetup.cs
using Microsoft.Data.Tools.Schema.Sql.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace [YourNamespace]Tests
{
[TestClass()]
public class SqlDatabaseSetup
{
[AssemblyInitialize()]
public static void InitializeAssembly(TestContext ctx)
{
var service = new OleDatabaseTestService();
SqlDatabaseTestClass.TestService = service;
SqlDatabaseTestClass.TestService.DeployDatabaseProject();
SqlDatabaseTestClass.TestService.GenerateData();
}
}
}
Please add a comment on where this would best be moved to. Or if
someone prefers this as a Question/self-Answered on its own (no need
to waste points).

Application Insights - TelemetryClient - DependencyTelemetry - UseSampling

I am trying to enable sampling with my AppInsightsHelper class which starts Depedancy operations to track performance.
This is how I am initializing my TelematryClient:
public ApplicationInsightsHelper(string key)
{
var config = TelemetryConfiguration.CreateDefault();
config.InstrumentationKey = key;
config.DefaultTelemetrySink.TelemetryProcessorChainBuilder.UseAdaptiveSampling(maxTelemetryItemsPerSecond: 1);
_telemetryClient = new TelemetryClient(config);
}
and then Starting and Stopping the operation:
IOperationHolder<DependencyTelemetry> operation = null;
operation = _telemetryClient.StartOperation<DependencyTelemetry>(friendlyName);
operation.Telemetry.Name = friendlyName;
operation.Telemetry.Type = type;
operation.Telemetry.Timestamp = DateTime.UtcNow;
operation.Telemetry.Duration = DateTime.UtcNow - operation.Telemetry.Timestamp;
_telemetryClient.StopOperation(operation);
The issue is that the above code seems to ignore the Sampling setting and all operations are traced. I have also included : excludedTypes: "Dependency" within the UseAdaptiveSampling to see if anything happens and as expected the Dependencies are not ignored.
If it's an azure function, you can set sampling via host.json, see here and here for details. An example as below:
{
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond" : 1
}
}
}
}
And if you want to use TelemetryClient with the settings, you should follow this article. In the constructor of the azure function, use code like below:
/// Using dependency injection will guarantee that you use the same configuration for telemetry collected automatically and manually.
public HttpTrigger2(TelemetryConfiguration telemetryConfiguration)
{
this.telemetryClient = new TelemetryClient(telemetryConfiguration);
}
But as of now, there is an issue by using telemetryConfiguration.
This has been worked for me for ASP.NET web application. I have added below configuration and specifically added my 'MaksingTelemetryInitializer'.
public void StartApplicationInsights(string logType)
{
string appInsightsComponentId = string.Empty;
try
{
telemetryClient = new TelemetryClient();
TelemetryConfiguration.Active.InstrumentationKey = GetConfigvalue("AppInsightsAppId"); ;
TelemetryConfiguration.Active.TelemetryInitializers.Add(new MaskingTelemetryInitializer());
}
catch (Exception exception)
{
// Log Exception to WadLog if logging to Wadlog is enabled
if (logType != LoggingType.Both) return;
WadLogWriter.LogToWadLogs(Logger.BuildErrorString(exception), EventLevel.Error);
}
}
Here I wanted mask PII data email id, it is working.

Localization of RequiredAttribute in ASP.NET Core 2.0

I'm struggling with localization in my new .NET Core project.
I have 2 projects:
DataAccess project with Models and DataAnnotations (e.g. RequiredAttribute)
Web project with MVC views etc.
My wish is to localize all validation attributes globally in one single place to have the similar behavior like MVC 5. Is this possible?
I do not want to have separate language files for Models/Views etc.
Microsofts documentation is not very clear on using SharedResources.resx file with localized DataAnnotation messages.
In MVC 5 I didn't take care of it. I only needed to set the locale to my language and everything was fine.
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
I already added .AddDataAnnotationsLocalization() in Startup.cs - but it seems to do nothing.
I've read several articles but I couldn't find the cause why it's not working.
EDIT:
What I have so far:
1.) LocService class
public class LocService
{
private readonly IStringLocalizer _localizer;
public LocService(IStringLocalizerFactory factory)
{
_localizer = factory.Create(typeof(Strings));
}
public LocalizedString GetLocalizedHtmlString(string key)
{
return _localizer[key];
}
}
2.) Added Folder "Resources" with Strings.cs (empty class with dummy constructor)
3.) Added Strings.de-DE.resx file with one item "RequiredAttribute_ValidationError"
4.) Modified my Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MessageService>();
services.AddDbContext<DataContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<LocService>();
services.AddLocalization(options => options.ResourcesPath = "Resources");
services.AddMvc()
.AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver())
.AddDataAnnotationsLocalization(
options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(Strings));
});
services.Configure<RequestLocalizationOptions>(
opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("de-DE"),
};
opts.DefaultRequestCulture = new RequestCulture("de-DE");
// Formatting numbers, dates, etc.
opts.SupportedCultures = supportedCultures;
// UI strings that we have localized.
opts.SupportedUICultures = supportedCultures;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
I've followed the instructions here but it doesn't work:
https://damienbod.com/2017/11/01/shared-localization-in-asp-net-core-mvc/
Please keep in mind that my Models are kept in a separate project.
As #Sven points out in his comment to Tseng's answer it still requires that you specify an explicit ErrorMessage, which gets quite tedious.
The problem arises from the logic ValidationAttributeAdapter<TAttribute>.GetErrorMessage() uses to decide whether to use the provided IStringLocalizer or not.
I use the following solution to get around that issue:
Create a custom IValidationAttributeAdapterProvider implementation that uses the default ValidationAttributeAdapterProvider like this:
public class LocalizedValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly ValidationAttributeAdapterProvider _originalProvider = new ValidationAttributeAdapterProvider();
public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
attribute.ErrorMessage = attribute.GetType().Name.Replace("Attribute", string.Empty);
if (attribute is DataTypeAttribute dataTypeAttribute)
attribute.ErrorMessage += "_" + dataTypeAttribute.DataType;
return _originalProvider.GetAttributeAdapter(attribute, stringLocalizer);
}
}
Register the adapter in Startup.ConfigureServices() Before calling AddMvc():
services.AddSingleton<Microsoft.AspNetCore.Mvc.DataAnnotations.IValidationAttributeAdapterProvider, LocalizedValidationAttributeAdapterProvider>();
I prefer to use "stricter" resource names based on the actual attributes, so the code above will look for resource names like "Required" and "DataType_Password", but this can of course be customized in many ways.
If you prefer resources names based on the default messages of the Attributes you could instead write something like:
attribute.ErrorMessage = attribute.FormatErrorMessage("{0}");
I tried setting the ErrorMessageResourceName and ErrorMessageResourceType to my shared resource file name "Strings.resx" and "Strings.de.resx" in the DataAccess project:
[Required(ErrorMessageResourceName = "RequiredAttribute_ValidationError", ErrorMessageResourceType = typeof(Strings))]
I also tried the setting name to be RequiredAttribute_ValidationError - but it's not working.
You were on the right track, but you don't necessarily need to set ErrorMessageResourceName / ErrorMessageResourceType properties.
Was we can see in the source code of ValidationAttributeAdapter<TAttribute>, the conditions to use the _stringLocalizer verison is when ErrorMessage is not null and ErrorMessageResourceName/ErrorMessageResourceType are null.
In other words, when you don't set any properties or only ErrorMessage. So a plain [Required] should just work (see source where is passed to the base classes constructor).
Now, when we look at the DataAnnotations resource file we see that the name is set to "RequiredAttribute_ValidationError" and the value to "The {0} field is required." which is the default English translation.
Now if you use "RequiredAttribute_ValidationError" with the German translation in your "Strings.de-DE.resx" (or just Strings.resx as fallback), it should work with the corrected namespace from the comments.
So using the above configuration and the strings from the GitHub repository you should be able to make the localization work without extra attributes.
It turned out that ValidationAttributeAdapterProvider approach doesn't work as it is meant to be used for "client side validation attributes" only (which doesn't make much sense to me because the attributes are specified on the server model).
But I found a solution that works to override all attributes with custom messages. It also is able to inject field name translations without spitting [Display] all over the place. It's convention-over-configuration in action.
Also, as a bonus, this solution overrides default model binding error texts that are used even before validation takes place. One caveat - if you receive JSON data, then Json.Net errors will be merged into ModelState errors and default binding errors won't be used. I haven't yet figured out how to prevent this from happening.
So, here are three classes you will need:
public class LocalizableValidationMetadataProvider : IValidationMetadataProvider
{
private IStringLocalizer _stringLocalizer;
private Type _injectableType;
public LocalizableValidationMetadataProvider(IStringLocalizer stringLocalizer, Type injectableType)
{
_stringLocalizer = stringLocalizer;
_injectableType = injectableType;
}
public void CreateValidationMetadata(ValidationMetadataProviderContext context)
{
// ignore non-properties and types that do not match some model base type
if (context.Key.ContainerType == null ||
!_injectableType.IsAssignableFrom(context.Key.ContainerType))
return;
// In the code below I assume that expected use of ErrorMessage will be:
// 1 - not set when it is ok to fill with the default translation from the resource file
// 2 - set to a specific key in the resources file to override my defaults
// 3 - never set to a final text value
var propertyName = context.Key.Name;
var modelName = context.Key.ContainerType.Name;
// sanity check
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
return;
foreach (var attribute in context.ValidationMetadata.ValidatorMetadata)
{
var tAttr = attribute as ValidationAttribute;
if (tAttr != null)
{
// at first, assume the text to be generic error
var errorName = tAttr.GetType().Name;
var fallbackName = errorName + "_ValidationError";
// Will look for generic widely known resource keys like
// MaxLengthAttribute_ValidationError
// RangeAttribute_ValidationError
// EmailAddressAttribute_ValidationError
// RequiredAttribute_ValidationError
// etc.
// Treat errormessage as resource name, if it's set,
// otherwise assume default.
var name = tAttr.ErrorMessage ?? fallbackName;
// At first, attempt to retrieve model specific text
var localized = _stringLocalizer[name];
// Some attributes come with texts already preset (breaking the rule 3),
// even if we didn't do that explicitly on the attribute.
// For example [EmailAddress] has entire message already filled in by MVC.
// Therefore we first check if we could find the value by the given key;
// if not, then fall back to default name.
// Final attempt - default name from property alone
if (localized.ResourceNotFound) // missing key or prefilled text
localized = _stringLocalizer[fallbackName];
// If not found yet, then give up, leave initially determined name as it is
var text = localized.ResourceNotFound ? name : localized;
tAttr.ErrorMessage = text;
}
}
}
}
public class LocalizableInjectingDisplayNameProvider : IDisplayMetadataProvider
{
private IStringLocalizer _stringLocalizer;
private Type _injectableType;
public LocalizableInjectingDisplayNameProvider(IStringLocalizer stringLocalizer, Type injectableType)
{
_stringLocalizer = stringLocalizer;
_injectableType = injectableType;
}
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
// ignore non-properties and types that do not match some model base type
if (context.Key.ContainerType == null ||
!_injectableType.IsAssignableFrom(context.Key.ContainerType))
return;
// In the code below I assume that expected use of field name will be:
// 1 - [Display] or Name not set when it is ok to fill with the default translation from the resource file
// 2 - [Display(Name = x)]set to a specific key in the resources file to override my defaults
var propertyName = context.Key.Name;
var modelName = context.Key.ContainerType.Name;
// sanity check
if (string.IsNullOrEmpty(propertyName) || string.IsNullOrEmpty(modelName))
return;
var fallbackName = propertyName + "_FieldName";
// If explicit name is missing, will try to fall back to generic widely known field name,
// which should exist in resources (such as "Name_FieldName", "Id_FieldName", "Version_FieldName", "DateCreated_FieldName" ...)
var name = fallbackName;
// If Display attribute was given, use the last of it
// to extract the name to use as resource key
foreach (var attribute in context.PropertyAttributes)
{
var tAttr = attribute as DisplayAttribute;
if (tAttr != null)
{
// Treat Display.Name as resource name, if it's set,
// otherwise assume default.
name = tAttr.Name ?? fallbackName;
}
}
// At first, attempt to retrieve model specific text
var localized = _stringLocalizer[name];
// Final attempt - default name from property alone
if (localized.ResourceNotFound)
localized = _stringLocalizer[fallbackName];
// If not found yet, then give up, leave initially determined name as it is
var text = localized.ResourceNotFound ? name : localized;
context.DisplayMetadata.DisplayName = () => text;
}
}
public static class LocalizedModelBindingMessageExtensions
{
public static IMvcBuilder AddModelBindingMessagesLocalizer(this IMvcBuilder mvc,
IServiceCollection services, Type modelBaseType)
{
var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
var VL = factory.Create(typeof(ValidationMessagesResource));
var DL = factory.Create(typeof(FieldNamesResource));
return mvc.AddMvcOptions(o =>
{
// for validation error messages
o.ModelMetadataDetailsProviders.Add(new LocalizableValidationMetadataProvider(VL, modelBaseType));
// for field names
o.ModelMetadataDetailsProviders.Add(new LocalizableInjectingDisplayNameProvider(DL, modelBaseType));
// does not work for JSON models - Json.Net throws its own error messages into ModelState :(
// ModelBindingMessageProvider is only for FromForm
// Json works for FromBody and needs a separate format interceptor
DefaultModelBindingMessageProvider provider = o.ModelBindingMessageProvider;
provider.SetValueIsInvalidAccessor((v) => VL["FormatHtmlGeneration_ValueIsInvalid", v]);
provider.SetAttemptedValueIsInvalidAccessor((v, x) => VL["FormatModelState_AttemptedValueIsInvalid", v, x]);
provider.SetMissingBindRequiredValueAccessor((v) => VL["FormatModelBinding_MissingBindRequiredMember", v]);
provider.SetMissingKeyOrValueAccessor(() => VL["FormatKeyValuePair_BothKeyAndValueMustBePresent" ]);
provider.SetMissingRequestBodyRequiredValueAccessor(() => VL["FormatModelBinding_MissingRequestBodyRequiredMember"]);
provider.SetNonPropertyAttemptedValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyAttemptedValueIsInvalid", v]);
provider.SetNonPropertyUnknownValueIsInvalidAccessor(() => VL["FormatModelState_UnknownValueIsInvalid"]);
provider.SetUnknownValueIsInvalidAccessor((v) => VL["FormatModelState_NonPropertyUnknownValueIsInvalid", v]);
provider.SetValueMustNotBeNullAccessor((v) => VL["FormatModelBinding_NullValueNotValid", v]);
provider.SetValueMustBeANumberAccessor((v) => VL["FormatHtmlGeneration_ValueMustBeNumber", v]);
provider.SetNonPropertyValueMustBeANumberAccessor(() => VL["FormatHtmlGeneration_NonPropertyValueMustBeNumber"]);
});
}
}
In ConfigureServices in your Startup.cs file:
services.AddMvc( ... )
.AddModelBindingMessagesLocalizer(services, typeof(IDtoModel));
I have used my custom empty IDtoModel interface here and applied it to all my API models that will need the automatic localization for errors and field names.
Create a folder Resources and put empty classes ValidationMessagesResource and FieldNamesResource inside it.
Create ValidationMessagesResource.ab-CD.resx and FieldNamesResource .ab-CD.resx files (replace ab-CD with your desired culture).
Fill in the values for the keys you need, e.g. FormatModelBinding_MissingBindRequiredMember, MaxLengthAttribute_ValidationError ...
When launching the API from a browser, make sure to modify accept-languages header to be your culture name, otherwise Core will use it instead of defaults. For API that needs single language only, I prefer to disable culture providers altogether using the following code:
private readonly CultureInfo[] _supportedCultures = new[] {
new CultureInfo("ab-CD")
};
...
var ci = new CultureInfo("ab-CD");
// can customize decimal separator to match your needs - some customers require to go against culture defaults and, for example, use . instead of , as decimal separator or use different date format
/*
ci.NumberFormat.NumberDecimalSeparator = ".";
ci.NumberFormat.CurrencyDecimalSeparator = ".";
*/
_defaultRequestCulture = new RequestCulture(ci, ci);
...
services.Configure<RequestLocalizationOptions>(options =>
{
options.DefaultRequestCulture = _defaultRequestCulture;
options.SupportedCultures = _supportedCultures;
options.SupportedUICultures = _supportedCultures;
options.RequestCultureProviders = new List<IRequestCultureProvider>(); // empty list - use default value always
});
unfortunately, it is not that simple to localize all error messages for data attributes in one single place! because there are different types of error messages,
Error messages for standard data attributes:
[Required]
[Range]
[StringLength]
[Compare]
...etc.
Error messages for ModelBinding:
ValueIsInvalid
ValueMustNotBeNull
PropertyValueMustBeANumber
...etc.
and Identity error messages:
DuplicateEmail
DuplicateRoleName
InvalidUserName
PasswordRequiresLower
PasswordRequiresUpper
...etc
each must be configured in the startup file. Additionaly client side validation must be considered as well.
you may check these articles for more details, it contains live demo and sample project on GitHub:
Developing multicultural web application:
http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application
Localizing data annotations:
http://www.ziyad.info/en/articles/16-Localizing_DataAnnotations
Localizing ModelBinding error messages:
http://www.ziyad.info/en/articles/18-Localizing_ModelBinding_Error_Messages
Localizing identity error messages:
http://www.ziyad.info/en/articles/20-Localizing_Identity_Error_Messages
and client side validation:
http://ziyad.info/en/articles/19-Configuring_Client_Side_Validation
hope it helps :)
public class RequiredExAttribute : RequiredAttribute
{
public override string FormatErrorMessage(string name)
{
string Format = GetAFormatStringFromSomewhereAccordingToCurrentCulture();
return string.Format(Format, name);
}
}
...
public class MyModel
{
[RequiredEx]
public string Name { get; set; }
}

Error while reading json file in dotnet core "the configured user limit (128) on the number of inotify instances has been reached"

I have an console application (in dot net core 1.1) which is scheduled in cron scheduler for every 1 min. Inside the application there is call to configuration file. I'm attaching the code below.
public static T GetAppConfig<T>(string key, T defaultValue = default(T))
{
T value = default(T);
logger.Debug($"Reading App Config {key}");
try
{
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var builder = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", true, true)
.AddJsonFile($"appsettings.{environmentName}.json", true, true)
.AddEnvironmentVariables();
var configuration = builder.Build();
T setting = (T)Convert.ChangeType(configuration[key], typeof(T));
value = setting;
if (setting == null)
value = defaultValue;
}
catch (Exception ex)
{
logger.Warn(ex, $"An exception occured reading app key {key} default value {defaultValue} applied.");
value = defaultValue;
}
return value;
}
After running the application for sometime, this error is getting in my log file "the configured user limit (128) on the number of inotify instances has been reached". Please find the full stack trace.
An exception occured reading app key DeviceId default value applied.
System.IO.IOException: The configured user limit (128) on the number of inotify instances has been reached.
at System.IO.FileSystemWatcher.StartRaisingEvents()
at System.IO.FileSystemWatcher.StartRaisingEventsIfNotDisposed()
at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.CreateFileChangeToken(String filter)
at Microsoft.Extensions.Primitives.ChangeToken.OnChange(Func`1 changeTokenProducer, Action changeTokenConsumer)
at Microsoft.Extensions.Configuration.Json.JsonConfigurationSource.Build(IConfigurationBuilder builder)
at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
at app.Shared.Utilities.GetAppConfig[T](String key, T defaultValue) in /var/app/source/app/app.Shared/Utilities.cs:line 33
at System.IO.FileSystemWatcher.StartRaisingEvents()
at System.IO.FileSystemWatcher.StartRaisingEventsIfNotDisposed()
at Microsoft.Extensions.FileProviders.Physical.PhysicalFilesWatcher.CreateFileChangeToken(String filter)
at Microsoft.Extensions.Primitives.ChangeToken.OnChange(Func`1 changeTokenProducer, Action changeTokenConsumer)
at Microsoft.Extensions.Configuration.Json.JsonConfigurationSource.Build(IConfigurationBuilder builder)
at Microsoft.Extensions.Configuration.ConfigurationBuilder.Build()
at app.Shared.Utilities.GetAppConfig[T](String key, T defaultValue) in /var/app/source/app/app.Shared/Utilities.cs:line 33
Please tell me what is wrong with this code.
var builder = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", true, true);
You are creating file watchers, every time you access an setting. The 3rd parameter is reloadOnChange.
You have to make sure,
var configuration = builder.Build()
is only called once in your application and store it in a place where you can access it (preferably AVOID static fields for it).
Or just disable the file watcher.
var builder = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", true, false);
or cleaner:
var builder = new ConfigurationBuilder()
.AddJsonFile($"appsettings.json", optional: true, reloadOnChange: false);
Best way is to abstract that behind an interface and use dependency injection.
public interface IConfigurationManager
{
T GetAppConfig<T>(string key, T defaultValue = default(T));
}
public class ConfigurationManager : IConfigurationManager
{
private readonly IConfigurationRoot config;
public ConfigurationManager(IConfigurationRoot config)
=> this.config ?? throw new ArgumentNullException(nameof(config));
public T GetAppConfig<T>(string key, T defaultValue = default(T))
{
T setting = (T)Convert.ChangeType(configuration[key], typeof(T));
value = setting;
if (setting == null)
value = defaultValue;
}
}
Then instantiate and register it
services.AddSingleton<IConfigurationManager>(new ConfigurationManager(this.Configuration));
and inject it into your services via constructor
The reason why error the configured user limit (128) on the number of inotify instances has been reached happens is right - on non Windows environment reloadOnChange cause the issue while accessing appSetting.json files.
But there is a think you could miss while adjusting this. I addition to setting reloadOnChange to false:
.AddJsonFile($"appsettings.json", optional: true, reloadOnChange: false);
you should also make sure you are not starting from default WebHost.CreateDefaultBuilder because inside it reloadOnChange is also set to true. So the best way to control what your web host is would be to configure it from scratch without not needed options (e.g. WebHost.CreateDefaultBuilder also does .UseIISIntegration() which probably don't need at all in your environment).
The example of custom web host - a copy of Microsoft WebHost.CreateDefaultBuilder but with IIS and FileWatcher dependencies removed e.g. for Linux environments.
It looks like the configuration will reload automatically when the file is changed. So, you should only build the configuration once, when the application starts, and then just read from that.

Categories

Resources