Use Automapper to hide database primary key/ID (convert member with ValueConverter) - entity-framework-core

I am trying to hide the real Ids of the objects in a database using Automapper's value converters, however they are not called when projecting one to the other.
Nothing special, I would like to use Hashids to convert the int ID to a random string ID (DB->DTO) and vice versa. I want to do this for every object and every ID, but instead of my converter getting called the id's in the database simply get converted strings (so 1 would become "1" instead of for example "sd2+a!F").
My Class:
public class Category
{
public Category(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public int? ParentCategoryId { get; set; }
public Category? ParentCategory { get; set; }
public ICollection<Foodstuff> Foodstuffs { get; } = new List<Foodstuff>();
public ICollection<int> FoodstuffIds { get; } = new List<int>();
public byte[] RowVersion { get; set; }
}
My DTO:
public record class Category : IGenerateETag
{
public Category(string name)
{
Name = name;
}
public string Id { get; init; }
[Required(ErrorMessage = "Category name is required.", AllowEmptyStrings = false)]
public string Name { get; init; }
public string? ParentCategoryId { get; init; }
public byte[] RowVersion { get; set; }
}
My converters:
public class HideIdConverter : IValueConverter<int, string>
{
public string Convert(int sourceMember, ResolutionContext context)
{
var hashids = new Hashids();
var shadow = hashids.Encode(sourceMember);
return shadow;
}
}
public class UnhideIdConverter : IValueConverter<string, int>
{
public int Convert(string sourceMember, ResolutionContext context)
{
var hashids = new Hashids();
var plain = hashids.Decode(sourceMember);
return plain[0]; // TODO check this;
}
}
Aaand my Automapper config:
CreateMap<Dal.Entities.Category, Category>()
.ForMember(dest => dest.Id, opt => opt.ConvertUsing(new HideIdConverter(), src => src.Id))
.ReverseMap()
.ForMember(dest => dest.Id, opt => opt.ConvertUsing(new UnhideIdConverter(), src => src.Id));

It turns out that it this is not possible, because I was using LINQ expressions, especially ProjectTo(). It is stated that:
Value converters are only used for in-memory mapping execution. They will not work for ProjectTo.
Source.
I guess I will have to map the objects after I queried them from my database.

Related

How to configure an entity to save the summed value from a nested list?

I have two entity classes (Order, OrderItem) and one Value Object cl(Money).
Simplified code:
public class Order
{
private Order()
{
}
public int OrderId { get; private set; }
public List<OrderItem> OrderItems { get; set; } = new ();
public Money TotalGrossAmount
{
get => new Money(OrderItems.Sum(x => x.TotalNetAmount.Amount), "USD");
private set { }
}
// ...rest of code
}
public class OrderItem
{
private OrderItem()
{
}
public int OrderItemId { get; private set; }
public Money TotalNetAmount { get; private set; }
// ...rest of code
}
public class Money
{
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public decimal Amount { get; private set; }
public string Currency { get; private set; }
}
For efficiency purposes, I'd like to have the summed amount of the order items in the database in the Order table.
The Order entity is configured like this:
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable(nameof(Order), DbSchemas.Default);
builder.HasKey(x => x.OrderId);
// ...rest of code
builder.OwnsOne(x => x.TotalGrossAmount, navigationBuilder =>
{
navigationBuilder.Property(p => p.Amount);
navigationBuilder.Property(p => p.Currency);
});
}
}
Migration creates the correct table structure with columns: TotalGrossAmount_Amount, TotalGrossAmount_Currency
But for this configuration, the application throws an exception:
The value of 'Order.TotalGrossAmount#Money.OrderId' is unknown when attempting to save changes. This is because the property is also part of a foreign key for which the principal entity in the relationship is not known.
How should I configure Order entities correctly?

Map an Entity iEnumerator To Dto Enumerator

I am using CQRS. I select my Entities IEnumerator from database and i want to map this to my Dto class.
My Dto class:
public class XCollectionDto
{
public IEnumerable<XReadDto> Entries { get; init; } = Enumerable.Empty<XReadDto>();
}
My mapper class:
public class XReadMapper : IEntityToDtoMapper<X, XCollectionDto>
{
public XCollectionDto Map(IEnumerable <X> source, XCollectionDto target)
{
//todo
Here i want to map source to target Entries list
}
}
How can i do that, without a for loop? I am not using AutoMaper, the mapping is manual
I think you could accompish your purpose with C# reflection
I created the two class for test:
public class somemodel
{
public int Id { get; set; }
public string Name { get; set; }
public List<int> Numlist { get; set; }
}
public class somemodelDTO
{
public int Id { get; set; }
public string SomeName { get; set; }
public List<int> Numlist { get; set; }
}
the method to bind properties of somemodelDTO which have the same name with properties of somemodel:
private static somemodelDTO GetMap<somemodel, somemodelDTO>(somemodel some)
{
somemodelDTO somemDTO = Activator.CreateInstance<somemodelDTO>();
var typesource = some.GetType();
var typedestination = typeof(somemodelDTO);
foreach(var sp in typesource.GetProperties())
{
foreach( var dp in typedestination.GetProperties())
{
if(sp.Name==dp.Name)
{
dp.SetValue(somemDTO, sp.GetValue(some, null), null);
}
}
}
return somemDTO;
}
The result:

EF6: Single relationship table for multiple related entities

I have a EF Model with many entities, like Nodes, Attributes, Tags, etc.
There is also an "Alias" entity, and pretty much every other entity else can have a many-to-many relationship with Aliases. One of the undesired things about this is the number of tables that are created to track these relationships (eg. NodeAlias, AttributeAlias, etc.).
Are there any design alternatives that could map an Alias to all of the other entities in a single table? I was thinking maybe something along these lines if it's possible:
+---------+--------+-------------+-----------+
| AliasId | NodeId | AttributeId | TagId |
+---------+--------+-------------+-----------+
| 1 | 1 | 2 | 3 |
+---------+--------+-------------+-----------+
I updated my solution to provide many-to-many relationships between aliases and every other entity.
I intentionally posted this as a separate answer so that my previous answer can also remain here if anyone would need it.
Step #1: I created extension methods for getting and setting property values using reflection in a convenient way:
public static class ObjectExtensions
{
public static TResult GetPropertyValue<TResult>(this object entity, string propertyName)
{
object propertyValue = entity?.GetType().GetProperty(propertyName)?.GetValue(entity);
try
{
return (TResult)propertyValue;
}
catch
{
return default(TResult);
}
}
public static void SetPropertyValue(this object entity, string propertyName, object value)
{
entity?.GetType().GetProperty(propertyName)?.SetValue(entity, value);
}
}
Step #2: I updated the models to provide many-to-many relationship.
public class Node
{
[Key]
public int NodeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Attribute
{
[Key]
public int AttributeId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual ICollection<AliasMapping> AliasMappings { get; set; }
}
public class AliasMapping
{
[Key]
public int Id { get; set; }
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int? NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int? AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int? TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3: Due to relationship changes the MyDbContext could have been simplified as the [ForeignKey] data annotations are enough.
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
}
Step #4: I also updated the extension methods so that you can create and remove alias mappings.
public static class AliasExtensions
{
public static void CreateMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
if (mappings.Any(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId))
{
// We already have the mapping between the specified entity and alias.
return;
}
bool usableMappingExists = true;
var usableMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == null);
if (usableMapping == null)
{
usableMappingExists = false;
usableMapping = new AliasMapping()
{
Alias = alias
};
}
usableMapping.SetPropertyValue(mappingEntityPropertyName, entity);
usableMapping.SetPropertyValue(entityKeyPropertyName, entityId);
if (!usableMappingExists)
{
context.AliasMappings.Add(usableMapping);
}
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
public static void RemoveMapping(this MyDbContext context, object entity, Alias alias)
{
if (entity == null || alias == null)
{
return;
}
string mappingEntityPropertyName = entity.GetType().Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
int entityId = entity.GetPropertyValue<int>(entityKeyPropertyName);
AliasMapping[] mappings =
context
.AliasMappings
.Where(mapping => mapping.AliasId == alias.AliasId)
.ToArray();
AliasMapping currentMapping = mappings.FirstOrDefault(mapping => mapping.GetPropertyValue<int?>(entityKeyPropertyName) == entityId);
if (currentMapping == null)
{
// There is no mapping between the specified entity and alias.
return;
}
currentMapping.SetPropertyValue(mappingEntityPropertyName, null);
currentMapping.SetPropertyValue(entityKeyPropertyName, null);
// This step is required here, I think due to using reflection.
context.SaveChanges();
}
}
Step #5: Updated the console app steps to align it with the changes.
class Program
{
static void Main(string[] args)
{
// Consider specify the appropriate database initializer!
// I use DropCreateDatabaseAlways<> strategy only for this example.
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
// Always save changes after adding an entity but before trying to create a mapping.
context.SaveChanges();
// One Alias To Many Entities
context.CreateMapping(nodes[0], aliases[0]);
context.CreateMapping(nodes[1], aliases[0]);
context.CreateMapping(nodes[2], aliases[0]);
context.CreateMapping(nodes[3], aliases[0]);
context.CreateMapping(attributes[0], aliases[0]);
context.CreateMapping(attributes[1], aliases[0]);
context.CreateMapping(attributes[2], aliases[0]);
context.CreateMapping(tags[0], aliases[0]);
context.CreateMapping(tags[1], aliases[0]);
// One Entity To Many Aliases
context.CreateMapping(nodes[4], aliases[0]);
context.CreateMapping(nodes[4], aliases[1]);
context.CreateMapping(nodes[4], aliases[2]);
context.CreateMapping(attributes[3], aliases[1]);
context.CreateMapping(attributes[3], aliases[3]);
context.CreateMapping(tags[2], aliases[2]);
context.CreateMapping(tags[2], aliases[3]);
// Remove mapping
context.RemoveMapping(nodes[4], aliases[0]);
// Not really needed here as both 'CreateMapping' and 'RemoveMapping' save the changes
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}
Please note: RemoveMapping() will not delete an AliasMapping even if no entity is associated with it! But CreateMapping() will make use of it later if needed. E.g. look at the screenshot below and check AliasMapping where Id = 5.
Screenshot about the execution result:
You were talking about many-to-many relationship but reading your post I think it is more likely a "special one-to-many" relationship, actually "combined multiple one-to-one" relationship as I see that an Alias can be mapped to a single Node AND/OR to a single Attribute AND/OR to a single Tag.
I think I found a solution for this case.
If it's not the case and an Alias can be mapped to multiple Node AND/OR to multiple Attribute AND/OR to multiple Tag then I think this solution below needs only a small change. :)
Step #1 - These are my example models
public class Node
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Attribute
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Tag
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
public class Alias
{
[Key]
public int AliasId { get; set; }
public string Name { get; set; }
public virtual AliasMapping AliasMapping { get; set; }
}
Step #2 - Creating the custom mapping table
public class AliasMapping
{
[Key]
[ForeignKey("Alias")]
public int AliasId { get; set; }
public Alias Alias { get; set; }
[ForeignKey("Node")]
public int NodeId { get; set; }
public virtual Node Node { get; set; }
[ForeignKey("Attribute")]
public int AttributeId { get; set; }
public virtual Attribute Attribute { get; set; }
[ForeignKey("Tag")]
public int TagId { get; set; }
public virtual Tag Tag { get; set; }
}
Step #3 - Creating the DbContext
public class MyDbContext : DbContext
{
public DbSet<Node> Nodes { get; set; }
public DbSet<Attribute> Attributes { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Alias> Aliases { get; set; }
public DbSet<AliasMapping> AliasMappings { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Attribute)
.WithOptionalPrincipal(attribute => attribute.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Node)
.WithOptionalPrincipal(node => node.AliasMapping)
.Map(config => config.MapKey("AliasId"));
modelBuilder
.Entity<AliasMapping>()
.HasOptional(mapping => mapping.Tag)
.WithOptionalPrincipal(tag => tag.AliasMapping)
.Map(config => config.MapKey("AliasId"));
}
}
Step #4 - Creating extension method so that creating a relationship will be easy
public static class AliasExtensions
{
public static void CreateMapping<TEntity>(this MyDbContext context, TEntity entity, Alias alias)
{
string mappingEntityPropertyName = typeof(TEntity).Name;
string entityKeyPropertyName = String.Concat(mappingEntityPropertyName, "Id");
bool entityExists = true;
var mapping = context.AliasMappings.Find(alias.AliasId);
if (mapping == null)
{
entityExists = false;
mapping = new AliasMapping()
{
Alias = alias
};
}
typeof(AliasMapping)
.GetProperty(mappingEntityPropertyName)
.SetValue(mapping, entity);
typeof(AliasMapping)
.GetProperty(entityKeyPropertyName)
.SetValue(mapping, typeof(TEntity).GetProperty("Id").GetValue(entity));
if (!entityExists)
{
context.AliasMappings.Add(mapping);
}
}
}
Step #5 - Created a console app to see this working
class Program
{
static readonly Random rnd = new Random(DateTime.Now.TimeOfDay.Milliseconds);
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyDbContext>());
var aliases =
Enumerable
.Range(1, 9)
.Select(index => new Alias() { Name = String.Format("Alias{0:00}", index) })
.ToList();
var attributes =
Enumerable
.Range(1, 5)
.Select(index => new Attribute() { Name = String.Format("Attribute{0:00}", index) })
.ToList();
var nodes =
Enumerable
.Range(1, 5)
.Select(index => new Node() { Name = String.Format("Node{0:00}", index) })
.ToList();
var tags =
Enumerable
.Range(1, 5)
.Select(index => new Tag() { Name = String.Format("Tag{0:00}", index) })
.ToList();
using (var context = new MyDbContext())
{
context.Aliases.AddRange(aliases);
context.Nodes.AddRange(nodes);
context.Attributes.AddRange(attributes);
context.Tags.AddRange(tags);
context.SaveChanges();
// Associate aliases to attributes
attributes.ForEach(attribute =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Attribute == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(attribute, selectedAlias);
});
// Associate aliases to nodes
nodes.ForEach(node =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Node == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(node, selectedAlias);
});
// Associate aliases to tags
tags.ForEach(tag =>
{
var usableAliases = aliases.Where(alias => alias.AliasMapping?.Tag == null).ToList();
var selectedAlias = usableAliases[rnd.Next(usableAliases.Count)];
context.CreateMapping(tag, selectedAlias);
});
context.SaveChanges();
}
Console.Write("Press any key to continue . . .");
Console.ReadKey(true);
}
}

NullReference error when adding a new model into a model property

I have the following entity framework code first models:
public class Member {
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string City { get; set; }
public String CardNumber { get; set; }
//Foreign Key
public virtual ICollection<Favorite> Favorites { get; set; }
[NotMapped]
public List<SelectListItem> FavoriteTypes { get; set; }
public Member() {
MembersDB db = new MembersDB();
FavoriteTypes = new List<SelectListItem>();
FavoriteTypes.AddRange(db.FavoriteTypes.ToList().Select(f => new SelectListItem { Text = f.Value, Value = f.ID.ToString() }));
}
}
public class FavoriteType {
public int ID { get; set; }
public string Value { get; set; }
}
public class Favorite {
public int ID { get; set; }
public String Value { get; set; }
//Foreign Keys
public virtual FavoriteType FavoriteType { get; set; }
public virtual Member Member { get; set; }
}
This creates a 1-M relationship for FavoriteTypes -> Favorites and 1-M relations for Member -> Favorites
Within my controller action, I retrieve most of the Member's info from Session saved at a couple pages back except for the favorites info which is gathered below. Then I gather the list of ID and input values to add to my new member as so:
[HttpPost]
public ActionResult AddFavs(List<int> ID, List<string> Value) {
MembersDB db = new MembersDB();
Member newMember = (Member)Session["member"];
if (ID != null && Value != null)
{
for (int i = 0; i < ID.Count(); i++)
{
int currentID = ID[i];
var test = new Favorite();
test.FavoriteType = db.FavoriteTypes.Where(f => f.ID == currentID).FirstOrDefault();
test.Value = Value[i];
newMember.Favorites.Add(test);
}
}
While running this code I get a NullReference error on this line newMember.Favorites.Add(test);
Not entirely sure why, any help would be appreciated.
EDIT: while troubleshooting in VS, the only null properties I can find are Favorites in newMember and Member in test
ICollection<Favorite> Favorites is null, so you can't add items to it. You should instantiate it in the constructor of your model:
public Member()
{
Favorites = new List<Favorite>();
// ...
}
Now it's an empty collection and you can add items to it.

Internationalization of content in Entity Framework

I keep coming across an i18n requirement where my data (not my UI) needs to be internationalized.
public class FooEntity
{
public long Id { get; set; }
public string Code { get; set; } // Some values might not need i18n
public string Name { get; set } // but e.g. this needs internationalized
public string Description { get; set; } // and this too
}
What are some approaches I could use?
Some things I've tried:-
1) Store a resource key in the db
public class FooEntity
{
...
public string NameKey { get; set; }
public string DescriptionKey { get; set; }
}
Pros: No need for complicated queries to get a translated entity. System.Globalization handles fallbacks for you.
Cons: Translations can't easily be managed by an admin user (have to deploy resource files whenever my Foos change).
2) Use a LocalizableString entity type
public class FooEntity
{
...
public int NameId { get; set; }
public virtual LocalizableString Name { get; set; }
public int NameId { get; set; }
public virtual LocalizableString Description { get; set; }
}
public class LocalizableString
{
public int Id { get; set; }
public ICollection<LocalizedString> LocalizedStrings { get; set; }
}
public class LocalizedString
{
public int Id { get; set; }
public int ParentId { get; set; }
public virtual LocalizableString Parent { get; set; }
public int LanguageId { get; set; }
public virtual Language Language { get; set; }
public string Value { get; set; }
}
public class Language
{
public int Id { get; set; }
public string Name { get; set; }
public string CultureCode { get; set; }
}
Pros: All localised strings in the same table. Validation can be performed per-string.
Cons: Queries are horrid. Have to .Include the LocalizedStrings table once for each localizable string on the parent entity. Fallbacks are hard and involve extensive joining. Haven't found a way to avoid N+1 when retrieving e.g. data for a table.
3) Use a parent entity with all the invariant properties and child entities containing all the localized properties
public class FooEntity
{
...
public ICollection<FooTranslation> Translations { get; set; }
}
public class FooTranslation
{
public long Id { get; set; }
public int ParentId { get; set; }
public virtual FooEntity Parent { get; set; }
public int LanguageId { get; set; }
public virtual Language Language { get; set; }
public string Name { get; set }
public string Description { get; set; }
}
public class Language
{
public int Id { get; set; }
public string Name { get; set; }
public string CultureCode { get; set; }
}
Pros: Not as hard (but still too hard!) to get a full translation of an entity into memory.
Cons: Double the number of entities. Can't handle partial translations of an entity - especially the case where, say, Name is coming from es but Description is coming from es-AR.
I have three requirements for a solution
Users can edit entities, languages, and translations at runtime
Users can supply partial translations with missing strings coming from a fallback as per System.Globalization
Entities can be brought into memory without running into e.g. N+1 issues
Why don't you take the best of both worlds?
Have a CustomResourceManager that handles the loading of resources and picking the right culture and use a CustomResourceReader that uses whatever backing store you like. A basic implementation could look like this, relying on convention of the Resourceky being Typename_PropertyName_PropertyValue. If for some reason the structure of the backingstore(csv/excel/mssql/table structure) need to change you only have the change the implementation of the ResourceReader.
As an added bonus I also got the real/transparent proxy going.
ResourceManager
class MyRM:ResourceManager
{
readonly Dictionary<CultureInfo, ResourceSet> sets = new Dictionary<CultureInfo, ResourceSet>();
public void UnCache(CultureInfo ci)
{
sets.Remove(ci):
}
protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
ResourceSet set;
if (!sets.TryGetValue(culture, out set))
{
IResourceReader rdr = new MyRR(culture);
set = new ResourceSet(rdr);
sets.Add(culture,set);
}
return set;
}
// sets Localized values on properties
public T GetEntity<T>(T obj)
{
var entityType = typeof(T);
foreach (var prop in entityType.GetProperties(
BindingFlags.Instance
| BindingFlags.Public)
.Where(p => p.PropertyType == typeof(string)
&& p.CanWrite
&& p.CanRead))
{
// FooEntity_Name_(content of Name field)
var key = String.Format("{0}_{1}_{2}",
entityType.Name,
prop.Name,
prop.GetValue(obj,null));
var val = GetString(key);
// only set if a value was found
if (!String.IsNullOrEmpty(val))
{
prop.SetValue(obj, val, null);
}
}
return obj;
}
}
ResourceReader
class MyRR:IResourceReader
{
private readonly Dictionary<string, string> _dict;
public MyRR(CultureInfo ci)
{
_dict = new Dictionary<string, string>();
// get from some storage (here a hardcoded Dictionary)
// You have to be able to deliver a IDictionaryEnumerator
switch (ci.Name)
{
case "nl-NL":
_dict.Add("FooEntity_Name_Dutch", "nederlands");
_dict.Add("FooEntity_Name_German", "duits");
break;
case "en-US":
_dict.Add("FooEntity_Name_Dutch", "The Netherlands");
break;
case "en":
_dict.Add("FooEntity_Name_Dutch", "undutchables");
_dict.Add("FooEntity_Name_German", "german");
break;
case "": // invariant
_dict.Add("FooEntity_Name_Dutch", "dutch");
_dict.Add("FooEntity_Name_German", "german?");
break;
default:
Trace.WriteLine(ci.Name+" has no resources");
break;
}
}
public System.Collections.IDictionaryEnumerator GetEnumerator()
{
return _dict.GetEnumerator();
}
// left out not implemented interface members
}
Usage
var rm = new MyRM();
var f = new FooEntity();
f.Name = "Dutch";
var fl = rm.GetEntity(f);
Console.WriteLine(f.Name);
Thread.CurrentThread.CurrentUICulture = new CultureInfo("nl-NL");
f.Name = "Dutch";
var dl = rm.GetEntity(f);
Console.WriteLine(f.Name);
RealProxy
public class Localizer<T>: RealProxy
{
MyRM rm = new MyRM();
private T obj;
public Localizer(T o)
: base(typeof(T))
{
obj = o;
}
public override IMessage Invoke(IMessage msg)
{
var meth = msg.Properties["__MethodName"].ToString();
var bf = BindingFlags.Public | BindingFlags.Instance ;
if (meth.StartsWith("set_"))
{
meth = meth.Substring(4);
bf |= BindingFlags.SetProperty;
}
if (meth.StartsWith("get_"))
{
// get the value...
meth = meth.Substring(4);
var key = String.Format("{0}_{1}_{2}",
typeof (T).Name,
meth,
typeof (T).GetProperty(meth, BindingFlags.Public | BindingFlags.Instance
|BindingFlags.GetProperty).
GetValue(obj, null));
// but use it for a localized lookup (rm is the ResourceManager)
var val = rm.GetString(key);
// return the localized value
return new ReturnMessage(val, null, 0, null, null);
}
var args = new object[0];
if (msg.Properties["__Args"] != null)
{
args = (object[]) msg.Properties["__Args"];
}
var res = typeof (T).InvokeMember(meth,
bf
, null, obj, args);
return new ReturnMessage(res, null, 0, null, null);
}
}
Real/Transparent proxy usage
var f = new FooEntity();
f.Name = "Dutch";
var l = new Localizer<FooEntity>(f);
var fp = (FooEntity) l.GetTransparentProxy();
fp.Name = "Dutch"; // notice you can use the proxy as is,
// it updates the actual FooEntity
var localizedValue = fp.Name;
First one is worthy if you have static content in database. For example if you have categories that relatively are not going to be changed by user. You can change them at next deploy. I do not like this solution personally. I do not consider this as a nice solution. This is just an escape of the problem.
Second one is the best but can cause a problem when you have two or more localizable fields in one entity. You can simplify it a bit and hard code languages on it like this
public class LocalizedString
{
public int Id { get; set; }
public string EnglishText { get; set; }
public string ItalianText { get; set; }
public string ArmenianText { get; set; }
}
Third one is not a good one neither. From this structure I can't be sure that all nodes (literals, lines, strings etc.) translated in specific culture.
Do not generalize too much. Each problem is kind of specialized and it needs specialized solution too. Too much generalization makes unjustified issues.