Localization of RequiredAttribute in ASP.NET Core 2.0 - c#

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; }
}

Related

Do repeated calls to AddHttpClient overwrite each other?

I have one NuGet package that has code like this in it:
services.AddHttpClient("CompanyStandardClient").AddCompanyAuthenticationHeaders();
And another Nuget project with code like this in it:
services.AddHttpClient("CompanyStandardClient").AddCompanyHeaderPropagation();
Basically, one NuGet sets up my company's authentication, and another sets up the company's header propagation.
I usually would do this code like this:
services.AddHttpClient("CompanyStandardClient").AddCompanyAuthenticationHeaders().AddCompanyHeaderPropagation()
I am worried that if I do them separate, only one will be in effect. I looked at the code on GitHub and it returns a newed DefaultHttpClientBuilder for each call.
return new DefaultHttpClientBuilder(services, name);
But I am not sure if this means that the previous entry was overwritten.
Can the same named client be "added" separately? Or will it overwrite?
I think it can be done for the same named client based on the internal comments here.
// See comments on HttpClientMappingRegistry.
private static void ReserveClient(IHttpClientBuilder builder, Type type, string name, bool validateSingleType)
{
var registry = (HttpClientMappingRegistry)builder.Services.Single(sd => sd.ServiceType == typeof(HttpClientMappingRegistry)).ImplementationInstance;
Debug.Assert(registry != null);
// Check for same name registered to two types. This won't work because we rely on named options for the configuration.
if (registry.NamedClientRegistrations.TryGetValue(name, out Type otherType) &&
// Allow using the same name with multiple types in some cases (see callers).
validateSingleType &&
// Allow registering the same name twice to the same type.
type != otherType)
{
string message =
$"The HttpClient factory already has a registered client with the name '{name}', bound to the type '{otherType.FullName}'. " +
$"Client names are computed based on the type name without considering the namespace ('{otherType.Name}'). " +
$"Use an overload of AddHttpClient that accepts a string and provide a unique name to resolve the conflict.";
throw new InvalidOperationException(message);
}
if (validateSingleType)
{
registry.NamedClientRegistrations[name] = type;
}
}
Source
The client options configurations will aggregate to a single option.

Command line Parser parses value eventhoug its not specified?

I have begun starting using this https://github.com/commandlineparser/commandline
to pass the input parameters parsed to my application.
My problem here is that the input parameter passed is not required, meaning you can start the application without speciying them.
I have so far defined my command line options as such
public class CommandLineOptions
{
[Option(longName: "client-id", Required = false, HelpText = "Id of the client")]
public string ClientId { get; set; }
[Option(longName: "pw", Required = false, HelpText = "pw.")]
public string Password{ get; set; }
}
and in my main I parse them like this
Access access= Parser.Default.ParseArguments<CommandLineOptions>(args)
.MapResult(parsedFunc: (CommandLineOptions opts) => new Access(opts.ClientId, opts.Password),
notParsedFunc: (IEnumerable<Error> a) => new Access());
I want to use the parsedfunc: in case it is specified, and notParsedFunc: in case it is not specified.
but this always triggers the parsedFunc and since the value of both parameters are null, my inner method fails?
I've also tried changing the option to not required, this then throws an error in the console window that these parameter has not been specified, but triggers the correct method.
From the documentation :
If parsing succeeds, you'll get a derived Parsed type that exposes an instance of T through its Value property.
If parsing fails, you'll get a derived NotParsed type with errors present in Errors sequence.
NotParsed is called when the parsing fail, but in you case the parsing success because empty password is allowed.
You need use Parsed and check manually if the argument is present :
Access access = Parser.Default.ParseArguments<CommandLineOptions>(args)
.MapResult(
opts => opts.Password == null ? new Access() : new Access(opts.ClientId, opts.Password),
_ => null
);
if(access == null)
{
// Fail to create access
// Close withe exit code 1
Environment.Exit(1);
}

C# REST API, Children of model should be of same type, and routes must be recursively defined

Hi I am having an architectural problem with .NET Core
I have a controller called SContent with this route ->
[Route("api/content")]
if you enter this route /api/content/ You will get all contents where Id is Guid.Empty;
if you enter this route /api/content/{id} You will get a specific content from the first level of contents (MasterId must be equal to Guid.Empty in this case)
if you enter this route /api/content/{id}/children you will get all children of the parent {id}
now what I want to do is to create a recursive Route for any of the following cases:
/api/content/{id}/children/{id2}
/api/content/{id}/children/{id2}/children
/api/content/{id}/children/{id2}/children/{id3}
and so on and so on...
is it possible to do something like that?
- children are of the same type of parent
- {id(N)} should always be a child of {id(N-1)}
Thanks
I'm afraid that there is no built-in route that can meet your needs . However, writing a custom middleware is easy .
short answer :
write a predicate which will set Context.Items["Ids"] and Context.Items["WantChildren"]
pass the predicate to a MapWhen() method .
write a middleware that will deal with logic to show content or get it's children according to Context.Items["Ids"] and Context.Items["WantChildren"].
Quick and Dirty Demo
Here's a quick and dirty demo :
app.MapWhen(
context =>{
var path=context.Request.Path.ToString().ToLower();
if (path.EndsWith("/")) {
path = path.Substring(0, path.Length-1);
}
if (!path.StartsWith("/api/content")) {
return false;
}
var ids = new List<int>();
var wantChildren = false;
var match= Regex.Match(path,"/(?<id>\\d+)(?<children>/children)?");
while (match.Success) {
var id = Convert.ToInt32(match.Groups["id"].Value); // todo: if throws an exception , ...
wantChildren= !String.IsNullOrEmpty(match.Groups["children"].Value);
ids.Add(id);
match = match.NextMatch();
}
context.Items["Ids"] = ids;
context.Items["WantChildren"] = wantChildren;
return true;
},
appBuilder => {
appBuilder.Run(async context =>{
var ids = (List<int>)(context.Items["Ids"]);
var wantChildren = (bool)(context.Items["WantChildren"]);
// just a demo
// the code below should be replaced with those that you do with id list and whether you should display children
foreach (var id in ids) {
await context.Response.WriteAsync(id.ToString());
await context.Response.WriteAsync(",");
}
await context.Response.WriteAsync(wantChildren.ToString());
});
}
);
here's a screenshot that works
Futher Refactoring
For better maintence , you can extract Ids and WantChildren to a single Class , for intance , ContentChildrenContext :
public class ContentChildrenContext{
public List<int> Ids {get;set;}
public bool WantChildren{get;set;}
}
you can also make abstraction around the middleware itself , for example, create a factory method which returns a RequestDelegate that can be used easily with app.Run():
Func<Func<ContentChildrenContext,Task>,RequestDelegate> CreateContentChildrenMiddleware(Func<ContentChildrenContext,Task> action){
return async content =>{
var ccc= (ContentChildrenContext)(context.Items["ContentChildrenContext"]);
await action(ccc);
};
}
Best Regards .

MongoDB Serialization C# - Adding Additional Encrypted Field Properties

I am trying to write a MongoDb serializer in c# that will allow me to decorate properties via a [Encrypt()] attribute and then at runtime it would allow me to generate an additional property called PropertyName_Encrypted which would contain the encrypted value.
On deserialization, the encrypted property value would be set in the parent property so that the default GET for the property always returns the encrypted value. Users will then call an optional Decrypt() method on the object to get decrypted values.
In doing so, I'm running into some interesting challenges:
How do I add Additional properties to the document when I am serializing current Element? How do I get the current element's name?
Is there a way I can read a specific property from the document/object? For e.g. say I want to pass a symmetric encryption key and read that to encrypt the data while serializing the current element? Is there any way I can do that?
Here are things I have done so far:
I've built an Encrypt Attribute as follows:
[AttributeUsage(AttributeTargets.Property)]
public class EncryptAttribute : Attribute
{
private readonly EncryptedFieldType _fieldType;
private readonly bool _tokenizeDisplay;
private readonly string _encryptedFieldName;
/// <summary>
///
/// </summary>
/// <param name="fieldType">The field type to encrypt. Useful if display needs to show some formatting. If no formatting is necessary, simply set to "Other".</param>
/// <param name="tokenizeDisplay">If set to true, will persist the tokenized value in the original field for display purposes.</param>
/// <param name="encryptedFieldName">Optional. If set, will save the encrypted value in the field name specified. By default all encrypted field values are stored in the corresponding _Encrypted field name. So EmailAddress field if encrypted, would have value under EmailAddress_Encrypted.</param>
public EncryptAttribute(EncryptedFieldType fieldType, bool tokenizeDisplay, string encryptedFieldName = "")
{
_fieldType = fieldType;
_tokenizeDisplay = tokenizeDisplay;
_encryptedFieldName = encryptedFieldName;
}
}
I read this Attribute on Startup and add an Encryption Serializer to the properties that are decorated using this attribute. The code that does that is like so:
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(x => x.FullName.StartsWith("MongoCustomSerializer"))
.ToList();
var mapper = new Mapper();
foreach (var assembly in assemblies)
{
mapper.Map(assembly);
}
The mapper simply checks which properties in the document have the Encrypt attribute to add the serializer:
public sealed class Mapper
{
public void Map(Assembly assembly)
{
var encryptableTypes = assembly.GetTypes().Where(p =>
typeof(IEncryptable).IsAssignableFrom(p) && p.IsClass && !p.IsInterface && !p.IsValueType &&
!p.IsAbstract).ToList();
if (encryptableTypes.Any())
{
foreach (var encryptableType in encryptableTypes)
{
Map(encryptableType);
}
}
}
private void Map(Type documentType)
{
var properties =
documentType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
if (properties.Length <= 0)
{
return;
}
foreach (var property in properties)
{
RegisterEncrpytionSerializer(property, typeof(EncryptAttribute), documentType);
}
}
private void RegisterEncrpytionSerializer(PropertyInfo property, Type encryptAttributeType, Type documentType)
{
var encryptAttributes = property.GetCustomAttributes(encryptAttributeType, false).ToList();
if (!encryptAttributes.Any()) return;
var memberMap = BsonClassMap.LookupClassMap(documentType).GetMemberMap(property.Name);
memberMap?.SetSerializer(new EncryptionSerializer());
}
}
In my unit tests, I'm getting an error stating that the Bson Class Map is already frozen. Even if I were to figure out a way to bypass that, how would this EncryptionSerializer class work to where I could write an additional property?
Would love to see if someone can assist!
UPDATE 1 - I was able to get the FREEZE error taken care of. It would appear that the LookupClassMap freezes the Member and Class Map info.
This change from the link allows me to take care of that issue:
private void RegisterEncrpytionSerializer(PropertyInfo property, Type encryptAttributeType, Type documentType)
{
var encryptAttributes = property.GetCustomAttributes(encryptAttributeType, false).ToList();
if (!encryptAttributes.Any()) return;
var classMapDefinition = typeof(BsonClassMap<>);
var classMapType = classMapDefinition.MakeGenericType(documentType);
var classMap = (BsonClassMap)Activator.CreateInstance(classMapType);
classMap.AutoMap();
var memberMap = classMap.GetMemberMap(property.Name);
memberMap?.SetSerializer(new KeyVaultEncryptionSerializer(memberMap.ElementName));
}
Are you using a service for saving/retrieving your items that actually call the DB?
I believe you should move the responsibility for writing/reading encrypted values to the calling service (i.e a repository implementation) instead of the BsonSerializer.
It would make sense to me that encryption/decryption is part of the persistence layer and something not handled in the application when needed.
Your implementation targets only the specified property you want to serialize. It doesn't make sense that it creates another property.
A second thought is that your suggested approach with properties that change value based on Decrypt() probably isn't a good idea since it makes your code unpredictable and hard to read. Make your properties dead simple.
What extra security in your code does it really give you if you can decrypt properties by just calling a method anyway?
If you still need to have a Decrypt() would suggest that you create methods for decrypting that return the decrypted value like GetUnencryptedCode() etc, it could just as well be an extension method but still not a readable property.
You should also be looking into using SecureString depending on your use case.

CodeFluent Aspect for Full-Text Index

I'm trying to develop a CodeFluent aspect to set a property of a entity to be a full-text index.
I've found this link, which does something similar to what I'm aiming for.
http://blog.codefluententities.com/2012/11/27/using-the-sql-server-template-producer-to-generate-clustered-indexes/
However this uses a SQL template producer. Are there anyway to set a property to be a full-text index entirely in the aspect itself, so I don't have to install/maintain both template producer and aspect for all projects?
Here's the C# aspect code I have so far:
public class FullTextIndexing : IProjectTemplate
{
public static readonly XmlDocument Descriptor;
public const string Namespace = "http://www.softfluent.com/aspects/samples/FullTextIndexing";
static FullTextIndexing()
{
Descriptor = new XmlDocument();
Descriptor.LoadXml(
#"<cf:project xmlns:cf='http://www.softfluent.com/codefluent/2005/1' defaultNamespace='FullTextIndexing'>
<cf:pattern name='Full Text Indexing' namespaceUri='" + Namespace + #"' preferredPrefix='fti' step='Tables'>
<cf:message class='_doc'>CodeFluent Full Text Indexing Aspect</cf:message>
<cf:descriptor name='fullTextIndexing'
typeName='boolean'
category='Full Text Indexing'
targets='Property'
defaultValue='false'
displayName='Full-Text Index'
description='Determines if property should be full text indexed.' />
</cf:pattern>
</cf:project>");
}
public Project Project { get; set; }
public XmlDocument Run(IDictionary context)
{
if (context == null || !context.Contains("Project"))
{
// we are probably called for meta data inspection, so we send back the descriptor xml
return Descriptor;
}
// the dictionary contains at least these two entries
Project = (Project)context["Project"];
// the dictionary contains at least these two entries
XmlElement element = (XmlElement)context["Element"];
Project project = (Project)context["Project"];
foreach (Entity entity in project.Entities)
{
Console.WriteLine(">>PROPERTY LOGGING FOR ENTITY "+entity.Name.ToUpper()+":<<");
foreach (Property property in entity.Properties)
{
Log(property);
if(MustFullTextIndex(property))
{
Console.WriteLine("CHANGING PROPERTY");
property.TypeName = "bool";
Log(property);
}
}
}
// we have no specific Xml to send back, but aspect description
return Descriptor;
}
private static bool MustFullTextIndex(Property property)
{
return property != null && property.IsPersistent && property.GetAttributeValue("fullTextIndexing", Namespace, false);
}
private static void Log(Property property)
{
Console.WriteLine(property.Trace());
}
}
EDIT ONE:
Following Meziantou's answer, I'm trying to create a template producer, but it's giving me compilation errors when I try to add the new template producer to the project producers list, so I'm probably doing it wrong.
The error says:
Cannot convert type 'CodeFluent.Model.Producer' to 'CodeFluent.Producers.SqlServer.TemplateProducer'
Here's the code I have thus far:
public XmlDocument Run(IDictionary context)
{
if (context == null || !context.Contains("Project"))
{
// we are probably called for meta data inspection, so we send back the descriptor xml
return Descriptor;
}
// the dictionary contains at least these two entries
XmlElement element = (XmlElement)context["Element"];
Project project = (Project)context["Project"];
CodeFluent.Producers.SqlServer.TemplateProducer producer = new CodeFluent.Producers.SqlServer.TemplateProducer();
producer.AddNamespace("CodeFluent.Model");
producer.AddNamespace("CodeFluent.Model.Persistence");
producer.AddNamespace("CodeFluent.Producers.SqlServer");
Console.WriteLine(producer.Element);
//TODO: Need to figure out how to modify the actual template's contents
project.Producers.Add(producer); //Error happens here
// we have no specific Xml to send back, but aspect description
return Descriptor;
}
In the sample code, the aspect is used only because it has a descriptor. Descriptors are used by CodeFluent Entities to populate the property grid:
<cf:descriptor name="IsClusteredIndex" typeName="boolean" targets="Property" defaultValue="false" displayName="IsClusteredIndex" />
So when you set the value of this property to true or false, the xml attribute ns:IsClusteredIndex is added or removed from the xml file.
Then the SQL Template reads the value of the attribute to generate the expected SQL file:
property.GetAttributeValue("sa:IsClusteredIndex", false)
So the aspect is not mandatory, but provides a graphical interface friendly way to add/remove the attribute. If you don't need to integrate into the graphical interface, you can safely remove the aspect.
If your goal is to integrate into the graphical interface, you need an aspect (XML or DLL) or a producer. If you don't want to create a producer, you can embed the template into your aspect. During the build, you can extract the SQL template and add the SQL Template producer to the project, this way everything is located in the aspect.

Categories

Resources