Is there an OCP-friendly way to dynamically choose which collection in a DbContext to access?

.net c# entity-framework-core open-closed-principle

Question

I'm working on a method to retrieve a collection of records from a database. The records are stored in separate tables based on an aspect of the data they contain. Suppose it looks like this.

public class EnglishPhrase : IPhrase
{
    public string Text {get; set;}
}

public class SpanishPhrase: IPhrase 
{ 
    public string Text {get; set;} 
}

// This is actually a DbContext with DbSets. 
// I have not implemented DbContext in this example to
// alleviate overhead when reproducing the situation.
public class MyContext
{
    public EnglishPhrase[] EnglishPhrases { get; set; }
    public SpanishPhrase[] SpanishPhrases { get; set; }
}

My method needs to pick either English or Spanish phrases based on a language argument. Right now I'm accomplishing it with a switch statement.

public IEnumerable<IPhrase> GetPhrases(string language)
{
    IEnumerable<IPhrase> result = null;

    MyContext context = new MyContext();

    switch(language)
    {
        case "English":
            result = context.EnglishPhrases.ToList();
            break;
        case "Spanish":
            result = context.SpanishPhrases.ToList();
            break;
        default:
            throw new Exception();
    }

    return result;
}

I used the switch because I'm going to be adding more languages later on, but that means I'll have to modify this method every time I do that. However, I can't help but feel like there could be a better way to do this.

Could I do something else, such as adding a Language property to the IPhrase interface, that would allow my method to access the right DbSet that way, or is the switch the tersest way to accomplish my goal?

1
2
8/24/2018 4:54:02 PM

Popular Answer

You can use TPH to simplify you context and action.

Models:

public enum PhraseType
{
    English,
    Spanish
}

public abstract class Phrase
{
    public int Id { get; set; }
    public string Text { get; set; }
    public PhraseType PhraseType { get; set; }
}

public class EnglishPhrase : Phrase {}

public class SpanishPhrase : Phrase {}

DbContext:

public DbSet<Phrase> Phrases { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //...

    modelBuilder.Entity<Phrase>()
        .HasDiscriminator(p => p.PhraseType)
        .HasValue<EnglishPhrase>(PhraseType.English)
        .HasValue<SpanishPhrase>(PhraseType.Spanish);
}

Action method:

public IEnumerable<Phrase> GetPhrases(string language)
{
    // assuming the language parameter is "english" or "spanish"
    var theType = $"YourNamespace.{language}Phrase";

    //assuming your models are in the current assembly
    Type type = TypeInfo.GetType(theType, true, true);

    MethodInfo method = typeof(Queryable).GetMethod("OfType").MakeGenericMethod(type);

    var obj = appContext.Phrases.AsQueryable();

    var result = method.Invoke(obj, new[] { obj });
    return result as IEnumerable<Phrase>;
}

A little explanation:

You Use TPH (Table per Hierarchy) so you only need to create one DbSet.

When you want to return all EnglishPhrases you can use context.Phrases.OfType<EnglishPhrase>() but since your parameter determines the type, you need to use reflection to call the correct OfType method.

You can put all those reflections codes into a helper class so your action will be cleaner.

In future when you want to add more languages you just have to edit the PhraseType enum and add extra HasValue in your Fluent API. No changes are needed in the Action method.

I just tested this on my system and it worked.

0
8/24/2018 8:47:38 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow